ernanhughes commited on
Commit
d20f268
·
1 Parent(s): 2a5ed13

initial

Files changed (5) hide show
  1. .gitignore +181 -0
  2. README.md +4 -12
  3. app.py +41 -0
  4. mars.py +218 -0
  5. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data/
2
+ *.log
3
+ *.db
4
+
5
+
6
+ # Byte-compiled / optimized / DLL files
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+
11
+ # C extensions
12
+ *.so
13
+
14
+ # Distribution / packaging
15
+ .Python
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ share/python-wheels/
29
+ *.egg-info/
30
+ .installed.cfg
31
+ *.egg
32
+ MANIFEST
33
+
34
+ # PyInstaller
35
+ # Usually these files are written by a python script from a template
36
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
37
+ *.manifest
38
+ *.spec
39
+
40
+ # Installer logs
41
+ pip-log.txt
42
+ pip-delete-this-directory.txt
43
+
44
+ # Unit test / coverage reports
45
+ htmlcov/
46
+ .tox/
47
+ .nox/
48
+ .coverage
49
+ .coverage.*
50
+ .cache
51
+ nosetests.xml
52
+ coverage.xml
53
+ *.cover
54
+ *.py,cover
55
+ .hypothesis/
56
+ .pytest_cache/
57
+ cover/
58
+
59
+ # Translations
60
+ *.mo
61
+ *.pot
62
+
63
+ # Django stuff:
64
+ *.log
65
+ local_settings.py
66
+ db.sqlite3
67
+ db.sqlite3-journal
68
+
69
+ # Flask stuff:
70
+ instance/
71
+ .webassets-cache
72
+
73
+ # Scrapy stuff:
74
+ .scrapy
75
+
76
+ # Sphinx documentation
77
+ docs/_build/
78
+
79
+ # PyBuilder
80
+ .pybuilder/
81
+ target/
82
+
83
+ # Jupyter Notebook
84
+ .ipynb_checkpoints
85
+
86
+ # IPython
87
+ profile_default/
88
+ ipython_config.py
89
+
90
+ # pyenv
91
+ # For a library or package, you might want to ignore these files since the code is
92
+ # intended to run in multiple environments; otherwise, check them in:
93
+ # .python-version
94
+
95
+ # pipenv
96
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
98
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
99
+ # install all needed dependencies.
100
+ #Pipfile.lock
101
+
102
+ # UV
103
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
104
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
105
+ # commonly ignored for libraries.
106
+ #uv.lock
107
+
108
+ # poetry
109
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
110
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
111
+ # commonly ignored for libraries.
112
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
113
+ #poetry.lock
114
+
115
+ # pdm
116
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
117
+ #pdm.lock
118
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
119
+ # in version control.
120
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
121
+ .pdm.toml
122
+ .pdm-python
123
+ .pdm-build/
124
+
125
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
126
+ __pypackages__/
127
+
128
+ # Celery stuff
129
+ celerybeat-schedule
130
+ celerybeat.pid
131
+
132
+ # SageMath parsed files
133
+ *.sage.py
134
+
135
+ # Environments
136
+ .env
137
+ .venv
138
+ env/
139
+ venv/
140
+ ENV/
141
+ env.bak/
142
+ venv.bak/
143
+
144
+ # Spyder project settings
145
+ .spyderproject
146
+ .spyproject
147
+
148
+ # Rope project settings
149
+ .ropeproject
150
+
151
+ # mkdocs documentation
152
+ /site
153
+
154
+ # mypy
155
+ .mypy_cache/
156
+ .dmypy.json
157
+ dmypy.json
158
+
159
+ # Pyre type checker
160
+ .pyre/
161
+
162
+ # pytype static type analyzer
163
+ .pytype/
164
+
165
+ # Cython debug symbols
166
+ cython_debug/
167
+
168
+ # PyCharm
169
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
170
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
171
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
172
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
173
+ #.idea/
174
+
175
+ # Ruff stuff:
176
+ .ruff_cache/
177
+
178
+ # PyPI configuration file
179
+ .pypirc
180
+ /.idea
181
+ /.gradio/flagged/*.csv
README.md CHANGED
@@ -1,13 +1,5 @@
1
- ---
2
- title: Mars
3
- emoji: 🌍
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 5.23.0
8
- app_file: app.py
9
- pinned: false
10
- short_description: DSPy implemetnation of MARS for Smarter Prompt Engineering
11
- ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ # mars
 
 
 
 
 
 
 
 
 
 
2
 
3
+ Implementation of a blog post:
4
+
5
+ htttps://programmers.ie/post/mars
app.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from mars import analyze_ticker
4
+
5
+
6
+ def run_analysis(ticker):
7
+ result = analyze_ticker(ticker)
8
+ return f"""
9
+ ### Plan
10
+ {result['plan']}
11
+
12
+ ### Teacher Question
13
+ {result['teacher_question']}
14
+
15
+ ### Critique
16
+ {result['critique']}
17
+
18
+ ### Final Question
19
+ {result['final_question']}
20
+
21
+ ### Signal
22
+ {result['signal']}
23
+
24
+ ### Rationale
25
+ {result['rationale']}
26
+ """
27
+
28
+
29
+ with gr.Blocks() as iface:
30
+ gr.Markdown("# MARS Financial Reasoning")
31
+ ticker_input = gr.Textbox(label="Enter stock ticker", placeholder="e.g., TSLA")
32
+ run_button = gr.Button("Analyze", variant="primary")
33
+ output_box = gr.Markdown()
34
+
35
+ run_button.click(fn=run_analysis,
36
+ inputs=ticker_input,
37
+ outputs=output_box,
38
+ show_progress=True) # <-- shows loading and disables button
39
+
40
+ if __name__ == "__main__":
41
+ iface.launch()
mars.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ import socket
4
+ import dspy
5
+ from dspy import Signature, InputField, OutputField, Module, Predict, ChainOfThought, LM
6
+ from edgar import Company, set_identity
7
+ from edgar.xbrl2 import XBRL
8
+
9
+ import litellm
10
+ litellm._turn_on_debug()
11
+ import logging
12
+ logging.basicConfig(level=logging.DEBUG,
13
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
14
+ handlers=[logging.FileHandler('mars.log', 'w', 'utf-8')])
15
+
16
+ logger = logging.getLogger(__name__)
17
+ logging.basicConfig(level=logging.INFO)
18
+
19
+ # ==== DSPy CONFIG ====
20
+ # Check if running on Hugging Face Spaces
21
+ running_in_spaces = os.getenv("SYSTEM") == "spaces" or "hf.space" in socket.getfqdn()
22
+ if running_in_spaces:
23
+ print("🔍 Detected: Running in Hugging Face Spaces")
24
+ dspy.configure(
25
+ lm=LM(
26
+ model='SUFE-AIFLM-Lab/Fin-R1',
27
+ api_base='https://api-inference.huggingface.co/models',
28
+ api_key=os.getenv("HF_API_KEY")
29
+ )
30
+ )
31
+ else:
32
+ print("💻 Detected: Running locally")
33
+ dspy.configure(
34
+ lm=LM(
35
+ model='ollama_chat/hf.co/ernanhughes/Fin-R1-Q8_0-GGUF',
36
+ api_base='http://localhost:11434',
37
+ api_key='' # Ollama does not require key
38
+ )
39
+ )
40
+
41
+
42
+ # ==== DSPy SIGNATURES ====
43
+ class AnalyzeMargins(Signature):
44
+ context = InputField()
45
+ question = InputField()
46
+ signal = OutputField()
47
+ rationale = OutputField()
48
+
49
+ class FinancialTrendAnalysis(Signature):
50
+ statements = InputField()
51
+ question = InputField()
52
+ signal = OutputField()
53
+ rationale = OutputField()
54
+
55
+ class PlannerSignature(Signature):
56
+ base_question = InputField()
57
+ steps = OutputField(desc="List of reasoning substeps to answer the question")
58
+
59
+ # ==== DSPy MODULES ====
60
+ class IncomeStatementAnalyzer(Module):
61
+ def __init__(self):
62
+ super().__init__()
63
+ self.analyze = Predict(FinancialTrendAnalysis)
64
+
65
+ def forward(self, statements, question):
66
+ return self.analyze(statements=statements, question=question)
67
+
68
+ class TeacherQuestion(Signature):
69
+ prompt = InputField()
70
+ question = OutputField()
71
+
72
+ class TeacherQuestioner(Module):
73
+ def __init__(self, use_chain_of_thought: bool = True):
74
+ super().__init__()
75
+ self.generate = ChainOfThought(TeacherQuestion) if use_chain_of_thought else Predict(TeacherQuestion)
76
+
77
+ def forward(self, prompt):
78
+ return self.generate(prompt=prompt)
79
+
80
+ class CritiqueQuestion(Signature):
81
+ question = InputField()
82
+ critique = OutputField()
83
+
84
+ class CriticJudge(Module):
85
+ def __init__(self):
86
+ super().__init__()
87
+ self.evaluate = Predict(CritiqueQuestion)
88
+
89
+ def forward(self, question):
90
+ return self.evaluate(question=question)
91
+
92
+ class MarginAnalyzer(Module):
93
+ def __init__(self):
94
+ super().__init__()
95
+ self.analyze = ChainOfThought(AnalyzeMargins)
96
+
97
+ def forward(self, context, question, teacher_question=None):
98
+ if teacher_question:
99
+ question = f"{question} Consider also: {teacher_question}"
100
+ return self.analyze(context=context, question=question)
101
+
102
+ class PlannerModule(Module):
103
+ def __init__(self):
104
+ super().__init__()
105
+ self.plan = ChainOfThought(PlannerSignature)
106
+
107
+ def forward(self, base_question):
108
+ return self.plan(base_question=base_question)
109
+
110
+ # ==== DSPy PROGRAM ====
111
+ class MarsAnalysisProgram(dspy.Program):
112
+ def __init__(self, planner, teacher, critic, student):
113
+ super().__init__()
114
+ self.planner = planner
115
+ self.teacher = teacher
116
+ self.critic = critic
117
+ self.student = student
118
+
119
+ def forward(self, context: str, base_question: str):
120
+ plan_out = self.planner(base_question=base_question)
121
+ teacher_out = self.teacher(prompt=context + "\n\n" + base_question)
122
+ critic_out = self.critic(question=teacher_out.question)
123
+
124
+ if "yes" in critic_out.critique.lower():
125
+ final_question = f"{base_question} Consider also: {teacher_out.question}"
126
+ else:
127
+ final_question = base_question
128
+
129
+ student_out = self.student(context=context, question=final_question)
130
+
131
+ return {
132
+ "plan": plan_out.steps,
133
+ "teacher_question": teacher_out.question,
134
+ "critique": critic_out.critique,
135
+ "final_question": final_question,
136
+ "signal": student_out.signal,
137
+ "rationale": student_out.rationale
138
+ }
139
+
140
+ # ==== UTILS ====
141
+ def estimate_token_count(markdown_list: list[str], chars_per_token: int = 4) -> int:
142
+ combined_text = "\n\n".join(markdown_list)
143
+ return len(combined_text) // chars_per_token
144
+
145
+ def build_analysis_prompt(ticker: str, markdown_list: list[str]) -> str:
146
+ header = f"You are a financial analysis model. Below are the last {len(markdown_list)} income statements from {ticker}.\n\n"
147
+ instructions = (
148
+ "Analyze the trend in revenue and operating income.\n"
149
+ "Decide if profitability is improving or declining.\n"
150
+ "Then provide a trading signal.\n\n"
151
+ "Respond with:\n"
152
+ "Signal: <Bullish/Bearish/Neutral>\n"
153
+ "Rationale: <short explanation>\n\n"
154
+ )
155
+ body = "\n\n".join(markdown_list)
156
+ return header + instructions + body
157
+
158
+ # ==== EDGAR FETCHER ====
159
+ class EDGARFetcher:
160
+ def __init__(self, ticker: str, form: str = "10-Q", n: int = 3):
161
+ self.identity = "[email protected]"
162
+ self.ticker = ticker
163
+ self.form = form
164
+ self.n = n
165
+ set_identity(self.identity)
166
+
167
+ def fetch_markdown_statements(self):
168
+ filings = Company(self.ticker).latest(form=self.form, n=self.n)
169
+ statements = []
170
+ for filing in filings:
171
+ xbrl = XBRL.from_filing(filing)
172
+ income_statement = xbrl.statements.income_statement()
173
+ df = income_statement.to_dataframe()
174
+ statements.append(self.rich_report_to_text(df))
175
+ return statements
176
+
177
+ @staticmethod
178
+ def rich_report_to_text(df: pd.DataFrame) -> str:
179
+ lines = []
180
+ for _, row in df.iterrows():
181
+ label = row.get("original_label") or row.get("label") or row.get("concept")
182
+ values = [
183
+ f"{col}: {row[col]}" for col in df.columns
184
+ if isinstance(col, str) and col.startswith("20") and pd.notna(row[col])
185
+ ]
186
+ if values:
187
+ lines.append(f"{label}: " + " | ".join(values))
188
+ return "\n".join(lines)
189
+
190
+ def analyze_ticker(ticker: str):
191
+ """
192
+ Run the full MARS analysis pipeline for a given stock ticker.
193
+
194
+ Args:
195
+ ticker (str): Stock symbol (e.g. 'TSLA')
196
+
197
+ Returns:
198
+ dict: MARS pipeline result containing plan, teacher_question, critique,
199
+ final_question, signal, and rationale
200
+ """
201
+ fetcher = EDGARFetcher(ticker=ticker)
202
+ statements = fetcher.fetch_markdown_statements()
203
+ prompt = build_analysis_prompt(ticker, statements)
204
+
205
+ planner = PlannerModule()
206
+ teacher = TeacherQuestioner()
207
+ critic = CriticJudge()
208
+ student = MarginAnalyzer()
209
+
210
+ program = MarsAnalysisProgram(planner, teacher, critic, student)
211
+ result = program(
212
+ context=prompt,
213
+ base_question="Is the company improving its profitability?"
214
+ )
215
+ logger.info(f"Result for stock {ticker}:\n{result}")
216
+
217
+ return result
218
+
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ edgartools
2
+ dspy
3
+ pandas
4
+ rich
5
+ gradio