ellements 0.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.
- ellements/__init__.py +57 -0
- ellements/agents/__init__.py +45 -0
- ellements/agents/backend.py +100 -0
- ellements/agents/builder.py +303 -0
- ellements/agents/claude_backend.py +200 -0
- ellements/agents/controller.py +237 -0
- ellements/agents/openai_backend.py +187 -0
- ellements/agents/py.typed +0 -0
- ellements/agents/runner.py +358 -0
- ellements/agents/tools.py +30 -0
- ellements/benchmarking/__init__.py +52 -0
- ellements/benchmarking/harness.py +342 -0
- ellements/benchmarking/py.typed +0 -0
- ellements/benchmarking/results.py +173 -0
- ellements/cli/__init__.py +80 -0
- ellements/cli/adapters.py +73 -0
- ellements/cli/agent_tui.py +1178 -0
- ellements/cli/components.py +411 -0
- ellements/cli/printer.py +229 -0
- ellements/cli/py.typed +0 -0
- ellements/core/__init__.py +112 -0
- ellements/core/async_utils.py +42 -0
- ellements/core/budgeting/__init__.py +43 -0
- ellements/core/budgeting/client.py +276 -0
- ellements/core/budgeting/protocol.py +51 -0
- ellements/core/budgeting/trackers.py +177 -0
- ellements/core/caching/__init__.py +51 -0
- ellements/core/caching/cache.py +73 -0
- ellements/core/caching/client.py +300 -0
- ellements/core/caching/disk.py +128 -0
- ellements/core/caching/keys.py +97 -0
- ellements/core/caching/memory.py +104 -0
- ellements/core/chunking.py +262 -0
- ellements/core/config.py +180 -0
- ellements/core/exceptions.py +145 -0
- ellements/core/llm/__init__.py +46 -0
- ellements/core/llm/client.py +1124 -0
- ellements/core/llm/images.py +226 -0
- ellements/core/llm/messages.py +202 -0
- ellements/core/llm/model_params.py +66 -0
- ellements/core/llm/protocol.py +146 -0
- ellements/core/llm/requests.py +100 -0
- ellements/core/llm/structured.py +91 -0
- ellements/core/llm/wrapper.py +79 -0
- ellements/core/observability/__init__.py +39 -0
- ellements/core/observability/events.py +169 -0
- ellements/core/observability/jsonl_logger.py +244 -0
- ellements/core/observability/markdown_formatter.py +197 -0
- ellements/core/observability/observer.py +56 -0
- ellements/core/prompting/__init__.py +14 -0
- ellements/core/prompting/context.py +185 -0
- ellements/core/prompting/guideline.py +133 -0
- ellements/core/prompting/persona.py +267 -0
- ellements/core/prompting/sources.py +92 -0
- ellements/core/py.typed +0 -0
- ellements/core/rate_limit/__init__.py +44 -0
- ellements/core/rate_limit/bucket.py +85 -0
- ellements/core/rate_limit/client.py +216 -0
- ellements/core/rate_limit/protocol.py +27 -0
- ellements/core/templating.py +126 -0
- ellements/core/tools/__init__.py +51 -0
- ellements/core/tools/dialects.py +136 -0
- ellements/core/tools/executor.py +48 -0
- ellements/core/tools/protocol.py +36 -0
- ellements/core/tools/records.py +28 -0
- ellements/core/tools/registry.py +205 -0
- ellements/core/tools/schemas.py +80 -0
- ellements/core/tools/simple.py +119 -0
- ellements/core/tools/spec.py +33 -0
- ellements/domain_specific/__init__.py +7 -0
- ellements/domain_specific/finance/__init__.py +188 -0
- ellements/domain_specific/finance/calculations.py +837 -0
- ellements/domain_specific/finance/charts.py +426 -0
- ellements/domain_specific/finance/portfolio.py +129 -0
- ellements/domain_specific/finance/quant_analysis.py +279 -0
- ellements/domain_specific/finance/risk.py +362 -0
- ellements/domain_specific/finance/technical_indicators.py +241 -0
- ellements/domain_specific/finance/tools.py +483 -0
- ellements/domain_specific/finance/valuation.py +312 -0
- ellements/domain_specific/finance/yahoo_finance.py +1523 -0
- ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
- ellements/domain_specific/py.typed +0 -0
- ellements/execution/__init__.py +56 -0
- ellements/execution/callbacks.py +149 -0
- ellements/execution/catalog.py +70 -0
- ellements/execution/collaborative.py +191 -0
- ellements/execution/config.py +135 -0
- ellements/execution/py.typed +0 -0
- ellements/execution/reflection.py +156 -0
- ellements/execution/self_consistency.py +189 -0
- ellements/execution/single_call.py +48 -0
- ellements/execution/strategies.py +237 -0
- ellements/execution/tree_of_thought.py +541 -0
- ellements/fslm/__init__.py +108 -0
- ellements/fslm/builtins.py +103 -0
- ellements/fslm/cli.py +199 -0
- ellements/fslm/context.py +50 -0
- ellements/fslm/definition.py +163 -0
- ellements/fslm/det.py +173 -0
- ellements/fslm/dsl.py +458 -0
- ellements/fslm/errors.py +38 -0
- ellements/fslm/evaluators.py +141 -0
- ellements/fslm/kernel.py +603 -0
- ellements/fslm/loading.py +123 -0
- ellements/fslm/models.py +623 -0
- ellements/fslm/nl.py +87 -0
- ellements/fslm/observers.py +107 -0
- ellements/fslm/persistence.py +72 -0
- ellements/fslm/py.typed +0 -0
- ellements/fslm/rendering.py +551 -0
- ellements/fslm/visualization.py +25 -0
- ellements/reporting/__init__.py +12 -0
- ellements/reporting/charts.py +364 -0
- ellements/reporting/html_generation.py +132 -0
- ellements/reporting/py.typed +0 -0
- ellements/reporting/visualization.py +73 -0
- ellements/standard_tools/__init__.py +22 -0
- ellements/standard_tools/py.typed +0 -0
- ellements/standard_tools/terminal.py +205 -0
- ellements/standard_tools/web/__init__.py +37 -0
- ellements/standard_tools/web/browser_viewer.py +214 -0
- ellements/standard_tools/web/crawler.py +454 -0
- ellements/standard_tools/web/search.py +399 -0
- ellements/standard_tools/web/youtube.py +1297 -0
- ellements-0.2.0.dist-info/METADATA +368 -0
- ellements-0.2.0.dist-info/RECORD +130 -0
- ellements-0.2.0.dist-info/WHEEL +5 -0
- ellements-0.2.0.dist-info/entry_points.txt +2 -0
- ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
- ellements-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""Finance chart helpers aligned with the existing matplotlib chart pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from ellements.reporting.charts import create_chart_artifacts
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from .technical_indicators import (
|
|
12
|
+
TechnicalIndicatorSnapshot,
|
|
13
|
+
compute_technical_indicators,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .yahoo_finance_models import HistoricalData
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TechnicalChartResult(BaseModel):
|
|
21
|
+
"""Structured technical chart output."""
|
|
22
|
+
|
|
23
|
+
symbol: str
|
|
24
|
+
title: str
|
|
25
|
+
chart_assets: dict[str, Any]
|
|
26
|
+
indicators: list[dict[str, Any]]
|
|
27
|
+
snapshot: TechnicalIndicatorSnapshot
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def mplfinance_available() -> bool:
|
|
31
|
+
"""Return True if mplfinance is importable."""
|
|
32
|
+
try:
|
|
33
|
+
import mplfinance # noqa: F401
|
|
34
|
+
except ImportError:
|
|
35
|
+
return False
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _historical_to_dataframe(history: HistoricalData) -> pd.DataFrame:
|
|
40
|
+
rows: list[dict[str, Any]] = []
|
|
41
|
+
for point in history.prices:
|
|
42
|
+
if point.date is None:
|
|
43
|
+
continue
|
|
44
|
+
rows.append(
|
|
45
|
+
{
|
|
46
|
+
"date": pd.Timestamp(point.date),
|
|
47
|
+
"open": point.open,
|
|
48
|
+
"high": point.high,
|
|
49
|
+
"low": point.low,
|
|
50
|
+
"close": point.close if point.close is not None else point.adj_close,
|
|
51
|
+
"adj_close": point.adj_close,
|
|
52
|
+
"volume": point.volume,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
frame = pd.DataFrame(rows)
|
|
56
|
+
if frame.empty:
|
|
57
|
+
raise ValueError("Historical data contains no rows.")
|
|
58
|
+
frame["date"] = pd.to_datetime(frame["date"], errors="coerce")
|
|
59
|
+
frame = frame.dropna(subset=["date"]).sort_values("date").set_index("date")
|
|
60
|
+
for column in ["open", "high", "low", "close", "adj_close", "volume"]:
|
|
61
|
+
frame[column] = pd.to_numeric(frame[column], errors="coerce")
|
|
62
|
+
if frame["close"].isna().all():
|
|
63
|
+
raise ValueError("Historical data has no usable close prices.")
|
|
64
|
+
return frame
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_chart_dataframe(data: pd.DataFrame) -> pd.DataFrame:
|
|
68
|
+
if data.empty:
|
|
69
|
+
raise ValueError("Input DataFrame is empty.")
|
|
70
|
+
|
|
71
|
+
frame = data.copy()
|
|
72
|
+
if "date" in frame.columns:
|
|
73
|
+
dates = pd.to_datetime(frame["date"], errors="coerce")
|
|
74
|
+
frame = frame.drop(columns=["date"])
|
|
75
|
+
frame.index = dates
|
|
76
|
+
else:
|
|
77
|
+
frame.index = pd.to_datetime(frame.index, errors="coerce")
|
|
78
|
+
|
|
79
|
+
frame = frame[~pd.isna(frame.index)].sort_index()
|
|
80
|
+
if frame.empty:
|
|
81
|
+
raise ValueError("Input DataFrame contains no valid datetime rows.")
|
|
82
|
+
|
|
83
|
+
normalized_columns: dict[object, str] = {}
|
|
84
|
+
for original in frame.columns:
|
|
85
|
+
raw = str(original).strip().lower().replace(" ", "_")
|
|
86
|
+
if raw in {"open", "high", "low", "close", "volume"}:
|
|
87
|
+
normalized_columns[original] = raw
|
|
88
|
+
elif raw in {"adj_close", "adjusted_close", "adjclose"}:
|
|
89
|
+
normalized_columns[original] = "adj_close"
|
|
90
|
+
|
|
91
|
+
if not normalized_columns:
|
|
92
|
+
raise ValueError("Input DataFrame must include close prices (close/adj_close).")
|
|
93
|
+
|
|
94
|
+
working = frame.rename(columns=normalized_columns)
|
|
95
|
+
selected_columns = [
|
|
96
|
+
col
|
|
97
|
+
for col in ["open", "high", "low", "close", "adj_close", "volume"]
|
|
98
|
+
if col in working.columns
|
|
99
|
+
]
|
|
100
|
+
working = working[selected_columns].copy()
|
|
101
|
+
|
|
102
|
+
if "close" not in working.columns:
|
|
103
|
+
if "adj_close" not in working.columns:
|
|
104
|
+
raise ValueError("Input DataFrame must include close prices (close/adj_close).")
|
|
105
|
+
working["close"] = working["adj_close"]
|
|
106
|
+
elif "adj_close" in working.columns:
|
|
107
|
+
working["close"] = working["close"].fillna(working["adj_close"])
|
|
108
|
+
|
|
109
|
+
for column in working.columns:
|
|
110
|
+
working[column] = pd.to_numeric(working[column], errors="coerce")
|
|
111
|
+
|
|
112
|
+
if working["close"].isna().all():
|
|
113
|
+
raise ValueError("Input DataFrame has no usable close prices.")
|
|
114
|
+
return working
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _float_or_none(value: Any) -> float | None:
|
|
118
|
+
try:
|
|
119
|
+
parsed = float(value)
|
|
120
|
+
except (TypeError, ValueError):
|
|
121
|
+
return None
|
|
122
|
+
if parsed != parsed: # NaN check
|
|
123
|
+
return None
|
|
124
|
+
return parsed
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_indicator_series_payloads(
|
|
128
|
+
indicators: pd.DataFrame,
|
|
129
|
+
) -> list[dict[str, object]]:
|
|
130
|
+
aligned = indicators.dropna(how="all")
|
|
131
|
+
if aligned.empty:
|
|
132
|
+
return []
|
|
133
|
+
x_axis = [stamp.isoformat() for stamp in pd.DatetimeIndex(aligned.index).to_pydatetime()]
|
|
134
|
+
payloads: list[dict[str, object]] = []
|
|
135
|
+
colors = {
|
|
136
|
+
"rsi": "#8b5cf6",
|
|
137
|
+
"macd": "#3b82f6",
|
|
138
|
+
"macd_signal": "#f59e0b",
|
|
139
|
+
"macd_histogram": "#94a3b8",
|
|
140
|
+
"stochastic_k": "#0ea5e9",
|
|
141
|
+
"stochastic_d": "#f97316",
|
|
142
|
+
"atr": "#ef4444",
|
|
143
|
+
"adx": "#14b8a6",
|
|
144
|
+
}
|
|
145
|
+
for column in aligned.columns:
|
|
146
|
+
if column in {
|
|
147
|
+
"sma",
|
|
148
|
+
"ema",
|
|
149
|
+
"bollinger_upper",
|
|
150
|
+
"bollinger_middle",
|
|
151
|
+
"bollinger_lower",
|
|
152
|
+
}:
|
|
153
|
+
continue
|
|
154
|
+
values = aligned[column].tolist()
|
|
155
|
+
if pd.Series(values).dropna().empty:
|
|
156
|
+
continue
|
|
157
|
+
payloads.append(
|
|
158
|
+
{
|
|
159
|
+
"name": column,
|
|
160
|
+
"x": x_axis,
|
|
161
|
+
"y": values,
|
|
162
|
+
"color": colors.get(column),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
return payloads
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _serialize_indicator_rows(indicators_df: pd.DataFrame) -> list[dict[str, Any]]:
|
|
169
|
+
records: list[dict[str, Any]] = []
|
|
170
|
+
for idx, row in indicators_df.iterrows():
|
|
171
|
+
idx_ts = idx if isinstance(idx, pd.Timestamp) else pd.Timestamp(idx)
|
|
172
|
+
payload: dict[str, Any] = {"date": idx_ts.isoformat()}
|
|
173
|
+
for col in indicators_df.columns:
|
|
174
|
+
payload[col] = _float_or_none(row[col])
|
|
175
|
+
records.append(payload)
|
|
176
|
+
return records
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _render_technical_chart_from_frame(
|
|
180
|
+
frame: pd.DataFrame,
|
|
181
|
+
*,
|
|
182
|
+
symbol: str,
|
|
183
|
+
title: str = "",
|
|
184
|
+
image_format: str = "png",
|
|
185
|
+
dpi: int = 120,
|
|
186
|
+
include_ohlc_fallback: bool = False,
|
|
187
|
+
rsi_period: int = 14,
|
|
188
|
+
macd_fast: int = 12,
|
|
189
|
+
macd_slow: int = 26,
|
|
190
|
+
macd_signal: int = 9,
|
|
191
|
+
sma_period: int = 20,
|
|
192
|
+
ema_period: int = 20,
|
|
193
|
+
bollinger_window: int = 20,
|
|
194
|
+
bollinger_dev: float = 2.0,
|
|
195
|
+
stochastic_window: int = 14,
|
|
196
|
+
stochastic_smooth_window: int = 3,
|
|
197
|
+
atr_window: int = 14,
|
|
198
|
+
adx_window: int = 14,
|
|
199
|
+
) -> TechnicalChartResult:
|
|
200
|
+
indicators_df, snapshot = compute_technical_indicators(
|
|
201
|
+
frame,
|
|
202
|
+
rsi_period=rsi_period,
|
|
203
|
+
macd_fast=macd_fast,
|
|
204
|
+
macd_slow=macd_slow,
|
|
205
|
+
macd_signal=macd_signal,
|
|
206
|
+
sma_period=sma_period,
|
|
207
|
+
ema_period=ema_period,
|
|
208
|
+
bollinger_window=bollinger_window,
|
|
209
|
+
bollinger_dev=bollinger_dev,
|
|
210
|
+
stochastic_window=stochastic_window,
|
|
211
|
+
stochastic_smooth_window=stochastic_smooth_window,
|
|
212
|
+
atr_window=atr_window,
|
|
213
|
+
adx_window=adx_window,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
resolved_title = title.strip() or f"{symbol} Technical Indicators"
|
|
217
|
+
x_axis = [stamp.isoformat() for stamp in pd.DatetimeIndex(frame.index).to_pydatetime()]
|
|
218
|
+
base_series: list[dict[str, object]] = [
|
|
219
|
+
{
|
|
220
|
+
"name": "close",
|
|
221
|
+
"x": x_axis,
|
|
222
|
+
"y": frame["close"].tolist(),
|
|
223
|
+
"color": "#2563eb",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": f"sma_{sma_period}",
|
|
227
|
+
"x": x_axis,
|
|
228
|
+
"y": indicators_df["sma"].tolist(),
|
|
229
|
+
"color": "#f97316",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"name": f"ema_{ema_period}",
|
|
233
|
+
"x": x_axis,
|
|
234
|
+
"y": indicators_df["ema"].tolist(),
|
|
235
|
+
"color": "#22c55e",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "bb_upper",
|
|
239
|
+
"x": x_axis,
|
|
240
|
+
"y": indicators_df["bollinger_upper"].tolist(),
|
|
241
|
+
"color": "#94a3b8",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "bb_middle",
|
|
245
|
+
"x": x_axis,
|
|
246
|
+
"y": indicators_df["bollinger_middle"].tolist(),
|
|
247
|
+
"color": "#64748b",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "bb_lower",
|
|
251
|
+
"x": x_axis,
|
|
252
|
+
"y": indicators_df["bollinger_lower"].tolist(),
|
|
253
|
+
"color": "#94a3b8",
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
series = base_series + _build_indicator_series_payloads(indicators_df)
|
|
257
|
+
|
|
258
|
+
chart_spec: dict[str, Any] = {
|
|
259
|
+
"type": "line",
|
|
260
|
+
"title": resolved_title,
|
|
261
|
+
"x_label": "Time",
|
|
262
|
+
"y_label": "Value",
|
|
263
|
+
"series": series,
|
|
264
|
+
"legend": True,
|
|
265
|
+
}
|
|
266
|
+
if include_ohlc_fallback:
|
|
267
|
+
chart_spec["caption"] = (
|
|
268
|
+
"Candlestick rendering can be layered later via mplfinance. "
|
|
269
|
+
"Current output keeps compatibility with the existing chart artifact pipeline."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
assets = create_chart_artifacts(
|
|
273
|
+
chart_spec,
|
|
274
|
+
title=resolved_title,
|
|
275
|
+
image_format=image_format,
|
|
276
|
+
dpi=dpi,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return TechnicalChartResult(
|
|
280
|
+
symbol=symbol,
|
|
281
|
+
title=resolved_title,
|
|
282
|
+
chart_assets=dict(assets),
|
|
283
|
+
indicators=_serialize_indicator_rows(indicators_df),
|
|
284
|
+
snapshot=snapshot,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def render_technical_chart(
|
|
289
|
+
history: HistoricalData,
|
|
290
|
+
*,
|
|
291
|
+
title: str = "",
|
|
292
|
+
image_format: str = "png",
|
|
293
|
+
dpi: int = 120,
|
|
294
|
+
include_ohlc_fallback: bool = False,
|
|
295
|
+
rsi_period: int = 14,
|
|
296
|
+
macd_fast: int = 12,
|
|
297
|
+
macd_slow: int = 26,
|
|
298
|
+
macd_signal: int = 9,
|
|
299
|
+
sma_period: int = 20,
|
|
300
|
+
ema_period: int = 20,
|
|
301
|
+
bollinger_window: int = 20,
|
|
302
|
+
bollinger_dev: float = 2.0,
|
|
303
|
+
stochastic_window: int = 14,
|
|
304
|
+
stochastic_smooth_window: int = 3,
|
|
305
|
+
atr_window: int = 14,
|
|
306
|
+
adx_window: int = 14,
|
|
307
|
+
) -> TechnicalChartResult:
|
|
308
|
+
"""Render a technical chart artifact and indicator data.
|
|
309
|
+
|
|
310
|
+
Harmonization note:
|
|
311
|
+
Uses the existing reports chart artifact pipeline (matplotlib -> data_uri),
|
|
312
|
+
so output is directly consumable by existing GUI/chat chart widgets.
|
|
313
|
+
"""
|
|
314
|
+
frame = _historical_to_dataframe(history)
|
|
315
|
+
return _render_technical_chart_from_frame(
|
|
316
|
+
frame,
|
|
317
|
+
symbol=history.symbol,
|
|
318
|
+
title=title,
|
|
319
|
+
image_format=image_format,
|
|
320
|
+
dpi=dpi,
|
|
321
|
+
include_ohlc_fallback=include_ohlc_fallback,
|
|
322
|
+
rsi_period=rsi_period,
|
|
323
|
+
macd_fast=macd_fast,
|
|
324
|
+
macd_slow=macd_slow,
|
|
325
|
+
macd_signal=macd_signal,
|
|
326
|
+
sma_period=sma_period,
|
|
327
|
+
ema_period=ema_period,
|
|
328
|
+
bollinger_window=bollinger_window,
|
|
329
|
+
bollinger_dev=bollinger_dev,
|
|
330
|
+
stochastic_window=stochastic_window,
|
|
331
|
+
stochastic_smooth_window=stochastic_smooth_window,
|
|
332
|
+
atr_window=atr_window,
|
|
333
|
+
adx_window=adx_window,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def render_technical_chart_from_dataframe(
|
|
338
|
+
data: pd.DataFrame,
|
|
339
|
+
*,
|
|
340
|
+
symbol: str = "DATAFRAME",
|
|
341
|
+
title: str = "",
|
|
342
|
+
image_format: str = "png",
|
|
343
|
+
dpi: int = 120,
|
|
344
|
+
include_ohlc_fallback: bool = False,
|
|
345
|
+
rsi_period: int = 14,
|
|
346
|
+
macd_fast: int = 12,
|
|
347
|
+
macd_slow: int = 26,
|
|
348
|
+
macd_signal: int = 9,
|
|
349
|
+
sma_period: int = 20,
|
|
350
|
+
ema_period: int = 20,
|
|
351
|
+
bollinger_window: int = 20,
|
|
352
|
+
bollinger_dev: float = 2.0,
|
|
353
|
+
stochastic_window: int = 14,
|
|
354
|
+
stochastic_smooth_window: int = 3,
|
|
355
|
+
atr_window: int = 14,
|
|
356
|
+
adx_window: int = 14,
|
|
357
|
+
) -> TechnicalChartResult:
|
|
358
|
+
"""Render a technical chart artifact directly from a pandas DataFrame."""
|
|
359
|
+
frame = _normalize_chart_dataframe(data)
|
|
360
|
+
return _render_technical_chart_from_frame(
|
|
361
|
+
frame,
|
|
362
|
+
symbol=symbol,
|
|
363
|
+
title=title,
|
|
364
|
+
image_format=image_format,
|
|
365
|
+
dpi=dpi,
|
|
366
|
+
include_ohlc_fallback=include_ohlc_fallback,
|
|
367
|
+
rsi_period=rsi_period,
|
|
368
|
+
macd_fast=macd_fast,
|
|
369
|
+
macd_slow=macd_slow,
|
|
370
|
+
macd_signal=macd_signal,
|
|
371
|
+
sma_period=sma_period,
|
|
372
|
+
ema_period=ema_period,
|
|
373
|
+
bollinger_window=bollinger_window,
|
|
374
|
+
bollinger_dev=bollinger_dev,
|
|
375
|
+
stochastic_window=stochastic_window,
|
|
376
|
+
stochastic_smooth_window=stochastic_smooth_window,
|
|
377
|
+
atr_window=atr_window,
|
|
378
|
+
adx_window=adx_window,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def technical_chart_assets(
|
|
383
|
+
history: HistoricalData,
|
|
384
|
+
*,
|
|
385
|
+
title: str = "",
|
|
386
|
+
image_format: str = "png",
|
|
387
|
+
dpi: int = 120,
|
|
388
|
+
include_ohlc_fallback: bool = False,
|
|
389
|
+
rsi_period: int = 14,
|
|
390
|
+
macd_fast: int = 12,
|
|
391
|
+
macd_slow: int = 26,
|
|
392
|
+
macd_signal: int = 9,
|
|
393
|
+
sma_period: int = 20,
|
|
394
|
+
ema_period: int = 20,
|
|
395
|
+
bollinger_window: int = 20,
|
|
396
|
+
bollinger_dev: float = 2.0,
|
|
397
|
+
stochastic_window: int = 14,
|
|
398
|
+
stochastic_smooth_window: int = 3,
|
|
399
|
+
atr_window: int = 14,
|
|
400
|
+
adx_window: int = 14,
|
|
401
|
+
) -> dict[str, Any]:
|
|
402
|
+
"""Convenience wrapper returning chart assets + indicator payloads."""
|
|
403
|
+
result = render_technical_chart(
|
|
404
|
+
history,
|
|
405
|
+
title=title,
|
|
406
|
+
image_format=image_format,
|
|
407
|
+
dpi=dpi,
|
|
408
|
+
include_ohlc_fallback=include_ohlc_fallback,
|
|
409
|
+
rsi_period=rsi_period,
|
|
410
|
+
macd_fast=macd_fast,
|
|
411
|
+
macd_slow=macd_slow,
|
|
412
|
+
macd_signal=macd_signal,
|
|
413
|
+
sma_period=sma_period,
|
|
414
|
+
ema_period=ema_period,
|
|
415
|
+
bollinger_window=bollinger_window,
|
|
416
|
+
bollinger_dev=bollinger_dev,
|
|
417
|
+
stochastic_window=stochastic_window,
|
|
418
|
+
stochastic_smooth_window=stochastic_smooth_window,
|
|
419
|
+
atr_window=atr_window,
|
|
420
|
+
adx_window=adx_window,
|
|
421
|
+
)
|
|
422
|
+
payload = dict(result.chart_assets)
|
|
423
|
+
payload["symbol"] = result.symbol
|
|
424
|
+
payload["snapshot"] = result.snapshot.model_dump()
|
|
425
|
+
payload["indicators"] = result.indicators
|
|
426
|
+
return payload
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Reusable portfolio schema models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PortfolioOwner(BaseModel):
|
|
11
|
+
"""Portfolio owner metadata."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(extra="allow")
|
|
14
|
+
|
|
15
|
+
name: str | None = None
|
|
16
|
+
country: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PortfolioMetadata(BaseModel):
|
|
20
|
+
"""Top-level portfolio metadata."""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(extra="allow")
|
|
23
|
+
|
|
24
|
+
id: str | None = None
|
|
25
|
+
name: str | None = None
|
|
26
|
+
as_of: str | None = None
|
|
27
|
+
base_currency: str | None = None
|
|
28
|
+
owner: PortfolioOwner = Field(default_factory=PortfolioOwner)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PortfolioAccount(BaseModel):
|
|
32
|
+
"""Portfolio account metadata."""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(extra="allow")
|
|
35
|
+
|
|
36
|
+
id: str = ""
|
|
37
|
+
name: str = ""
|
|
38
|
+
account_type: str = ""
|
|
39
|
+
currency: str | None = None
|
|
40
|
+
institution: str | None = None
|
|
41
|
+
account_number: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PortfolioActivityEntry(BaseModel):
|
|
45
|
+
"""Position activity event."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(extra="allow")
|
|
48
|
+
|
|
49
|
+
date: str | None = None
|
|
50
|
+
type: str = ""
|
|
51
|
+
account_id: str | None = None
|
|
52
|
+
quantity: float | None = None
|
|
53
|
+
price: float | None = None
|
|
54
|
+
amount: float | None = None
|
|
55
|
+
balance: float | None = None
|
|
56
|
+
cost_basis_total: float | None = None
|
|
57
|
+
market_value: float | None = None
|
|
58
|
+
fees: float | None = None
|
|
59
|
+
how: str | None = None
|
|
60
|
+
notes: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PortfolioPosition(BaseModel):
|
|
64
|
+
"""Portfolio holding row."""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra="allow")
|
|
67
|
+
|
|
68
|
+
id: str = ""
|
|
69
|
+
account_id: str = ""
|
|
70
|
+
asset_class: str = ""
|
|
71
|
+
asset_type: str = ""
|
|
72
|
+
name: str = ""
|
|
73
|
+
ticker: str | None = None
|
|
74
|
+
symbol: str | None = None
|
|
75
|
+
currency: str | None = None
|
|
76
|
+
quantity: float | None = None
|
|
77
|
+
avg_cost: float | None = None
|
|
78
|
+
cost_basis_total: float | None = None
|
|
79
|
+
market_value: float | None = None
|
|
80
|
+
target_weight: float | None = None
|
|
81
|
+
activity: list[PortfolioActivityEntry] = Field(default_factory=list)
|
|
82
|
+
notes: str | None = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PortfolioTargets(BaseModel):
|
|
86
|
+
"""Portfolio target allocations."""
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(extra="allow")
|
|
89
|
+
|
|
90
|
+
by_asset_class: dict[str, float] = Field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PortfolioWatchAsset(BaseModel):
|
|
94
|
+
"""Watched opportunity row."""
|
|
95
|
+
|
|
96
|
+
model_config = ConfigDict(extra="allow")
|
|
97
|
+
|
|
98
|
+
id: str = ""
|
|
99
|
+
name: str = ""
|
|
100
|
+
ticker: str | None = None
|
|
101
|
+
symbol: str | None = None
|
|
102
|
+
asset_class: str | None = None
|
|
103
|
+
asset_type: str | None = None
|
|
104
|
+
description: str | None = None
|
|
105
|
+
criteria: str | None = None
|
|
106
|
+
watch_for: str | None = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PortfolioWatchlist(BaseModel):
|
|
110
|
+
"""Watchlist container."""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="allow")
|
|
113
|
+
|
|
114
|
+
assets: list[PortfolioWatchAsset] = Field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PortfolioDocument(BaseModel):
|
|
118
|
+
"""Root portfolio document schema."""
|
|
119
|
+
|
|
120
|
+
model_config = ConfigDict(extra="allow")
|
|
121
|
+
|
|
122
|
+
schema_version: str | None = None
|
|
123
|
+
portfolio: PortfolioMetadata = Field(default_factory=PortfolioMetadata)
|
|
124
|
+
accounts: list[PortfolioAccount] = Field(default_factory=list)
|
|
125
|
+
targets: PortfolioTargets = Field(default_factory=PortfolioTargets)
|
|
126
|
+
positions: list[PortfolioPosition] = Field(default_factory=list)
|
|
127
|
+
watchlist: PortfolioWatchlist = Field(default_factory=PortfolioWatchlist)
|
|
128
|
+
watch_assets: list[PortfolioWatchAsset] = Field(default_factory=list)
|
|
129
|
+
market_data: dict[str, Any] = Field(default_factory=dict)
|