Italianhype commited on
Commit
3fd4738
·
verified ·
1 Parent(s): e5b1a5b

Add Decision Superiority Business Quality Portfolio Intelligence

Browse files
README.md CHANGED
@@ -935,6 +935,90 @@ export SELF_IMPROVEMENT_ROLLBACK_ENABLED=true
935
 
936
  The learning loops only update database memory, confidence adjustments, proprietary reasoning examples and reversible scoring-weight versions.
937
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
  ## Docker
939
 
940
  ```bash
 
935
 
936
  The learning loops only update database memory, confidence adjustments, proprietary reasoning examples and reversible scoring-weight versions.
937
 
938
+ ## Decision Superiority, Business Quality and Portfolio Intelligence
939
+
940
+ This release upgrades BLUM from trade-outcome analysis to decision-quality analysis.
941
+
942
+ The core question is no longer only:
943
+
944
+ > Did this trade work?
945
+
946
+ The new question is:
947
+
948
+ > Was this the best available decision at that moment, considering alternative opportunities, business quality, portfolio context and benchmark alternatives?
949
+
950
+ ### Decision Superiority Engine
951
+
952
+ `DecisionSuperiorityEngine` evaluates every comparable BLUM decision against the opportunity set available in the same decision window.
953
+
954
+ It measures:
955
+
956
+ - Opportunity Recall: how many future outperformers BLUM identified before they outperformed.
957
+ - Opportunity Precision: how many selected opportunities became outperformers.
958
+ - Alpha Capture Rate: how much available alpha BLUM captured versus what was available.
959
+ - Ranking Accuracy: whether BLUM ranked the best future performers near the top.
960
+ - Missed Opportunities: candidates BLUM ignored that later performed better.
961
+ - Best Decisions and Worst Decisions: auditable evidence, not narrative claims.
962
+
963
+ Main API:
964
+
965
+ - `GET /api/decision-intelligence/dashboard`
966
+ - `GET /api/decision-intelligence/superiority`
967
+ - `POST /api/decision-intelligence/superiority/recalculate`
968
+ - `GET /api/decision-intelligence/universe-snapshots`
969
+ - `POST /api/decision-intelligence/universe-snapshots/recalculate`
970
+ - `GET /api/decision-intelligence/missed-opportunities`
971
+
972
+ ### Business Quality Engine
973
+
974
+ `BusinessQualityEngine` scores company quality from stored fundamental evidence. If fundamentals are missing, BLUM explicitly marks the company as `insufficient_fundamental_evidence` and penalizes the score.
975
+
976
+ It evaluates:
977
+
978
+ - growth quality;
979
+ - profitability quality;
980
+ - cash-flow quality;
981
+ - balance-sheet quality;
982
+ - capital-allocation quality;
983
+ - moat quality;
984
+ - management-quality proxies;
985
+ - fundamental alpha patterns from historical outcomes.
986
+
987
+ Main API:
988
+
989
+ - `GET /api/business-quality/dashboard`
990
+ - `GET /api/business-quality/scores`
991
+ - `POST /api/business-quality/recalculate`
992
+
993
+ ### Portfolio Intelligence Engine
994
+
995
+ `PortfolioIntelligenceEngine` measures whether a position improves the simulated portfolio, not only whether the position made money.
996
+
997
+ It evaluates:
998
+
999
+ - return contribution;
1000
+ - risk contribution;
1001
+ - drawdown contribution;
1002
+ - alpha contribution;
1003
+ - portfolio concentration;
1004
+ - correlation between positions;
1005
+ - position-sizing outcomes;
1006
+ - portfolio quality score.
1007
+
1008
+ Main API:
1009
+
1010
+ - `GET /api/portfolio-intelligence/dashboard`
1011
+ - `GET /api/portfolio-intelligence/quality`
1012
+ - `POST /api/portfolio-intelligence/recalculate`
1013
+
1014
+ ### Guardrails
1015
+
1016
+ - No decision-superiority claim is valid with insufficient comparable samples.
1017
+ - BLUM must say when a better opportunity existed.
1018
+ - Business quality is not inferred as fact when fundamentals are missing.
1019
+ - Portfolio quality is penalized when P/L is concentrated in too few positions.
1020
+ - Chat responses use stored dashboard evidence only. If the evidence is missing, BLUM must answer `Insufficient evidence`.
1021
+
1022
  ## Docker
1023
 
1024
  ```bash
