sigma-terminal 2.0.1__py3-none-any.whl → 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigma/__init__.py +182 -6
- sigma/__main__.py +2 -2
- sigma/analytics/__init__.py +636 -0
- sigma/app.py +563 -898
- sigma/backtest.py +372 -0
- sigma/charts.py +407 -0
- sigma/cli.py +434 -0
- sigma/comparison.py +611 -0
- sigma/config.py +195 -0
- sigma/core/__init__.py +4 -17
- sigma/core/engine.py +493 -0
- sigma/core/intent.py +595 -0
- sigma/core/models.py +516 -125
- sigma/data/__init__.py +681 -0
- sigma/data/models.py +130 -0
- sigma/llm.py +401 -0
- sigma/monitoring.py +666 -0
- sigma/portfolio.py +697 -0
- sigma/reporting.py +658 -0
- sigma/robustness.py +675 -0
- sigma/setup.py +305 -402
- sigma/strategy.py +753 -0
- sigma/tools/backtest.py +23 -5
- sigma/tools.py +617 -0
- sigma/visualization.py +766 -0
- sigma_terminal-3.2.0.dist-info/METADATA +298 -0
- sigma_terminal-3.2.0.dist-info/RECORD +30 -0
- sigma_terminal-3.2.0.dist-info/entry_points.txt +6 -0
- sigma_terminal-3.2.0.dist-info/licenses/LICENSE +25 -0
- sigma/core/agent.py +0 -205
- sigma/core/config.py +0 -119
- sigma/core/llm.py +0 -794
- sigma/tools/__init__.py +0 -5
- sigma/tools/charts.py +0 -400
- sigma/tools/financial.py +0 -1457
- sigma/ui/__init__.py +0 -1
- sigma_terminal-2.0.1.dist-info/METADATA +0 -222
- sigma_terminal-2.0.1.dist-info/RECORD +0 -19
- sigma_terminal-2.0.1.dist-info/entry_points.txt +0 -2
- sigma_terminal-2.0.1.dist-info/licenses/LICENSE +0 -42
- {sigma_terminal-2.0.1.dist-info → sigma_terminal-3.2.0.dist-info}/WHEEL +0 -0
sigma/core/intent.py
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
"""Intent parsing and research plan generation for Sigma."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
from datetime import date
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
AssetClass,
|
|
9
|
+
TimeHorizon,
|
|
10
|
+
RiskProfile,
|
|
11
|
+
DeliverableType,
|
|
12
|
+
ResearchPlan,
|
|
13
|
+
Constraint,
|
|
14
|
+
detect_asset_class,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ============================================================================
|
|
19
|
+
# INTENT PATTERNS
|
|
20
|
+
# ============================================================================
|
|
21
|
+
|
|
22
|
+
# Deliverable type detection
|
|
23
|
+
DELIVERABLE_PATTERNS = {
|
|
24
|
+
DeliverableType.COMPARISON: [
|
|
25
|
+
r"\bcompare\b", r"\bvs\b", r"\bversus\b", r"\bagainst\b",
|
|
26
|
+
r"\bwhich is better\b", r"\bwhich one\b", r"\bdifference between\b",
|
|
27
|
+
],
|
|
28
|
+
DeliverableType.BACKTEST: [
|
|
29
|
+
r"\bbacktest\b", r"\btest strategy\b", r"\bhistorical performance\b",
|
|
30
|
+
r"\bwould have performed\b", r"\bsimulate\b", r"\bstrategy test\b",
|
|
31
|
+
],
|
|
32
|
+
DeliverableType.PORTFOLIO: [
|
|
33
|
+
r"\bportfolio\b", r"\ballocation\b", r"\bdiversif\b", r"\brebalance\b",
|
|
34
|
+
r"\bweight\b", r"\brisk parity\b", r"\boptimize\b",
|
|
35
|
+
],
|
|
36
|
+
DeliverableType.STRATEGY: [
|
|
37
|
+
r"\bstrategy\b", r"\btrading system\b", r"\balgorithm\b", r"\brules?\b",
|
|
38
|
+
r"\bwhen to buy\b", r"\bwhen to sell\b", r"\bsignal\b",
|
|
39
|
+
],
|
|
40
|
+
DeliverableType.CHART: [
|
|
41
|
+
r"\bchart\b", r"\bgraph\b", r"\bplot\b", r"\bvisualize\b",
|
|
42
|
+
r"\bshow me\b", r"\bdraw\b",
|
|
43
|
+
],
|
|
44
|
+
DeliverableType.REPORT: [
|
|
45
|
+
r"\breport\b", r"\banalysis report\b", r"\bresearch memo\b",
|
|
46
|
+
r"\bfull analysis\b", r"\bdeep dive\b",
|
|
47
|
+
],
|
|
48
|
+
DeliverableType.ALERT: [
|
|
49
|
+
r"\balert\b", r"\bnotify\b", r"\bwatch\b", r"\bmonitor\b",
|
|
50
|
+
r"\btell me when\b", r"\bwarn me\b",
|
|
51
|
+
],
|
|
52
|
+
DeliverableType.ANALYSIS: [
|
|
53
|
+
r"\banalyze\b", r"\banalysis\b", r"\blook at\b", r"\bexamine\b",
|
|
54
|
+
r"\bwhat do you think\b", r"\bopinion on\b", r"\bsentiment\b",
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Time horizon detection
|
|
59
|
+
HORIZON_PATTERNS = {
|
|
60
|
+
TimeHorizon.INTRADAY: [r"\bintraday\b", r"\bday trad\b", r"\bscalp\b", r"\bminute\b", r"\bhour\b"],
|
|
61
|
+
TimeHorizon.DAILY: [r"\bdaily\b", r"\bswing\b", r"\bshort.?term\b", r"\bdays?\b"],
|
|
62
|
+
TimeHorizon.WEEKLY: [r"\bweekly\b", r"\bweeks?\b"],
|
|
63
|
+
TimeHorizon.MONTHLY: [r"\bmonthly\b", r"\bmonths?\b", r"\bmedium.?term\b"],
|
|
64
|
+
TimeHorizon.QUARTERLY: [r"\bquarterly\b", r"\bquarters?\b"],
|
|
65
|
+
TimeHorizon.YEARLY: [r"\byearly\b", r"\bannual\b", r"\byears?\b"],
|
|
66
|
+
TimeHorizon.MULTI_YEAR: [r"\blong.?term\b", r"\bmulti.?year\b", r"\bdecade\b", r"\bretirement\b"],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Risk profile detection
|
|
70
|
+
RISK_PATTERNS = {
|
|
71
|
+
RiskProfile.CONSERVATIVE: [
|
|
72
|
+
r"\bconservative\b", r"\bsafe\b", r"\blow risk\b", r"\bstable\b",
|
|
73
|
+
r"\bdefensive\b", r"\bcapital preservation\b",
|
|
74
|
+
],
|
|
75
|
+
RiskProfile.MODERATE: [
|
|
76
|
+
r"\bmoderate\b", r"\bbalanced\b", r"\bmid risk\b",
|
|
77
|
+
],
|
|
78
|
+
RiskProfile.AGGRESSIVE: [
|
|
79
|
+
r"\baggressive\b", r"\bhigh risk\b", r"\bgrowth\b", r"\bhigh return\b",
|
|
80
|
+
],
|
|
81
|
+
RiskProfile.VERY_AGGRESSIVE: [
|
|
82
|
+
r"\bspeculative\b", r"\bvery aggressive\b", r"\bmaximum\b", r"\bleverage\b",
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Constraint detection
|
|
87
|
+
CONSTRAINT_PATTERNS = [
|
|
88
|
+
(r"max(?:imum)?\s+(?:position\s+)?weight\s*(?:of\s*)?(\d+)%?", "max_weight"),
|
|
89
|
+
(r"max(?:imum)?\s+drawdown\s*(?:of\s*)?(\d+)%?", "max_drawdown"),
|
|
90
|
+
(r"(?:no\s+)?leverage", "leverage"),
|
|
91
|
+
(r"max(?:imum)?\s+leverage\s*(?:of\s*)?([\d.]+)x?", "max_leverage"),
|
|
92
|
+
(r"turnover\s*(?:cap|limit)?\s*(?:of\s*)?(\d+)%?", "max_turnover"),
|
|
93
|
+
(r"sector\s+(?:cap|limit)\s*(?:of\s*)?(\d+)%?", "sector_cap"),
|
|
94
|
+
(r"(?:no\s+)?(?:short|shorting)", "no_shorts"),
|
|
95
|
+
(r"tax.?(?:efficient|aware|sensitive)", "tax_aware"),
|
|
96
|
+
(r"(?:ESG|sustainable|socially responsible)", "esg"),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# Account type detection
|
|
100
|
+
ACCOUNT_PATTERNS = {
|
|
101
|
+
"taxable": [r"\btaxable\b", r"\bbrokerage\b", r"\bindividual\b"],
|
|
102
|
+
"ira": [r"\bira\b", r"\broth\b", r"\btraditional ira\b"],
|
|
103
|
+
"401k": [r"\b401k\b", r"\b401\(k\)\b", r"\bretirement\b"],
|
|
104
|
+
"margin": [r"\bmargin\b"],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ============================================================================
|
|
109
|
+
# TICKER EXTRACTION
|
|
110
|
+
# ============================================================================
|
|
111
|
+
|
|
112
|
+
# Common tickers to help with extraction
|
|
113
|
+
COMMON_TICKERS = {
|
|
114
|
+
"AAPL", "MSFT", "GOOGL", "GOOG", "AMZN", "NVDA", "META", "TSLA", "BRK.A", "BRK.B",
|
|
115
|
+
"JPM", "V", "JNJ", "WMT", "MA", "PG", "UNH", "HD", "DIS", "BAC",
|
|
116
|
+
"SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VEA", "VWO", "BND", "AGG",
|
|
117
|
+
"XLK", "XLF", "XLE", "XLV", "XLI", "XLP", "XLY", "XLB", "XLU", "XLRE",
|
|
118
|
+
"GLD", "SLV", "USO", "UNG", "TLT", "IEF", "SHY", "LQD", "HYG", "JNK",
|
|
119
|
+
"BTC", "ETH", "SOL", "DOGE", "ADA", "XRP", "DOT", "AVAX", "LINK", "MATIC",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Ticker pattern
|
|
123
|
+
TICKER_PATTERN = re.compile(r'\b([A-Z]{1,5}(?:\.[A-Z])?)\b')
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def extract_tickers(text: str) -> List[str]:
|
|
127
|
+
"""Extract stock tickers from text."""
|
|
128
|
+
text_upper = text.upper()
|
|
129
|
+
|
|
130
|
+
# Find all potential tickers
|
|
131
|
+
matches = TICKER_PATTERN.findall(text_upper)
|
|
132
|
+
|
|
133
|
+
# Filter out common words
|
|
134
|
+
excluded = {
|
|
135
|
+
"I", "A", "THE", "AND", "OR", "FOR", "TO", "IN", "ON", "AT", "IS", "IT",
|
|
136
|
+
"AS", "BE", "BY", "AN", "IF", "VS", "AM", "PM", "US", "UK", "EU", "OF",
|
|
137
|
+
"MY", "ME", "DO", "SO", "NO", "UP", "HE", "WE", "GO", "CEO", "CFO", "CTO",
|
|
138
|
+
"ETF", "IPO", "EPS", "PE", "PB", "ROE", "ROA", "CAGR", "YOY", "QOQ", "MOM",
|
|
139
|
+
"MAX", "MIN", "AVG", "SMA", "EMA", "RSI", "ATR", "ADX", "MACD", "BB",
|
|
140
|
+
"LEAN", "API", "CSV", "PDF", "PNG", "SVG",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
tickers = []
|
|
144
|
+
for match in matches:
|
|
145
|
+
if match not in excluded:
|
|
146
|
+
# Prefer known tickers
|
|
147
|
+
if match in COMMON_TICKERS:
|
|
148
|
+
tickers.append(match)
|
|
149
|
+
elif len(match) >= 2: # At least 2 chars for unknown tickers
|
|
150
|
+
tickers.append(match)
|
|
151
|
+
|
|
152
|
+
return list(dict.fromkeys(tickers)) # Remove duplicates, preserve order
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ============================================================================
|
|
156
|
+
# INTENT PARSER
|
|
157
|
+
# ============================================================================
|
|
158
|
+
|
|
159
|
+
class IntentParser:
|
|
160
|
+
"""Parse user intent into structured research plan."""
|
|
161
|
+
|
|
162
|
+
def __init__(self):
|
|
163
|
+
self.default_benchmark = "SPY"
|
|
164
|
+
self.default_horizon = TimeHorizon.DAILY
|
|
165
|
+
self.default_risk = RiskProfile.MODERATE
|
|
166
|
+
|
|
167
|
+
def parse(self, query: str) -> ResearchPlan:
|
|
168
|
+
"""Parse user query into a research plan."""
|
|
169
|
+
query_lower = query.lower()
|
|
170
|
+
|
|
171
|
+
# Extract tickers
|
|
172
|
+
tickers = extract_tickers(query)
|
|
173
|
+
|
|
174
|
+
# Detect asset classes
|
|
175
|
+
asset_classes = [detect_asset_class(t) for t in tickers]
|
|
176
|
+
asset_classes = list(set(asset_classes)) # Unique classes
|
|
177
|
+
|
|
178
|
+
# Detect deliverable type
|
|
179
|
+
deliverable = self._detect_deliverable(query_lower)
|
|
180
|
+
|
|
181
|
+
# Detect time horizon
|
|
182
|
+
horizon = self._detect_horizon(query_lower)
|
|
183
|
+
|
|
184
|
+
# Detect risk profile
|
|
185
|
+
risk_profile = self._detect_risk_profile(query_lower)
|
|
186
|
+
|
|
187
|
+
# Detect constraints
|
|
188
|
+
constraints = self._detect_constraints(query_lower)
|
|
189
|
+
|
|
190
|
+
# Detect account type
|
|
191
|
+
account_type = self._detect_account_type(query_lower)
|
|
192
|
+
|
|
193
|
+
# Detect leverage
|
|
194
|
+
leverage_allowed = "leverage" in query_lower and "no leverage" not in query_lower
|
|
195
|
+
max_leverage = self._extract_leverage(query_lower) if leverage_allowed else 1.0
|
|
196
|
+
|
|
197
|
+
# Detect benchmark
|
|
198
|
+
benchmark = self._detect_benchmark(query_lower, tickers)
|
|
199
|
+
|
|
200
|
+
# Detect date range
|
|
201
|
+
start_date, end_date, lookback = self._detect_dates(query_lower)
|
|
202
|
+
|
|
203
|
+
# Generate goal summary
|
|
204
|
+
goal = self._generate_goal(query, deliverable)
|
|
205
|
+
|
|
206
|
+
# Check for clarifications needed
|
|
207
|
+
clarifications = self._check_clarifications(
|
|
208
|
+
query, tickers, deliverable, horizon, constraints
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return ResearchPlan(
|
|
212
|
+
goal=goal,
|
|
213
|
+
assets=tickers,
|
|
214
|
+
asset_classes=asset_classes,
|
|
215
|
+
horizon=horizon,
|
|
216
|
+
benchmark=benchmark,
|
|
217
|
+
risk_profile=risk_profile,
|
|
218
|
+
constraints=constraints,
|
|
219
|
+
deliverable=deliverable,
|
|
220
|
+
account_type=account_type,
|
|
221
|
+
leverage_allowed=leverage_allowed,
|
|
222
|
+
max_leverage=max_leverage,
|
|
223
|
+
tax_aware="tax" in query_lower,
|
|
224
|
+
start_date=start_date,
|
|
225
|
+
end_date=end_date,
|
|
226
|
+
lookback_period=lookback,
|
|
227
|
+
clarifications_needed=clarifications,
|
|
228
|
+
context={"original_query": query},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _detect_deliverable(self, text: str) -> DeliverableType:
|
|
232
|
+
"""Detect the type of deliverable requested."""
|
|
233
|
+
for dtype, patterns in DELIVERABLE_PATTERNS.items():
|
|
234
|
+
for pattern in patterns:
|
|
235
|
+
if re.search(pattern, text):
|
|
236
|
+
return dtype
|
|
237
|
+
return DeliverableType.ANALYSIS
|
|
238
|
+
|
|
239
|
+
def _detect_horizon(self, text: str) -> TimeHorizon:
|
|
240
|
+
"""Detect time horizon from text."""
|
|
241
|
+
for horizon, patterns in HORIZON_PATTERNS.items():
|
|
242
|
+
for pattern in patterns:
|
|
243
|
+
if re.search(pattern, text):
|
|
244
|
+
return horizon
|
|
245
|
+
return self.default_horizon
|
|
246
|
+
|
|
247
|
+
def _detect_risk_profile(self, text: str) -> RiskProfile:
|
|
248
|
+
"""Detect risk profile from text."""
|
|
249
|
+
for profile, patterns in RISK_PATTERNS.items():
|
|
250
|
+
for pattern in patterns:
|
|
251
|
+
if re.search(pattern, text):
|
|
252
|
+
return profile
|
|
253
|
+
return self.default_risk
|
|
254
|
+
|
|
255
|
+
def _detect_constraints(self, text: str) -> List[Constraint]:
|
|
256
|
+
"""Extract constraints from text."""
|
|
257
|
+
constraints = []
|
|
258
|
+
|
|
259
|
+
for pattern, constraint_type in CONSTRAINT_PATTERNS:
|
|
260
|
+
match = re.search(pattern, text)
|
|
261
|
+
if match:
|
|
262
|
+
value = float(match.group(1)) if match.groups() else 1.0
|
|
263
|
+
|
|
264
|
+
# Normalize percentage values
|
|
265
|
+
if constraint_type in ["max_weight", "max_drawdown", "max_turnover", "sector_cap"]:
|
|
266
|
+
if value > 1: # Assume it's a percentage
|
|
267
|
+
value = value / 100
|
|
268
|
+
|
|
269
|
+
constraints.append(Constraint(
|
|
270
|
+
name=constraint_type,
|
|
271
|
+
type=constraint_type,
|
|
272
|
+
value=value,
|
|
273
|
+
))
|
|
274
|
+
|
|
275
|
+
return constraints
|
|
276
|
+
|
|
277
|
+
def _detect_account_type(self, text: str) -> Optional[str]:
|
|
278
|
+
"""Detect account type from text."""
|
|
279
|
+
for account, patterns in ACCOUNT_PATTERNS.items():
|
|
280
|
+
for pattern in patterns:
|
|
281
|
+
if re.search(pattern, text):
|
|
282
|
+
return account
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def _extract_leverage(self, text: str) -> float:
|
|
286
|
+
"""Extract leverage multiplier from text."""
|
|
287
|
+
match = re.search(r"(\d+(?:\.\d+)?)\s*x\s*leverage", text)
|
|
288
|
+
if match:
|
|
289
|
+
return float(match.group(1))
|
|
290
|
+
|
|
291
|
+
match = re.search(r"leverage\s*(?:of\s*)?(\d+(?:\.\d+)?)", text)
|
|
292
|
+
if match:
|
|
293
|
+
return float(match.group(1))
|
|
294
|
+
|
|
295
|
+
return 2.0 # Default leverage if mentioned but not specified
|
|
296
|
+
|
|
297
|
+
def _detect_benchmark(self, text: str, tickers: List[str]) -> str:
|
|
298
|
+
"""Detect benchmark from text or infer from context."""
|
|
299
|
+
# Explicit benchmark mention
|
|
300
|
+
match = re.search(r"(?:benchmark|vs|against|compared to)\s+([A-Z]{2,5})", text.upper())
|
|
301
|
+
if match:
|
|
302
|
+
return match.group(1)
|
|
303
|
+
|
|
304
|
+
# Infer from asset classes
|
|
305
|
+
if any(detect_asset_class(t) == AssetClass.CRYPTO for t in tickers):
|
|
306
|
+
return "BTC"
|
|
307
|
+
elif any(detect_asset_class(t) in [AssetClass.RATES, AssetClass.COMMODITY] for t in tickers):
|
|
308
|
+
return "SPY" # Default
|
|
309
|
+
|
|
310
|
+
return self.default_benchmark
|
|
311
|
+
|
|
312
|
+
def _detect_dates(self, text: str) -> Tuple[Optional[date], Optional[date], str]:
|
|
313
|
+
"""Detect date range from text."""
|
|
314
|
+
today = date.today()
|
|
315
|
+
|
|
316
|
+
# Explicit date patterns
|
|
317
|
+
date_match = re.search(
|
|
318
|
+
r"from\s+(\d{4}-\d{2}-\d{2})\s+to\s+(\d{4}-\d{2}-\d{2})",
|
|
319
|
+
text
|
|
320
|
+
)
|
|
321
|
+
if date_match:
|
|
322
|
+
return (
|
|
323
|
+
date.fromisoformat(date_match.group(1)),
|
|
324
|
+
date.fromisoformat(date_match.group(2)),
|
|
325
|
+
""
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Lookback patterns
|
|
329
|
+
lookback_patterns = [
|
|
330
|
+
(r"(\d+)\s*years?", lambda m: f"{m.group(1)}y"),
|
|
331
|
+
(r"(\d+)\s*months?", lambda m: f"{m.group(1)}mo"),
|
|
332
|
+
(r"(\d+)\s*weeks?", lambda m: f"{m.group(1)}w"),
|
|
333
|
+
(r"(\d+)\s*days?", lambda m: f"{m.group(1)}d"),
|
|
334
|
+
(r"\b(ytd|mtd)\b", lambda m: m.group(1)),
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
for pattern, extractor in lookback_patterns:
|
|
338
|
+
match = re.search(pattern, text)
|
|
339
|
+
if match:
|
|
340
|
+
return None, None, extractor(match)
|
|
341
|
+
|
|
342
|
+
# Default lookback
|
|
343
|
+
return None, None, "2y"
|
|
344
|
+
|
|
345
|
+
def _generate_goal(self, query: str, deliverable: DeliverableType) -> str:
|
|
346
|
+
"""Generate a concise goal statement."""
|
|
347
|
+
# Clean and truncate query
|
|
348
|
+
goal = query.strip()
|
|
349
|
+
if len(goal) > 200:
|
|
350
|
+
goal = goal[:197] + "..."
|
|
351
|
+
return goal
|
|
352
|
+
|
|
353
|
+
def _check_clarifications(
|
|
354
|
+
self,
|
|
355
|
+
query: str,
|
|
356
|
+
tickers: List[str],
|
|
357
|
+
deliverable: DeliverableType,
|
|
358
|
+
horizon: TimeHorizon,
|
|
359
|
+
constraints: List[Constraint],
|
|
360
|
+
) -> List[str]:
|
|
361
|
+
"""Check if clarifications are needed."""
|
|
362
|
+
clarifications = []
|
|
363
|
+
|
|
364
|
+
# No tickers found
|
|
365
|
+
if not tickers and deliverable in [
|
|
366
|
+
DeliverableType.ANALYSIS,
|
|
367
|
+
DeliverableType.COMPARISON,
|
|
368
|
+
DeliverableType.BACKTEST,
|
|
369
|
+
]:
|
|
370
|
+
clarifications.append("Which ticker(s) would you like to analyze?")
|
|
371
|
+
|
|
372
|
+
# Vague comparison
|
|
373
|
+
if deliverable == DeliverableType.COMPARISON and len(tickers) < 2:
|
|
374
|
+
clarifications.append("Please specify at least two assets to compare.")
|
|
375
|
+
|
|
376
|
+
# Strategy without specifics
|
|
377
|
+
if deliverable == DeliverableType.STRATEGY and "strategy" in query.lower():
|
|
378
|
+
if not any(word in query.lower() for word in ["momentum", "trend", "mean reversion", "value", "carry"]):
|
|
379
|
+
clarifications.append("What type of strategy are you interested in? (momentum, mean reversion, value, trend following, etc.)")
|
|
380
|
+
|
|
381
|
+
# Backtest without strategy
|
|
382
|
+
if deliverable == DeliverableType.BACKTEST:
|
|
383
|
+
strategy_words = ["sma", "ema", "rsi", "macd", "momentum", "mean reversion", "crossover"]
|
|
384
|
+
if not any(word in query.lower() for word in strategy_words):
|
|
385
|
+
clarifications.append("Which strategy would you like to backtest?")
|
|
386
|
+
|
|
387
|
+
return clarifications
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ============================================================================
|
|
391
|
+
# PROMPT TEMPLATES
|
|
392
|
+
# ============================================================================
|
|
393
|
+
|
|
394
|
+
class PromptPresets:
|
|
395
|
+
"""Pre-built prompt templates for common tasks."""
|
|
396
|
+
|
|
397
|
+
PRESETS = {
|
|
398
|
+
"etf_due_diligence": {
|
|
399
|
+
"name": "ETF Due Diligence",
|
|
400
|
+
"description": "Comprehensive analysis of an ETF",
|
|
401
|
+
"template": "Perform a comprehensive due diligence on {ticker}: holdings analysis, concentration risk, factor tilts, fees, tracking error, liquidity, and how it compares to alternatives.",
|
|
402
|
+
},
|
|
403
|
+
"pairs_trade": {
|
|
404
|
+
"name": "Pairs Trade Analysis",
|
|
405
|
+
"description": "Statistical arbitrage pair analysis",
|
|
406
|
+
"template": "Analyze {ticker1} vs {ticker2} as a pairs trade: cointegration test, spread analysis, optimal hedge ratio, entry/exit signals, and historical performance.",
|
|
407
|
+
},
|
|
408
|
+
"defensive_portfolio": {
|
|
409
|
+
"name": "Defensive Portfolio",
|
|
410
|
+
"description": "Low-volatility, capital preservation focus",
|
|
411
|
+
"template": "Build a defensive portfolio with {tickers}: minimize drawdown, target volatility < 10%, focus on quality and low-beta stocks, suggest hedges.",
|
|
412
|
+
},
|
|
413
|
+
"momentum_rotation": {
|
|
414
|
+
"name": "Momentum Rotation",
|
|
415
|
+
"description": "Sector/asset momentum strategy",
|
|
416
|
+
"template": "Design a momentum rotation strategy for {tickers}: rank by momentum score, rebalance frequency, position sizing, and backtest with transaction costs.",
|
|
417
|
+
},
|
|
418
|
+
"earnings_preview": {
|
|
419
|
+
"name": "Earnings Preview",
|
|
420
|
+
"description": "Pre-earnings analysis",
|
|
421
|
+
"template": "Earnings preview for {ticker}: expected move from options, historical earnings reactions, consensus estimates, key metrics to watch, and trade setup.",
|
|
422
|
+
},
|
|
423
|
+
"risk_assessment": {
|
|
424
|
+
"name": "Risk Assessment",
|
|
425
|
+
"description": "Comprehensive risk analysis",
|
|
426
|
+
"template": "Full risk assessment of {ticker}: factor exposures, tail risk metrics, stress test scenarios, correlation to macro factors, and hedging suggestions.",
|
|
427
|
+
},
|
|
428
|
+
"valuation_deep_dive": {
|
|
429
|
+
"name": "Valuation Deep Dive",
|
|
430
|
+
"description": "Fundamental valuation analysis",
|
|
431
|
+
"template": "Deep dive valuation of {ticker}: DCF model assumptions, comparable analysis, margin of safety, key value drivers, and sensitivity analysis.",
|
|
432
|
+
},
|
|
433
|
+
"sector_rotation": {
|
|
434
|
+
"name": "Sector Rotation",
|
|
435
|
+
"description": "Sector allocation strategy",
|
|
436
|
+
"template": "Sector rotation analysis: current sector momentum, macro environment assessment, recommended tilts, and sector ETF suggestions.",
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
@classmethod
|
|
441
|
+
def get_preset(cls, name: str, **kwargs) -> Optional[str]:
|
|
442
|
+
"""Get a preset template with filled parameters."""
|
|
443
|
+
if name not in cls.PRESETS:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
preset = cls.PRESETS[name]
|
|
447
|
+
template = preset["template"]
|
|
448
|
+
|
|
449
|
+
# Fill in placeholders
|
|
450
|
+
for key, value in kwargs.items():
|
|
451
|
+
if isinstance(value, list):
|
|
452
|
+
value = ", ".join(value)
|
|
453
|
+
template = template.replace(f"{{{key}}}", str(value))
|
|
454
|
+
|
|
455
|
+
return template
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def list_presets(cls) -> List[Dict[str, str]]:
|
|
459
|
+
"""List all available presets."""
|
|
460
|
+
return [
|
|
461
|
+
{"name": key, **value}
|
|
462
|
+
for key, value in cls.PRESETS.items()
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ============================================================================
|
|
467
|
+
# DECISIVENESS ENGINE
|
|
468
|
+
# ============================================================================
|
|
469
|
+
|
|
470
|
+
class DecisivenessEngine:
|
|
471
|
+
"""Convert vague prompts into measurable criteria and score tradeoffs."""
|
|
472
|
+
|
|
473
|
+
# Mapping of vague terms to measurable criteria
|
|
474
|
+
VAGUE_TO_MEASURABLE = {
|
|
475
|
+
"safer": ["lower volatility", "smaller max drawdown", "higher Sharpe ratio", "lower beta"],
|
|
476
|
+
"better": ["higher Sharpe ratio", "higher total return", "lower max drawdown"],
|
|
477
|
+
"riskier": ["higher volatility", "larger max drawdown", "higher beta"],
|
|
478
|
+
"stable": ["lower volatility", "smaller drawdowns", "consistent returns"],
|
|
479
|
+
"growth": ["higher CAGR", "higher momentum", "higher earnings growth"],
|
|
480
|
+
"value": ["lower P/E", "lower P/B", "higher dividend yield"],
|
|
481
|
+
"defensive": ["lower beta", "lower volatility", "smaller drawdowns"],
|
|
482
|
+
"aggressive": ["higher beta", "higher volatility", "higher returns"],
|
|
483
|
+
"liquid": ["higher average volume", "tighter spreads", "larger market cap"],
|
|
484
|
+
"diversified": ["lower concentration", "more holdings", "lower correlation"],
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@classmethod
|
|
488
|
+
def translate_vague_query(cls, query: str) -> Dict[str, Any]:
|
|
489
|
+
"""Translate vague query into measurable criteria."""
|
|
490
|
+
query_lower = query.lower()
|
|
491
|
+
|
|
492
|
+
criteria = []
|
|
493
|
+
weights = {}
|
|
494
|
+
|
|
495
|
+
for vague_term, measurables in cls.VAGUE_TO_MEASURABLE.items():
|
|
496
|
+
if vague_term in query_lower:
|
|
497
|
+
criteria.extend(measurables)
|
|
498
|
+
for m in measurables:
|
|
499
|
+
weights[m] = weights.get(m, 0) + 1
|
|
500
|
+
|
|
501
|
+
# Normalize weights
|
|
502
|
+
total = sum(weights.values()) or 1
|
|
503
|
+
weights = {k: v / total for k, v in weights.items()}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
"criteria": list(set(criteria)),
|
|
507
|
+
"weights": weights,
|
|
508
|
+
"interpretation": cls._generate_interpretation(criteria),
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@classmethod
|
|
512
|
+
def _generate_interpretation(cls, criteria: List[str]) -> str:
|
|
513
|
+
"""Generate human-readable interpretation."""
|
|
514
|
+
if not criteria:
|
|
515
|
+
return "No specific criteria detected. Using balanced evaluation."
|
|
516
|
+
|
|
517
|
+
return f"Evaluating based on: {', '.join(criteria[:5])}"
|
|
518
|
+
|
|
519
|
+
@classmethod
|
|
520
|
+
def score_assets(
|
|
521
|
+
cls,
|
|
522
|
+
assets: List[str],
|
|
523
|
+
metrics: Dict[str, Dict[str, float]],
|
|
524
|
+
weights: Dict[str, float],
|
|
525
|
+
) -> List[Dict[str, Any]]:
|
|
526
|
+
"""Score and rank assets based on criteria."""
|
|
527
|
+
scores = []
|
|
528
|
+
|
|
529
|
+
metric_mapping = {
|
|
530
|
+
"lower volatility": ("volatility", -1),
|
|
531
|
+
"smaller max drawdown": ("max_drawdown", -1),
|
|
532
|
+
"higher Sharpe ratio": ("sharpe_ratio", 1),
|
|
533
|
+
"lower beta": ("beta", -1),
|
|
534
|
+
"higher total return": ("total_return", 1),
|
|
535
|
+
"higher CAGR": ("cagr", 1),
|
|
536
|
+
"higher momentum": ("momentum", 1),
|
|
537
|
+
"lower P/E": ("pe_ratio", -1),
|
|
538
|
+
"higher dividend yield": ("dividend_yield", 1),
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
for asset in assets:
|
|
542
|
+
asset_metrics = metrics.get(asset, {})
|
|
543
|
+
score = 0
|
|
544
|
+
details = {}
|
|
545
|
+
|
|
546
|
+
for criterion, weight in weights.items():
|
|
547
|
+
if criterion in metric_mapping:
|
|
548
|
+
metric_name, direction = metric_mapping[criterion]
|
|
549
|
+
value = asset_metrics.get(metric_name, 0)
|
|
550
|
+
contribution = value * direction * weight
|
|
551
|
+
score += contribution
|
|
552
|
+
details[criterion] = {
|
|
553
|
+
"value": value,
|
|
554
|
+
"contribution": contribution,
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
scores.append({
|
|
558
|
+
"asset": asset,
|
|
559
|
+
"total_score": score,
|
|
560
|
+
"details": details,
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
# Sort by score
|
|
564
|
+
scores.sort(key=lambda x: x["total_score"], reverse=True)
|
|
565
|
+
|
|
566
|
+
return scores
|
|
567
|
+
|
|
568
|
+
@classmethod
|
|
569
|
+
def explain_tradeoffs(
|
|
570
|
+
cls,
|
|
571
|
+
scores: List[Dict[str, Any]],
|
|
572
|
+
criteria: List[str],
|
|
573
|
+
) -> str:
|
|
574
|
+
"""Generate tradeoff explanation."""
|
|
575
|
+
if len(scores) < 2:
|
|
576
|
+
return "Not enough assets to compare tradeoffs."
|
|
577
|
+
|
|
578
|
+
best = scores[0]
|
|
579
|
+
second = scores[1]
|
|
580
|
+
|
|
581
|
+
explanations = []
|
|
582
|
+
explanations.append(f"{best['asset']} ranks highest overall.")
|
|
583
|
+
|
|
584
|
+
# Find where second asset is better
|
|
585
|
+
for criterion in criteria:
|
|
586
|
+
if criterion in best["details"] and criterion in second["details"]:
|
|
587
|
+
best_val = best["details"][criterion]["value"]
|
|
588
|
+
second_val = second["details"][criterion]["value"]
|
|
589
|
+
|
|
590
|
+
if abs(second_val) > abs(best_val):
|
|
591
|
+
explanations.append(
|
|
592
|
+
f"However, {second['asset']} has better {criterion} ({second_val:.2f} vs {best_val:.2f})."
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
return " ".join(explanations)
|