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.
Files changed (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. 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)