backend/alembic/versions/0019_decision_quality_engines.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from alembic import op
4
+ import sqlalchemy as sa
5
+ from sqlalchemy.dialects import postgresql
6
+
7
+
8
+ revision = "0019_decision_quality"
9
+ down_revision = "0018_learning_intel_benchmark"
10
+ branch_labels = None
11
+ depends_on = None
12
+
13
+
14
+ json_type = postgresql.JSONB(astext_type=sa.Text())
15
+
16
+
17
+ def create_index(table: str, columns: list[str], suffix: str | None = None) -> None:
18
+ name = f"ix_{table}_{suffix or '_'.join(columns)}"
19
+ if len(name) > 63:
20
+ name = f"ix_{table[:30]}_{(suffix or '_'.join(columns))[:22]}"
21
+ op.create_index(name, table, columns)
22
+
23
+
24
+ def upgrade() -> None:
25
+ op.create_table(
26
+ "decision_universe_snapshots",
27
+ sa.Column("id", sa.Integer(), nullable=False),
28
+ sa.Column("timestamp", sa.DateTime(), nullable=False),
29
+ sa.Column("market_regime", sa.String(length=120), nullable=True),
30
+ sa.Column("volatility_regime", sa.String(length=120), nullable=True),
31
+ sa.Column("selected_asset", sa.String(length=32), nullable=False),
32
+ sa.Column("selected_rank", sa.Integer(), nullable=True),
33
+ sa.Column("selected_score", sa.Float(), nullable=True),
34
+ sa.Column("total_candidates", sa.Integer(), nullable=False),
35
+ sa.Column("candidates_json", json_type, nullable=True),
36
+ sa.Column("benchmark_snapshot", json_type, nullable=True),
37
+ sa.Column("created_at", sa.DateTime(), nullable=False),
38
+ sa.PrimaryKeyConstraint("id"),
39
+ )
40
+ create_index("decision_universe_snapshots", ["selected_asset", "timestamp"], "selected_created")
41
+ create_index("decision_universe_snapshots", ["market_regime", "timestamp"], "regime_created")
42
+
43
+ op.create_table(
44
+ "opportunity_recall_metrics",
45
+ sa.Column("id", sa.Integer(), nullable=False),
46
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
47
+ sa.Column("sector", sa.String(length=120), nullable=True),
48
+ sa.Column("setup", sa.String(length=120), nullable=True),
49
+ sa.Column("regime", sa.String(length=120), nullable=True),
50
+ sa.Column("timeframe", sa.String(length=80), nullable=True),
51
+ sa.Column("captured_outperformers", sa.Integer(), nullable=False),
52
+ sa.Column("total_outperformers", sa.Integer(), nullable=False),
53
+ sa.Column("opportunity_recall", sa.Float(), nullable=True),
54
+ sa.Column("evidence_json", json_type, nullable=True),
55
+ sa.PrimaryKeyConstraint("id"),
56
+ )
57
+ create_index("opportunity_recall_metrics", ["sector", "setup", "regime", "timeframe"], "scope")
58
+
59
+ op.create_table(
60
+ "opportunity_precision_metrics",
61
+ sa.Column("id", sa.Integer(), nullable=False),
62
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
63
+ sa.Column("sector", sa.String(length=120), nullable=True),
64
+ sa.Column("setup", sa.String(length=120), nullable=True),
65
+ sa.Column("regime", sa.String(length=120), nullable=True),
66
+ sa.Column("timeframe", sa.String(length=80), nullable=True),
67
+ sa.Column("successful_opportunities", sa.Integer(), nullable=False),
68
+ sa.Column("selected_opportunities", sa.Integer(), nullable=False),
69
+ sa.Column("opportunity_precision", sa.Float(), nullable=True),
70
+ sa.Column("evidence_json", json_type, nullable=True),
71
+ sa.PrimaryKeyConstraint("id"),
72
+ )
73
+ create_index("opportunity_precision_metrics", ["sector", "setup", "regime", "timeframe"], "scope")
74
+
75
+ op.create_table(
76
+ "alpha_capture_metrics",
77
+ sa.Column("id", sa.Integer(), nullable=False),
78
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
79
+ sa.Column("ticker", sa.String(length=32), nullable=True),
80
+ sa.Column("sector", sa.String(length=120), nullable=True),
81
+ sa.Column("regime", sa.String(length=120), nullable=True),
82
+ sa.Column("timeframe", sa.String(length=80), nullable=True),
83
+ sa.Column("available_alpha", sa.Float(), nullable=True),
84
+ sa.Column("captured_alpha", sa.Float(), nullable=True),
85
+ sa.Column("alpha_capture_rate", sa.Float(), nullable=True),
86
+ sa.Column("evidence_json", json_type, nullable=True),
87
+ sa.PrimaryKeyConstraint("id"),
88
+ )
89
+ create_index("alpha_capture_metrics", ["ticker", "sector", "regime", "timeframe"], "scope")
90
+
91
+ op.create_table(
92
+ "ranking_accuracy_metrics",
93
+ sa.Column("id", sa.Integer(), nullable=False),
94
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
95
+ sa.Column("sector", sa.String(length=120), nullable=True),
96
+ sa.Column("setup", sa.String(length=120), nullable=True),
97
+ sa.Column("regime", sa.String(length=120), nullable=True),
98
+ sa.Column("timeframe", sa.String(length=80), nullable=True),
99
+ sa.Column("sample_size", sa.Integer(), nullable=False),
100
+ sa.Column("top1_accuracy", sa.Float(), nullable=True),
101
+ sa.Column("top3_accuracy", sa.Float(), nullable=True),
102
+ sa.Column("top5_accuracy", sa.Float(), nullable=True),
103
+ sa.Column("ranking_correlation", sa.Float(), nullable=True),
104
+ sa.Column("ranking_decay", sa.Float(), nullable=True),
105
+ sa.Column("evidence_json", json_type, nullable=True),
106
+ sa.PrimaryKeyConstraint("id"),
107
+ )
108
+ create_index("ranking_accuracy_metrics", ["sector", "setup", "regime", "timeframe"], "scope")
109
+
110
+ op.create_table(
111
+ "decision_superiority_scores",
112
+ sa.Column("id", sa.Integer(), nullable=False),
113
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
114
+ sa.Column("mode", sa.String(length=80), nullable=False),
115
+ sa.Column("scope", sa.String(length=120), nullable=False),
116
+ sa.Column("score", sa.Float(), nullable=False),
117
+ sa.Column("classification", sa.String(length=120), nullable=False),
118
+ sa.Column("opportunity_recall", sa.Float(), nullable=True),
119
+ sa.Column("opportunity_precision", sa.Float(), nullable=True),
120
+ sa.Column("alpha_capture", sa.Float(), nullable=True),
121
+ sa.Column("ranking_accuracy", sa.Float(), nullable=True),
122
+ sa.Column("benchmark_excess", sa.Float(), nullable=True),
123
+ sa.Column("live_validation", sa.Float(), nullable=True),
124
+ sa.Column("regime_consistency", sa.Float(), nullable=True),
125
+ sa.Column("reproducibility", sa.Float(), nullable=True),
126
+ sa.Column("drawdown_control", sa.Float(), nullable=True),
127
+ sa.Column("explanation", sa.Text(), nullable=False),
128
+ sa.Column("warnings_json", json_type, nullable=True),
129
+ sa.PrimaryKeyConstraint("id"),
130
+ )
131
+ create_index("decision_superiority_scores", ["mode", "calculated_at"], "mode_created")
132
+ create_index("decision_superiority_scores", ["score", "classification"], "score_class")
133
+
134
+ op.create_table(
135
+ "business_quality_profiles",
136
+ sa.Column("id", sa.Integer(), nullable=False),
137
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
138
+ sa.Column("asset_id", sa.Integer(), nullable=True),
139
+ sa.Column("ticker", sa.String(length=32), nullable=False),
140
+ sa.Column("growth_quality", sa.Float(), nullable=True),
141
+ sa.Column("profitability_quality", sa.Float(), nullable=True),
142
+ sa.Column("cash_flow_quality", sa.Float(), nullable=True),
143
+ sa.Column("balance_sheet_quality", sa.Float(), nullable=True),
144
+ sa.Column("capital_allocation_quality", sa.Float(), nullable=True),
145
+ sa.Column("moat_quality", sa.Float(), nullable=True),
146
+ sa.Column("evidence_json", json_type, nullable=True),
147
+ sa.ForeignKeyConstraint(["asset_id"], ["assets.id"], ondelete="SET NULL"),
148
+ sa.PrimaryKeyConstraint("id"),
149
+ )
150
+ create_index("business_quality_profiles", ["ticker", "calculated_at"], "ticker_created")
151
+
152
+ op.create_table(
153
+ "management_quality_profiles",
154
+ sa.Column("id", sa.Integer(), nullable=False),
155
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
156
+ sa.Column("asset_id", sa.Integer(), nullable=True),
157
+ sa.Column("ticker", sa.String(length=32), nullable=False),
158
+ sa.Column("insider_alignment", sa.Float(), nullable=True),
159
+ sa.Column("execution_consistency", sa.Float(), nullable=True),
160
+ sa.Column("earnings_delivery", sa.Float(), nullable=True),
161
+ sa.Column("management_quality", sa.Float(), nullable=True),
162
+ sa.Column("evidence_json", json_type, nullable=True),
163
+ sa.ForeignKeyConstraint(["asset_id"], ["assets.id"], ondelete="SET NULL"),
164
+ sa.PrimaryKeyConstraint("id"),
165
+ )
166
+ create_index("management_quality_profiles", ["ticker", "calculated_at"], "ticker_created")
167
+
168
+ op.create_table(
169
+ "fundamental_alpha_patterns",
170
+ sa.Column("id", sa.Integer(), nullable=False),
171
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
172
+ sa.Column("pattern_name", sa.String(length=160), nullable=False),
173
+ sa.Column("sector", sa.String(length=120), nullable=True),
174
+ sa.Column("timeframe", sa.String(length=80), nullable=True),
175
+ sa.Column("sample_size", sa.Integer(), nullable=False),
176
+ sa.Column("average_forward_return", sa.Float(), nullable=True),
177
+ sa.Column("hit_rate", sa.Float(), nullable=True),
178
+ sa.Column("evidence_json", json_type, nullable=True),
179
+ sa.PrimaryKeyConstraint("id"),
180
+ )
181
+ create_index("fundamental_alpha_patterns", ["pattern_name", "sector", "timeframe"], "scope")
182
+
183
+ op.create_table(
184
+ "business_quality_scores",
185
+ sa.Column("id", sa.Integer(), nullable=False),
186
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
187
+ sa.Column("asset_id", sa.Integer(), nullable=True),
188
+ sa.Column("ticker", sa.String(length=32), nullable=False),
189
+ sa.Column("sector", sa.String(length=120), nullable=True),
190
+ sa.Column("business_quality_score", sa.Float(), nullable=False),
191
+ sa.Column("growth_quality", sa.Float(), nullable=True),
192
+ sa.Column("profitability_quality", sa.Float(), nullable=True),
193
+ sa.Column("cash_flow_quality", sa.Float(), nullable=True),
194
+ sa.Column("balance_sheet_quality", sa.Float(), nullable=True),
195
+ sa.Column("capital_allocation_quality", sa.Float(), nullable=True),
196
+ sa.Column("moat_quality", sa.Float(), nullable=True),
197
+ sa.Column("management_quality", sa.Float(), nullable=True),
198
+ sa.Column("data_quality_score", sa.Float(), nullable=False),
199
+ sa.Column("evidence_json", json_type, nullable=True),
200
+ sa.ForeignKeyConstraint(["asset_id"], ["assets.id"], ondelete="SET NULL"),
201
+ sa.PrimaryKeyConstraint("id"),
202
+ )
203
+ create_index("business_quality_scores", ["ticker", "calculated_at"], "ticker_created")
204
+ create_index("business_quality_scores", ["business_quality_score"], "score")
205
+
206
+ op.create_table(
207
+ "portfolio_contributions",
208
+ sa.Column("id", sa.Integer(), nullable=False),
209
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
210
+ sa.Column("game_id", sa.Integer(), nullable=True),
211
+ sa.Column("ticker", sa.String(length=32), nullable=False),
212
+ sa.Column("sector", sa.String(length=120), nullable=True),
213
+ sa.Column("return_contribution", sa.Float(), nullable=True),
214
+ sa.Column("risk_contribution", sa.Float(), nullable=True),
215
+ sa.Column("drawdown_contribution", sa.Float(), nullable=True),
216
+ sa.Column("alpha_contribution", sa.Float(), nullable=True),
217
+ sa.Column("evidence_json", json_type, nullable=True),
218
+ sa.ForeignKeyConstraint(["game_id"], ["trading_games.id"], ondelete="CASCADE"),
219
+ sa.PrimaryKeyConstraint("id"),
220
+ )
221
+ create_index("portfolio_contributions", ["game_id", "ticker", "calculated_at"], "scope")
222
+
223
+ op.create_table(
224
+ "portfolio_correlations",
225
+ sa.Column("id", sa.Integer(), nullable=False),
226
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
227
+ sa.Column("scope", sa.String(length=120), nullable=False),
228
+ sa.Column("asset_a", sa.String(length=32), nullable=False),
229
+ sa.Column("asset_b", sa.String(length=32), nullable=False),
230
+ sa.Column("correlation", sa.Float(), nullable=True),
231
+ sa.Column("correlation_type", sa.String(length=80), nullable=False),
232
+ sa.Column("evidence_json", json_type, nullable=True),
233
+ sa.PrimaryKeyConstraint("id"),
234
+ sa.UniqueConstraint("scope", "asset_a", "asset_b", name="uq_portfolio_correlation_pair"),
235
+ )
236
+ create_index("portfolio_correlations", ["scope", "correlation"], "scope_corr")
237
+
238
+ op.create_table(
239
+ "portfolio_alpha_scores",
240
+ sa.Column("id", sa.Integer(), nullable=False),
241
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
242
+ sa.Column("game_id", sa.Integer(), nullable=True),
243
+ sa.Column("ticker", sa.String(length=32), nullable=False),
244
+ sa.Column("portfolio_alpha_score", sa.Float(), nullable=False),
245
+ sa.Column("marginal_return_score", sa.Float(), nullable=True),
246
+ sa.Column("marginal_risk_score", sa.Float(), nullable=True),
247
+ sa.Column("diversification_score", sa.Float(), nullable=True),
248
+ sa.Column("benchmark_excess_score", sa.Float(), nullable=True),
249
+ sa.Column("evidence_json", json_type, nullable=True),
250
+ sa.ForeignKeyConstraint(["game_id"], ["trading_games.id"], ondelete="CASCADE"),
251
+ sa.PrimaryKeyConstraint("id"),
252
+ )
253
+ create_index("portfolio_alpha_scores", ["ticker", "calculated_at"], "ticker_created")
254
+
255
+ op.create_table(
256
+ "position_sizing_outcomes",
257
+ sa.Column("id", sa.Integer(), nullable=False),
258
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
259
+ sa.Column("sizing_logic", sa.String(length=120), nullable=False),
260
+ sa.Column("timeframe", sa.String(length=80), nullable=True),
261
+ sa.Column("sample_size", sa.Integer(), nullable=False),
262
+ sa.Column("average_r", sa.Float(), nullable=True),
263
+ sa.Column("drawdown_impact", sa.Float(), nullable=True),
264
+ sa.Column("capital_efficiency", sa.Float(), nullable=True),
265
+ sa.Column("evidence_json", json_type, nullable=True),
266
+ sa.PrimaryKeyConstraint("id"),
267
+ )
268
+ create_index("position_sizing_outcomes", ["sizing_logic", "timeframe"], "logic_timeframe")
269
+
270
+ op.create_table(
271
+ "portfolio_quality_scores",
272
+ sa.Column("id", sa.Integer(), nullable=False),
273
+ sa.Column("calculated_at", sa.DateTime(), nullable=False),
274
+ sa.Column("game_id", sa.Integer(), nullable=True),
275
+ sa.Column("portfolio_quality_score", sa.Float(), nullable=False),
276
+ sa.Column("diversification", sa.Float(), nullable=True),
277
+ sa.Column("concentration_risk", sa.Float(), nullable=True),
278
+ sa.Column("drawdown_control", sa.Float(), nullable=True),
279
+ sa.Column("alpha_generation", sa.Float(), nullable=True),
280
+ sa.Column("benchmark_excess", sa.Float(), nullable=True),
281
+ sa.Column("capital_efficiency", sa.Float(), nullable=True),
282
+ sa.Column("explanation", sa.Text(), nullable=False),
283
+ sa.Column("warnings_json", json_type, nullable=True),
284
+ sa.ForeignKeyConstraint(["game_id"], ["trading_games.id"], ondelete="CASCADE"),
285
+ sa.PrimaryKeyConstraint("id"),
286
+ )
287
+ create_index("portfolio_quality_scores", ["game_id", "calculated_at"], "game_created")
288
+ create_index("portfolio_quality_scores", ["portfolio_quality_score"], "score")
289
+
290
+
291
+ def downgrade() -> None:
292
+ for table in [
293
+ "portfolio_quality_scores",
294
+ "position_sizing_outcomes",
295
+ "portfolio_alpha_scores",
296
+ "portfolio_correlations",
297
+ "portfolio_contributions",
298
+ "business_quality_scores",
299
+ "fundamental_alpha_patterns",
300
+ "management_quality_profiles",
301
+ "business_quality_profiles",
302
+ "decision_superiority_scores",
303
+ "ranking_accuracy_metrics",
304
+ "alpha_capture_metrics",
305
+ "opportunity_precision_metrics",
306
+ "opportunity_recall_metrics",
307
+ "decision_universe_snapshots",
308
+ ]:
309
+ op.drop_table(table)
backend/app/api/routes.py CHANGED
@@ -16,6 +16,7 @@ from app.ingestion.news_ingestor import NewsIngestor
16
  from app.models import (
17
  AIInsight,
18
  AccuracySnapshot,
 
19
  Asset,
20
  AutonomousEngineRun,
21
  BenchmarkRelativeOutcome,
@@ -32,6 +33,8 @@ from app.models import (
32
  BlumThesisOutcome,
33
  BlumThesisQualityScore,
34
  BlumTrainingExample,
 
 
35
  ChartAnalysis,
36
  ChartPatternMemory,
37
  ChatMessage,
@@ -39,10 +42,13 @@ from app.models import (
39
  CompetingThesis,
40
  ConfidenceCalibrationBucket,
41
  ConfidenceAdjustment,
 
 
42
  EmbeddingVector,
43
  EngineVote,
44
  EnsembleWeightVersion,
45
  ExternalDatasetSource,
 
46
  FundamentalSnapshot,
47
  HistoricalPrediction,
48
  HistoricalSimilarityCase,
@@ -95,6 +101,15 @@ from app.models import (
95
  MetaLearningEvent,
96
  LearningProgressSnapshot,
97
  LearningStrengthWeaknessMap,
 
 
 
 
 
 
 
 
 
98
  SelfImprovementAction,
99
  ThesisCompetition,
100
  ThesisConvictionHistory,
@@ -155,6 +170,12 @@ from app.services.learning_intelligence import (
155
  LearningWeaknessMapService,
156
  SelfImprovementActionEngine,
157
  )
 
 
 
 
 
 
158
  from app.services.live import live_news, market_sentiment
159
  from app.services.macro import macro_overview, update_macro_snapshots
160
  from app.services.market_brain import build_market_brain, latest_market_brain, market_brain_history
@@ -339,6 +360,9 @@ def system_status(db: Session = Depends(get_db)) -> dict:
339
  "official_benchmark_comparison": True,
340
  "learning_weakness_map": True,
341
  "self_improvement_action_engine": True,
 
 
 
342
  },
343
  "database_counts": {
344
  "assets": int(db.scalar(select(func.count(Asset.id))) or 0),
@@ -386,6 +410,21 @@ def system_status(db: Session = Depends(get_db)) -> dict:
386
  "learning_progress_snapshots": int(db.scalar(select(func.count(LearningProgressSnapshot.id))) or 0),
387
  "learning_strength_weakness_map": int(db.scalar(select(func.count(LearningStrengthWeaknessMap.id))) or 0),
388
  "self_improvement_actions": int(db.scalar(select(func.count(SelfImprovementAction.id))) or 0),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  "model_weight_versions": int(db.scalar(select(func.count(ModelWeightVersion.id))) or 0),
390
  "historical_similarity_cases": int(db.scalar(select(func.count(HistoricalSimilarityCase.id))) or 0),
391
  "confidence_adjustments": int(db.scalar(select(func.count(ConfidenceAdjustment.id))) or 0),
@@ -431,7 +470,7 @@ def system_status(db: Session = Depends(get_db)) -> dict:
431
  "Hugging Face serves the previous image until the Docker build finishes successfully.",
432
  "The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
433
  "Existing snapshots are refreshed by the autonomous engine after a successful deployment.",
434
- "Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.16.0.",
435
  ],
436
  }
437
 
@@ -1033,6 +1072,74 @@ def learning_intelligence_evaluate_self_improvement(action_id: int, db: Session
1033
  return SelfImprovementActionEngine().evaluate(db, action_id=action_id)
1034
 
1035
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1036
  @router.get("/model/status")
1037
  def blum_model_status(db: Session = Depends(get_db)) -> dict:
1038
  return model_status(db)
 
16
  from app.models import (
17
  AIInsight,
18
  AccuracySnapshot,
19
+ AlphaCaptureMetric,
20
  Asset,
21
  AutonomousEngineRun,
22
  BenchmarkRelativeOutcome,
 
33
  BlumThesisOutcome,
34
  BlumThesisQualityScore,
35
  BlumTrainingExample,
36
+ BusinessQualityProfile,
37
+ BusinessQualityScore,
38
  ChartAnalysis,
39
  ChartPatternMemory,
40
  ChatMessage,
 
42
  CompetingThesis,
43
  ConfidenceCalibrationBucket,
44
  ConfidenceAdjustment,
45
+ DecisionSuperiorityScore,
46
+ DecisionUniverseSnapshot,
47
  EmbeddingVector,
48
  EngineVote,
49
  EnsembleWeightVersion,
50
  ExternalDatasetSource,
51
+ FundamentalAlphaPattern,
52
  FundamentalSnapshot,
53
  HistoricalPrediction,
54
  HistoricalSimilarityCase,
 
101
  MetaLearningEvent,
102
  LearningProgressSnapshot,
103
  LearningStrengthWeaknessMap,
104
+ ManagementQualityProfile,
105
+ OpportunityPrecisionMetric,
106
+ OpportunityRecallMetric,
107
+ PortfolioAlphaScore,
108
+ PortfolioContribution,
109
+ PortfolioCorrelation,
110
+ PortfolioQualityScore,
111
+ PositionSizingOutcome,
112
+ RankingAccuracyMetric,
113
  SelfImprovementAction,
114
  ThesisCompetition,
115
  ThesisConvictionHistory,
 
170
  LearningWeaknessMapService,
171
  SelfImprovementActionEngine,
172
  )
173
+ from app.services.decision_intelligence import (
174
+ BusinessQualityEngine,
175
+ DecisionIntelligenceDashboardService,
176
+ DecisionSuperiorityEngine,
177
+ PortfolioIntelligenceEngine,
178
+ )
179
  from app.services.live import live_news, market_sentiment
180
  from app.services.macro import macro_overview, update_macro_snapshots
181
  from app.services.market_brain import build_market_brain, latest_market_brain, market_brain_history
 
360
  "official_benchmark_comparison": True,
361
  "learning_weakness_map": True,
362
  "self_improvement_action_engine": True,
363
+ "decision_superiority_engine": True,
364
+ "business_quality_engine": True,
365
+ "portfolio_intelligence_engine": True,
366
  },
367
  "database_counts": {
368
  "assets": int(db.scalar(select(func.count(Asset.id))) or 0),
 
410
  "learning_progress_snapshots": int(db.scalar(select(func.count(LearningProgressSnapshot.id))) or 0),
411
  "learning_strength_weakness_map": int(db.scalar(select(func.count(LearningStrengthWeaknessMap.id))) or 0),
412
  "self_improvement_actions": int(db.scalar(select(func.count(SelfImprovementAction.id))) or 0),
413
+ "decision_universe_snapshots": int(db.scalar(select(func.count(DecisionUniverseSnapshot.id))) or 0),
414
+ "opportunity_recall_metrics": int(db.scalar(select(func.count(OpportunityRecallMetric.id))) or 0),
415
+ "opportunity_precision_metrics": int(db.scalar(select(func.count(OpportunityPrecisionMetric.id))) or 0),
416
+ "alpha_capture_metrics": int(db.scalar(select(func.count(AlphaCaptureMetric.id))) or 0),
417
+ "ranking_accuracy_metrics": int(db.scalar(select(func.count(RankingAccuracyMetric.id))) or 0),
418
+ "decision_superiority_scores": int(db.scalar(select(func.count(DecisionSuperiorityScore.id))) or 0),
419
+ "business_quality_profiles": int(db.scalar(select(func.count(BusinessQualityProfile.id))) or 0),
420
+ "management_quality_profiles": int(db.scalar(select(func.count(ManagementQualityProfile.id))) or 0),
421
+ "fundamental_alpha_patterns": int(db.scalar(select(func.count(FundamentalAlphaPattern.id))) or 0),
422
+ "business_quality_scores": int(db.scalar(select(func.count(BusinessQualityScore.id))) or 0),
423
+ "portfolio_contributions": int(db.scalar(select(func.count(PortfolioContribution.id))) or 0),
424
+ "portfolio_correlations": int(db.scalar(select(func.count(PortfolioCorrelation.id))) or 0),
425
+ "portfolio_alpha_scores": int(db.scalar(select(func.count(PortfolioAlphaScore.id))) or 0),
426
+ "position_sizing_outcomes": int(db.scalar(select(func.count(PositionSizingOutcome.id))) or 0),
427
+ "portfolio_quality_scores": int(db.scalar(select(func.count(PortfolioQualityScore.id))) or 0),
428
  "model_weight_versions": int(db.scalar(select(func.count(ModelWeightVersion.id))) or 0),
429
  "historical_similarity_cases": int(db.scalar(select(func.count(HistoricalSimilarityCase.id))) or 0),
430
  "confidence_adjustments": int(db.scalar(select(func.count(ConfidenceAdjustment.id))) or 0),
 
470
  "Hugging Face serves the previous image until the Docker build finishes successfully.",
471
  "The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
472
  "Existing snapshots are refreshed by the autonomous engine after a successful deployment.",
473
+ "Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.17.0.",
474
  ],
475
  }
476
 
 
1072
  return SelfImprovementActionEngine().evaluate(db, action_id=action_id)
1073
 
1074
 
1075
+ @router.get("/api/decision-intelligence/dashboard")
1076
+ def decision_intelligence_dashboard(db: Session = Depends(get_db)) -> dict:
1077
+ return DecisionIntelligenceDashboardService().dashboard(db)
1078
+
1079
+
1080
+ @router.get("/api/decision-intelligence/superiority")
1081
+ def decision_intelligence_superiority(db: Session = Depends(get_db)) -> dict:
1082
+ return DecisionSuperiorityEngine().score(db, persist=False)
1083
+
1084
+
1085
+ @router.post("/api/decision-intelligence/superiority/recalculate")
1086
+ def decision_intelligence_superiority_recalculate(db: Session = Depends(get_db)) -> dict:
1087
+ return DecisionSuperiorityEngine().score(db, persist=True)
1088
+
1089
+
1090
+ @router.get("/api/decision-intelligence/universe-snapshots")
1091
+ def decision_intelligence_universe_snapshots(db: Session = Depends(get_db)) -> dict:
1092
+ return DecisionSuperiorityEngine().universe_snapshots(db, persist=False)
1093
+
1094
+
1095
+ @router.post("/api/decision-intelligence/universe-snapshots/recalculate")
1096
+ def decision_intelligence_universe_snapshots_recalculate(db: Session = Depends(get_db)) -> dict:
1097
+ return DecisionSuperiorityEngine().universe_snapshots(db, persist=True)
1098
+
1099
+
1100
+ @router.get("/api/decision-intelligence/missed-opportunities")
1101
+ def decision_intelligence_missed_opportunities(db: Session = Depends(get_db)) -> list[dict]:
1102
+ return DecisionSuperiorityEngine().top_missed_opportunities(db)
1103
+
1104
+
1105
+ @router.get("/api/business-quality/dashboard")
1106
+ def business_quality_dashboard(db: Session = Depends(get_db)) -> dict:
1107
+ return BusinessQualityEngine().dashboard(db)
1108
+
1109
+
1110
+ @router.get("/api/business-quality/scores")
1111
+ def business_quality_scores(limit: int = Query(default=80, ge=1, le=300), db: Session = Depends(get_db)) -> dict:
1112
+ return BusinessQualityEngine().scores(db, limit=limit, persist=False)
1113
+
1114
+
1115
+ @router.post("/api/business-quality/recalculate")
1116
+ def business_quality_recalculate(limit: int = Query(default=80, ge=1, le=300), db: Session = Depends(get_db)) -> dict:
1117
+ return BusinessQualityEngine().scores(db, limit=limit, persist=True)
1118
+
1119
+
1120
+ @router.get("/api/portfolio-intelligence/dashboard")
1121
+ def portfolio_intelligence_dashboard(db: Session = Depends(get_db)) -> dict:
1122
+ return PortfolioIntelligenceEngine().dashboard(db)
1123
+
1124
+
1125
+ @router.get("/api/portfolio-intelligence/quality")
1126
+ def portfolio_intelligence_quality(db: Session = Depends(get_db)) -> dict:
1127
+ return PortfolioIntelligenceEngine().quality_score(db, persist=False)
1128
+
1129
+
1130
+ @router.post("/api/portfolio-intelligence/recalculate")
1131
+ def portfolio_intelligence_recalculate(db: Session = Depends(get_db)) -> dict:
1132
+ engine = PortfolioIntelligenceEngine()
1133
+ return {
1134
+ "status": "ok",
1135
+ "quality": engine.quality_score(db, persist=True),
1136
+ "contributions": engine.contributions(db, persist=True),
1137
+ "correlations": engine.correlations(db, persist=True),
1138
+ "portfolio_alpha": engine.alpha_scores(db, persist=True),
1139
+ "position_sizing": engine.position_sizing_outcomes(db, persist=True),
1140
+ }
1141
+
1142
+
1143
  @router.get("/model/status")
1144
  def blum_model_status(db: Session = Depends(get_db)) -> dict:
1145
  return model_status(db)
backend/app/core/config.py CHANGED
@@ -5,7 +5,7 @@ from pydantic_settings import BaseSettings
5
 
6
  class Settings(BaseSettings):
7
  app_name: str = "Blum AI Financial Intelligence"
8
- app_version: str = "0.16.0"
9
  environment: str = Field(default="demo", alias="ENVIRONMENT")
10
  database_url: str = Field(
11
  default="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum",
 
5
 
6
  class Settings(BaseSettings):
7
  app_name: str = "Blum AI Financial Intelligence"
8
+ app_version: str = "0.17.0"
9
  environment: str = Field(default="demo", alias="ENVIRONMENT")
10
  database_url: str = Field(
11
  default="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum",
backend/app/models.py CHANGED
@@ -2420,3 +2420,284 @@ class SelfImprovementAction(Base):
2420
  after_metric: Mapped[float | None] = mapped_column(Float)
2421
  improvement_observed: Mapped[bool | None] = mapped_column(Boolean, index=True)
2422
  notes_json: Mapped[dict] = mapped_column(JsonType, default=dict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2420
  after_metric: Mapped[float | None] = mapped_column(Float)
2421
  improvement_observed: Mapped[bool | None] = mapped_column(Boolean, index=True)
2422
  notes_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2423
+
2424
+
2425
+ class DecisionUniverseSnapshot(Base):
2426
+ __tablename__ = "decision_universe_snapshots"
2427
+ __table_args__ = (
2428
+ Index("ix_decision_universe_selected_created", "selected_asset", "timestamp"),
2429
+ Index("ix_decision_universe_regime_created", "market_regime", "timestamp"),
2430
+ )
2431
+
2432
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2433
+ timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2434
+ market_regime: Mapped[str | None] = mapped_column(String(120), index=True)
2435
+ volatility_regime: Mapped[str | None] = mapped_column(String(120), index=True)
2436
+ selected_asset: Mapped[str] = mapped_column(String(32), index=True)
2437
+ selected_rank: Mapped[int | None] = mapped_column(Integer)
2438
+ selected_score: Mapped[float | None] = mapped_column(Float)
2439
+ total_candidates: Mapped[int] = mapped_column(Integer, default=0)
2440
+ candidates_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2441
+ benchmark_snapshot: Mapped[dict] = mapped_column(JsonType, default=dict)
2442
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2443
+
2444
+
2445
+ class OpportunityRecallMetric(Base):
2446
+ __tablename__ = "opportunity_recall_metrics"
2447
+ __table_args__ = (Index("ix_opportunity_recall_scope", "sector", "setup", "regime", "timeframe"),)
2448
+
2449
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2450
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2451
+ sector: Mapped[str | None] = mapped_column(String(120), index=True)
2452
+ setup: Mapped[str | None] = mapped_column(String(120), index=True)
2453
+ regime: Mapped[str | None] = mapped_column(String(120), index=True)
2454
+ timeframe: Mapped[str | None] = mapped_column(String(80), index=True)
2455
+ captured_outperformers: Mapped[int] = mapped_column(Integer, default=0)
2456
+ total_outperformers: Mapped[int] = mapped_column(Integer, default=0)
2457
+ opportunity_recall: Mapped[float | None] = mapped_column(Float, index=True)
2458
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2459
+
2460
+
2461
+ class OpportunityPrecisionMetric(Base):
2462
+ __tablename__ = "opportunity_precision_metrics"
2463
+ __table_args__ = (Index("ix_opportunity_precision_scope", "sector", "setup", "regime", "timeframe"),)
2464
+
2465
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2466
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2467
+ sector: Mapped[str | None] = mapped_column(String(120), index=True)
2468
+ setup: Mapped[str | None] = mapped_column(String(120), index=True)
2469
+ regime: Mapped[str | None] = mapped_column(String(120), index=True)
2470
+ timeframe: Mapped[str | None] = mapped_column(String(80), index=True)
2471
+ successful_opportunities: Mapped[int] = mapped_column(Integer, default=0)
2472
+ selected_opportunities: Mapped[int] = mapped_column(Integer, default=0)
2473
+ opportunity_precision: Mapped[float | None] = mapped_column(Float, index=True)
2474
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2475
+
2476
+
2477
+ class AlphaCaptureMetric(Base):
2478
+ __tablename__ = "alpha_capture_metrics"
2479
+ __table_args__ = (Index("ix_alpha_capture_scope", "ticker", "sector", "regime", "timeframe"),)
2480
+
2481
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2482
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2483
+ ticker: Mapped[str | None] = mapped_column(String(32), index=True)
2484
+ sector: Mapped[str | None] = mapped_column(String(120), index=True)
2485
+ regime: Mapped[str | None] = mapped_column(String(120), index=True)
2486
+ timeframe: Mapped[str | None] = mapped_column(String(80), index=True)
2487
+ available_alpha: Mapped[float | None] = mapped_column(Float)
2488
+ captured_alpha: Mapped[float | None] = mapped_column(Float)
2489
+ alpha_capture_rate: Mapped[float | None] = mapped_column(Float, index=True)
2490
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2491
+
2492
+
2493
+ class RankingAccuracyMetric(Base):
2494
+ __tablename__ = "ranking_accuracy_metrics"
2495
+ __table_args__ = (Index("ix_ranking_accuracy_scope", "sector", "setup", "regime", "timeframe"),)
2496
+
2497
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2498
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2499
+ sector: Mapped[str | None] = mapped_column(String(120), index=True)
2500
+ setup: Mapped[str | None] = mapped_column(String(120), index=True)
2501
+ regime: Mapped[str | None] = mapped_column(String(120), index=True)
2502
+ timeframe: Mapped[str | None] = mapped_column(String(80), index=True)
2503
+ sample_size: Mapped[int] = mapped_column(Integer, default=0, index=True)
2504
+ top1_accuracy: Mapped[float | None] = mapped_column(Float)
2505
+ top3_accuracy: Mapped[float | None] = mapped_column(Float)
2506
+ top5_accuracy: Mapped[float | None] = mapped_column(Float)
2507
+ ranking_correlation: Mapped[float | None] = mapped_column(Float)
2508
+ ranking_decay: Mapped[float | None] = mapped_column(Float)
2509
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2510
+
2511
+
2512
+ class DecisionSuperiorityScore(Base):
2513
+ __tablename__ = "decision_superiority_scores"
2514
+ __table_args__ = (
2515
+ Index("ix_decision_superiority_mode_created", "mode", "calculated_at"),
2516
+ Index("ix_decision_superiority_score", "score", "classification"),
2517
+ )
2518
+
2519
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2520
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2521
+ mode: Mapped[str] = mapped_column(String(80), default="historical_plus_live", index=True)
2522
+ scope: Mapped[str] = mapped_column(String(120), default="global", index=True)
2523
+ score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
2524
+ classification: Mapped[str] = mapped_column(String(120), default="Weak", index=True)
2525
+ opportunity_recall: Mapped[float | None] = mapped_column(Float)
2526
+ opportunity_precision: Mapped[float | None] = mapped_column(Float)
2527
+ alpha_capture: Mapped[float | None] = mapped_column(Float)
2528
+ ranking_accuracy: Mapped[float | None] = mapped_column(Float)
2529
+ benchmark_excess: Mapped[float | None] = mapped_column(Float)
2530
+ live_validation: Mapped[float | None] = mapped_column(Float)
2531
+ regime_consistency: Mapped[float | None] = mapped_column(Float)
2532
+ reproducibility: Mapped[float | None] = mapped_column(Float)
2533
+ drawdown_control: Mapped[float | None] = mapped_column(Float)
2534
+ explanation: Mapped[str] = mapped_column(Text, default="")
2535
+ warnings_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2536
+
2537
+
2538
+ class BusinessQualityProfile(Base):
2539
+ __tablename__ = "business_quality_profiles"
2540
+ __table_args__ = (Index("ix_business_quality_profiles_ticker_created", "ticker", "calculated_at"),)
2541
+
2542
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2543
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2544
+ asset_id: Mapped[int | None] = mapped_column(ForeignKey("assets.id", ondelete="SET NULL"), index=True)
2545
+ ticker: Mapped[str] = mapped_column(String(32), index=True)
2546
+ growth_quality: Mapped[float | None] = mapped_column(Float)
2547
+ profitability_quality: Mapped[float | None] = mapped_column(Float)
2548
+ cash_flow_quality: Mapped[float | None] = mapped_column(Float)
2549
+ balance_sheet_quality: Mapped[float | None] = mapped_column(Float)
2550
+ capital_allocation_quality: Mapped[float | None] = mapped_column(Float)
2551
+ moat_quality: Mapped[float | None] = mapped_column(Float)
2552
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2553
+
2554
+ asset = relationship("Asset")
2555
+
2556
+
2557
+ class ManagementQualityProfile(Base):
2558
+ __tablename__ = "management_quality_profiles"
2559
+ __table_args__ = (Index("ix_management_quality_profiles_ticker_created", "ticker", "calculated_at"),)
2560
+
2561
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2562
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2563
+ asset_id: Mapped[int | None] = mapped_column(ForeignKey("assets.id", ondelete="SET NULL"), index=True)
2564
+ ticker: Mapped[str] = mapped_column(String(32), index=True)
2565
+ insider_alignment: Mapped[float | None] = mapped_column(Float)
2566
+ execution_consistency: Mapped[float | None] = mapped_column(Float)
2567
+ earnings_delivery: Mapped[float | None] = mapped_column(Float)
2568
+ management_quality: Mapped[float | None] = mapped_column(Float, index=True)
2569
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2570
+
2571
+ asset = relationship("Asset")
2572
+
2573
+
2574
+ class FundamentalAlphaPattern(Base):
2575
+ __tablename__ = "fundamental_alpha_patterns"
2576
+ __table_args__ = (Index("ix_fundamental_alpha_pattern_scope", "pattern_name", "sector", "timeframe"),)
2577
+
2578
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2579
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2580
+ pattern_name: Mapped[str] = mapped_column(String(160), index=True)
2581
+ sector: Mapped[str | None] = mapped_column(String(120), index=True)
2582
+ timeframe: Mapped[str | None] = mapped_column(String(80), index=True)
2583
+ sample_size: Mapped[int] = mapped_column(Integer, default=0, index=True)
2584
+ average_forward_return: Mapped[float | None] = mapped_column(Float)
2585
+ hit_rate: Mapped[float | None] = mapped_column(Float)
2586
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2587
+
2588
+
2589
+ class BusinessQualityScore(Base):
2590
+ __tablename__ = "business_quality_scores"
2591
+ __table_args__ = (
2592
+ Index("ix_business_quality_scores_ticker_created", "ticker", "calculated_at"),
2593
+ Index("ix_business_quality_scores_score", "business_quality_score"),
2594
+ )
2595
+
2596
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2597
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2598
+ asset_id: Mapped[int | None] = mapped_column(ForeignKey("assets.id", ondelete="SET NULL"), index=True)
2599
+ ticker: Mapped[str] = mapped_column(String(32), index=True)
2600
+ sector: Mapped[str | None] = mapped_column(String(120), index=True)
2601
+ business_quality_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
2602
+ growth_quality: Mapped[float | None] = mapped_column(Float)
2603
+ profitability_quality: Mapped[float | None] = mapped_column(Float)
2604
+ cash_flow_quality: Mapped[float | None] = mapped_column(Float)
2605
+ balance_sheet_quality: Mapped[float | None] = mapped_column(Float)
2606
+ capital_allocation_quality: Mapped[float | None] = mapped_column(Float)
2607
+ moat_quality: Mapped[float | None] = mapped_column(Float)
2608
+ management_quality: Mapped[float | None] = mapped_column(Float)
2609
+ data_quality_score: Mapped[float] = mapped_column(Float, default=0.0)
2610
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2611
+
2612
+ asset = relationship("Asset")
2613
+
2614
+
2615
+ class PortfolioContribution(Base):
2616
+ __tablename__ = "portfolio_contributions"
2617
+ __table_args__ = (Index("ix_portfolio_contributions_scope", "game_id", "ticker", "calculated_at"),)
2618
+
2619
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2620
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2621
+ game_id: Mapped[int | None] = mapped_column(ForeignKey("trading_games.id", ondelete="CASCADE"), index=True)
2622
+ ticker: Mapped[str] = mapped_column(String(32), index=True)
2623
+ sector: Mapped[str | None] = mapped_column(String(120), index=True)
2624
+ return_contribution: Mapped[float | None] = mapped_column(Float)
2625
+ risk_contribution: Mapped[float | None] = mapped_column(Float)
2626
+ drawdown_contribution: Mapped[float | None] = mapped_column(Float)
2627
+ alpha_contribution: Mapped[float | None] = mapped_column(Float)
2628
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2629
+
2630
+ game = relationship("TradingGame")
2631
+
2632
+
2633
+ class PortfolioCorrelation(Base):
2634
+ __tablename__ = "portfolio_correlations"
2635
+ __table_args__ = (
2636
+ UniqueConstraint("scope", "asset_a", "asset_b", name="uq_portfolio_correlation_pair"),
2637
+ Index("ix_portfolio_correlations_scope", "scope", "correlation"),
2638
+ )
2639
+
2640
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2641
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2642
+ scope: Mapped[str] = mapped_column(String(120), default="active_game", index=True)
2643
+ asset_a: Mapped[str] = mapped_column(String(32), index=True)
2644
+ asset_b: Mapped[str] = mapped_column(String(32), index=True)
2645
+ correlation: Mapped[float | None] = mapped_column(Float, index=True)
2646
+ correlation_type: Mapped[str] = mapped_column(String(80), default="price_return", index=True)
2647
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2648
+
2649
+
2650
+ class PortfolioAlphaScore(Base):
2651
+ __tablename__ = "portfolio_alpha_scores"
2652
+ __table_args__ = (Index("ix_portfolio_alpha_scores_ticker_created", "ticker", "calculated_at"),)
2653
+
2654
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2655
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2656
+ game_id: Mapped[int | None] = mapped_column(ForeignKey("trading_games.id", ondelete="CASCADE"), index=True)
2657
+ ticker: Mapped[str] = mapped_column(String(32), index=True)
2658
+ portfolio_alpha_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
2659
+ marginal_return_score: Mapped[float | None] = mapped_column(Float)
2660
+ marginal_risk_score: Mapped[float | None] = mapped_column(Float)
2661
+ diversification_score: Mapped[float | None] = mapped_column(Float)
2662
+ benchmark_excess_score: Mapped[float | None] = mapped_column(Float)
2663
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2664
+
2665
+ game = relationship("TradingGame")
2666
+
2667
+
2668
+ class PositionSizingOutcome(Base):
2669
+ __tablename__ = "position_sizing_outcomes"
2670
+ __table_args__ = (Index("ix_position_sizing_outcomes_logic", "sizing_logic", "timeframe"),)
2671
+
2672
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2673
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2674
+ sizing_logic: Mapped[str] = mapped_column(String(120), index=True)
2675
+ timeframe: Mapped[str | None] = mapped_column(String(80), index=True)
2676
+ sample_size: Mapped[int] = mapped_column(Integer, default=0, index=True)
2677
+ average_r: Mapped[float | None] = mapped_column(Float)
2678
+ drawdown_impact: Mapped[float | None] = mapped_column(Float)
2679
+ capital_efficiency: Mapped[float | None] = mapped_column(Float)
2680
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2681
+
2682
+
2683
+ class PortfolioQualityScore(Base):
2684
+ __tablename__ = "portfolio_quality_scores"
2685
+ __table_args__ = (
2686
+ Index("ix_portfolio_quality_game_created", "game_id", "calculated_at"),
2687
+ Index("ix_portfolio_quality_score", "portfolio_quality_score"),
2688
+ )
2689
+
2690
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
2691
+ calculated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
2692
+ game_id: Mapped[int | None] = mapped_column(ForeignKey("trading_games.id", ondelete="CASCADE"), index=True)
2693
+ portfolio_quality_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
2694
+ diversification: Mapped[float | None] = mapped_column(Float)
2695
+ concentration_risk: Mapped[float | None] = mapped_column(Float)
2696
+ drawdown_control: Mapped[float | None] = mapped_column(Float)
2697
+ alpha_generation: Mapped[float | None] = mapped_column(Float)
2698
+ benchmark_excess: Mapped[float | None] = mapped_column(Float)
2699
+ capital_efficiency: Mapped[float | None] = mapped_column(Float)
2700
+ explanation: Mapped[str] = mapped_column(Text, default="")
2701
+ warnings_json: Mapped[dict] = mapped_column(JsonType, default=dict)
2702
+
2703
+ game = relationship("TradingGame")
backend/app/services/decision_intelligence.py ADDED
@@ -0,0 +1,927 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter, defaultdict
4
+ from datetime import date, datetime, timedelta
5
+ from math import sqrt
6
+ from statistics import mean, median, pstdev
7
+
8
+ from sqlalchemy import desc, select
9
+ from sqlalchemy.orm import Session
10
+
11
+ from app.models import (
12
+ AlphaCaptureMetric,
13
+ Asset,
14
+ BusinessQualityProfile,
15
+ BusinessQualityScore,
16
+ DecisionSuperiorityScore,
17
+ DecisionUniverseSnapshot,
18
+ FundamentalAlphaPattern,
19
+ FundamentalSnapshot,
20
+ ManagementQualityProfile,
21
+ OpportunityPrecisionMetric,
22
+ OpportunityRecallMetric,
23
+ PortfolioAlphaScore,
24
+ PortfolioContribution,
25
+ PortfolioCorrelation,
26
+ PortfolioQualityScore,
27
+ PositionSizingOutcome,
28
+ PriceHistory,
29
+ RankingAccuracyMetric,
30
+ SignalSnapshot,
31
+ TradingGame,
32
+ TradingGameTrade,
33
+ )
34
+ from app.services.learning_intelligence import (
35
+ BenchmarkComparisonService,
36
+ game_trades,
37
+ latest_trading_game,
38
+ round_or_none,
39
+ statistical_confidence_label,
40
+ )
41
+ from app.services.trade_transparency import clamp, safe_float
42
+ from app.services.trading_intelligence_lab import executable_trades, metric_payload, sample_context
43
+
44
+
45
+ DECISION_INTELLIGENCE_POLICY = (
46
+ "Decision Intelligence measures selection quality against available alternatives, business quality and portfolio context. "
47
+ "It must report insufficient evidence instead of claiming decision superiority without comparable samples."
48
+ )
49
+
50
+
51
+ class DecisionSuperiorityEngine:
52
+ """Measures whether BLUM selected the best available opportunity, not only whether a trade worked."""
53
+
54
+ def dashboard(self, db: Session) -> dict:
55
+ return {
56
+ "status": "ok",
57
+ "policy": DECISION_INTELLIGENCE_POLICY,
58
+ "decision_superiority": self.score(db, persist=False),
59
+ "universe_snapshots": self.universe_snapshots(db, persist=False),
60
+ "top_missed_opportunities": self.top_missed_opportunities(db),
61
+ "best_decisions": self.best_decisions(db),
62
+ "worst_decisions": self.worst_decisions(db),
63
+ }
64
+
65
+ def score(self, db: Session, persist: bool = False) -> dict:
66
+ game = latest_trading_game(db)
67
+ rows = executable_trades(game_trades(db, game.id if game else None))
68
+ decisions = [decision_evidence_for_trade(db, row) for row in rows]
69
+ decisions = [item for item in decisions if item["candidate_count"] >= 2]
70
+ metrics = decision_superiority_metrics(decisions, rows)
71
+ components = decision_superiority_components(metrics, rows)
72
+ score = weighted_average(
73
+ [
74
+ components["opportunity_recall"],
75
+ components["opportunity_precision"],
76
+ components["alpha_capture"],
77
+ components["ranking_accuracy"],
78
+ components["benchmark_excess"],
79
+ components["live_validation"],
80
+ components["regime_consistency"],
81
+ components["reproducibility"],
82
+ components["drawdown_control"],
83
+ ]
84
+ )
85
+ warnings = decision_superiority_warnings(metrics, rows)
86
+ classification = classify_decision_superiority(score)
87
+ explanation = decision_superiority_explanation(score, classification, metrics, warnings)
88
+ payload = {
89
+ "status": "ok" if game else "no_game",
90
+ "calculated_at": datetime.utcnow().isoformat(),
91
+ "mode": "historical_plus_live",
92
+ "scope": "global",
93
+ "score": round(score, 2),
94
+ "classification": classification,
95
+ "components": components,
96
+ "metrics": metrics,
97
+ "warnings": warnings,
98
+ "sample_size": len(decisions),
99
+ "statistical_confidence": statistical_confidence_label(len(decisions), count_live(rows), sample_context(rows)),
100
+ "explanation": explanation,
101
+ "policy": DECISION_INTELLIGENCE_POLICY,
102
+ }
103
+ if persist:
104
+ db.add(
105
+ OpportunityRecallMetric(
106
+ sector="All",
107
+ setup="All",
108
+ regime="All",
109
+ timeframe="All",
110
+ captured_outperformers=metrics.get("captured_outperformers", 0),
111
+ total_outperformers=metrics.get("total_outperformers", 0),
112
+ opportunity_recall=metrics.get("opportunity_recall"),
113
+ evidence_json=metrics,
114
+ )
115
+ )
116
+ db.add(
117
+ OpportunityPrecisionMetric(
118
+ sector="All",
119
+ setup="All",
120
+ regime="All",
121
+ timeframe="All",
122
+ successful_opportunities=metrics.get("successful_opportunities", 0),
123
+ selected_opportunities=metrics.get("selected_opportunities", 0),
124
+ opportunity_precision=metrics.get("opportunity_precision"),
125
+ evidence_json=metrics,
126
+ )
127
+ )
128
+ db.add(
129
+ AlphaCaptureMetric(
130
+ ticker="All",
131
+ sector="All",
132
+ regime="All",
133
+ timeframe="All",
134
+ available_alpha=metrics.get("available_alpha"),
135
+ captured_alpha=metrics.get("captured_alpha"),
136
+ alpha_capture_rate=metrics.get("alpha_capture_rate"),
137
+ evidence_json=metrics,
138
+ )
139
+ )
140
+ db.add(
141
+ RankingAccuracyMetric(
142
+ sector="All",
143
+ setup="All",
144
+ regime="All",
145
+ timeframe="All",
146
+ sample_size=metrics.get("sample_size", 0),
147
+ top1_accuracy=metrics.get("top1_accuracy"),
148
+ top3_accuracy=None,
149
+ top5_accuracy=None,
150
+ ranking_correlation=metrics.get("ranking_accuracy"),
151
+ ranking_decay=None,
152
+ evidence_json=metrics,
153
+ )
154
+ )
155
+ db.add(
156
+ DecisionSuperiorityScore(
157
+ mode=payload["mode"],
158
+ scope=payload["scope"],
159
+ score=payload["score"],
160
+ classification=classification,
161
+ opportunity_recall=metrics.get("opportunity_recall"),
162
+ opportunity_precision=metrics.get("opportunity_precision"),
163
+ alpha_capture=metrics.get("alpha_capture_rate"),
164
+ ranking_accuracy=metrics.get("ranking_accuracy"),
165
+ benchmark_excess=metrics.get("benchmark_excess"),
166
+ live_validation=components["live_validation"],
167
+ regime_consistency=components["regime_consistency"],
168
+ reproducibility=components["reproducibility"],
169
+ drawdown_control=components["drawdown_control"],
170
+ explanation=explanation,
171
+ warnings_json={"warnings": warnings, "metrics": metrics},
172
+ )
173
+ )
174
+ db.commit()
175
+ return payload
176
+
177
+ def universe_snapshots(self, db: Session, persist: bool = False) -> dict:
178
+ game = latest_trading_game(db)
179
+ rows = executable_trades(game_trades(db, game.id if game else None))[:80]
180
+ snapshots = [decision_universe_snapshot_payload(db, row) for row in rows]
181
+ if persist:
182
+ for item in snapshots:
183
+ db.add(
184
+ DecisionUniverseSnapshot(
185
+ timestamp=parse_dt(item["timestamp"]) or datetime.utcnow(),
186
+ market_regime=item.get("market_regime"),
187
+ volatility_regime=item.get("volatility_regime"),
188
+ selected_asset=item["selected_asset"],
189
+ selected_rank=item.get("selected_rank"),
190
+ selected_score=item.get("selected_score"),
191
+ total_candidates=item.get("total_candidates", 0),
192
+ candidates_json={"candidates": item.get("candidates", [])},
193
+ benchmark_snapshot=item.get("benchmark_snapshot", {}),
194
+ )
195
+ )
196
+ db.commit()
197
+ return {"status": "ok" if game else "no_game", "snapshots": snapshots, "policy": DECISION_INTELLIGENCE_POLICY}
198
+
199
+ def top_missed_opportunities(self, db: Session) -> list[dict]:
200
+ rows = executable_trades(game_trades(db, latest_trading_game(db).id if latest_trading_game(db) else None))
201
+ missed = []
202
+ for row in rows:
203
+ evidence = decision_evidence_for_trade(db, row)
204
+ if evidence.get("missed_best"):
205
+ missed.append(evidence)
206
+ return sorted(missed, key=lambda item: safe_float(item.get("opportunity_gap")), reverse=True)[:12]
207
+
208
+ def best_decisions(self, db: Session) -> list[dict]:
209
+ rows = executable_trades(game_trades(db, latest_trading_game(db).id if latest_trading_game(db) else None))
210
+ evidence = [decision_evidence_for_trade(db, row) for row in rows]
211
+ return sorted(evidence, key=lambda item: safe_float(item.get("selection_quality")), reverse=True)[:12]
212
+
213
+ def worst_decisions(self, db: Session) -> list[dict]:
214
+ rows = executable_trades(game_trades(db, latest_trading_game(db).id if latest_trading_game(db) else None))
215
+ evidence = [decision_evidence_for_trade(db, row) for row in rows]
216
+ return sorted(evidence, key=lambda item: safe_float(item.get("selection_quality")))[:12]
217
+
218
+
219
+ class BusinessQualityEngine:
220
+ """Scores business quality from stored fundamental evidence and clearly penalizes missing data."""
221
+
222
+ def dashboard(self, db: Session, limit: int = 40) -> dict:
223
+ rows = self.scores(db, limit=limit, persist=False)["rows"]
224
+ return {
225
+ "status": "ok",
226
+ "policy": DECISION_INTELLIGENCE_POLICY,
227
+ "highest_quality_companies": rows[:12],
228
+ "strongest_moats": sorted(rows, key=lambda item: safe_float(item.get("moat_quality")), reverse=True)[:12],
229
+ "best_capital_allocation": sorted(rows, key=lambda item: safe_float(item.get("capital_allocation_quality")), reverse=True)[:12],
230
+ "highest_fundamental_alpha_score": sorted(rows, key=lambda item: safe_float(item.get("fundamental_alpha_score")), reverse=True)[:12],
231
+ "improving_businesses": [row for row in rows if row.get("trend_label") == "improving"][:12],
232
+ "deteriorating_businesses": [row for row in rows if row.get("trend_label") == "deteriorating"][:12],
233
+ "fundamental_alpha_patterns": self.fundamental_alpha_patterns(db, persist=False)["rows"],
234
+ }
235
+
236
+ def scores(self, db: Session, limit: int = 80, persist: bool = False) -> dict:
237
+ assets = db.scalars(
238
+ select(Asset)
239
+ .where(Asset.asset_type == "Stock", Asset.is_active.is_(True))
240
+ .order_by(Asset.ticker)
241
+ .limit(limit)
242
+ ).all()
243
+ rows = [business_quality_for_asset(db, asset) for asset in assets]
244
+ rows = sorted(rows, key=lambda item: safe_float(item.get("business_quality_score")), reverse=True)
245
+ if persist:
246
+ for item in rows:
247
+ asset = db.scalar(select(Asset).where(Asset.ticker == item["ticker"]))
248
+ db.add(
249
+ BusinessQualityProfile(
250
+ asset_id=asset.id if asset else None,
251
+ ticker=item["ticker"],
252
+ growth_quality=item.get("growth_quality"),
253
+ profitability_quality=item.get("profitability_quality"),
254
+ cash_flow_quality=item.get("cash_flow_quality"),
255
+ balance_sheet_quality=item.get("balance_sheet_quality"),
256
+ capital_allocation_quality=item.get("capital_allocation_quality"),
257
+ moat_quality=item.get("moat_quality"),
258
+ evidence_json=item.get("evidence", {}),
259
+ )
260
+ )
261
+ db.add(
262
+ ManagementQualityProfile(
263
+ asset_id=asset.id if asset else None,
264
+ ticker=item["ticker"],
265
+ insider_alignment=item.get("management_components", {}).get("insider_alignment"),
266
+ execution_consistency=item.get("management_components", {}).get("execution_consistency"),
267
+ earnings_delivery=item.get("management_components", {}).get("earnings_delivery"),
268
+ management_quality=item.get("management_quality"),
269
+ evidence_json=item.get("management_components", {}),
270
+ )
271
+ )
272
+ db.add(
273
+ BusinessQualityScore(
274
+ asset_id=asset.id if asset else None,
275
+ ticker=item["ticker"],
276
+ sector=item.get("sector"),
277
+ business_quality_score=item["business_quality_score"],
278
+ growth_quality=item.get("growth_quality"),
279
+ profitability_quality=item.get("profitability_quality"),
280
+ cash_flow_quality=item.get("cash_flow_quality"),
281
+ balance_sheet_quality=item.get("balance_sheet_quality"),
282
+ capital_allocation_quality=item.get("capital_allocation_quality"),
283
+ moat_quality=item.get("moat_quality"),
284
+ management_quality=item.get("management_quality"),
285
+ data_quality_score=item.get("data_quality_score", 0),
286
+ evidence_json=item.get("evidence", {}),
287
+ )
288
+ )
289
+ db.commit()
290
+ return {"status": "ok", "rows": rows, "policy": DECISION_INTELLIGENCE_POLICY}
291
+
292
+ def fundamental_alpha_patterns(self, db: Session, persist: bool = False) -> dict:
293
+ rows = []
294
+ trades = executable_trades(game_trades(db, latest_trading_game(db).id if latest_trading_game(db) else None))
295
+ by_sector: dict[str, list[TradingGameTrade]] = defaultdict(list)
296
+ for trade in trades:
297
+ by_sector[trade.sector or "Unknown"].append(trade)
298
+ for sector, group in by_sector.items():
299
+ returns = [safe_float(row.pnl_percent if row.pnl_percent is not None else row.excess_return_vs_benchmark) for row in group]
300
+ rows.append(
301
+ {
302
+ "pattern_name": "quality_plus_positive_trade_outcome",
303
+ "sector": sector,
304
+ "timeframe": "trade_horizon",
305
+ "sample_size": len(group),
306
+ "average_forward_return": round_or_none(mean(returns) if returns else None),
307
+ "hit_rate": round_or_none(sum(1 for value in returns if value > 0) / max(1, len(returns))),
308
+ "evidence": {"tickers": sorted({row.ticker for row in group})[:20]},
309
+ }
310
+ )
311
+ if persist:
312
+ for item in rows:
313
+ db.add(
314
+ FundamentalAlphaPattern(
315
+ pattern_name=item["pattern_name"],
316
+ sector=item.get("sector"),
317
+ timeframe=item.get("timeframe"),
318
+ sample_size=item["sample_size"],
319
+ average_forward_return=item.get("average_forward_return"),
320
+ hit_rate=item.get("hit_rate"),
321
+ evidence_json=item.get("evidence", {}),
322
+ )
323
+ )
324
+ db.commit()
325
+ return {"status": "ok", "rows": rows, "policy": DECISION_INTELLIGENCE_POLICY}
326
+
327
+
328
+ class PortfolioIntelligenceEngine:
329
+ """Measures whether individual decisions improve the simulated portfolio."""
330
+
331
+ def dashboard(self, db: Session) -> dict:
332
+ return {
333
+ "status": "ok",
334
+ "policy": DECISION_INTELLIGENCE_POLICY,
335
+ "portfolio_quality": self.quality_score(db, persist=False),
336
+ "contributions": self.contributions(db, persist=False)["rows"],
337
+ "correlations": self.correlations(db, persist=False)["rows"],
338
+ "portfolio_alpha": self.alpha_scores(db, persist=False)["rows"],
339
+ "position_sizing": self.position_sizing_outcomes(db, persist=False)["rows"],
340
+ }
341
+
342
+ def quality_score(self, db: Session, persist: bool = False) -> dict:
343
+ game = latest_trading_game(db)
344
+ rows = executable_trades(game_trades(db, game.id if game else None))
345
+ metrics = metric_payload(rows, "portfolio", str(game.id) if game else None, "all", None)
346
+ contributions = portfolio_contribution_rows(rows)
347
+ concentration = concentration_score(contributions)
348
+ diversification = clamp(100 - concentration)
349
+ drawdown_control = clamp(100 + safe_float(metrics.get("max_drawdown")) * 3)
350
+ alpha_generation = clamp(50 + safe_float(metrics.get("benchmark_excess")) * 2)
351
+ capital_efficiency = clamp(50 + safe_float(metrics.get("expectancy_r")) * 25)
352
+ score = weighted_average([diversification, drawdown_control, alpha_generation, capital_efficiency, safe_float(metrics.get("risk_reward_quality_score"), 50)])
353
+ warnings = portfolio_warnings(rows, concentration, metrics)
354
+ explanation = f"Portfolio Quality Score {score:.1f}/100. Diversification {diversification:.1f}, drawdown control {drawdown_control:.1f}, alpha generation {alpha_generation:.1f}."
355
+ payload = {
356
+ "status": "ok" if game else "no_game",
357
+ "score": round(score, 2),
358
+ "components": {
359
+ "diversification": round(diversification, 2),
360
+ "concentration_risk": round(concentration, 2),
361
+ "drawdown_control": round(drawdown_control, 2),
362
+ "alpha_generation": round(alpha_generation, 2),
363
+ "benchmark_excess": round_or_none(metrics.get("benchmark_excess")),
364
+ "capital_efficiency": round(capital_efficiency, 2),
365
+ },
366
+ "sample_size": len(rows),
367
+ "warnings": warnings,
368
+ "explanation": explanation,
369
+ "policy": DECISION_INTELLIGENCE_POLICY,
370
+ }
371
+ if persist:
372
+ db.add(
373
+ PortfolioQualityScore(
374
+ game_id=game.id if game else None,
375
+ portfolio_quality_score=payload["score"],
376
+ diversification=payload["components"]["diversification"],
377
+ concentration_risk=payload["components"]["concentration_risk"],
378
+ drawdown_control=payload["components"]["drawdown_control"],
379
+ alpha_generation=payload["components"]["alpha_generation"],
380
+ benchmark_excess=payload["components"]["benchmark_excess"],
381
+ capital_efficiency=payload["components"]["capital_efficiency"],
382
+ explanation=explanation,
383
+ warnings_json={"warnings": warnings},
384
+ )
385
+ )
386
+ db.commit()
387
+ return payload
388
+
389
+ def contributions(self, db: Session, persist: bool = False) -> dict:
390
+ game = latest_trading_game(db)
391
+ rows = portfolio_contribution_rows(executable_trades(game_trades(db, game.id if game else None)))
392
+ if persist:
393
+ for item in rows:
394
+ db.add(
395
+ PortfolioContribution(
396
+ game_id=game.id if game else None,
397
+ ticker=item["ticker"],
398
+ sector=item.get("sector"),
399
+ return_contribution=item.get("return_contribution"),
400
+ risk_contribution=item.get("risk_contribution"),
401
+ drawdown_contribution=item.get("drawdown_contribution"),
402
+ alpha_contribution=item.get("alpha_contribution"),
403
+ evidence_json=item,
404
+ )
405
+ )
406
+ db.commit()
407
+ return {"status": "ok" if game else "no_game", "rows": rows, "policy": DECISION_INTELLIGENCE_POLICY}
408
+
409
+ def correlations(self, db: Session, persist: bool = False) -> dict:
410
+ game = latest_trading_game(db)
411
+ tickers = sorted({row.ticker for row in executable_trades(game_trades(db, game.id if game else None))})[:14]
412
+ rows = correlation_rows(db, tickers)
413
+ if persist:
414
+ for item in rows:
415
+ db.add(
416
+ PortfolioCorrelation(
417
+ scope=f"game:{game.id}" if game else "global",
418
+ asset_a=item["asset_a"],
419
+ asset_b=item["asset_b"],
420
+ correlation=item.get("correlation"),
421
+ correlation_type="price_return",
422
+ evidence_json=item.get("evidence", {}),
423
+ )
424
+ )
425
+ db.commit()
426
+ return {"status": "ok" if game else "no_game", "rows": rows, "policy": DECISION_INTELLIGENCE_POLICY}
427
+
428
+ def alpha_scores(self, db: Session, persist: bool = False) -> dict:
429
+ game = latest_trading_game(db)
430
+ contributions = portfolio_contribution_rows(executable_trades(game_trades(db, game.id if game else None)))
431
+ rows = []
432
+ for item in contributions:
433
+ score = clamp(50 + safe_float(item.get("alpha_contribution")) * 2 - safe_float(item.get("risk_contribution")) * 0.2 + (20 if safe_float(item.get("return_contribution")) > 0 else -10))
434
+ rows.append({**item, "portfolio_alpha_score": round(score, 2), "marginal_return_score": round(clamp(50 + safe_float(item.get("return_contribution")) * 2), 2), "marginal_risk_score": round(clamp(100 - safe_float(item.get("risk_contribution"))), 2), "diversification_score": None, "benchmark_excess_score": round(clamp(50 + safe_float(item.get("alpha_contribution")) * 2), 2)})
435
+ rows = sorted(rows, key=lambda item: safe_float(item.get("portfolio_alpha_score")), reverse=True)
436
+ if persist:
437
+ for item in rows:
438
+ db.add(
439
+ PortfolioAlphaScore(
440
+ game_id=game.id if game else None,
441
+ ticker=item["ticker"],
442
+ portfolio_alpha_score=item["portfolio_alpha_score"],
443
+ marginal_return_score=item.get("marginal_return_score"),
444
+ marginal_risk_score=item.get("marginal_risk_score"),
445
+ diversification_score=item.get("diversification_score"),
446
+ benchmark_excess_score=item.get("benchmark_excess_score"),
447
+ evidence_json=item,
448
+ )
449
+ )
450
+ db.commit()
451
+ return {"status": "ok" if game else "no_game", "rows": rows, "policy": DECISION_INTELLIGENCE_POLICY}
452
+
453
+ def position_sizing_outcomes(self, db: Session, persist: bool = False) -> dict:
454
+ rows = executable_trades(game_trades(db, latest_trading_game(db).id if latest_trading_game(db) else None))
455
+ buckets: dict[str, list[TradingGameTrade]] = defaultdict(list)
456
+ for row in rows:
457
+ logic = "confidence_adjusted" if safe_float(row.confidence_at_entry) >= 65 else "fixed_fractional"
458
+ if safe_float(row.risk_percent) > 1.5:
459
+ logic = "higher_risk_fractional"
460
+ buckets[logic].append(row)
461
+ output = []
462
+ for logic, group in buckets.items():
463
+ r_values = [safe_float(row.realized_r_multiple) for row in group if row.realized_r_multiple is not None]
464
+ output.append(
465
+ {
466
+ "sizing_logic": logic,
467
+ "timeframe": "trade_horizon",
468
+ "sample_size": len(group),
469
+ "average_r": round_or_none(mean(r_values) if r_values else None),
470
+ "drawdown_impact": round_or_none(min([safe_float(row.pnl_percent) for row in group], default=0)),
471
+ "capital_efficiency": round_or_none(mean([safe_float(row.net_pnl_eur if row.net_pnl_eur is not None else row.realized_pl) for row in group]) if group else None),
472
+ "evidence": {"tickers": sorted({row.ticker for row in group})[:16]},
473
+ }
474
+ )
475
+ if persist:
476
+ for item in output:
477
+ db.add(
478
+ PositionSizingOutcome(
479
+ sizing_logic=item["sizing_logic"],
480
+ timeframe=item.get("timeframe"),
481
+ sample_size=item["sample_size"],
482
+ average_r=item.get("average_r"),
483
+ drawdown_impact=item.get("drawdown_impact"),
484
+ capital_efficiency=item.get("capital_efficiency"),
485
+ evidence_json=item.get("evidence", {}),
486
+ )
487
+ )
488
+ db.commit()
489
+ return {"status": "ok", "rows": output, "policy": DECISION_INTELLIGENCE_POLICY}
490
+
491
+
492
+ class DecisionIntelligenceDashboardService:
493
+ def dashboard(self, db: Session) -> dict:
494
+ return {
495
+ "status": "ok",
496
+ "generated_at": datetime.utcnow().isoformat(),
497
+ "decision": DecisionSuperiorityEngine().dashboard(db),
498
+ "business_quality": BusinessQualityEngine().dashboard(db),
499
+ "portfolio": PortfolioIntelligenceEngine().dashboard(db),
500
+ "policy": DECISION_INTELLIGENCE_POLICY,
501
+ }
502
+
503
+
504
+ def decision_universe_snapshot_payload(db: Session, trade: TradingGameTrade) -> dict:
505
+ evidence = decision_evidence_for_trade(db, trade)
506
+ return {
507
+ "timestamp": (trade.entry_date or trade.created_at.date()).isoformat() if hasattr((trade.entry_date or trade.created_at), "isoformat") else datetime.utcnow().isoformat(),
508
+ "market_regime": trade.market_regime_at_entry or "unknown",
509
+ "volatility_regime": volatility_regime_for_trade(trade),
510
+ "selected_asset": trade.ticker,
511
+ "selected_rank": evidence.get("selected_rank"),
512
+ "selected_score": evidence.get("selected_score"),
513
+ "total_candidates": evidence.get("candidate_count", 0),
514
+ "candidates": evidence.get("candidates", [])[:20],
515
+ "benchmark_snapshot": {"benchmark": trade.benchmark_ticker or "SPY", "benchmark_return": trade.benchmark_return_same_period or trade.benchmark_return},
516
+ }
517
+
518
+
519
+ def decision_evidence_for_trade(db: Session, trade: TradingGameTrade) -> dict:
520
+ candidates = candidate_returns_for_trade(db, trade)
521
+ selected_score = safe_float(trade.sniper_score_at_entry if trade.sniper_score_at_entry is not None else trade.opportunity_score_at_entry)
522
+ if not any(item["ticker"] == trade.ticker for item in candidates):
523
+ candidates.append(
524
+ {
525
+ "ticker": trade.ticker,
526
+ "score": selected_score,
527
+ "realized_return": selected_return(trade),
528
+ "sector": trade.sector,
529
+ "selected": True,
530
+ }
531
+ )
532
+ candidates = dedupe_candidates(candidates)
533
+ ranked = sorted(candidates, key=lambda item: safe_float(item.get("score")), reverse=True)
534
+ realized_sorted = sorted(candidates, key=lambda item: safe_float(item.get("realized_return")), reverse=True)
535
+ selected = next((item for item in ranked if item["ticker"] == trade.ticker), ranked[0] if ranked else None)
536
+ selected_rank = next((index + 1 for index, item in enumerate(ranked) if item["ticker"] == trade.ticker), None)
537
+ best = realized_sorted[0] if realized_sorted else selected
538
+ selected_ret = safe_float(selected.get("realized_return") if selected else selected_return(trade))
539
+ best_ret = safe_float(best.get("realized_return") if best else selected_ret)
540
+ benchmark = safe_float(trade.benchmark_return_same_period if trade.benchmark_return_same_period is not None else trade.benchmark_return)
541
+ opportunity_gap = max(0.0, best_ret - selected_ret)
542
+ selected_outperformed = selected_ret > benchmark and selected_ret > 0
543
+ missed_best = bool(best and best.get("ticker") != trade.ticker and opportunity_gap > 1.0)
544
+ top_score_ticker = ranked[0]["ticker"] if ranked else None
545
+ best_return_ticker = best["ticker"] if best else None
546
+ return {
547
+ "trade_id": trade.id,
548
+ "ticker": trade.ticker,
549
+ "setup_type": trade.setup_type,
550
+ "sector": trade.sector,
551
+ "regime": trade.market_regime_at_entry,
552
+ "timeframe": trade.timeframe,
553
+ "selected_rank": selected_rank,
554
+ "selected_score": round_or_none(selected_score),
555
+ "selected_return": round_or_none(selected_ret),
556
+ "best_available_ticker": best_return_ticker,
557
+ "best_available_return": round_or_none(best_ret),
558
+ "top_ranked_ticker": top_score_ticker,
559
+ "benchmark_return": round_or_none(benchmark),
560
+ "opportunity_gap": round_or_none(opportunity_gap),
561
+ "selected_outperformed": selected_outperformed,
562
+ "missed_best": missed_best,
563
+ "candidate_count": len(candidates),
564
+ "ranking_correct": top_score_ticker == best_return_ticker if top_score_ticker and best_return_ticker else None,
565
+ "selection_quality": round_or_none(clamp(100 - opportunity_gap * 4 + (15 if selected_outperformed else -10))),
566
+ "candidates": ranked[:20],
567
+ }
568
+
569
+
570
+ def candidate_returns_for_trade(db: Session, trade: TradingGameTrade) -> list[dict]:
571
+ start = trade.entry_date or (trade.created_at.date() if trade.created_at else None)
572
+ end = trade.exit_date or (start + timedelta(days=30) if start else None)
573
+ if not start or not end:
574
+ return []
575
+ signal_candidates = db.scalars(
576
+ select(SignalSnapshot)
577
+ .where(SignalSnapshot.created_at <= datetime.combine(start, datetime.max.time()))
578
+ .order_by(desc(SignalSnapshot.created_at), desc(SignalSnapshot.blum_score))
579
+ .limit(80)
580
+ ).all()
581
+ seen = set()
582
+ output = []
583
+ for signal in signal_candidates:
584
+ if signal.ticker in seen:
585
+ continue
586
+ seen.add(signal.ticker)
587
+ realized = ticker_return_between(db, signal.ticker, start, end)
588
+ if realized is None:
589
+ continue
590
+ output.append({"ticker": signal.ticker, "score": safe_float(signal.blum_score), "realized_return": round(realized, 4), "sector": signal.asset.sector if signal.asset else None, "selected": signal.ticker == trade.ticker})
591
+ if len(output) >= 20:
592
+ break
593
+ if len(output) < 2:
594
+ same_day = db.scalars(select(TradingGameTrade).where(TradingGameTrade.entry_date == start).limit(30)).all()
595
+ for row in same_day:
596
+ if row.ticker in seen:
597
+ continue
598
+ seen.add(row.ticker)
599
+ output.append({"ticker": row.ticker, "score": safe_float(row.sniper_score_at_entry if row.sniper_score_at_entry is not None else row.opportunity_score_at_entry), "realized_return": selected_return(row), "sector": row.sector, "selected": row.ticker == trade.ticker})
600
+ return output
601
+
602
+
603
+ def decision_superiority_metrics(decisions: list[dict], rows: list[TradingGameTrade]) -> dict:
604
+ if not decisions:
605
+ return {"status": "insufficient_evidence", "sample_size": 0}
606
+ outperformers = [item for item in decisions if item.get("best_available_return") is not None and safe_float(item.get("best_available_return")) > safe_float(item.get("benchmark_return"))]
607
+ captured = [item for item in outperformers if item.get("selected_outperformed")]
608
+ selected_success = [item for item in decisions if item.get("selected_outperformed")]
609
+ available_alpha = sum(max(0, safe_float(item.get("best_available_return")) - safe_float(item.get("benchmark_return"))) for item in decisions)
610
+ captured_alpha = sum(max(0, safe_float(item.get("selected_return")) - safe_float(item.get("benchmark_return"))) for item in decisions)
611
+ ranking_known = [item for item in decisions if item.get("ranking_correct") is not None]
612
+ ranking_accuracy = sum(1 for item in ranking_known if item.get("ranking_correct")) / max(1, len(ranking_known))
613
+ benchmark_excess_values = [safe_float(row.excess_return_vs_benchmark) for row in rows if row.excess_return_vs_benchmark is not None]
614
+ return {
615
+ "status": "ok",
616
+ "sample_size": len(decisions),
617
+ "opportunity_recall": round_or_none(len(captured) / max(1, len(outperformers))),
618
+ "captured_outperformers": len(captured),
619
+ "total_outperformers": len(outperformers),
620
+ "opportunity_precision": round_or_none(len(selected_success) / max(1, len(decisions))),
621
+ "successful_opportunities": len(selected_success),
622
+ "selected_opportunities": len(decisions),
623
+ "alpha_capture_rate": round_or_none(captured_alpha / available_alpha if available_alpha else None),
624
+ "available_alpha": round_or_none(available_alpha),
625
+ "captured_alpha": round_or_none(captured_alpha),
626
+ "ranking_accuracy": round_or_none(ranking_accuracy),
627
+ "top1_accuracy": round_or_none(ranking_accuracy),
628
+ "missed_opportunities": sum(1 for item in decisions if item.get("missed_best")),
629
+ "benchmark_excess": round_or_none(mean(benchmark_excess_values) if benchmark_excess_values else None),
630
+ }
631
+
632
+
633
+ def decision_superiority_components(metrics: dict, rows: list[TradingGameTrade]) -> dict:
634
+ if metrics.get("status") == "insufficient_evidence":
635
+ return {key: 0 for key in ["opportunity_recall", "opportunity_precision", "alpha_capture", "ranking_accuracy", "benchmark_excess", "live_validation", "regime_consistency", "reproducibility", "drawdown_control"]}
636
+ live = [row for row in rows if row.mode == "live_forward_paper"]
637
+ regimes = {row.market_regime_at_entry for row in rows if row.market_regime_at_entry}
638
+ reproducibility = mean([safe_float(row.reproducibility_score) for row in rows]) if rows else 0
639
+ drawdown_values = [safe_float(row.max_adverse_excursion) for row in rows if row.max_adverse_excursion is not None]
640
+ return {
641
+ "opportunity_recall": round(clamp(safe_float(metrics.get("opportunity_recall")) * 100), 2),
642
+ "opportunity_precision": round(clamp(safe_float(metrics.get("opportunity_precision")) * 100), 2),
643
+ "alpha_capture": round(clamp(safe_float(metrics.get("alpha_capture_rate")) * 100), 2),
644
+ "ranking_accuracy": round(clamp(safe_float(metrics.get("ranking_accuracy")) * 100), 2),
645
+ "benchmark_excess": round(clamp(50 + safe_float(metrics.get("benchmark_excess")) * 3), 2),
646
+ "live_validation": round(clamp(min(100, len(live) * 3)), 2),
647
+ "regime_consistency": round(clamp(len(regimes) * 18), 2),
648
+ "reproducibility": round(clamp(reproducibility), 2),
649
+ "drawdown_control": round(clamp(100 + (min(drawdown_values) if drawdown_values else 0) * 2), 2),
650
+ }
651
+
652
+
653
+ def decision_superiority_warnings(metrics: dict, rows: list[TradingGameTrade]) -> list[str]:
654
+ warnings = []
655
+ if metrics.get("sample_size", 0) < 30:
656
+ warnings.append("Insufficient comparable decision samples for a strong superiority claim.")
657
+ if count_live(rows) < 30:
658
+ warnings.append("Live forward evidence is not mature enough to validate selection superiority.")
659
+ if safe_float(metrics.get("alpha_capture_rate")) < 0.5 and metrics.get("alpha_capture_rate") is not None:
660
+ warnings.append("BLUM is leaving more than half of available alpha uncaptured in comparable samples.")
661
+ if safe_float(metrics.get("ranking_accuracy")) < 0.5 and metrics.get("ranking_accuracy") is not None:
662
+ warnings.append("Ranking accuracy is weak; BLUM may identify candidates but order them poorly.")
663
+ return warnings
664
+
665
+
666
+ def business_quality_for_asset(db: Session, asset: Asset) -> dict:
667
+ snapshots = db.scalars(select(FundamentalSnapshot).where(FundamentalSnapshot.asset_id == asset.id).order_by(desc(FundamentalSnapshot.period_end), desc(FundamentalSnapshot.created_at)).limit(6)).all()
668
+ latest = snapshots[0] if snapshots else None
669
+ metrics = latest.metrics if latest else {}
670
+ data_quality = safe_float(latest.quality_score if latest else 0)
671
+ revenue = metric_value(metrics, "revenue")
672
+ net_income = metric_value(metrics, "net_income")
673
+ operating_income = metric_value(metrics, "operating_income")
674
+ operating_cash_flow = metric_value(metrics, "operating_cash_flow")
675
+ capex = abs(metric_value(metrics, "capex") or 0)
676
+ assets = metric_value(metrics, "assets")
677
+ liabilities = metric_value(metrics, "liabilities")
678
+ equity = metric_value(metrics, "equity")
679
+ fcf = operating_cash_flow - capex if operating_cash_flow is not None else None
680
+ growth = trend_score([metric_value(row.metrics, "revenue") for row in snapshots])
681
+ profitability = clamp(45 + ratio_pct(net_income, revenue) * 2 + ratio_pct(operating_income, revenue))
682
+ cash_flow = clamp(45 + ratio_pct(fcf, revenue) * 2)
683
+ balance = clamp(55 + ratio_pct(equity, assets) - ratio_pct(liabilities, assets) * 0.35)
684
+ capital_allocation = clamp(50 + ratio_pct(fcf, assets) * 3)
685
+ moat = moat_score(asset, profitability, cash_flow)
686
+ management = management_score(data_quality, growth, profitability)
687
+ raw_score = weighted_average([growth, profitability, cash_flow, balance, capital_allocation, moat, management])
688
+ score = clamp(raw_score * (0.45 + min(0.55, data_quality / 100)))
689
+ status = "ready" if latest else "insufficient_fundamental_evidence"
690
+ trend = "improving" if growth >= 62 and profitability >= 55 else "deteriorating" if growth < 38 or profitability < 35 else "stable"
691
+ return {
692
+ "ticker": asset.ticker,
693
+ "name": asset.name,
694
+ "sector": asset.sector,
695
+ "business_quality_score": round(score, 2),
696
+ "growth_quality": round(growth, 2),
697
+ "profitability_quality": round(profitability, 2),
698
+ "cash_flow_quality": round(cash_flow, 2),
699
+ "balance_sheet_quality": round(balance, 2),
700
+ "capital_allocation_quality": round(capital_allocation, 2),
701
+ "moat_quality": round(moat, 2),
702
+ "management_quality": round(management, 2),
703
+ "fundamental_alpha_score": round(clamp(score * 0.65 + growth * 0.2 + cash_flow * 0.15), 2),
704
+ "data_quality_score": round(data_quality, 2),
705
+ "status": status,
706
+ "trend_label": trend,
707
+ "evidence": {
708
+ "period_end": latest.period_end.isoformat() if latest and latest.period_end else None,
709
+ "provider": latest.provider if latest else None,
710
+ "metrics_available": sorted(metrics.keys()) if isinstance(metrics, dict) else [],
711
+ "warning": None if latest else "No stored fundamental snapshot; score is penalized and should not be treated as business-quality proof.",
712
+ },
713
+ "management_components": {
714
+ "insider_alignment": None,
715
+ "execution_consistency": round(clamp((data_quality + growth) / 2), 2),
716
+ "earnings_delivery": None,
717
+ },
718
+ }
719
+
720
+
721
+ def portfolio_contribution_rows(rows: list[TradingGameTrade]) -> list[dict]:
722
+ total_pl = sum(safe_float(row.net_pnl_eur if row.net_pnl_eur is not None else row.realized_pl) for row in rows)
723
+ total_risk = sum(abs(safe_float(row.max_adverse_excursion if row.max_adverse_excursion is not None else row.risk_amount)) for row in rows)
724
+ grouped: dict[str, list[TradingGameTrade]] = defaultdict(list)
725
+ for row in rows:
726
+ grouped[row.ticker].append(row)
727
+ output = []
728
+ for ticker, group in grouped.items():
729
+ pl = sum(safe_float(row.net_pnl_eur if row.net_pnl_eur is not None else row.realized_pl) for row in group)
730
+ risk = sum(abs(safe_float(row.max_adverse_excursion if row.max_adverse_excursion is not None else row.risk_amount)) for row in group)
731
+ alpha = sum(safe_float(row.excess_return_vs_benchmark) for row in group if row.excess_return_vs_benchmark is not None)
732
+ output.append(
733
+ {
734
+ "ticker": ticker,
735
+ "sector": group[0].sector,
736
+ "trades": len(group),
737
+ "return_contribution": round_or_none(pl / total_pl * 100 if total_pl else None),
738
+ "risk_contribution": round_or_none(risk / total_risk * 100 if total_risk else None),
739
+ "drawdown_contribution": round_or_none(min([safe_float(row.pnl_percent) for row in group], default=0)),
740
+ "alpha_contribution": round_or_none(alpha / max(1, len(group))),
741
+ }
742
+ )
743
+ return sorted(output, key=lambda item: abs(safe_float(item.get("return_contribution"))), reverse=True)
744
+
745
+
746
+ def correlation_rows(db: Session, tickers: list[str]) -> list[dict]:
747
+ output = []
748
+ series = {ticker: daily_returns(db, ticker) for ticker in tickers}
749
+ for index, ticker_a in enumerate(tickers):
750
+ for ticker_b in tickers[index + 1 :]:
751
+ corr = pearson(series.get(ticker_a, []), series.get(ticker_b, []))
752
+ output.append({"asset_a": ticker_a, "asset_b": ticker_b, "correlation": round_or_none(corr), "evidence": {"points": min(len(series.get(ticker_a, [])), len(series.get(ticker_b, [])))}})
753
+ return sorted(output, key=lambda item: abs(safe_float(item.get("correlation"))), reverse=True)[:40]
754
+
755
+
756
+ def candidate_scope_metric_rows(decisions: list[dict], key: str) -> list[dict]:
757
+ grouped: dict[str, list[dict]] = defaultdict(list)
758
+ for item in decisions:
759
+ grouped[str(item.get(key) or "Unknown")].append(item)
760
+ return [{"scope": key, "entity": entity, **decision_superiority_metrics(items, [])} for entity, items in grouped.items()]
761
+
762
+
763
+ def classify_decision_superiority(score: float) -> str:
764
+ if score <= 20:
765
+ return "Weak"
766
+ if score <= 40:
767
+ return "Experimental"
768
+ if score <= 60:
769
+ return "Learning"
770
+ if score <= 75:
771
+ return "Competitive"
772
+ if score <= 90:
773
+ return "Strong Alpha Research"
774
+ return "Exceptional"
775
+
776
+
777
+ def decision_superiority_explanation(score: float, classification: str, metrics: dict, warnings: list[str]) -> str:
778
+ if metrics.get("status") == "insufficient_evidence":
779
+ return "Insufficient evidence. BLUM does not yet have enough comparable decision snapshots to claim selection superiority."
780
+ base = f"Decision Superiority Score {score:.1f}/100 ({classification}). Opportunity recall {pct(metrics.get('opportunity_recall'))}, precision {pct(metrics.get('opportunity_precision'))}, alpha capture {pct(metrics.get('alpha_capture_rate'))}."
781
+ if warnings:
782
+ base += f" Main warning: {warnings[0]}"
783
+ return base
784
+
785
+
786
+ def selected_return(trade: TradingGameTrade) -> float:
787
+ if trade.pnl_percent is not None:
788
+ return safe_float(trade.pnl_percent)
789
+ if trade.entry_price and trade.exit_price:
790
+ return (trade.exit_price / trade.entry_price - 1) * 100
791
+ return safe_float(trade.excess_return_vs_benchmark if trade.excess_return_vs_benchmark is not None else trade.realized_r_multiple)
792
+
793
+
794
+ def ticker_return_between(db: Session, ticker: str, start: date, end: date) -> float | None:
795
+ asset = db.scalar(select(Asset).where(Asset.ticker == ticker))
796
+ if not asset:
797
+ return None
798
+ start_row = db.scalar(select(PriceHistory).where(PriceHistory.asset_id == asset.id, PriceHistory.date >= start).order_by(PriceHistory.date).limit(1))
799
+ end_row = db.scalar(select(PriceHistory).where(PriceHistory.asset_id == asset.id, PriceHistory.date <= end).order_by(desc(PriceHistory.date)).limit(1))
800
+ if not start_row or not end_row or not start_row.close:
801
+ return None
802
+ return (safe_float(end_row.close) / safe_float(start_row.close) - 1) * 100
803
+
804
+
805
+ def dedupe_candidates(candidates: list[dict]) -> list[dict]:
806
+ best: dict[str, dict] = {}
807
+ for item in candidates:
808
+ ticker = item.get("ticker")
809
+ if not ticker:
810
+ continue
811
+ if ticker not in best or safe_float(item.get("score")) > safe_float(best[ticker].get("score")):
812
+ best[ticker] = item
813
+ return list(best.values())
814
+
815
+
816
+ def weighted_average(values: list[float | None]) -> float:
817
+ clean = [safe_float(value) for value in values if value is not None]
818
+ return clamp(mean(clean) if clean else 0)
819
+
820
+
821
+ def pct(value: float | None) -> str:
822
+ return "n/a" if value is None else f"{safe_float(value) * 100:.1f}%"
823
+
824
+
825
+ def count_live(rows: list[TradingGameTrade]) -> int:
826
+ return sum(1 for row in rows if row.mode == "live_forward_paper")
827
+
828
+
829
+ def volatility_regime_for_trade(trade: TradingGameTrade) -> str:
830
+ mae = abs(safe_float(trade.max_adverse_excursion))
831
+ if mae >= 10:
832
+ return "high_volatility"
833
+ if mae <= 2:
834
+ return "low_volatility"
835
+ return "normal_volatility"
836
+
837
+
838
+ def metric_value(metrics: dict, key: str) -> float | None:
839
+ raw = metrics.get(key) if isinstance(metrics, dict) else None
840
+ if isinstance(raw, dict):
841
+ raw = raw.get("value")
842
+ if raw is None:
843
+ return None
844
+ try:
845
+ return float(raw)
846
+ except (TypeError, ValueError):
847
+ return None
848
+
849
+
850
+ def ratio_pct(numerator: float | None, denominator: float | None) -> float:
851
+ if numerator is None or denominator in (None, 0):
852
+ return 0.0
853
+ return numerator / denominator * 100
854
+
855
+
856
+ def trend_score(values: list[float | None]) -> float:
857
+ clean = [float(value) for value in values if value not in (None, 0)]
858
+ if len(clean) < 2:
859
+ return 45.0
860
+ latest, oldest = clean[0], clean[-1]
861
+ if oldest == 0:
862
+ return 45.0
863
+ return clamp(50 + (latest / oldest - 1) * 100)
864
+
865
+
866
+ def moat_score(asset: Asset, profitability: float, cash_flow: float) -> float:
867
+ text = f"{asset.category} {asset.sector} {asset.industry} {asset.description}".lower()
868
+ boost = 0
869
+ for token in ["platform", "ecosystem", "semiconductor", "cloud", "security", "defense", "luxury", "healthcare", "ai"]:
870
+ if token in text:
871
+ boost += 4
872
+ return clamp(45 + boost + (profitability - 50) * 0.25 + (cash_flow - 50) * 0.2)
873
+
874
+
875
+ def management_score(data_quality: float, growth: float, profitability: float) -> float:
876
+ return clamp(42 + data_quality * 0.25 + growth * 0.2 + profitability * 0.2)
877
+
878
+
879
+ def concentration_score(contributions: list[dict]) -> float:
880
+ values = sorted([abs(safe_float(item.get("return_contribution"))) for item in contributions], reverse=True)
881
+ if not values:
882
+ return 100.0
883
+ return clamp(sum(values[:3]))
884
+
885
+
886
+ def portfolio_warnings(rows: list[TradingGameTrade], concentration: float, metrics: dict) -> list[str]:
887
+ warnings = []
888
+ if len(rows) < 30:
889
+ warnings.append("Portfolio evidence is still low sample.")
890
+ if concentration >= 70:
891
+ warnings.append("Portfolio result is concentrated in the top contributors.")
892
+ if safe_float(metrics.get("benchmark_excess")) < 0:
893
+ warnings.append("Portfolio is underperforming its benchmark context.")
894
+ return warnings
895
+
896
+
897
+ def daily_returns(db: Session, ticker: str) -> list[float]:
898
+ asset = db.scalar(select(Asset).where(Asset.ticker == ticker))
899
+ if not asset:
900
+ return []
901
+ rows = db.scalars(select(PriceHistory).where(PriceHistory.asset_id == asset.id).order_by(desc(PriceHistory.date)).limit(260)).all()
902
+ values = [safe_float(row.close) for row in reversed(rows) if row.close]
903
+ return [(values[index] / values[index - 1] - 1) * 100 for index in range(1, len(values)) if values[index - 1]]
904
+
905
+
906
+ def pearson(a: list[float], b: list[float]) -> float | None:
907
+ n = min(len(a), len(b))
908
+ if n < 20:
909
+ return None
910
+ x, y = a[-n:], b[-n:]
911
+ mean_x, mean_y = mean(x), mean(y)
912
+ numerator = sum((xv - mean_x) * (yv - mean_y) for xv, yv in zip(x, y))
913
+ den_x = sqrt(sum((xv - mean_x) ** 2 for xv in x))
914
+ den_y = sqrt(sum((yv - mean_y) ** 2 for yv in y))
915
+ if den_x == 0 or den_y == 0:
916
+ return None
917
+ return numerator / (den_x * den_y)
918
+
919
+
920
+ def parse_dt(value: str | None) -> datetime | None:
921
+ if not value:
922
+ return None
923
+ try:
924
+ parsed = datetime.fromisoformat(value)
925
+ return parsed if isinstance(parsed, datetime) else datetime.combine(parsed, datetime.min.time())
926
+ except ValueError:
927
+ return None
backend/app/services/financial_chat.py CHANGED
@@ -33,6 +33,7 @@ from app.services.fundamentals import fundamentals_for_asset
33
  from app.services.live import market_sentiment
34
  from app.services.learning_loop import LearningDashboardService
35
  from app.services.learning_intelligence import LearningIntelligenceDashboardService
 
36
  from app.services.market_data import market_snapshot_for_asset
37
  from app.services.market_sniper import MarketSniperEngine
38
  from app.services.reasoning_precision import (
@@ -483,6 +484,7 @@ def trading_game_context_for_chat(db: Session) -> dict:
483
  "live_forward": LiveForwardPaperTradingService().status(db),
484
  "historical_vs_live": HistoricalLiveComparisonService().compare(db),
485
  "learning_intelligence": LearningIntelligenceDashboardService().dashboard(db),
 
486
  "pnl_breakdown": PnLBreakdownService().game_breakdown(db),
487
  "reality_check": TradingGameRealityCheckService().evaluate(db),
488
  "failures": engine.failures(db, limit=12),
@@ -1469,6 +1471,7 @@ def build_trading_game_response(language: str, context: dict) -> dict:
1469
  pnl_breakdown = context.get("pnl_breakdown") or {}
1470
  reality_check = context.get("reality_check") or {}
1471
  learning_intelligence = context.get("learning_intelligence") or {}
 
1472
  if not game:
1473
  message = "BLUM non ha ancora un Trading Game persistito. Serve almeno un ciclo Sniper/Learning Loop per creare simulazioni P/L reali." if language == "it" else "BLUM does not have a persisted Trading Game yet. It needs at least one Sniper/Learning Loop cycle to create real P/L simulations."
1474
  return build_error_response(language, message, [])
@@ -1495,6 +1498,7 @@ def build_trading_game_response(language: str, context: dict) -> dict:
1495
  "Nessuna dichiarazione di outperformance e valida se il campione e piccolo o incompleto.",
1496
  ]},
1497
  {"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
 
1498
  {"key": "trade_ledger", "title": "Trade ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
1499
  {"key": "intelligence_metrics", "title": "Trading intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
1500
  {"key": "live_forward", "title": "Storico vs live paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
@@ -1533,6 +1537,7 @@ def build_trading_game_response(language: str, context: dict) -> dict:
1533
  "No outperformance claim is valid when sample size or benchmark coverage is insufficient.",
1534
  ]},
1535
  {"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
 
1536
  {"key": "trade_ledger", "title": "Trade Ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
1537
  {"key": "intelligence_metrics", "title": "Trading Intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
1538
  {"key": "live_forward", "title": "Historical vs Live Paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
@@ -1562,7 +1567,7 @@ def build_trading_game_response(language: str, context: dict) -> dict:
1562
  "executive_view": summary,
1563
  "risk_reward_view": f"Expectancy {format_number(game.get('expectancy_r'))}R, drawdown {format_signed(game.get('max_drawdown'))}%.",
1564
  "data_quality": {"sample_warning": sample_warning, "trades": game.get("trade_count"), "reproducibility": reproducibility, "cycles": cycle_stats, "live_sample_warning": (historical_vs_live.get("sample_warning") if isinstance(historical_vs_live, dict) else None)},
1565
- "learning_loop_memory": {"trading_game": game, "lessons": lessons[:6], "latest_trades": trades[:6], "ledger": ledger_rows[:8], "reality_check": reality_check, "cycles": cycle_stats, "intelligence_metrics": intelligence_metrics, "historical_vs_live": historical_vs_live, "learning_intelligence": learning_intelligence},
1566
  "answer_to_user": summary,
1567
  }
1568
 
@@ -1602,6 +1607,42 @@ def learning_intelligence_lines(payload: dict, language: str) -> list[str]:
1602
  return dedupe_warnings(lines)
1603
 
1604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1605
  def trade_ledger_lines(rows: list[dict], language: str) -> list[str]:
1606
  if not rows:
1607
  return ["BLUM ha metriche di gioco, ma il trade ledger dettagliato non e ancora disponibile." if language == "it" else "BLUM has game-level metrics, but the detailed trade ledger is not available yet."]
@@ -2144,6 +2185,7 @@ def summarize_trading_game_context(context: dict) -> dict:
2144
  intelligence = (context or {}).get("intelligence_metrics") or {}
2145
  historical_vs_live = (context or {}).get("historical_vs_live") or {}
2146
  learning_intelligence = (context or {}).get("learning_intelligence") or {}
 
2147
  return {
2148
  "current_game": {
2149
  "status": game.get("status"),
@@ -2189,6 +2231,12 @@ def summarize_trading_game_context(context: dict) -> dict:
2189
  "weakness_map": (learning_intelligence.get("weakness_map") or {}).get("rows", [])[:6],
2190
  "self_improvement": (learning_intelligence.get("self_improvement") or {}).get("actions", [])[:6],
2191
  },
 
 
 
 
 
 
2192
  }
2193
 
2194
 
@@ -2388,7 +2436,7 @@ def infer_intent(message: str, mode: str | None = None) -> str:
2388
  return "fundamental_analysis"
2389
  if any(term in normalized for term in ["tesi", "thesis", "convinzione", "conviction", "ancora valida", "still valid", "sopravviss", "survival", "decay", "decad", "bull bear neutral", "tesi bull", "tesi bear", "tesi neutral", "motore", "engine vote", "sta migliorando", "dove ha sbagliato", "reasoning core", "batte spy", "batte qqq", "vs spy", "vs qqq"]):
2390
  return "reasoning_memory_question"
2391
- if any(term in normalized for term in ["capitale virtuale", "trading game", "sta battendo", "batte il mercato", "benchmark", "drawdown", "profit factor", "expectancy", "p/l", "pl ", "peggior errore", "andato a zero", "rischio per trade", "riproducibil", "reproducib", "win rate", "quali trade", "trade hanno", "dove e entrato", "dove e uscito", "entrato blum", "uscito blum", "per azione", "fortuna", "profitto arriva", "ledger", "trade piu importante", "100 eur", "10,000", "10000", "target cycle", "ciclo capitale", "cicli capitale", "quante volte", "live paper", "forward paper", "storico vs live", "historical vs live", "sta migliorando", "intelligence growth", "missed entry", "stop hit", "target hit", "trading power", "power score", "dove e scarso", "piu scarso", "weakness", "self improvement", "auto miglior", "prossima azione", "baseline semplice", "stiamo battendo spy", "stiamo battendo qqq"]):
2392
  return "trading_game"
2393
  if any(term in normalized for term in ["sniper", "entrabile", "meglio aspettare", "ingresso", "entry", "risk/reward", "uscita", "exit", "target", "invalidazione", "invalidation", "profitto", "take profit"]):
2394
  return "market_sniper"
 
33
  from app.services.live import market_sentiment
34
  from app.services.learning_loop import LearningDashboardService
35
  from app.services.learning_intelligence import LearningIntelligenceDashboardService
36
+ from app.services.decision_intelligence import DecisionIntelligenceDashboardService
37
  from app.services.market_data import market_snapshot_for_asset
38
  from app.services.market_sniper import MarketSniperEngine
39
  from app.services.reasoning_precision import (
 
484
  "live_forward": LiveForwardPaperTradingService().status(db),
485
  "historical_vs_live": HistoricalLiveComparisonService().compare(db),
486
  "learning_intelligence": LearningIntelligenceDashboardService().dashboard(db),
487
+ "decision_intelligence": DecisionIntelligenceDashboardService().dashboard(db),
488
  "pnl_breakdown": PnLBreakdownService().game_breakdown(db),
489
  "reality_check": TradingGameRealityCheckService().evaluate(db),
490
  "failures": engine.failures(db, limit=12),
 
1471
  pnl_breakdown = context.get("pnl_breakdown") or {}
1472
  reality_check = context.get("reality_check") or {}
1473
  learning_intelligence = context.get("learning_intelligence") or {}
1474
+ decision_intelligence = context.get("decision_intelligence") or {}
1475
  if not game:
1476
  message = "BLUM non ha ancora un Trading Game persistito. Serve almeno un ciclo Sniper/Learning Loop per creare simulazioni P/L reali." if language == "it" else "BLUM does not have a persisted Trading Game yet. It needs at least one Sniper/Learning Loop cycle to create real P/L simulations."
1477
  return build_error_response(language, message, [])
 
1498
  "Nessuna dichiarazione di outperformance e valida se il campione e piccolo o incompleto.",
1499
  ]},
1500
  {"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
1501
+ {"key": "decision_intelligence", "title": "Decision Intelligence", "bullets": decision_intelligence_lines(decision_intelligence, language)},
1502
  {"key": "trade_ledger", "title": "Trade ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
1503
  {"key": "intelligence_metrics", "title": "Trading intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
1504
  {"key": "live_forward", "title": "Storico vs live paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
 
1537
  "No outperformance claim is valid when sample size or benchmark coverage is insufficient.",
1538
  ]},
1539
  {"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
1540
+ {"key": "decision_intelligence", "title": "Decision Intelligence", "bullets": decision_intelligence_lines(decision_intelligence, language)},
1541
  {"key": "trade_ledger", "title": "Trade Ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
1542
  {"key": "intelligence_metrics", "title": "Trading Intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
1543
  {"key": "live_forward", "title": "Historical vs Live Paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
 
1567
  "executive_view": summary,
1568
  "risk_reward_view": f"Expectancy {format_number(game.get('expectancy_r'))}R, drawdown {format_signed(game.get('max_drawdown'))}%.",
1569
  "data_quality": {"sample_warning": sample_warning, "trades": game.get("trade_count"), "reproducibility": reproducibility, "cycles": cycle_stats, "live_sample_warning": (historical_vs_live.get("sample_warning") if isinstance(historical_vs_live, dict) else None)},
1570
+ "learning_loop_memory": {"trading_game": game, "lessons": lessons[:6], "latest_trades": trades[:6], "ledger": ledger_rows[:8], "reality_check": reality_check, "cycles": cycle_stats, "intelligence_metrics": intelligence_metrics, "historical_vs_live": historical_vs_live, "learning_intelligence": learning_intelligence, "decision_intelligence": decision_intelligence},
1571
  "answer_to_user": summary,
1572
  }
1573
 
 
1607
  return dedupe_warnings(lines)
1608
 
1609
 
1610
+ def decision_intelligence_lines(payload: dict, language: str) -> list[str]:
1611
+ if not payload or payload.get("status") == "unavailable":
1612
+ return ["Decision Intelligence non disponibile: non invento opportunita mancate o qualita portfolio." if language == "it" else "Decision Intelligence is unavailable; I will not invent missed opportunities or portfolio-quality evidence."]
1613
+ decision = ((payload.get("decision") or {}).get("decision_superiority") or {})
1614
+ business = (payload.get("business_quality") or {}).get("highest_quality_companies") or []
1615
+ portfolio = ((payload.get("portfolio") or {}).get("portfolio_quality") or {})
1616
+ missed = (payload.get("decision") or {}).get("top_missed_opportunities") or []
1617
+ if language == "it":
1618
+ lines = [
1619
+ f"Decision Superiority Score: {format_number(decision.get('score'))}/100 | {decision.get('classification', 'insufficient evidence')} | campione {decision.get('sample_size', 0)}.",
1620
+ f"Opportunity recall {format_pct(decision.get('metrics', {}).get('opportunity_recall'))}; precision {format_pct(decision.get('metrics', {}).get('opportunity_precision'))}; alpha capture {format_pct(decision.get('metrics', {}).get('alpha_capture_rate'))}.",
1621
+ f"Portfolio Quality Score: {format_number(portfolio.get('score'))}/100 | concentrazione {format_number((portfolio.get('components') or {}).get('concentration_risk'))}/100.",
1622
+ ]
1623
+ if missed:
1624
+ top = missed[0]
1625
+ lines.append(f"Possibile opportunita mancata: BLUM ha scelto {top.get('ticker')}, ma {top.get('best_available_ticker')} aveva un outcome migliore nello stesso confronto.")
1626
+ if business:
1627
+ lines.append("Business quality piu alta: " + ", ".join(f"{row.get('ticker')} {format_number(row.get('business_quality_score'))}/100" for row in business[:3]))
1628
+ if decision.get("warnings"):
1629
+ lines.extend(str(item) for item in decision.get("warnings", [])[:2])
1630
+ return dedupe_warnings(lines)
1631
+ lines = [
1632
+ f"Decision Superiority Score: {format_number(decision.get('score'))}/100 | {decision.get('classification', 'insufficient evidence')} | sample {decision.get('sample_size', 0)}.",
1633
+ f"Opportunity recall {format_pct(decision.get('metrics', {}).get('opportunity_recall'))}; precision {format_pct(decision.get('metrics', {}).get('opportunity_precision'))}; alpha capture {format_pct(decision.get('metrics', {}).get('alpha_capture_rate'))}.",
1634
+ f"Portfolio Quality Score: {format_number(portfolio.get('score'))}/100 | concentration {format_number((portfolio.get('components') or {}).get('concentration_risk'))}/100.",
1635
+ ]
1636
+ if missed:
1637
+ top = missed[0]
1638
+ lines.append(f"Possible missed opportunity: BLUM selected {top.get('ticker')}, while {top.get('best_available_ticker')} had a better outcome in the same comparison.")
1639
+ if business:
1640
+ lines.append("Highest business quality: " + ", ".join(f"{row.get('ticker')} {format_number(row.get('business_quality_score'))}/100" for row in business[:3]))
1641
+ if decision.get("warnings"):
1642
+ lines.extend(str(item) for item in decision.get("warnings", [])[:2])
1643
+ return dedupe_warnings(lines)
1644
+
1645
+
1646
  def trade_ledger_lines(rows: list[dict], language: str) -> list[str]:
1647
  if not rows:
1648
  return ["BLUM ha metriche di gioco, ma il trade ledger dettagliato non e ancora disponibile." if language == "it" else "BLUM has game-level metrics, but the detailed trade ledger is not available yet."]
 
2185
  intelligence = (context or {}).get("intelligence_metrics") or {}
2186
  historical_vs_live = (context or {}).get("historical_vs_live") or {}
2187
  learning_intelligence = (context or {}).get("learning_intelligence") or {}
2188
+ decision_intelligence = (context or {}).get("decision_intelligence") or {}
2189
  return {
2190
  "current_game": {
2191
  "status": game.get("status"),
 
2231
  "weakness_map": (learning_intelligence.get("weakness_map") or {}).get("rows", [])[:6],
2232
  "self_improvement": (learning_intelligence.get("self_improvement") or {}).get("actions", [])[:6],
2233
  },
2234
+ "decision_intelligence": {
2235
+ "decision_superiority": ((decision_intelligence.get("decision") or {}).get("decision_superiority") or {}),
2236
+ "missed_opportunities": ((decision_intelligence.get("decision") or {}).get("top_missed_opportunities") or [])[:6],
2237
+ "business_quality": ((decision_intelligence.get("business_quality") or {}).get("highest_quality_companies") or [])[:6],
2238
+ "portfolio_quality": ((decision_intelligence.get("portfolio") or {}).get("portfolio_quality") or {}),
2239
+ },
2240
  }
2241
 
2242
 
 
2436
  return "fundamental_analysis"
2437
  if any(term in normalized for term in ["tesi", "thesis", "convinzione", "conviction", "ancora valida", "still valid", "sopravviss", "survival", "decay", "decad", "bull bear neutral", "tesi bull", "tesi bear", "tesi neutral", "motore", "engine vote", "sta migliorando", "dove ha sbagliato", "reasoning core", "batte spy", "batte qqq", "vs spy", "vs qqq"]):
2438
  return "reasoning_memory_question"
2439
+ if any(term in normalized for term in ["capitale virtuale", "trading game", "sta battendo", "batte il mercato", "benchmark", "drawdown", "profit factor", "expectancy", "p/l", "pl ", "peggior errore", "andato a zero", "rischio per trade", "riproducibil", "reproducib", "win rate", "quali trade", "trade hanno", "dove e entrato", "dove e uscito", "entrato blum", "uscito blum", "per azione", "fortuna", "profitto arriva", "ledger", "trade piu importante", "100 eur", "10,000", "10000", "target cycle", "ciclo capitale", "cicli capitale", "quante volte", "live paper", "forward paper", "storico vs live", "historical vs live", "sta migliorando", "intelligence growth", "missed entry", "stop hit", "target hit", "trading power", "power score", "dove e scarso", "piu scarso", "weakness", "self improvement", "auto miglior", "prossima azione", "baseline semplice", "stiamo battendo spy", "stiamo battendo qqq", "best opportunity", "migliore opportunita", "opportunita migliore", "ha scelto il migliore", "decision superiority", "superiorita decisionale", "alpha capture", "opportunita mancata", "missed opportunity", "business quality", "qualita business", "moat", "management quality", "portfolio quality", "qualita portafoglio", "contribuisce piu alpha", "contribution", "concentrazione portfolio"]):
2440
  return "trading_game"
2441
  if any(term in normalized for term in ["sniper", "entrabile", "meglio aspettare", "ingresso", "entry", "risk/reward", "uscita", "exit", "target", "invalidazione", "invalidation", "profitto", "take profit"]):
2442
  return "market_sniper"
backend/tests/test_decision_intelligence.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from types import SimpleNamespace
2
+
3
+ from app.services.decision_intelligence import (
4
+ classify_decision_superiority,
5
+ concentration_score,
6
+ decision_superiority_components,
7
+ decision_superiority_metrics,
8
+ )
9
+
10
+
11
+ def test_decision_superiority_classification():
12
+ assert classify_decision_superiority(15) == "Weak"
13
+ assert classify_decision_superiority(35) == "Experimental"
14
+ assert classify_decision_superiority(55) == "Learning"
15
+ assert classify_decision_superiority(70) == "Competitive"
16
+ assert classify_decision_superiority(84) == "Strong Alpha Research"
17
+ assert classify_decision_superiority(94) == "Exceptional"
18
+
19
+
20
+ def test_decision_metrics_capture_missed_best_opportunity():
21
+ decisions = [
22
+ {
23
+ "selected_outperformed": True,
24
+ "best_available_return": 8.0,
25
+ "selected_return": 6.0,
26
+ "benchmark_return": 2.0,
27
+ "ranking_correct": False,
28
+ "missed_best": True,
29
+ },
30
+ {
31
+ "selected_outperformed": False,
32
+ "best_available_return": 5.0,
33
+ "selected_return": -1.0,
34
+ "benchmark_return": 1.0,
35
+ "ranking_correct": True,
36
+ "missed_best": True,
37
+ },
38
+ ]
39
+ metrics = decision_superiority_metrics(decisions, [])
40
+ assert metrics["total_outperformers"] == 2
41
+ assert metrics["captured_outperformers"] == 1
42
+ assert metrics["opportunity_recall"] == 0.5
43
+ assert metrics["opportunity_precision"] == 0.5
44
+ assert metrics["missed_opportunities"] == 2
45
+ assert 0 < metrics["alpha_capture_rate"] < 1
46
+
47
+
48
+ def test_decision_components_penalize_low_live_validation():
49
+ metrics = {
50
+ "opportunity_recall": 0.5,
51
+ "opportunity_precision": 0.5,
52
+ "alpha_capture_rate": 0.4,
53
+ "ranking_accuracy": 0.5,
54
+ "benchmark_excess": 1.2,
55
+ "status": "ok",
56
+ }
57
+ rows = [
58
+ SimpleNamespace(mode="historical_simulation", market_regime_at_entry="risk_on", reproducibility_score=80, max_adverse_excursion=-2),
59
+ SimpleNamespace(mode="historical_simulation", market_regime_at_entry="risk_on", reproducibility_score=75, max_adverse_excursion=-3),
60
+ ]
61
+ components = decision_superiority_components(metrics, rows)
62
+ assert components["live_validation"] == 0
63
+ assert components["reproducibility"] > 70
64
+
65
+
66
+ def test_concentration_score_reads_top_contributors():
67
+ rows = [
68
+ {"ticker": "A", "return_contribution": 50},
69
+ {"ticker": "B", "return_contribution": 20},
70
+ {"ticker": "C", "return_contribution": 10},
71
+ {"ticker": "D", "return_contribution": 5},
72
+ ]
73
+ assert concentration_score(rows) == 80
frontend/app/learning/page.tsx CHANGED
@@ -23,7 +23,7 @@ export default function LearningPage() {
23
  let mounted = true;
24
  async function load() {
25
  setError("");
26
- const [dashResult, runsResult, predictionsResult, memoryResult, tradingStatusResult, equityResult, annotatedEquityResult, tradesResult, ledgerResult, ledgerSummaryResult, cyclesResult, currentCycleResult, cycleStatsResult, intelligenceMetricsResult, rollingMetricsResult, metricsBySetupResult, metricsByRegimeResult, metricsBySectorResult, historicalVsLiveResult, liveStatusResult, livePositionsResult, liveMetricsResult, learningIntelligenceResult, learningEvidenceResult, realityCheckResult, pnlBreakdownResult, failuresResult, lessonsResult, benchmarkResult, reproducibilityResult, reasoningStatusResult, survivalResult, convictionResult, reliabilityResult, competitionResult, ensembleResult, benchmarkRelativeResult, trainingQualityResult] = await Promise.allSettled([
27
  api.learningDashboard(),
28
  api.learningRuns(20),
29
  api.learningPredictions(36),
@@ -47,6 +47,7 @@ export default function LearningPage() {
47
  api.liveTradingGamePositions(),
48
  api.liveTradingGameMetrics(),
49
  api.learningIntelligenceDashboard(),
 
50
  api.tradingGameLearningEvidence(60),
51
  api.tradingGameRealityCheck(),
52
  api.tradingGamePnlBreakdown(),
@@ -88,6 +89,7 @@ export default function LearningPage() {
88
  livePositions: livePositionsResult.status === "fulfilled" ? livePositionsResult.value : null,
89
  liveMetrics: liveMetricsResult.status === "fulfilled" ? liveMetricsResult.value : null,
90
  learningIntelligence: learningIntelligenceResult.status === "fulfilled" ? learningIntelligenceResult.value : null,
 
91
  learningEvidence: learningEvidenceResult.status === "fulfilled" ? learningEvidenceResult.value : null,
92
  realityCheck: realityCheckResult.status === "fulfilled" ? realityCheckResult.value : null,
93
  pnlBreakdown: pnlBreakdownResult.status === "fulfilled" ? pnlBreakdownResult.value : null,
@@ -145,6 +147,13 @@ export default function LearningPage() {
145
  const livePositions = Array.isArray(livePositionRows) ? livePositionRows : [];
146
  const liveMetrics = trading?.liveMetrics?.metrics ?? {};
147
  const learningIntelligence = trading?.learningIntelligence ?? {};
 
 
 
 
 
 
 
148
  const tradingPower = learningIntelligence?.trading_power ?? {};
149
  const tradingPowerComponents = tradingPower?.components ?? {};
150
  const benchmarkArenaRows = learningIntelligence?.benchmarks?.rows ?? [];
@@ -270,6 +279,51 @@ export default function LearningPage() {
270
  </div>
271
  </section>
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  <section className="grid-3" style={{ marginTop: 12 }}>
274
  <div className="panel">
275
  <div className="panel-head"><span>Benchmark Arena</span><strong>{benchmarkArenaRows.length}</strong></div>
 
23
  let mounted = true;
24
  async function load() {
25
  setError("");
26
+ const [dashResult, runsResult, predictionsResult, memoryResult, tradingStatusResult, equityResult, annotatedEquityResult, tradesResult, ledgerResult, ledgerSummaryResult, cyclesResult, currentCycleResult, cycleStatsResult, intelligenceMetricsResult, rollingMetricsResult, metricsBySetupResult, metricsByRegimeResult, metricsBySectorResult, historicalVsLiveResult, liveStatusResult, livePositionsResult, liveMetricsResult, learningIntelligenceResult, decisionIntelligenceResult, learningEvidenceResult, realityCheckResult, pnlBreakdownResult, failuresResult, lessonsResult, benchmarkResult, reproducibilityResult, reasoningStatusResult, survivalResult, convictionResult, reliabilityResult, competitionResult, ensembleResult, benchmarkRelativeResult, trainingQualityResult] = await Promise.allSettled([
27
  api.learningDashboard(),
28
  api.learningRuns(20),
29
  api.learningPredictions(36),
 
47
  api.liveTradingGamePositions(),
48
  api.liveTradingGameMetrics(),
49
  api.learningIntelligenceDashboard(),
50
+ api.decisionIntelligenceDashboard(),
51
  api.tradingGameLearningEvidence(60),
52
  api.tradingGameRealityCheck(),
53
  api.tradingGamePnlBreakdown(),
 
89
  livePositions: livePositionsResult.status === "fulfilled" ? livePositionsResult.value : null,
90
  liveMetrics: liveMetricsResult.status === "fulfilled" ? liveMetricsResult.value : null,
91
  learningIntelligence: learningIntelligenceResult.status === "fulfilled" ? learningIntelligenceResult.value : null,
92
+ decisionIntelligence: decisionIntelligenceResult.status === "fulfilled" ? decisionIntelligenceResult.value : null,
93
  learningEvidence: learningEvidenceResult.status === "fulfilled" ? learningEvidenceResult.value : null,
94
  realityCheck: realityCheckResult.status === "fulfilled" ? realityCheckResult.value : null,
95
  pnlBreakdown: pnlBreakdownResult.status === "fulfilled" ? pnlBreakdownResult.value : null,
 
147
  const livePositions = Array.isArray(livePositionRows) ? livePositionRows : [];
148
  const liveMetrics = trading?.liveMetrics?.metrics ?? {};
149
  const learningIntelligence = trading?.learningIntelligence ?? {};
150
+ const decisionIntelligence = trading?.decisionIntelligence ?? {};
151
+ const decisionSuperiority = decisionIntelligence?.decision?.decision_superiority ?? {};
152
+ const missedOpportunityRows = decisionIntelligence?.decision?.top_missed_opportunities ?? [];
153
+ const businessQualityRows = decisionIntelligence?.business_quality?.highest_quality_companies ?? [];
154
+ const portfolioQuality = decisionIntelligence?.portfolio?.portfolio_quality ?? {};
155
+ const portfolioContributionRows = decisionIntelligence?.portfolio?.contributions ?? [];
156
+ const portfolioCorrelationRows = decisionIntelligence?.portfolio?.correlations ?? [];
157
  const tradingPower = learningIntelligence?.trading_power ?? {};
158
  const tradingPowerComponents = tradingPower?.components ?? {};
159
  const benchmarkArenaRows = learningIntelligence?.benchmarks?.rows ?? [];
 
279
  </div>
280
  </section>
281
 
282
+ <section className="grid-3" style={{ marginTop: 12 }}>
283
+ <div className="panel lab-command-panel">
284
+ <div className="panel-head"><span>Decision Superiority</span><strong>{formatNumber(decisionSuperiority?.score)}/100</strong></div>
285
+ <div className="cycle-progress-track"><span style={{ width: `${Math.max(0, Math.min(100, Number(decisionSuperiority?.score ?? 0)))}%` }} /></div>
286
+ <div className="evidence-grid">
287
+ <SmallDatum label="Classification" value={decisionSuperiority?.classification ?? "insufficient evidence"} />
288
+ <SmallDatum label="Recall" value={formatPct(decisionSuperiority?.metrics?.opportunity_recall)} />
289
+ <SmallDatum label="Precision" value={formatPct(decisionSuperiority?.metrics?.opportunity_precision)} />
290
+ <SmallDatum label="Alpha Capture" value={formatPct(decisionSuperiority?.metrics?.alpha_capture_rate)} />
291
+ <SmallDatum label="Ranking" value={formatPct(decisionSuperiority?.metrics?.ranking_accuracy)} />
292
+ <SmallDatum label="Comparable Decisions" value={decisionSuperiority?.sample_size ?? 0} />
293
+ </div>
294
+ <p>{decisionSuperiority?.explanation ?? "BLUM needs comparable decision snapshots before it can claim it selected superior opportunities."}</p>
295
+ {missedOpportunityRows.length > 0 && <div className="tag-row">{missedOpportunityRows.slice(0, 4).map((row: any) => <span key={`${row.trade_id}-${row.best_available_ticker}`}>Missed {row.best_available_ticker} vs {row.ticker}</span>)}</div>}
296
+ </div>
297
+
298
+ <div className="panel lab-command-panel">
299
+ <div className="panel-head"><span>Business Quality Lab</span><strong>{businessQualityRows.length}</strong></div>
300
+ <div className="brain-list dense">
301
+ {(businessQualityRows.length ? businessQualityRows : []).slice(0, 5).map((row: any) => (
302
+ <div key={row.ticker}>
303
+ <StatusBadge label={`${row.ticker} | ${row.status}`} />
304
+ <div className="opportunity-line"><strong>{row.name}</strong><span>{formatNumber(row.business_quality_score)}/100</span></div>
305
+ <p>Growth {formatNumber(row.growth_quality)} | FCF {formatNumber(row.cash_flow_quality)} | moat {formatNumber(row.moat_quality)} | data {formatNumber(row.data_quality_score)}</p>
306
+ </div>
307
+ ))}
308
+ {businessQualityRows.length === 0 && <div className="empty-state compact">No business-quality evidence yet. Stored fundamental snapshots are required.</div>}
309
+ </div>
310
+ </div>
311
+
312
+ <div className="panel lab-command-panel">
313
+ <div className="panel-head"><span>Portfolio Intelligence</span><strong>{formatNumber(portfolioQuality?.score)}/100</strong></div>
314
+ <div className="evidence-grid">
315
+ <SmallDatum label="Diversification" value={formatNumber(portfolioQuality?.components?.diversification)} />
316
+ <SmallDatum label="Concentration Risk" value={formatNumber(portfolioQuality?.components?.concentration_risk)} />
317
+ <SmallDatum label="Drawdown Control" value={formatNumber(portfolioQuality?.components?.drawdown_control)} />
318
+ <SmallDatum label="Alpha Generation" value={formatNumber(portfolioQuality?.components?.alpha_generation)} />
319
+ <SmallDatum label="Contributors" value={portfolioContributionRows.length} />
320
+ <SmallDatum label="Correlations" value={portfolioCorrelationRows.length} />
321
+ </div>
322
+ <p>{portfolioQuality?.explanation ?? "BLUM needs portfolio trade evidence before contribution, concentration and portfolio-alpha quality can be measured."}</p>
323
+ {(portfolioQuality?.warnings ?? []).length > 0 && <div className="tag-row">{portfolioQuality.warnings.slice(0, 4).map((item: string) => <span key={item}>{item}</span>)}</div>}
324
+ </div>
325
+ </section>
326
+
327
  <section className="grid-3" style={{ marginTop: 12 }}>
328
  <div className="panel">
329
  <div className="panel-head"><span>Benchmark Arena</span><strong>{benchmarkArenaRows.length}</strong></div>
frontend/lib/api.ts CHANGED
@@ -128,6 +128,18 @@ export const api = {
128
  generateLearningSelfImprovement: () => postJson<any>("/api/learning-intelligence/self-improvement/generate", {}),
129
  applyLearningSelfImprovement: (actionId: number) => postJson<any>(`/api/learning-intelligence/self-improvement/apply/${actionId}`, {}),
130
  evaluateLearningSelfImprovement: (actionId: number) => postJson<any>(`/api/learning-intelligence/self-improvement/evaluate/${actionId}`, {}),
 
 
 
 
 
 
 
 
 
 
 
 
131
  chartAnalyzeTicker: (ticker: string, timeframe = "6M", period = "1y", includeVisual = false) => getPostChart<ChartReport>(`/chart/analyze-ticker?ticker=${encodeURIComponent(ticker)}&timeframe=${encodeURIComponent(timeframe)}&period=${encodeURIComponent(period)}&include_visual=${includeVisual ? "true" : "false"}`),
132
  chartTechnicalReport: (ticker: string, timeframe = "6M") => getJson<ChartReport>(`/chart/technical-report/${encodeURIComponent(ticker)}?timeframe=${encodeURIComponent(timeframe)}`),
133
  chartLevels: (ticker: string, timeframe = "6M") => getJson<any>(`/chart/levels/${encodeURIComponent(ticker)}?timeframe=${encodeURIComponent(timeframe)}`),
 
128
  generateLearningSelfImprovement: () => postJson<any>("/api/learning-intelligence/self-improvement/generate", {}),
129
  applyLearningSelfImprovement: (actionId: number) => postJson<any>(`/api/learning-intelligence/self-improvement/apply/${actionId}`, {}),
130
  evaluateLearningSelfImprovement: (actionId: number) => postJson<any>(`/api/learning-intelligence/self-improvement/evaluate/${actionId}`, {}),
131
+ decisionIntelligenceDashboard: () => getJson<any>("/api/decision-intelligence/dashboard"),
132
+ decisionSuperiority: () => getJson<any>("/api/decision-intelligence/superiority"),
133
+ recalculateDecisionSuperiority: () => postJson<any>("/api/decision-intelligence/superiority/recalculate", {}),
134
+ decisionUniverseSnapshots: () => getJson<any>("/api/decision-intelligence/universe-snapshots"),
135
+ recalculateDecisionUniverseSnapshots: () => postJson<any>("/api/decision-intelligence/universe-snapshots/recalculate", {}),
136
+ missedOpportunities: () => getJson<any>("/api/decision-intelligence/missed-opportunities"),
137
+ businessQualityDashboard: () => getJson<any>("/api/business-quality/dashboard"),
138
+ businessQualityScores: (limit = 80) => getJson<any>(`/api/business-quality/scores?limit=${limit}`),
139
+ recalculateBusinessQuality: (limit = 80) => postJson<any>(`/api/business-quality/recalculate?limit=${limit}`, {}),
140
+ portfolioIntelligenceDashboard: () => getJson<any>("/api/portfolio-intelligence/dashboard"),
141
+ portfolioQuality: () => getJson<any>("/api/portfolio-intelligence/quality"),
142
+ recalculatePortfolioIntelligence: () => postJson<any>("/api/portfolio-intelligence/recalculate", {}),
143
  chartAnalyzeTicker: (ticker: string, timeframe = "6M", period = "1y", includeVisual = false) => getPostChart<ChartReport>(`/chart/analyze-ticker?ticker=${encodeURIComponent(ticker)}&timeframe=${encodeURIComponent(timeframe)}&period=${encodeURIComponent(period)}&include_visual=${includeVisual ? "true" : "false"}`),
144
  chartTechnicalReport: (ticker: string, timeframe = "6M") => getJson<ChartReport>(`/chart/technical-report/${encodeURIComponent(ticker)}?timeframe=${encodeURIComponent(timeframe)}`),
145
  chartLevels: (ticker: string, timeframe = "6M") => getJson<any>(`/chart/levels/${encodeURIComponent(ticker)}?timeframe=${encodeURIComponent(timeframe)}`),
frontend/package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "blum-ai-financial-intelligence-frontend",
3
- "version": "0.16.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p 3000",
 
1
  {
2
  "name": "blum-ai-financial-intelligence-frontend",
3
+ "version": "0.17.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p 3000",
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "blum-ai-financial-intelligence",
3
- "version": "0.16.0",
4
  "private": true,
5
  "scripts": {
6
  "frontend:dev": "npm --prefix frontend run dev",
 
1
  {
2
  "name": "blum-ai-financial-intelligence",
3
+ "version": "0.17.0",
4
  "private": true,
5
  "scripts": {
6
  "frontend:dev": "npm --prefix frontend run dev",