Italianhype commited on
Commit
d143e5c
·
verified ·
1 Parent(s): 5657266

Deploy BLUM v1.0.0 alpha operating system

Browse files
CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## v1.0.0 | alpha-operating-system
4
+
5
+ - Promoted runtime identity from `market-sniper-engine-v1` to `alpha-operating-system`.
6
+ - Added Trading Game readiness diagnostics so the Learning page explains whether evidence is ready, building, stale, insufficient, failed or data-quality blocked.
7
+ - Added compact brain command endpoints for Command Center capability status, learning evolution, benchmark truth and copy readiness.
8
+ - Added Alpha Readiness, Edge Map and Alpha Gates read-only services.
9
+ - Added paper-copy persistent models and paper-copy operating endpoints.
10
+ - Updated Learning Trading Game tab to load readiness first and avoid permanent generic loading.
11
+ - Updated Copy Trading page to read the paper-copy operating summary.
12
+ - Preserved existing Trading Game, Learning Loop, Market Sniper, Decision Intelligence and snapshot-first runtime behavior.
13
+
MIGRATION_v1.0.0.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BLUM v1.0.0 Migration Notes
2
+
3
+ Apply Alembic revision:
4
+
5
+ ```bash
6
+ cd backend
7
+ alembic upgrade head
8
+ ```
9
+
10
+ New durable tables:
11
+
12
+ - `trading_game_readiness_snapshots`
13
+ - `alpha_readiness_snapshots`
14
+ - `alpha_gate_snapshots`
15
+ - `edge_map_snapshots`
16
+ - `paper_copy_strategies`
17
+ - `paper_copy_portfolios`
18
+ - `paper_copy_orders`
19
+ - `paper_copy_positions`
20
+ - `paper_copy_portfolio_snapshots`
21
+
22
+ The new endpoints can work with empty tables and return `INSUFFICIENT_EVIDENCE`, `WAITING_FOR_SOURCE_DATA` or candidate-only states. This is intentional: the UI should observe backend progress, not force training or recalculation during render.
23
+
README.md CHANGED
@@ -1534,26 +1534,62 @@ The Space serves the FastAPI backend and the exported Next.js frontend on port `
1534
 
1535
  Backtesting is included for research validation only. It reports historical hit rate, average forward return over 5D/20D/60D, max adverse excursion, max favorable excursion and false positives. It does not predict or guarantee future returns.
1536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1537
  ## Command Brain Level
1538
 
1539
- The Command Center includes a lightweight `BLUM Brain Level` panel powered by `GET /api/learning-intelligence/summary`. It is snapshot-first and shows:
1540
 
1541
  - current learning status and latest run timestamp;
1542
  - Trading Power / evidence classification when precomputed;
1543
  - Trading Game capital progress, win rate and expectancy R;
1544
  - benchmark pressure versus stored market benchmarks;
1545
- - latest weakness, latest lesson and next learning focus.
 
1546
 
1547
  This panel does not run training, recalculation or heavy diagnostics. It is intended to answer quickly whether BLUM's learning evidence is improving, deteriorating or still insufficient.
1548
 
1549
- ## Copy Trading Intelligence
1550
 
1551
- `/copy-trading` is a paper-only intelligence surface for conditional mirror plans. The backend exposes:
1552
 
1553
  - `GET /api/copy-trading/status`
1554
  - `GET /api/copy-trading/candidates`
1555
  - `GET /api/copy-trading/dashboard`
1556
 
 
 
 
 
 
 
 
 
1557
  The service reads persisted `TradePlan`, `SniperScore` and Trading Game evidence. It does not recalculate signals, connect to brokers, place orders or claim performance. A candidate is downgraded when it is missing entry trigger, invalidation or target/risk-plan evidence.
1558
 
1559
  The output is designed to answer:
@@ -1563,7 +1599,7 @@ The output is designed to answer:
1563
  - which setups are blocked by risk-plan gaps or weak actionability;
1564
  - what BLUM has learned from recent paper trades tied to the same ticker.
1565
 
1566
- Copy Trading Intelligence is a decision-audit layer, not an execution system.
1567
 
1568
  ## Limitations
1569
 
 
1534
 
1535
  Backtesting is included for research validation only. It reports historical hit rate, average forward return over 5D/20D/60D, max adverse excursion, max favorable excursion and false positives. It does not predict or guarantee future returns.
1536
 
1537
+ ## BLUM v1.0.0 Alpha Operating System
1538
+
1539
+ `v1.0.0 | alpha-operating-system` turns BLUM's existing learning, trading, benchmark and meta-cognition layers into a clearer operating layer:
1540
+
1541
+ - `GET /brain/command-summary` returns the compact Command Center brain state.
1542
+ - `GET /brain/capabilities` exposes capability rows for trading, decision, business, portfolio, alpha and paper-copy readiness.
1543
+ - `GET /brain/evolution` reads stored Trading Power snapshots.
1544
+ - `GET /trading-game/readiness` explains why Trading Game evidence is ready, building, stale, insufficient, failed or data-quality blocked.
1545
+ - `GET /alpha/readiness`, `GET /alpha/edge-map` and `GET /alpha/gates` expose strict alpha-readiness evidence without running recalculation.
1546
+ - `GET /paper-copy/summary`, `GET /paper-copy/strategies`, `GET /paper-copy/positions`, `GET /paper-copy/readiness` and `GET /paper-copy/portfolio/{id}` expose paper-only copy intelligence state.
1547
+
1548
+ The release preserves the snapshot-first runtime architecture: Command, Learning and Copy surfaces read lightweight stored evidence and do not trigger training, recalculation, broker activity or heavy Trading Game rebuilds during render.
1549
+
1550
+ ## Trading Game Readiness
1551
+
1552
+ The Learning page now asks `GET /api/trading-game/readiness` before loading equity, ledger and benchmark details. The UI must never remain in generic loading. It shows one of:
1553
+
1554
+ - `READY`
1555
+ - `BUILDING`
1556
+ - `WAITING_FOR_SOURCE_DATA`
1557
+ - `STALE_BUT_USABLE`
1558
+ - `FAILED`
1559
+ - `INSUFFICIENT_EVIDENCE`
1560
+ - `DATA_QUALITY_BLOCKED`
1561
+
1562
+ Readiness includes source decision counts, source trade counts, eligible trade counts, ledger/equity/benchmark snapshot status, worker status, blocker, next required action, evidence grade and methodology version. It is diagnostic evidence, not a trading signal.
1563
+
1564
  ## Command Brain Level
1565
 
1566
+ The Command Center includes a lightweight `BLUM Brain Level` panel powered primarily by `GET /brain/command-summary`. It is snapshot-first and shows:
1567
 
1568
  - current learning status and latest run timestamp;
1569
  - Trading Power / evidence classification when precomputed;
1570
  - Trading Game capital progress, win rate and expectancy R;
1571
  - benchmark pressure versus stored market benchmarks;
1572
+ - latest weakness, latest lesson and next learning focus;
1573
+ - alpha readiness and paper-copy readiness.
1574
 
1575
  This panel does not run training, recalculation or heavy diagnostics. It is intended to answer quickly whether BLUM's learning evidence is improving, deteriorating or still insufficient.
1576
 
1577
+ ## Paper Copy Trading Intelligence
1578
 
1579
+ `/copy-trading` is a paper-only intelligence surface for conditional mirror plans. The backend keeps the original compatibility endpoints:
1580
 
1581
  - `GET /api/copy-trading/status`
1582
  - `GET /api/copy-trading/candidates`
1583
  - `GET /api/copy-trading/dashboard`
1584
 
1585
+ and adds the v1.0 paper-copy operating endpoints:
1586
+
1587
+ - `GET /paper-copy/summary`
1588
+ - `GET /paper-copy/readiness`
1589
+ - `GET /paper-copy/strategies`
1590
+ - `GET /paper-copy/positions`
1591
+ - `GET /paper-copy/portfolio/{portfolio_id}`
1592
+
1593
  The service reads persisted `TradePlan`, `SniperScore` and Trading Game evidence. It does not recalculate signals, connect to brokers, place orders or claim performance. A candidate is downgraded when it is missing entry trigger, invalidation or target/risk-plan evidence.
1594
 
1595
  The output is designed to answer:
 
1599
  - which setups are blocked by risk-plan gaps or weak actionability;
1600
  - what BLUM has learned from recent paper trades tied to the same ticker.
1601
 
1602
+ Paper Copy Trading Intelligence is a decision-audit layer and paper portfolio research surface, not an execution system. It never connects to a broker and never emits direct financial advice.
1603
 
1604
  ## Limitations
1605
 
RELEASE_NOTES_v1.0.0.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BLUM v1.0.0 Release Notes
2
+
3
+ Release identity: `v1.0.0 | alpha-operating-system`
4
+
5
+ ## What Changed
6
+
7
+ - BLUM now exposes a compact Alpha Operating System layer around the existing runtime.
8
+ - Trading Game evidence has a readiness state before the UI tries to load heavy evidence.
9
+ - Command Center can show the level of BLUM's brain through capability, benchmark, learning and copy-readiness summaries.
10
+ - Alpha Readiness measures whether stored evidence is mature enough to discuss possible edge.
11
+ - Alpha Gates block unsupported claims when samples, benchmarks or live paper evidence are missing.
12
+ - Paper Copy Trading is now modeled as a paper-only operating layer with durable schema support.
13
+
14
+ ## What Did Not Change
15
+
16
+ - No broker integration was added.
17
+ - No real trades are executed.
18
+ - Existing Trading Game, Learning Loop, Sniper, Decision, Portfolio, Alpha Recovery and Meta-Cognition logic remain backward compatible.
19
+ - GET endpoints remain read-only and do not run heavy recalculation.
20
+
21
+ ## Validation Standard
22
+
23
+ Performance and alpha claims are still evidence-bound. BLUM must say insufficient evidence when samples, benchmark comparisons or live paper validation are not mature.
24
+
backend/alembic/versions/0026_alpha_operating_system.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = "0026_alpha_operating_system"
9
+ down_revision = "0025_tg_runtime_snap"
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
+ "trading_game_readiness_snapshots",
27
+ sa.Column("id", sa.Integer(), nullable=False),
28
+ sa.Column("game_id", sa.Integer(), nullable=True),
29
+ sa.Column("status", sa.String(length=80), nullable=False, server_default="WAITING_FOR_SOURCE_DATA"),
30
+ sa.Column("evidence_grade", sa.String(length=80), nullable=False, server_default="insufficient"),
31
+ sa.Column("blocker", sa.Text(), nullable=False, server_default=""),
32
+ sa.Column("next_required_action", sa.Text(), nullable=False, server_default=""),
33
+ sa.Column("payload_json", json_type, nullable=True),
34
+ sa.Column("warnings_json", json_type, nullable=True),
35
+ sa.Column("methodology_version", sa.String(length=80), nullable=False, server_default="trading-game-readiness-v1"),
36
+ sa.Column("generated_at", sa.DateTime(), nullable=False),
37
+ sa.Column("expires_at", sa.DateTime(), nullable=True),
38
+ sa.Column("is_stale", sa.Boolean(), nullable=False, server_default=sa.false()),
39
+ sa.ForeignKeyConstraint(["game_id"], ["trading_games.id"], ondelete="SET NULL"),
40
+ sa.PrimaryKeyConstraint("id"),
41
+ )
42
+ create_index("trading_game_readiness_snapshots", ["generated_at"], "generated")
43
+ create_index("trading_game_readiness_snapshots", ["status", "evidence_grade"], "status_grade")
44
+ create_index("trading_game_readiness_snapshots", ["game_id", "generated_at"], "game_generated")
45
+
46
+ op.create_table(
47
+ "alpha_readiness_snapshots",
48
+ sa.Column("id", sa.Integer(), nullable=False),
49
+ sa.Column("status", sa.String(length=80), nullable=False, server_default="INSUFFICIENT_EVIDENCE"),
50
+ sa.Column("alpha_readiness_score", sa.Float(), nullable=False, server_default="0"),
51
+ sa.Column("evidence_grade", sa.String(length=80), nullable=False, server_default="insufficient"),
52
+ sa.Column("classification", sa.String(length=120), nullable=False, server_default="not_ready"),
53
+ sa.Column("payload_json", json_type, nullable=True),
54
+ sa.Column("warnings_json", json_type, nullable=True),
55
+ sa.Column("methodology_version", sa.String(length=80), nullable=False, server_default="alpha-readiness-v1"),
56
+ sa.Column("generated_at", sa.DateTime(), nullable=False),
57
+ sa.Column("expires_at", sa.DateTime(), nullable=True),
58
+ sa.Column("is_stale", sa.Boolean(), nullable=False, server_default=sa.false()),
59
+ sa.PrimaryKeyConstraint("id"),
60
+ )
61
+ create_index("alpha_readiness_snapshots", ["generated_at"], "generated")
62
+ create_index("alpha_readiness_snapshots", ["alpha_readiness_score", "evidence_grade"], "score")
63
+
64
+ op.create_table(
65
+ "alpha_gate_snapshots",
66
+ sa.Column("id", sa.Integer(), nullable=False),
67
+ sa.Column("gate_name", sa.String(length=120), nullable=False),
68
+ sa.Column("status", sa.String(length=80), nullable=False, server_default="blocked"),
69
+ sa.Column("score", sa.Float(), nullable=False, server_default="0"),
70
+ sa.Column("payload_json", json_type, nullable=True),
71
+ sa.Column("warnings_json", json_type, nullable=True),
72
+ sa.Column("generated_at", sa.DateTime(), nullable=False),
73
+ sa.PrimaryKeyConstraint("id"),
74
+ )
75
+ create_index("alpha_gate_snapshots", ["generated_at"], "generated")
76
+ create_index("alpha_gate_snapshots", ["gate_name", "status"], "status")
77
+
78
+ op.create_table(
79
+ "edge_map_snapshots",
80
+ sa.Column("id", sa.Integer(), nullable=False),
81
+ sa.Column("scope", sa.String(length=120), nullable=False, server_default="global"),
82
+ sa.Column("evidence_grade", sa.String(length=80), nullable=False, server_default="insufficient"),
83
+ sa.Column("payload_json", json_type, nullable=True),
84
+ sa.Column("warnings_json", json_type, nullable=True),
85
+ sa.Column("generated_at", sa.DateTime(), nullable=False),
86
+ sa.PrimaryKeyConstraint("id"),
87
+ )
88
+ create_index("edge_map_snapshots", ["generated_at"], "generated")
89
+ create_index("edge_map_snapshots", ["scope", "evidence_grade"], "scope")
90
+
91
+ op.create_table(
92
+ "paper_copy_strategies",
93
+ sa.Column("id", sa.Integer(), nullable=False),
94
+ sa.Column("strategy_id", sa.String(length=100), nullable=False),
95
+ sa.Column("name", sa.String(length=180), nullable=False, server_default="BLUM Paper Copy Strategy"),
96
+ sa.Column("status", sa.String(length=80), nullable=False, server_default="paper_only"),
97
+ sa.Column("strategy_type", sa.String(length=120), nullable=False, server_default="conditional_copy_watchlist"),
98
+ sa.Column("copyability_score", sa.Float(), nullable=False, server_default="0"),
99
+ sa.Column("risk_budget_percent", sa.Float(), nullable=False, server_default="1"),
100
+ sa.Column("max_open_positions", sa.Integer(), nullable=False, server_default="5"),
101
+ sa.Column("rules_json", json_type, nullable=True),
102
+ sa.Column("evidence_json", json_type, nullable=True),
103
+ sa.Column("warnings_json", json_type, nullable=True),
104
+ sa.Column("paper_only", sa.Boolean(), nullable=False, server_default=sa.true()),
105
+ sa.Column("no_broker_execution", sa.Boolean(), nullable=False, server_default=sa.true()),
106
+ sa.Column("created_at", sa.DateTime(), nullable=False),
107
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
108
+ sa.PrimaryKeyConstraint("id"),
109
+ sa.UniqueConstraint("strategy_id"),
110
+ )
111
+ create_index("paper_copy_strategies", ["strategy_id"], "strategy_id")
112
+ create_index("paper_copy_strategies", ["status", "created_at"], "status_created")
113
+ create_index("paper_copy_strategies", ["copyability_score"], "score")
114
+
115
+ op.create_table(
116
+ "paper_copy_portfolios",
117
+ sa.Column("id", sa.Integer(), nullable=False),
118
+ sa.Column("portfolio_id", sa.String(length=100), nullable=False),
119
+ sa.Column("strategy_id", sa.Integer(), nullable=True),
120
+ sa.Column("status", sa.String(length=80), nullable=False, server_default="paper_active"),
121
+ sa.Column("starting_capital", sa.Float(), nullable=False, server_default="100"),
122
+ sa.Column("current_capital", sa.Float(), nullable=False, server_default="100"),
123
+ sa.Column("cash", sa.Float(), nullable=False, server_default="100"),
124
+ sa.Column("exposure", sa.Float(), nullable=False, server_default="0"),
125
+ sa.Column("realized_pnl", sa.Float(), nullable=False, server_default="0"),
126
+ sa.Column("unrealized_pnl", sa.Float(), nullable=False, server_default="0"),
127
+ sa.Column("benchmark_ticker", sa.String(length=32), nullable=False, server_default="SPY"),
128
+ sa.Column("risk_state", sa.String(length=80), nullable=False, server_default="conservative"),
129
+ sa.Column("evidence_json", json_type, nullable=True),
130
+ sa.Column("warnings_json", json_type, nullable=True),
131
+ sa.Column("created_at", sa.DateTime(), nullable=False),
132
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
133
+ sa.ForeignKeyConstraint(["strategy_id"], ["paper_copy_strategies.id"], ondelete="SET NULL"),
134
+ sa.PrimaryKeyConstraint("id"),
135
+ sa.UniqueConstraint("portfolio_id"),
136
+ )
137
+ create_index("paper_copy_portfolios", ["portfolio_id"], "portfolio_id")
138
+ create_index("paper_copy_portfolios", ["status", "updated_at"], "status_updated")
139
+ create_index("paper_copy_portfolios", ["strategy_id"], "strategy")
140
+
141
+ op.create_table(
142
+ "paper_copy_orders",
143
+ sa.Column("id", sa.Integer(), nullable=False),
144
+ sa.Column("portfolio_id", sa.Integer(), nullable=True),
145
+ sa.Column("strategy_id", sa.Integer(), nullable=True),
146
+ sa.Column("source_trade_plan_id", sa.Integer(), nullable=True),
147
+ sa.Column("ticker", sa.String(length=32), nullable=False),
148
+ sa.Column("side", sa.String(length=40), nullable=False, server_default="paper_buy"),
149
+ sa.Column("status", sa.String(length=80), nullable=False, server_default="planned"),
150
+ sa.Column("order_type", sa.String(length=80), nullable=False, server_default="conditional_paper"),
151
+ sa.Column("trigger_condition", sa.Text(), nullable=False, server_default=""),
152
+ sa.Column("paper_price", sa.Float(), nullable=True),
153
+ sa.Column("paper_quantity", sa.Float(), nullable=True),
154
+ sa.Column("risk_amount", sa.Float(), nullable=True),
155
+ sa.Column("invalidation_level", sa.Float(), nullable=True),
156
+ sa.Column("target_1", sa.Float(), nullable=True),
157
+ sa.Column("target_2", sa.Float(), nullable=True),
158
+ sa.Column("evidence_json", json_type, nullable=True),
159
+ sa.Column("created_at", sa.DateTime(), nullable=False),
160
+ sa.ForeignKeyConstraint(["portfolio_id"], ["paper_copy_portfolios.id"], ondelete="CASCADE"),
161
+ sa.ForeignKeyConstraint(["strategy_id"], ["paper_copy_strategies.id"], ondelete="SET NULL"),
162
+ sa.ForeignKeyConstraint(["source_trade_plan_id"], ["trade_plans.id"], ondelete="SET NULL"),
163
+ sa.PrimaryKeyConstraint("id"),
164
+ )
165
+ create_index("paper_copy_orders", ["portfolio_id", "created_at"], "portfolio_created")
166
+ create_index("paper_copy_orders", ["ticker", "status"], "ticker_status")
167
+ create_index("paper_copy_orders", ["strategy_id"], "strategy")
168
+ create_index("paper_copy_orders", ["source_trade_plan_id"], "source_plan")
169
+
170
+ op.create_table(
171
+ "paper_copy_positions",
172
+ sa.Column("id", sa.Integer(), nullable=False),
173
+ sa.Column("portfolio_id", sa.Integer(), nullable=True),
174
+ sa.Column("strategy_id", sa.Integer(), nullable=True),
175
+ sa.Column("source_order_id", sa.Integer(), nullable=True),
176
+ sa.Column("ticker", sa.String(length=32), nullable=False),
177
+ sa.Column("status", sa.String(length=80), nullable=False, server_default="open"),
178
+ sa.Column("quantity", sa.Float(), nullable=False, server_default="0"),
179
+ sa.Column("entry_price", sa.Float(), nullable=True),
180
+ sa.Column("current_price", sa.Float(), nullable=True),
181
+ sa.Column("market_value", sa.Float(), nullable=True),
182
+ sa.Column("unrealized_pnl", sa.Float(), nullable=True),
183
+ sa.Column("realized_pnl", sa.Float(), nullable=True),
184
+ sa.Column("invalidation_level", sa.Float(), nullable=True),
185
+ sa.Column("target_1", sa.Float(), nullable=True),
186
+ sa.Column("target_2", sa.Float(), nullable=True),
187
+ sa.Column("opened_at", sa.DateTime(), nullable=True),
188
+ sa.Column("closed_at", sa.DateTime(), nullable=True),
189
+ sa.Column("evidence_json", json_type, nullable=True),
190
+ sa.ForeignKeyConstraint(["portfolio_id"], ["paper_copy_portfolios.id"], ondelete="CASCADE"),
191
+ sa.ForeignKeyConstraint(["strategy_id"], ["paper_copy_strategies.id"], ondelete="SET NULL"),
192
+ sa.ForeignKeyConstraint(["source_order_id"], ["paper_copy_orders.id"], ondelete="SET NULL"),
193
+ sa.PrimaryKeyConstraint("id"),
194
+ )
195
+ create_index("paper_copy_positions", ["portfolio_id", "status"], "portfolio_status")
196
+ create_index("paper_copy_positions", ["ticker", "opened_at"], "ticker_opened")
197
+ create_index("paper_copy_positions", ["strategy_id"], "strategy")
198
+ create_index("paper_copy_positions", ["source_order_id"], "source_order")
199
+
200
+ op.create_table(
201
+ "paper_copy_portfolio_snapshots",
202
+ sa.Column("id", sa.Integer(), nullable=False),
203
+ sa.Column("portfolio_id", sa.Integer(), nullable=True),
204
+ sa.Column("strategy_id", sa.Integer(), nullable=True),
205
+ sa.Column("created_at", sa.DateTime(), nullable=False),
206
+ sa.Column("capital", sa.Float(), nullable=True),
207
+ sa.Column("exposure", sa.Float(), nullable=True),
208
+ sa.Column("open_positions", sa.Integer(), nullable=False, server_default="0"),
209
+ sa.Column("pending_orders", sa.Integer(), nullable=False, server_default="0"),
210
+ sa.Column("copyability_score", sa.Float(), nullable=False, server_default="0"),
211
+ sa.Column("evidence_grade", sa.String(length=80), nullable=False, server_default="insufficient"),
212
+ sa.Column("payload_json", json_type, nullable=True),
213
+ sa.Column("warnings_json", json_type, nullable=True),
214
+ sa.ForeignKeyConstraint(["portfolio_id"], ["paper_copy_portfolios.id"], ondelete="CASCADE"),
215
+ sa.ForeignKeyConstraint(["strategy_id"], ["paper_copy_strategies.id"], ondelete="SET NULL"),
216
+ sa.PrimaryKeyConstraint("id"),
217
+ )
218
+ create_index("paper_copy_portfolio_snapshots", ["portfolio_id", "created_at"], "portfolio_created")
219
+ create_index("paper_copy_portfolio_snapshots", ["copyability_score", "evidence_grade"], "score")
220
+ create_index("paper_copy_portfolio_snapshots", ["strategy_id"], "strategy")
221
+
222
+
223
+ def downgrade() -> None:
224
+ op.drop_table("paper_copy_portfolio_snapshots")
225
+ op.drop_table("paper_copy_positions")
226
+ op.drop_table("paper_copy_orders")
227
+ op.drop_table("paper_copy_portfolios")
228
+ op.drop_table("paper_copy_strategies")
229
+ op.drop_table("edge_map_snapshots")
230
+ op.drop_table("alpha_gate_snapshots")
231
+ op.drop_table("alpha_readiness_snapshots")
232
+ op.drop_table("trading_game_readiness_snapshots")
backend/app/api/routes.py CHANGED
@@ -143,6 +143,15 @@ from app.services.alpha_recovery import (
143
  BenchmarkMethodologyValidator,
144
  MissedWinnersEngine,
145
  )
 
 
 
 
 
 
 
 
 
146
  from app.services.autonomous_engine import AutonomousResearchEngine, latest_autonomous_status
147
  from app.services.blum_financial_model import (
148
  build_training_dataset,
@@ -324,6 +333,21 @@ def brain_runtime_state(db: Session = Depends(get_db)) -> dict:
324
  return CentralBrainRuntime().state(db)
325
 
326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  @router.get("/snapshots/health")
328
  def snapshots_health(db: Session = Depends(get_db)) -> dict:
329
  return SnapshotWatchdogService().health(db, queue_rebuild=False)
@@ -390,7 +414,7 @@ def system_status(db: Session = Depends(get_db)) -> dict:
390
  return {
391
  "service": "blum-ai-financial-intelligence",
392
  "app_version": settings.app_version,
393
- "feature_set": "market-sniper-engine-v1",
394
  "environment": settings.environment,
395
  "generated_at": datetime.utcnow().isoformat(),
396
  "hugging_face": {
@@ -627,7 +651,7 @@ def system_status(db: Session = Depends(get_db)) -> dict:
627
  "Hugging Face serves the previous image until the Docker build finishes successfully.",
628
  "The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
629
  "Existing snapshots are refreshed by the autonomous engine after a successful deployment.",
630
- "Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.20.0.",
631
  ],
632
  }
633
 
@@ -815,6 +839,12 @@ def trading_game_status(db: Session = Depends(get_db)) -> dict:
815
  return TradingGameSimulator().status(db)
816
 
817
 
 
 
 
 
 
 
818
  @router.get("/api/copy-trading/status")
819
  def copy_trading_status(db: Session = Depends(get_db)) -> dict:
820
  return CopyTradingIntelligenceService().status(db)
@@ -830,6 +860,54 @@ def copy_trading_dashboard(limit: int = Query(default=25, ge=1, le=100), db: Ses
830
  return CopyTradingIntelligenceService().dashboard(db, limit=limit)
831
 
832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
  @router.post("/api/trading-game/run")
834
  def trading_game_run(
835
  batch_size: int = Query(default=settings.trading_game_batch_size, ge=1, le=500),
 
143
  BenchmarkMethodologyValidator,
144
  MissedWinnersEngine,
145
  )
146
+ from app.services.alpha_operating_system import (
147
+ AlphaGateService,
148
+ AlphaReadinessEngine,
149
+ BrainCommandSummaryService,
150
+ EdgeMapService,
151
+ PaperCopyTradingService,
152
+ TradingGameReadinessService,
153
+ V1_FEATURE_SET,
154
+ )
155
  from app.services.autonomous_engine import AutonomousResearchEngine, latest_autonomous_status
156
  from app.services.blum_financial_model import (
157
  build_training_dataset,
 
333
  return CentralBrainRuntime().state(db)
334
 
335
 
336
+ @router.get("/brain/command-summary")
337
+ def brain_command_summary(db: Session = Depends(get_db)) -> dict:
338
+ return BrainCommandSummaryService().summary(db)
339
+
340
+
341
+ @router.get("/brain/capabilities")
342
+ def brain_capabilities(db: Session = Depends(get_db)) -> dict:
343
+ return BrainCommandSummaryService().capabilities(db)
344
+
345
+
346
+ @router.get("/brain/evolution")
347
+ def brain_evolution(db: Session = Depends(get_db)) -> dict:
348
+ return BrainCommandSummaryService().evolution(db)
349
+
350
+
351
  @router.get("/snapshots/health")
352
  def snapshots_health(db: Session = Depends(get_db)) -> dict:
353
  return SnapshotWatchdogService().health(db, queue_rebuild=False)
 
414
  return {
415
  "service": "blum-ai-financial-intelligence",
416
  "app_version": settings.app_version,
417
+ "feature_set": V1_FEATURE_SET,
418
  "environment": settings.environment,
419
  "generated_at": datetime.utcnow().isoformat(),
420
  "hugging_face": {
 
651
  "Hugging Face serves the previous image until the Docker build finishes successfully.",
652
  "The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
653
  "Existing snapshots are refreshed by the autonomous engine after a successful deployment.",
654
+ "Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 1.0.0.",
655
  ],
656
  }
657
 
 
839
  return TradingGameSimulator().status(db)
840
 
841
 
842
+ @router.get("/trading-game/readiness")
843
+ @router.get("/api/trading-game/readiness")
844
+ def trading_game_readiness(db: Session = Depends(get_db)) -> dict:
845
+ return TradingGameReadinessService().readiness(db)
846
+
847
+
848
  @router.get("/api/copy-trading/status")
849
  def copy_trading_status(db: Session = Depends(get_db)) -> dict:
850
  return CopyTradingIntelligenceService().status(db)
 
860
  return CopyTradingIntelligenceService().dashboard(db, limit=limit)
861
 
862
 
863
+ @router.get("/alpha/readiness")
864
+ @router.get("/api/alpha/readiness")
865
+ def alpha_readiness(db: Session = Depends(get_db)) -> dict:
866
+ return AlphaReadinessEngine().readiness(db)
867
+
868
+
869
+ @router.get("/alpha/edge-map")
870
+ @router.get("/api/alpha/edge-map")
871
+ def alpha_edge_map(limit: int = Query(default=12, ge=1, le=50), db: Session = Depends(get_db)) -> dict:
872
+ return EdgeMapService().edge_map(db, limit=limit)
873
+
874
+
875
+ @router.get("/alpha/gates")
876
+ @router.get("/api/alpha/gates")
877
+ def alpha_gates(db: Session = Depends(get_db)) -> dict:
878
+ return AlphaGateService().gates(db)
879
+
880
+
881
+ @router.get("/paper-copy/summary")
882
+ @router.get("/api/paper-copy/summary")
883
+ def paper_copy_summary(limit: int = Query(default=12, ge=1, le=50), db: Session = Depends(get_db)) -> dict:
884
+ return PaperCopyTradingService().summary(db, limit=limit)
885
+
886
+
887
+ @router.get("/paper-copy/readiness")
888
+ @router.get("/api/paper-copy/readiness")
889
+ def paper_copy_readiness(db: Session = Depends(get_db)) -> dict:
890
+ return PaperCopyTradingService().readiness(db)
891
+
892
+
893
+ @router.get("/paper-copy/strategies")
894
+ @router.get("/api/paper-copy/strategies")
895
+ def paper_copy_strategies(limit: int = Query(default=40, ge=1, le=100), db: Session = Depends(get_db)) -> dict:
896
+ return PaperCopyTradingService().strategies(db, limit=limit)
897
+
898
+
899
+ @router.get("/paper-copy/positions")
900
+ @router.get("/api/paper-copy/positions")
901
+ def paper_copy_positions(limit: int = Query(default=80, ge=1, le=200), db: Session = Depends(get_db)) -> dict:
902
+ return PaperCopyTradingService().positions(db, limit=limit)
903
+
904
+
905
+ @router.get("/paper-copy/portfolio/{portfolio_id}")
906
+ @router.get("/api/paper-copy/portfolio/{portfolio_id}")
907
+ def paper_copy_portfolio(portfolio_id: str, db: Session = Depends(get_db)) -> dict:
908
+ return PaperCopyTradingService().portfolio(db, portfolio_id=portfolio_id)
909
+
910
+
911
  @router.post("/api/trading-game/run")
912
  def trading_game_run(
913
  batch_size: int = Query(default=settings.trading_game_batch_size, ge=1, le=500),
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.20.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 = "1.0.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
@@ -3186,3 +3186,214 @@ class CapitalInteractionRisk(Base):
3186
  evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3187
 
3188
  game = relationship("TradingGame")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3186
  evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3187
 
3188
  game = relationship("TradingGame")
3189
+
3190
+
3191
+ class TradingGameReadinessSnapshot(Base):
3192
+ __tablename__ = "trading_game_readiness_snapshots"
3193
+ __table_args__ = (
3194
+ Index("ix_tg_readiness_generated", "generated_at"),
3195
+ Index("ix_tg_readiness_status_grade", "status", "evidence_grade"),
3196
+ Index("ix_tg_readiness_game_generated", "game_id", "generated_at"),
3197
+ )
3198
+
3199
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3200
+ game_id: Mapped[int | None] = mapped_column(ForeignKey("trading_games.id", ondelete="SET NULL"), index=True)
3201
+ status: Mapped[str] = mapped_column(String(80), default="WAITING_FOR_SOURCE_DATA", index=True)
3202
+ evidence_grade: Mapped[str] = mapped_column(String(80), default="insufficient", index=True)
3203
+ blocker: Mapped[str] = mapped_column(Text, default="")
3204
+ next_required_action: Mapped[str] = mapped_column(Text, default="")
3205
+ payload_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3206
+ warnings_json: Mapped[list] = mapped_column(JsonType, default=list)
3207
+ methodology_version: Mapped[str] = mapped_column(String(80), default="trading-game-readiness-v1", index=True)
3208
+ generated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3209
+ expires_at: Mapped[datetime | None] = mapped_column(DateTime, index=True)
3210
+ is_stale: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
3211
+
3212
+ game = relationship("TradingGame")
3213
+
3214
+
3215
+ class AlphaReadinessSnapshot(Base):
3216
+ __tablename__ = "alpha_readiness_snapshots"
3217
+ __table_args__ = (
3218
+ Index("ix_alpha_readiness_generated", "generated_at"),
3219
+ Index("ix_alpha_readiness_score", "alpha_readiness_score", "evidence_grade"),
3220
+ )
3221
+
3222
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3223
+ status: Mapped[str] = mapped_column(String(80), default="INSUFFICIENT_EVIDENCE", index=True)
3224
+ alpha_readiness_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
3225
+ evidence_grade: Mapped[str] = mapped_column(String(80), default="insufficient", index=True)
3226
+ classification: Mapped[str] = mapped_column(String(120), default="not_ready", index=True)
3227
+ payload_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3228
+ warnings_json: Mapped[list] = mapped_column(JsonType, default=list)
3229
+ methodology_version: Mapped[str] = mapped_column(String(80), default="alpha-readiness-v1", index=True)
3230
+ generated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3231
+ expires_at: Mapped[datetime | None] = mapped_column(DateTime, index=True)
3232
+ is_stale: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
3233
+
3234
+
3235
+ class AlphaGateSnapshot(Base):
3236
+ __tablename__ = "alpha_gate_snapshots"
3237
+ __table_args__ = (
3238
+ Index("ix_alpha_gate_generated", "generated_at"),
3239
+ Index("ix_alpha_gate_status", "gate_name", "status"),
3240
+ )
3241
+
3242
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3243
+ gate_name: Mapped[str] = mapped_column(String(120), index=True)
3244
+ status: Mapped[str] = mapped_column(String(80), default="blocked", index=True)
3245
+ score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
3246
+ payload_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3247
+ warnings_json: Mapped[list] = mapped_column(JsonType, default=list)
3248
+ generated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3249
+
3250
+
3251
+ class EdgeMapSnapshot(Base):
3252
+ __tablename__ = "edge_map_snapshots"
3253
+ __table_args__ = (
3254
+ Index("ix_edge_map_generated", "generated_at"),
3255
+ Index("ix_edge_map_scope", "scope", "evidence_grade"),
3256
+ )
3257
+
3258
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3259
+ scope: Mapped[str] = mapped_column(String(120), default="global", index=True)
3260
+ evidence_grade: Mapped[str] = mapped_column(String(80), default="insufficient", index=True)
3261
+ payload_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3262
+ warnings_json: Mapped[list] = mapped_column(JsonType, default=list)
3263
+ generated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3264
+
3265
+
3266
+ class PaperCopyStrategy(Base):
3267
+ __tablename__ = "paper_copy_strategies"
3268
+ __table_args__ = (
3269
+ Index("ix_paper_copy_strategy_status_created", "status", "created_at"),
3270
+ Index("ix_paper_copy_strategy_score", "copyability_score"),
3271
+ )
3272
+
3273
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3274
+ strategy_id: Mapped[str] = mapped_column(String(100), unique=True, index=True)
3275
+ name: Mapped[str] = mapped_column(String(180), default="BLUM Paper Copy Strategy")
3276
+ status: Mapped[str] = mapped_column(String(80), default="paper_only", index=True)
3277
+ strategy_type: Mapped[str] = mapped_column(String(120), default="conditional_copy_watchlist", index=True)
3278
+ copyability_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
3279
+ risk_budget_percent: Mapped[float] = mapped_column(Float, default=1.0)
3280
+ max_open_positions: Mapped[int] = mapped_column(Integer, default=5)
3281
+ rules_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3282
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3283
+ warnings_json: Mapped[list] = mapped_column(JsonType, default=list)
3284
+ paper_only: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
3285
+ no_broker_execution: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
3286
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3287
+ updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)
3288
+
3289
+
3290
+ class PaperCopyPortfolio(Base):
3291
+ __tablename__ = "paper_copy_portfolios"
3292
+ __table_args__ = (
3293
+ Index("ix_paper_copy_portfolio_status_updated", "status", "updated_at"),
3294
+ Index("ix_paper_copy_portfolio_strategy", "strategy_id"),
3295
+ )
3296
+
3297
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3298
+ portfolio_id: Mapped[str] = mapped_column(String(100), unique=True, index=True)
3299
+ strategy_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_strategies.id", ondelete="SET NULL"), index=True)
3300
+ status: Mapped[str] = mapped_column(String(80), default="paper_active", index=True)
3301
+ starting_capital: Mapped[float] = mapped_column(Float, default=100.0)
3302
+ current_capital: Mapped[float] = mapped_column(Float, default=100.0, index=True)
3303
+ cash: Mapped[float] = mapped_column(Float, default=100.0)
3304
+ exposure: Mapped[float] = mapped_column(Float, default=0.0)
3305
+ realized_pnl: Mapped[float] = mapped_column(Float, default=0.0)
3306
+ unrealized_pnl: Mapped[float] = mapped_column(Float, default=0.0)
3307
+ benchmark_ticker: Mapped[str] = mapped_column(String(32), default="SPY", index=True)
3308
+ risk_state: Mapped[str] = mapped_column(String(80), default="conservative", index=True)
3309
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3310
+ warnings_json: Mapped[list] = mapped_column(JsonType, default=list)
3311
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3312
+ updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)
3313
+
3314
+ strategy = relationship("PaperCopyStrategy")
3315
+
3316
+
3317
+ class PaperCopyOrder(Base):
3318
+ __tablename__ = "paper_copy_orders"
3319
+ __table_args__ = (
3320
+ Index("ix_paper_copy_orders_portfolio_created", "portfolio_id", "created_at"),
3321
+ Index("ix_paper_copy_orders_ticker_status", "ticker", "status"),
3322
+ )
3323
+
3324
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3325
+ portfolio_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_portfolios.id", ondelete="CASCADE"), index=True)
3326
+ strategy_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_strategies.id", ondelete="SET NULL"), index=True)
3327
+ source_trade_plan_id: Mapped[int | None] = mapped_column(ForeignKey("trade_plans.id", ondelete="SET NULL"), index=True)
3328
+ ticker: Mapped[str] = mapped_column(String(32), index=True)
3329
+ side: Mapped[str] = mapped_column(String(40), default="paper_buy", index=True)
3330
+ status: Mapped[str] = mapped_column(String(80), default="planned", index=True)
3331
+ order_type: Mapped[str] = mapped_column(String(80), default="conditional_paper", index=True)
3332
+ trigger_condition: Mapped[str] = mapped_column(Text, default="")
3333
+ paper_price: Mapped[float | None] = mapped_column(Float)
3334
+ paper_quantity: Mapped[float | None] = mapped_column(Float)
3335
+ risk_amount: Mapped[float | None] = mapped_column(Float)
3336
+ invalidation_level: Mapped[float | None] = mapped_column(Float)
3337
+ target_1: Mapped[float | None] = mapped_column(Float)
3338
+ target_2: Mapped[float | None] = mapped_column(Float)
3339
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3340
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3341
+
3342
+ portfolio = relationship("PaperCopyPortfolio")
3343
+ strategy = relationship("PaperCopyStrategy")
3344
+ source_trade_plan = relationship("TradePlan")
3345
+
3346
+
3347
+ class PaperCopyPosition(Base):
3348
+ __tablename__ = "paper_copy_positions"
3349
+ __table_args__ = (
3350
+ Index("ix_paper_copy_positions_portfolio_status", "portfolio_id", "status"),
3351
+ Index("ix_paper_copy_positions_ticker_opened", "ticker", "opened_at"),
3352
+ )
3353
+
3354
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3355
+ portfolio_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_portfolios.id", ondelete="CASCADE"), index=True)
3356
+ strategy_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_strategies.id", ondelete="SET NULL"), index=True)
3357
+ source_order_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_orders.id", ondelete="SET NULL"), index=True)
3358
+ ticker: Mapped[str] = mapped_column(String(32), index=True)
3359
+ status: Mapped[str] = mapped_column(String(80), default="open", index=True)
3360
+ quantity: Mapped[float] = mapped_column(Float, default=0.0)
3361
+ entry_price: Mapped[float | None] = mapped_column(Float)
3362
+ current_price: Mapped[float | None] = mapped_column(Float)
3363
+ market_value: Mapped[float | None] = mapped_column(Float)
3364
+ unrealized_pnl: Mapped[float | None] = mapped_column(Float)
3365
+ realized_pnl: Mapped[float | None] = mapped_column(Float)
3366
+ invalidation_level: Mapped[float | None] = mapped_column(Float)
3367
+ target_1: Mapped[float | None] = mapped_column(Float)
3368
+ target_2: Mapped[float | None] = mapped_column(Float)
3369
+ opened_at: Mapped[datetime | None] = mapped_column(DateTime, index=True)
3370
+ closed_at: Mapped[datetime | None] = mapped_column(DateTime, index=True)
3371
+ evidence_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3372
+
3373
+ portfolio = relationship("PaperCopyPortfolio")
3374
+ strategy = relationship("PaperCopyStrategy")
3375
+ source_order = relationship("PaperCopyOrder")
3376
+
3377
+
3378
+ class PaperCopyPortfolioSnapshot(Base):
3379
+ __tablename__ = "paper_copy_portfolio_snapshots"
3380
+ __table_args__ = (
3381
+ Index("ix_paper_copy_portfolio_snapshots_portfolio_created", "portfolio_id", "created_at"),
3382
+ Index("ix_paper_copy_portfolio_snapshots_score", "copyability_score", "evidence_grade"),
3383
+ )
3384
+
3385
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
3386
+ portfolio_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_portfolios.id", ondelete="CASCADE"), index=True)
3387
+ strategy_id: Mapped[int | None] = mapped_column(ForeignKey("paper_copy_strategies.id", ondelete="SET NULL"), index=True)
3388
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
3389
+ capital: Mapped[float | None] = mapped_column(Float)
3390
+ exposure: Mapped[float | None] = mapped_column(Float)
3391
+ open_positions: Mapped[int] = mapped_column(Integer, default=0)
3392
+ pending_orders: Mapped[int] = mapped_column(Integer, default=0)
3393
+ copyability_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
3394
+ evidence_grade: Mapped[str] = mapped_column(String(80), default="insufficient", index=True)
3395
+ payload_json: Mapped[dict] = mapped_column(JsonType, default=dict)
3396
+ warnings_json: Mapped[list] = mapped_column(JsonType, default=list)
3397
+
3398
+ portfolio = relationship("PaperCopyPortfolio")
3399
+ strategy = relationship("PaperCopyStrategy")
backend/app/services/alpha_operating_system.py ADDED
@@ -0,0 +1,815 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter, defaultdict
4
+ from datetime import datetime
5
+ from statistics import mean
6
+ from typing import Any
7
+
8
+ from sqlalchemy import desc, func, select
9
+ from sqlalchemy.orm import Session
10
+
11
+ from app.models import (
12
+ AlphaLossAttribution,
13
+ AlphaRecoveryAction,
14
+ BackgroundJobState,
15
+ BlumTradingPowerScore,
16
+ BusinessQualityScore,
17
+ DashboardSnapshot,
18
+ DecisionSuperiorityScore,
19
+ EquityCurveSnapshot,
20
+ LearningBenchmarkComparison,
21
+ LearningFocusPriority,
22
+ LearningRun,
23
+ LearningStrengthWeaknessMap,
24
+ PaperCopyOrder,
25
+ PaperCopyPortfolio,
26
+ PaperCopyPortfolioSnapshot,
27
+ PaperCopyPosition,
28
+ PaperCopyStrategy,
29
+ PortfolioQualityScore,
30
+ SniperScore,
31
+ TradeLearningEvidence,
32
+ TradePlan,
33
+ TradingGame,
34
+ TradingGameLedgerSnapshot,
35
+ TradingGameReadinessSnapshot,
36
+ TradingGameTrade,
37
+ TradingIntelligenceMetric,
38
+ )
39
+ from app.services.copy_trading_intelligence import COPY_TRADING_POLICY, CopyTradingIntelligenceService
40
+ from app.services.dashboard_snapshots import DashboardSnapshotService
41
+ from app.services.learning_summary import LearningSummaryService
42
+
43
+
44
+ V1_VERSION = "1.0.0"
45
+ V1_FEATURE_SET = "alpha-operating-system"
46
+ READINESS_METHODOLOGY = "trading-game-readiness-v1"
47
+ ALPHA_METHODOLOGY = "alpha-readiness-v1"
48
+
49
+
50
+ class TradingGameReadinessService:
51
+ """Explains why the Trading Game can or cannot render usable evidence.
52
+
53
+ GET endpoints call `readiness`, which is bounded and read-only. Snapshot
54
+ producer jobs may call `snapshot_payload` and persist it through the generic
55
+ dashboard snapshot table.
56
+ """
57
+
58
+ def readiness(self, db: Session) -> dict:
59
+ game = latest_game(db)
60
+ ledger = latest_row(db, TradingGameLedgerSnapshot)
61
+ equity = latest_row(db, EquityCurveSnapshot)
62
+ latest_trade = db.scalar(select(TradingGameTrade).order_by(desc(TradingGameTrade.created_at)).limit(1))
63
+ source_decision_count = int(db.scalar(select(func.count(TradePlan.id))) or 0)
64
+ source_sniper_count = int(db.scalar(select(func.count(SniperScore.id))) or 0)
65
+ source_trade_count = int(db.scalar(select(func.count(TradingGameTrade.id))) or 0)
66
+ completed_trade_count = int(db.scalar(select(func.count(TradingGameTrade.id)).where(TradingGameTrade.exit_date.is_not(None))) or 0)
67
+ open_trade_count = int(db.scalar(select(func.count(TradingGameTrade.id)).where(TradingGameTrade.exit_date.is_(None))) or 0)
68
+ eligible_trade_count = int(
69
+ db.scalar(
70
+ select(func.count(TradingGameTrade.id)).where(
71
+ TradingGameTrade.entry_price.is_not(None),
72
+ TradingGameTrade.position_size > 0,
73
+ TradingGameTrade.invalidation_level.is_not(None),
74
+ )
75
+ )
76
+ or 0
77
+ )
78
+ latest_job = latest_background_job(db, "blum_trading_game")
79
+ latest_snapshot_job = latest_background_job(db, "snapshot_producer")
80
+ ledger_status = snapshot_status(ledger)
81
+ equity_status = snapshot_status(equity)
82
+ warnings: list[str] = []
83
+ blocker = ""
84
+ next_action = "Backend learning and snapshot workers can continue normally."
85
+
86
+ if game is None:
87
+ status = "WAITING_FOR_SOURCE_DATA"
88
+ blocker = "No TradingGame row exists yet."
89
+ next_action = "Let the backend Trading Game worker create the first paper game."
90
+ elif source_trade_count == 0 and source_decision_count == 0 and source_sniper_count == 0:
91
+ status = "WAITING_FOR_SOURCE_DATA"
92
+ blocker = "No source decisions, sniper scores or trades exist yet."
93
+ next_action = "Let Market Sniper and Learning Loop generate source decisions."
94
+ elif source_trade_count == 0:
95
+ status = "BUILDING"
96
+ blocker = "Trade plans or sniper evidence exist, but no paper trades are stored yet."
97
+ next_action = "Let the Trading Game worker convert eligible decisions into paper trades."
98
+ elif ledger is None or equity is None:
99
+ status = "BUILDING"
100
+ blocker = "Trading Game rows exist, but ledger/equity snapshots are not ready."
101
+ next_action = "Run the snapshot producer in background; do not block the UI."
102
+ warnings.append("Trading evidence exists but snapshots are missing.")
103
+ elif ledger_status == "failed" or equity_status == "failed":
104
+ status = "FAILED"
105
+ blocker = "A required Trading Game snapshot is marked failed."
106
+ next_action = "Check snapshot producer logs and rebuild the affected snapshot."
107
+ elif ledger_status == "stale" or equity_status == "stale":
108
+ status = "STALE_BUT_USABLE"
109
+ warnings.append("Trading Game snapshot is stale; UI can render while refresh catches up.")
110
+ elif eligible_trade_count < 3:
111
+ status = "INSUFFICIENT_EVIDENCE"
112
+ blocker = "Fewer than three eligible trades have full risk plan evidence."
113
+ next_action = "Keep collecting paper trades before drawing performance conclusions."
114
+ else:
115
+ status = "READY"
116
+
117
+ data_quality_blockers = []
118
+ if source_trade_count and eligible_trade_count == 0:
119
+ data_quality_blockers.append("stored trades lack entry/risk/invalidation fields")
120
+ if data_quality_blockers:
121
+ status = "DATA_QUALITY_BLOCKED"
122
+ blocker = "; ".join(data_quality_blockers)
123
+ next_action = "Repair trade transparency enrichment before evaluating performance."
124
+
125
+ evidence_grade = evidence_grade_for(completed_trade_count, live_trade_count=db_live_trade_count(db), has_benchmark=has_benchmark_rows(db))
126
+ payload = {
127
+ "status": status,
128
+ "generated_at": datetime.utcnow().isoformat(),
129
+ "methodology_version": READINESS_METHODOLOGY,
130
+ "game_id": game.game_id if game else None,
131
+ "game_pk": game.id if game else None,
132
+ "source_decision_count": source_decision_count + source_sniper_count,
133
+ "source_trade_plan_count": source_decision_count,
134
+ "source_sniper_score_count": source_sniper_count,
135
+ "source_trade_count": source_trade_count,
136
+ "completed_trade_count": completed_trade_count,
137
+ "open_trade_count": open_trade_count,
138
+ "eligible_trade_count": eligible_trade_count,
139
+ "ledger_snapshot_status": ledger_status,
140
+ "equity_snapshot_status": equity_status,
141
+ "benchmark_snapshot_status": benchmark_snapshot_status(db),
142
+ "last_trade_at": latest_trade.created_at.isoformat() if latest_trade and latest_trade.created_at else None,
143
+ "last_ledger_snapshot_at": ledger.created_at.isoformat() if ledger and ledger.created_at else None,
144
+ "last_equity_snapshot_at": equity.created_at.isoformat() if equity and equity.created_at else None,
145
+ "worker_status": {
146
+ "trading_game": serialize_job(latest_job),
147
+ "snapshot_producer": serialize_job(latest_snapshot_job),
148
+ },
149
+ "worker_phase": latest_job.stage_name if latest_job else None,
150
+ "blocker": blocker,
151
+ "next_required_action": next_action,
152
+ "evidence_grade": evidence_grade,
153
+ "warnings": warnings,
154
+ "ui_state_policy": "Never show permanent generic loading. Render one of READY, BUILDING, WAITING_FOR_SOURCE_DATA, STALE_BUT_USABLE, FAILED, INSUFFICIENT_EVIDENCE, DATA_QUALITY_BLOCKED.",
155
+ }
156
+ return payload
157
+
158
+ def latest_snapshot(self, db: Session) -> dict:
159
+ row = latest_row(db, TradingGameReadinessSnapshot)
160
+ if row is None:
161
+ return {"status": "missing", "payload": None}
162
+ return {
163
+ "status": "stale" if is_stale(row) else "ready",
164
+ "payload": row.payload_json or {},
165
+ "created_at": row.generated_at.isoformat() if row.generated_at else None,
166
+ "expires_at": row.expires_at.isoformat() if row.expires_at else None,
167
+ "warnings": row.warnings_json or [],
168
+ }
169
+
170
+
171
+ class BrainCommandSummaryService:
172
+ """Compact status of BLUM's brain for the Command page."""
173
+
174
+ def summary(self, db: Session) -> dict:
175
+ learning = LearningSummaryService().summary(db)
176
+ readiness = TradingGameReadinessService().readiness(db)
177
+ alpha = AlphaReadinessEngine().readiness(db)
178
+ paper = PaperCopyTradingService().summary(db, limit=8)
179
+ latest_power = latest_row(db, BlumTradingPowerScore)
180
+ latest_decision = latest_row(db, DecisionSuperiorityScore)
181
+ latest_business = latest_row(db, BusinessQualityScore)
182
+ latest_portfolio = latest_row(db, PortfolioQualityScore)
183
+ latest_metric = latest_row(db, TradingIntelligenceMetric)
184
+ latest_run = latest_row(db, LearningRun)
185
+ weakness = db.scalar(select(LearningStrengthWeaknessMap).order_by(desc(LearningStrengthWeaknessMap.weakness_score), desc(LearningStrengthWeaknessMap.calculated_at)).limit(1))
186
+ focus = db.scalar(
187
+ select(LearningFocusPriority)
188
+ .where(LearningFocusPriority.status.in_(["active", "proposed"]))
189
+ .order_by(desc(LearningFocusPriority.expected_learning_value), desc(LearningFocusPriority.created_at))
190
+ .limit(1)
191
+ )
192
+ capabilities = capability_matrix(
193
+ trading_power=latest_power,
194
+ decision=latest_decision,
195
+ business=latest_business,
196
+ portfolio=latest_portfolio,
197
+ metric=latest_metric,
198
+ readiness=readiness,
199
+ alpha=alpha,
200
+ paper=paper,
201
+ )
202
+ score_values = [item["score"] for item in capabilities if item["score"] is not None]
203
+ brain_score = round(mean(score_values), 2) if score_values else None
204
+ warnings = list(learning.get("warnings") or [])[:3] + alpha.get("warnings", [])[:3] + paper.get("warnings", [])[:3]
205
+ return {
206
+ "status": "ready" if brain_score is not None else "initializing",
207
+ "version": V1_VERSION,
208
+ "feature_set": V1_FEATURE_SET,
209
+ "generated_at": datetime.utcnow().isoformat(),
210
+ "brain_capability_score": brain_score,
211
+ "brain_classification": classify_brain_score(brain_score),
212
+ "brain_status_strip": {
213
+ "learning_status": learning.get("learning_loop_status"),
214
+ "trading_game_readiness": readiness.get("status"),
215
+ "alpha_readiness": alpha.get("status"),
216
+ "paper_copy_readiness": paper.get("readiness", {}).get("status"),
217
+ "latest_run_at": learning.get("latest_learning_run_at"),
218
+ },
219
+ "learning_evolution": {
220
+ "latest_run_status": getattr(latest_run, "status", None) if latest_run else learning.get("latest_learning_run_status"),
221
+ "win_rate": learning.get("win_rate"),
222
+ "expectancy_r": learning.get("expectancy_r"),
223
+ "target_progress": learning.get("target_progress"),
224
+ "trading_power_score": learning.get("trading_power_score"),
225
+ "trading_power_classification": learning.get("trading_power_classification"),
226
+ },
227
+ "capability_matrix": capabilities,
228
+ "benchmark_truth": learning.get("benchmark_summary"),
229
+ "improvement_regression": {
230
+ "top_weakness": serialize_weakness(weakness),
231
+ "next_focus": serialize_focus(focus),
232
+ "latest_lesson": learning.get("latest_lesson_learned"),
233
+ "truth_panel": learning.get("truth_panel") or [],
234
+ },
235
+ "copy_readiness": paper.get("readiness"),
236
+ "warnings": dedupe(warnings),
237
+ "data_freshness": {
238
+ "learning_summary": learning.get("generated_at"),
239
+ "trading_game_readiness": readiness.get("generated_at"),
240
+ "alpha_readiness": alpha.get("generated_at"),
241
+ "paper_copy": paper.get("generated_at"),
242
+ },
243
+ "policy": "Command reads compact evidence summaries only. It does not trigger learning, recalculation, trading or broker execution.",
244
+ }
245
+
246
+ def capabilities(self, db: Session) -> dict:
247
+ summary = self.summary(db)
248
+ return {"status": summary["status"], "rows": summary["capability_matrix"], "generated_at": summary["generated_at"]}
249
+
250
+ def evolution(self, db: Session) -> dict:
251
+ rows = db.scalars(select(BlumTradingPowerScore).order_by(desc(BlumTradingPowerScore.calculated_at)).limit(30)).all()
252
+ return {
253
+ "status": "ready" if rows else "insufficient_evidence",
254
+ "rows": [
255
+ {
256
+ "calculated_at": row.calculated_at.isoformat() if row.calculated_at else None,
257
+ "score": row.score,
258
+ "classification": row.classification,
259
+ "benchmark_relative_score": row.benchmark_relative_score,
260
+ "learning_velocity_score": row.learning_velocity_score,
261
+ }
262
+ for row in rows
263
+ ],
264
+ "policy": "Evolution uses stored Trading Power snapshots only.",
265
+ }
266
+
267
+
268
+ class AlphaReadinessEngine:
269
+ """Strict alpha readiness summary from stored evidence only."""
270
+
271
+ def readiness(self, db: Session) -> dict:
272
+ power = latest_row(db, BlumTradingPowerScore)
273
+ decision = latest_row(db, DecisionSuperiorityScore)
274
+ portfolio = latest_row(db, PortfolioQualityScore)
275
+ benchmark_rows = db.scalars(select(LearningBenchmarkComparison).order_by(desc(LearningBenchmarkComparison.calculated_at)).limit(20)).all()
276
+ trade_count = int(db.scalar(select(func.count(TradingGameTrade.id))) or 0)
277
+ live_count = db_live_trade_count(db)
278
+ warnings: list[str] = []
279
+ components = {
280
+ "trading_power": getattr(power, "score", None),
281
+ "decision_superiority": getattr(decision, "score", None),
282
+ "portfolio_quality": getattr(portfolio, "portfolio_quality_score", None),
283
+ "benchmark_evidence": benchmark_component(benchmark_rows),
284
+ "sample_depth": sample_depth_score(trade_count),
285
+ "live_validation": sample_depth_score(live_count),
286
+ }
287
+ values = [value for value in components.values() if value is not None]
288
+ score = round(mean(values), 2) if values else 0.0
289
+ if trade_count < 30:
290
+ score = min(score, 60.0)
291
+ warnings.append("insufficient_trade_sample_caps_alpha_readiness_at_60")
292
+ if live_count < 10:
293
+ score = min(score, 75.0)
294
+ warnings.append("live_forward_evidence_is_not_mature")
295
+ if not benchmark_rows:
296
+ score = min(score, 50.0)
297
+ warnings.append("benchmark_comparison_missing")
298
+ evidence_grade = evidence_grade_for(trade_count, live_count, bool(benchmark_rows))
299
+ status = "READY_FOR_RESEARCH" if score >= 60 and evidence_grade not in {"insufficient", "very_low"} else "INSUFFICIENT_EVIDENCE"
300
+ return {
301
+ "status": status,
302
+ "generated_at": datetime.utcnow().isoformat(),
303
+ "methodology_version": ALPHA_METHODOLOGY,
304
+ "alpha_readiness_score": score,
305
+ "classification": classify_alpha_score(score),
306
+ "evidence_grade": evidence_grade,
307
+ "components": components,
308
+ "benchmark_summary": benchmark_truth(benchmark_rows),
309
+ "sample": {"trades": trade_count, "live_forward_trades": live_count, "benchmarks": len(benchmark_rows)},
310
+ "warnings": dedupe(warnings),
311
+ "truth_layer": alpha_truth(score, benchmark_rows, trade_count, live_count),
312
+ "policy": "No alpha claim is valid without benchmark-relative evidence, sample size and live paper validation.",
313
+ }
314
+
315
+
316
+ class EdgeMapService:
317
+ """Lightweight map of where stored Trading Game evidence appears strongest/weakest."""
318
+
319
+ def edge_map(self, db: Session, limit: int = 12) -> dict:
320
+ rows = db.scalars(select(TradingGameTrade).order_by(desc(TradingGameTrade.created_at)).limit(500)).all()
321
+ by_setup = aggregate_edges(rows, "setup_type")
322
+ by_sector = aggregate_edges(rows, "sector")
323
+ by_regime = aggregate_edges(rows, "market_regime_at_entry")
324
+ return {
325
+ "status": "ready" if rows else "insufficient_evidence",
326
+ "generated_at": datetime.utcnow().isoformat(),
327
+ "sample_size": len(rows),
328
+ "evidence_grade": evidence_grade_for(len(rows), db_live_trade_count(db), has_benchmark_rows(db)),
329
+ "best_setups": sorted(by_setup, key=lambda item: item["edge_score"], reverse=True)[:limit],
330
+ "weakest_setups": sorted(by_setup, key=lambda item: item["edge_score"])[:limit],
331
+ "best_sectors": sorted(by_sector, key=lambda item: item["edge_score"], reverse=True)[:limit],
332
+ "weakest_sectors": sorted(by_sector, key=lambda item: item["edge_score"])[:limit],
333
+ "regimes": sorted(by_regime, key=lambda item: item["edge_score"], reverse=True)[:limit],
334
+ "warnings": ["sample_size_low"] if len(rows) < 50 else [],
335
+ "policy": "Edge map is descriptive stored evidence, not a prediction or guarantee.",
336
+ }
337
+
338
+
339
+ class AlphaGateService:
340
+ """Converts evidence into transparent gates before BLUM can call a setup copyable."""
341
+
342
+ def gates(self, db: Session) -> dict:
343
+ alpha = AlphaReadinessEngine().readiness(db)
344
+ readiness = TradingGameReadinessService().readiness(db)
345
+ paper = PaperCopyTradingService().summary(db, limit=5)
346
+ gates = [
347
+ gate("sample_depth", alpha["sample"]["trades"] >= 30, alpha["sample"]["trades"], ">= 30 stored paper trades"),
348
+ gate("live_validation", alpha["sample"]["live_forward_trades"] >= 10, alpha["sample"]["live_forward_trades"], ">= 10 live forward paper trades"),
349
+ gate("benchmark_evidence", alpha["sample"]["benchmarks"] > 0, alpha["sample"]["benchmarks"], "benchmark rows exist"),
350
+ gate("trading_game_renderable", readiness["status"] in {"READY", "STALE_BUT_USABLE", "INSUFFICIENT_EVIDENCE"}, readiness["status"], "Trading Game evidence can render"),
351
+ gate("paper_copy_guardrails", paper["paper_only"] and paper["no_broker_execution"], "paper_only", "no broker execution"),
352
+ ]
353
+ return {
354
+ "status": "ready",
355
+ "generated_at": datetime.utcnow().isoformat(),
356
+ "alpha_readiness_score": alpha["alpha_readiness_score"],
357
+ "all_required_gates_passed": all(item["passed"] for item in gates if item["required"]),
358
+ "rows": gates,
359
+ "policy": "Gates are pre-trade research controls. They do not authorize real trading.",
360
+ }
361
+
362
+
363
+ class PaperCopyTradingService:
364
+ """Paper-only copy trading operating layer.
365
+
366
+ This wraps existing CopyTradingIntelligenceService evidence and exposes
367
+ durable portfolio/strategy state when present. It never connects to brokers.
368
+ """
369
+
370
+ def readiness(self, db: Session) -> dict:
371
+ candidate_payload = CopyTradingIntelligenceService().candidates(db, limit=20)
372
+ strategy_count = int(db.scalar(select(func.count(PaperCopyStrategy.id))) or 0)
373
+ portfolio_count = int(db.scalar(select(func.count(PaperCopyPortfolio.id))) or 0)
374
+ position_count = int(db.scalar(select(func.count(PaperCopyPosition.id))) or 0)
375
+ order_count = int(db.scalar(select(func.count(PaperCopyOrder.id))) or 0)
376
+ copyable = [row for row in candidate_payload.get("rows", []) if row.get("copy_readiness") in {"copy_ready_if_triggered", "wait_for_trigger"}]
377
+ status = "READY_FOR_PAPER_MONITORING" if copyable else "WAITING_FOR_COPYABLE_SETUPS"
378
+ warnings = []
379
+ if not strategy_count:
380
+ warnings.append("no_durable_paper_copy_strategy_created_yet")
381
+ if not portfolio_count:
382
+ warnings.append("no_durable_paper_copy_portfolio_created_yet")
383
+ return {
384
+ "status": status,
385
+ "candidate_count": len(candidate_payload.get("rows", [])),
386
+ "copyable_candidate_count": len(copyable),
387
+ "strategy_count": strategy_count,
388
+ "portfolio_count": portfolio_count,
389
+ "open_position_count": position_count,
390
+ "pending_order_count": order_count,
391
+ "warnings": warnings,
392
+ }
393
+
394
+ def summary(self, db: Session, limit: int = 12) -> dict:
395
+ dashboard = CopyTradingIntelligenceService().dashboard(db, limit=limit)
396
+ readiness = self.readiness(db)
397
+ latest_strategy = latest_row(db, PaperCopyStrategy)
398
+ latest_portfolio = latest_row(db, PaperCopyPortfolio)
399
+ latest_snapshot = latest_row(db, PaperCopyPortfolioSnapshot)
400
+ return {
401
+ "status": dashboard.get("status", "ok"),
402
+ "generated_at": datetime.utcnow().isoformat(),
403
+ "mode": "paper_copy_operating_system",
404
+ "paper_only": True,
405
+ "no_broker_execution": True,
406
+ "readiness": readiness,
407
+ "summary": dashboard.get("summary", {}),
408
+ "rows": dashboard.get("rows", []),
409
+ "strategy": serialize_paper_strategy(latest_strategy),
410
+ "portfolio": serialize_paper_portfolio(latest_portfolio),
411
+ "portfolio_snapshot": serialize_paper_snapshot(latest_snapshot),
412
+ "guardrails": dashboard.get("guardrails", []),
413
+ "truth_layer": dashboard.get("truth_layer", []),
414
+ "warnings": readiness.get("warnings", []),
415
+ "policy": COPY_TRADING_POLICY,
416
+ }
417
+
418
+ def strategies(self, db: Session, limit: int = 40) -> dict:
419
+ rows = db.scalars(select(PaperCopyStrategy).order_by(desc(PaperCopyStrategy.created_at)).limit(limit)).all()
420
+ if not rows:
421
+ summary = self.summary(db, limit=8)
422
+ return {
423
+ "status": "no_durable_strategy_yet",
424
+ "rows": [],
425
+ "candidate_strategy": strategy_candidate_from_summary(summary),
426
+ "policy": COPY_TRADING_POLICY,
427
+ }
428
+ return {"status": "ready", "rows": [serialize_paper_strategy(row) for row in rows], "policy": COPY_TRADING_POLICY}
429
+
430
+ def positions(self, db: Session, limit: int = 80) -> dict:
431
+ rows = db.scalars(select(PaperCopyPosition).order_by(desc(PaperCopyPosition.opened_at)).limit(limit)).all()
432
+ return {"status": "ready" if rows else "empty", "rows": [serialize_paper_position(row) for row in rows], "policy": COPY_TRADING_POLICY}
433
+
434
+ def portfolio(self, db: Session, portfolio_id: str) -> dict:
435
+ row = db.scalar(select(PaperCopyPortfolio).where(PaperCopyPortfolio.portfolio_id == portfolio_id).limit(1))
436
+ if row is None:
437
+ return {"status": "not_found", "portfolio_id": portfolio_id, "policy": COPY_TRADING_POLICY}
438
+ positions = db.scalars(select(PaperCopyPosition).where(PaperCopyPosition.portfolio_id == row.id).order_by(desc(PaperCopyPosition.opened_at)).limit(80)).all()
439
+ snapshots = db.scalars(select(PaperCopyPortfolioSnapshot).where(PaperCopyPortfolioSnapshot.portfolio_id == row.id).order_by(desc(PaperCopyPortfolioSnapshot.created_at)).limit(80)).all()
440
+ return {
441
+ "status": "ready",
442
+ "portfolio": serialize_paper_portfolio(row),
443
+ "positions": [serialize_paper_position(item) for item in positions],
444
+ "snapshots": [serialize_paper_snapshot(item) for item in snapshots],
445
+ "policy": COPY_TRADING_POLICY,
446
+ }
447
+
448
+
449
+ def latest_game(db: Session) -> TradingGame | None:
450
+ return db.scalar(select(TradingGame).order_by(desc(TradingGame.updated_at)).limit(1))
451
+
452
+
453
+ def latest_row(db: Session, model):
454
+ order_column = None
455
+ for name in ["created_at", "generated_at", "calculated_at", "updated_at"]:
456
+ if hasattr(model, name):
457
+ order_column = getattr(model, name)
458
+ break
459
+ if order_column is None:
460
+ return db.scalar(select(model).limit(1))
461
+ return db.scalar(select(model).order_by(desc(order_column)).limit(1))
462
+
463
+
464
+ def latest_background_job(db: Session, job_name: str) -> BackgroundJobState | None:
465
+ return db.scalar(select(BackgroundJobState).where(BackgroundJobState.job_name == job_name).order_by(desc(BackgroundJobState.last_started_at)).limit(1))
466
+
467
+
468
+ def snapshot_status(row: Any | None) -> str:
469
+ if row is None:
470
+ return "missing"
471
+ if getattr(row, "is_stale", False):
472
+ return "stale"
473
+ expires = getattr(row, "expires_at", None)
474
+ if expires and expires < datetime.utcnow():
475
+ return "stale"
476
+ if (getattr(row, "payload_json", None) or {}).get("status") == "failed":
477
+ return "failed"
478
+ return "ready"
479
+
480
+
481
+ def benchmark_snapshot_status(db: Session) -> str:
482
+ snapshot = db.scalar(select(DashboardSnapshot).where(DashboardSnapshot.snapshot_type == "benchmark_summary").order_by(desc(DashboardSnapshot.created_at)).limit(1))
483
+ if snapshot is None:
484
+ return "missing"
485
+ return "stale" if is_stale(snapshot) else "ready"
486
+
487
+
488
+ def is_stale(row: Any) -> bool:
489
+ expires = getattr(row, "expires_at", None)
490
+ return bool(getattr(row, "is_stale", False) or (expires is not None and expires < datetime.utcnow()))
491
+
492
+
493
+ def db_live_trade_count(db: Session) -> int:
494
+ return int(db.scalar(select(func.count(TradingGameTrade.id)).where(TradingGameTrade.mode.like("%live%"))) or 0)
495
+
496
+
497
+ def has_benchmark_rows(db: Session) -> bool:
498
+ return bool(db.scalar(select(LearningBenchmarkComparison.id).limit(1)))
499
+
500
+
501
+ def evidence_grade_for(trades: int, live_trade_count: int, has_benchmark: bool) -> str:
502
+ if trades < 10:
503
+ return "insufficient"
504
+ if trades < 30:
505
+ return "very_low"
506
+ if trades < 100:
507
+ return "low" if not has_benchmark else "medium_low"
508
+ if live_trade_count < 30:
509
+ return "medium"
510
+ return "strong"
511
+
512
+
513
+ def benchmark_component(rows: list[LearningBenchmarkComparison]) -> float | None:
514
+ if not rows:
515
+ return None
516
+ values = []
517
+ for row in rows:
518
+ excess = row.excess_return
519
+ if excess is None:
520
+ continue
521
+ values.append(max(0.0, min(100.0, 50.0 + float(excess))))
522
+ return round(mean(values), 2) if values else None
523
+
524
+
525
+ def sample_depth_score(count: int) -> float:
526
+ if count <= 0:
527
+ return 0.0
528
+ if count >= 250:
529
+ return 100.0
530
+ return round(min(100.0, count / 2.5), 2)
531
+
532
+
533
+ def benchmark_truth(rows: list[LearningBenchmarkComparison]) -> dict:
534
+ latest: dict[str, LearningBenchmarkComparison] = {}
535
+ for row in rows:
536
+ latest.setdefault(row.benchmark_name, row)
537
+ return {
538
+ name: {
539
+ "result_label": row.result_label,
540
+ "excess_return": row.excess_return,
541
+ "sample_size": row.sample_size,
542
+ "statistical_confidence": row.statistical_confidence,
543
+ }
544
+ for name, row in latest.items()
545
+ }
546
+
547
+
548
+ def alpha_truth(score: float, rows: list[LearningBenchmarkComparison], trade_count: int, live_count: int) -> list[str]:
549
+ lines = [f"Alpha Readiness is {score:.1f}/100 from stored evidence only."]
550
+ if not rows:
551
+ lines.append("Benchmark evidence is missing, so no outperformance claim is valid.")
552
+ if trade_count < 30:
553
+ lines.append("Trade sample is too small for durable conclusions.")
554
+ if live_count < 10:
555
+ lines.append("Live paper validation is not mature yet.")
556
+ under = [row for row in rows if row.result_label == "underperforming"]
557
+ if under:
558
+ lines.append(f"BLUM is underperforming {under[0].benchmark_name} on the latest stored comparison.")
559
+ return lines[:5]
560
+
561
+
562
+ def classify_alpha_score(score: float) -> str:
563
+ if score < 25:
564
+ return "not_ready"
565
+ if score < 50:
566
+ return "experimental"
567
+ if score < 70:
568
+ return "research_candidate"
569
+ if score < 85:
570
+ return "paper_evidence_promising"
571
+ return "requires_external_validation"
572
+
573
+
574
+ def classify_brain_score(score: float | None) -> str:
575
+ if score is None:
576
+ return "initializing"
577
+ if score < 25:
578
+ return "evidence poor"
579
+ if score < 50:
580
+ return "learning but weak"
581
+ if score < 70:
582
+ return "research grade"
583
+ if score < 85:
584
+ return "strong paper evidence"
585
+ return "advanced, needs external validation"
586
+
587
+
588
+ def capability_matrix(
589
+ *,
590
+ trading_power: BlumTradingPowerScore | None,
591
+ decision: DecisionSuperiorityScore | None,
592
+ business: BusinessQualityScore | None,
593
+ portfolio: PortfolioQualityScore | None,
594
+ metric: TradingIntelligenceMetric | None,
595
+ readiness: dict,
596
+ alpha: dict,
597
+ paper: dict,
598
+ ) -> list[dict]:
599
+ return [
600
+ capability("Trading Intelligence", getattr(trading_power, "score", None), getattr(trading_power, "classification", None), sample_from_metric(metric), "paper P/L, expectancy, benchmark context"),
601
+ capability("Decision Superiority", getattr(decision, "score", None), getattr(decision, "classification", None), None, "opportunity recall, precision and ranking accuracy"),
602
+ capability("Business Quality", getattr(business, "business_quality_score", None), getattr(business, "ticker", None), None, "fundamental quality evidence"),
603
+ capability("Portfolio Intelligence", getattr(portfolio, "portfolio_quality_score", None), None, None, "risk contribution, concentration and capital efficiency"),
604
+ capability("Trading Game Readiness", readiness_score(readiness.get("status")), readiness.get("status"), readiness.get("completed_trade_count"), "ledger, equity and benchmark renderability"),
605
+ capability("Alpha Readiness", alpha.get("alpha_readiness_score"), alpha.get("classification"), alpha.get("sample", {}).get("trades"), "strict capped alpha evidence"),
606
+ capability("Paper Copy Readiness", copy_score_from_summary(paper), paper.get("readiness", {}).get("status"), paper.get("readiness", {}).get("candidate_count"), "paper-only mirror candidates"),
607
+ ]
608
+
609
+
610
+ def capability(name: str, score: float | None, status: str | None, sample_size: int | None, evidence: str) -> dict:
611
+ return {
612
+ "name": name,
613
+ "score": round(float(score), 2) if score is not None else None,
614
+ "status": status or "initializing",
615
+ "sample_size": sample_size,
616
+ "evidence": evidence,
617
+ "warning": "insufficient evidence" if sample_size is not None and sample_size < 30 else "",
618
+ }
619
+
620
+
621
+ def readiness_score(status: str | None) -> float:
622
+ return {
623
+ "READY": 82.0,
624
+ "STALE_BUT_USABLE": 68.0,
625
+ "INSUFFICIENT_EVIDENCE": 45.0,
626
+ "BUILDING": 35.0,
627
+ "WAITING_FOR_SOURCE_DATA": 20.0,
628
+ "DATA_QUALITY_BLOCKED": 15.0,
629
+ "FAILED": 0.0,
630
+ }.get(status or "", 20.0)
631
+
632
+
633
+ def copy_score_from_summary(summary: dict) -> float | None:
634
+ value = (summary.get("summary") or {}).get("average_readiness_score")
635
+ return float(value) if value is not None else None
636
+
637
+
638
+ def sample_from_metric(metric: TradingIntelligenceMetric | None) -> int | None:
639
+ return metric.trades_count if metric else None
640
+
641
+
642
+ def aggregate_edges(rows: list[TradingGameTrade], attr: str) -> list[dict]:
643
+ groups: dict[str, list[TradingGameTrade]] = defaultdict(list)
644
+ for row in rows:
645
+ key = getattr(row, attr, None) or "unknown"
646
+ groups[str(key)].append(row)
647
+ output = []
648
+ for key, group in groups.items():
649
+ r_values = [float(row.realized_r_multiple) for row in group if row.realized_r_multiple is not None]
650
+ excess_values = [float(row.excess_return_vs_benchmark) for row in group if row.excess_return_vs_benchmark is not None]
651
+ wins = sum(1 for row in group if (row.realized_r_multiple or 0) > 0 or row.outcome_label in {"win", "target_hit"})
652
+ sample = len(group)
653
+ avg_r = mean(r_values) if r_values else 0.0
654
+ avg_excess = mean(excess_values) if excess_values else 0.0
655
+ edge_score = max(0.0, min(100.0, 50.0 + avg_r * 12.0 + avg_excess * 0.8 + (wins / max(1, sample) - 0.5) * 30.0))
656
+ output.append(
657
+ {
658
+ "entity": key,
659
+ "sample_size": sample,
660
+ "win_rate": round(wins / max(1, sample), 4),
661
+ "average_r": round(avg_r, 4),
662
+ "average_excess_return": round(avg_excess, 4),
663
+ "edge_score": round(edge_score, 2),
664
+ "evidence_grade": "weak" if sample < 20 else "medium" if sample < 80 else "strong",
665
+ }
666
+ )
667
+ return output
668
+
669
+
670
+ def gate(name: str, passed: bool, observed: Any, requirement: str, required: bool = True) -> dict:
671
+ return {
672
+ "gate": name,
673
+ "passed": bool(passed),
674
+ "required": required,
675
+ "observed": observed,
676
+ "requirement": requirement,
677
+ "status": "pass" if passed else "blocked",
678
+ }
679
+
680
+
681
+ def serialize_job(row: BackgroundJobState | None) -> dict | None:
682
+ if row is None:
683
+ return None
684
+ return {
685
+ "job_name": row.job_name,
686
+ "stage_name": row.stage_name,
687
+ "status": row.status,
688
+ "last_started_at": row.last_started_at.isoformat() if row.last_started_at else None,
689
+ "last_completed_at": row.last_completed_at.isoformat() if row.last_completed_at else None,
690
+ "duration_ms": row.duration_ms,
691
+ "items_processed": row.items_processed,
692
+ "error_message": row.error_message,
693
+ }
694
+
695
+
696
+ def serialize_weakness(row: LearningStrengthWeaknessMap | None) -> dict | None:
697
+ if row is None:
698
+ return None
699
+ return {
700
+ "dimension": row.dimension,
701
+ "entity": row.entity,
702
+ "weakness_score": row.weakness_score,
703
+ "strength_score": row.strength_score,
704
+ "sample_size": row.sample_size,
705
+ "main_problem": row.main_problem,
706
+ "recommended_action": row.recommended_action,
707
+ "priority": row.priority,
708
+ }
709
+
710
+
711
+ def serialize_focus(row: LearningFocusPriority | None) -> dict | None:
712
+ if row is None:
713
+ return None
714
+ return {
715
+ "priority_type": row.priority_type,
716
+ "target": row.target,
717
+ "reason": row.reason,
718
+ "expected_learning_value": row.expected_learning_value,
719
+ "urgency": row.urgency,
720
+ "status": row.status,
721
+ }
722
+
723
+
724
+ def serialize_paper_strategy(row: PaperCopyStrategy | None) -> dict | None:
725
+ if row is None:
726
+ return None
727
+ return {
728
+ "id": row.id,
729
+ "strategy_id": row.strategy_id,
730
+ "name": row.name,
731
+ "status": row.status,
732
+ "strategy_type": row.strategy_type,
733
+ "copyability_score": row.copyability_score,
734
+ "risk_budget_percent": row.risk_budget_percent,
735
+ "max_open_positions": row.max_open_positions,
736
+ "paper_only": row.paper_only,
737
+ "no_broker_execution": row.no_broker_execution,
738
+ "updated_at": row.updated_at.isoformat() if row.updated_at else None,
739
+ }
740
+
741
+
742
+ def serialize_paper_portfolio(row: PaperCopyPortfolio | None) -> dict | None:
743
+ if row is None:
744
+ return None
745
+ return {
746
+ "id": row.id,
747
+ "portfolio_id": row.portfolio_id,
748
+ "status": row.status,
749
+ "starting_capital": row.starting_capital,
750
+ "current_capital": row.current_capital,
751
+ "cash": row.cash,
752
+ "exposure": row.exposure,
753
+ "realized_pnl": row.realized_pnl,
754
+ "unrealized_pnl": row.unrealized_pnl,
755
+ "benchmark_ticker": row.benchmark_ticker,
756
+ "risk_state": row.risk_state,
757
+ "updated_at": row.updated_at.isoformat() if row.updated_at else None,
758
+ }
759
+
760
+
761
+ def serialize_paper_position(row: PaperCopyPosition) -> dict:
762
+ return {
763
+ "id": row.id,
764
+ "ticker": row.ticker,
765
+ "status": row.status,
766
+ "quantity": row.quantity,
767
+ "entry_price": row.entry_price,
768
+ "current_price": row.current_price,
769
+ "market_value": row.market_value,
770
+ "unrealized_pnl": row.unrealized_pnl,
771
+ "realized_pnl": row.realized_pnl,
772
+ "invalidation_level": row.invalidation_level,
773
+ "target_1": row.target_1,
774
+ "target_2": row.target_2,
775
+ "opened_at": row.opened_at.isoformat() if row.opened_at else None,
776
+ "closed_at": row.closed_at.isoformat() if row.closed_at else None,
777
+ }
778
+
779
+
780
+ def serialize_paper_snapshot(row: PaperCopyPortfolioSnapshot | None) -> dict | None:
781
+ if row is None:
782
+ return None
783
+ return {
784
+ "id": row.id,
785
+ "created_at": row.created_at.isoformat() if row.created_at else None,
786
+ "capital": row.capital,
787
+ "exposure": row.exposure,
788
+ "open_positions": row.open_positions,
789
+ "pending_orders": row.pending_orders,
790
+ "copyability_score": row.copyability_score,
791
+ "evidence_grade": row.evidence_grade,
792
+ "payload": row.payload_json or {},
793
+ "warnings": row.warnings_json or [],
794
+ }
795
+
796
+
797
+ def strategy_candidate_from_summary(summary: dict) -> dict:
798
+ rows = summary.get("rows") or []
799
+ scores = [row.get("copy_readiness_score") for row in rows if row.get("copy_readiness_score") is not None]
800
+ return {
801
+ "name": "BLUM Paper Copy Strategy Candidate",
802
+ "strategy_type": "conditional_copy_watchlist",
803
+ "copyability_score": round(mean(scores), 2) if scores else 0.0,
804
+ "candidate_count": len(rows),
805
+ "rules": [
806
+ "paper only",
807
+ "copy only if trigger and invalidation exist",
808
+ "risk budget remains educational and capped",
809
+ "no broker execution",
810
+ ],
811
+ }
812
+
813
+
814
+ def dedupe(items: list[str]) -> list[str]:
815
+ return list(dict.fromkeys([str(item) for item in items if item]))
backend/app/services/central_brain_runtime.py CHANGED
@@ -58,6 +58,12 @@ CRITICAL_SNAPSHOT_TYPES = [
58
  "capital_allocation_summary",
59
  "alpha_recovery_summary",
60
  "meta_cognition_summary",
 
 
 
 
 
 
61
  "trading_game_ledger_snapshot",
62
  "equity_curve_snapshot",
63
  ]
@@ -355,6 +361,30 @@ class SnapshotProducerService:
355
  from app.services.meta_cognition import MetaCognitionEngine
356
 
357
  return MetaCognitionEngine().summary(db)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  missing_sections.append("unknown_snapshot_type")
359
  return {"status": "unknown_snapshot_type", "snapshot_type": snapshot_type}
360
 
 
58
  "capital_allocation_summary",
59
  "alpha_recovery_summary",
60
  "meta_cognition_summary",
61
+ "trading_game_readiness",
62
+ "brain_command_summary",
63
+ "alpha_readiness_summary",
64
+ "alpha_edge_map_summary",
65
+ "alpha_gates_summary",
66
+ "paper_copy_summary",
67
  "trading_game_ledger_snapshot",
68
  "equity_curve_snapshot",
69
  ]
 
361
  from app.services.meta_cognition import MetaCognitionEngine
362
 
363
  return MetaCognitionEngine().summary(db)
364
+ if snapshot_type == "trading_game_readiness":
365
+ from app.services.alpha_operating_system import TradingGameReadinessService
366
+
367
+ return TradingGameReadinessService().readiness(db)
368
+ if snapshot_type == "brain_command_summary":
369
+ from app.services.alpha_operating_system import BrainCommandSummaryService
370
+
371
+ return BrainCommandSummaryService().summary(db)
372
+ if snapshot_type == "alpha_readiness_summary":
373
+ from app.services.alpha_operating_system import AlphaReadinessEngine
374
+
375
+ return AlphaReadinessEngine().readiness(db)
376
+ if snapshot_type == "alpha_edge_map_summary":
377
+ from app.services.alpha_operating_system import EdgeMapService
378
+
379
+ return EdgeMapService().edge_map(db)
380
+ if snapshot_type == "alpha_gates_summary":
381
+ from app.services.alpha_operating_system import AlphaGateService
382
+
383
+ return AlphaGateService().gates(db)
384
+ if snapshot_type == "paper_copy_summary":
385
+ from app.services.alpha_operating_system import PaperCopyTradingService
386
+
387
+ return PaperCopyTradingService().summary(db)
388
  missing_sections.append("unknown_snapshot_type")
389
  return {"status": "unknown_snapshot_type", "snapshot_type": snapshot_type}
390
 
backend/tests/test_alpha_operating_system.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import date, datetime
2
+
3
+ from sqlalchemy import create_engine
4
+ from sqlalchemy.orm import Session
5
+
6
+ from app.core.database import Base
7
+ from app.models import (
8
+ LearningBenchmarkComparison,
9
+ SniperScore,
10
+ TradePlan,
11
+ TradingGame,
12
+ TradingGameTrade,
13
+ )
14
+ from app.services.alpha_operating_system import (
15
+ AlphaGateService,
16
+ AlphaReadinessEngine,
17
+ BrainCommandSummaryService,
18
+ EdgeMapService,
19
+ PaperCopyTradingService,
20
+ TradingGameReadinessService,
21
+ )
22
+ from app.services.trading_game_runtime import TradingGameRuntimeSnapshotService
23
+
24
+
25
+ def setup_db() -> Session:
26
+ engine = create_engine("sqlite:///:memory:", future=True)
27
+ Base.metadata.create_all(engine)
28
+ return Session(engine)
29
+
30
+
31
+ def test_trading_game_readiness_explains_missing_source_data():
32
+ with setup_db() as db:
33
+ payload = TradingGameReadinessService().readiness(db)
34
+
35
+ assert payload["status"] == "WAITING_FOR_SOURCE_DATA"
36
+ assert "No TradingGame row" in payload["blocker"]
37
+ assert payload["evidence_grade"] == "insufficient"
38
+
39
+
40
+ def test_trading_game_readiness_ready_when_snapshots_and_eligible_trades_exist():
41
+ with setup_db() as db:
42
+ game = TradingGame(game_id="readiness-test", current_capital=105.0, target_capital=10000.0)
43
+ db.add(game)
44
+ db.flush()
45
+ for index in range(3):
46
+ db.add(
47
+ TradingGameTrade(
48
+ game_id=game.id,
49
+ ticker=f"T{index}",
50
+ setup_type="momentum_breakout",
51
+ entry_date=date(2025, 1, 2 + index),
52
+ exit_date=date(2025, 1, 5 + index),
53
+ entry_price=100.0,
54
+ position_size=0.2,
55
+ invalidation_level=96.0,
56
+ realized_r_multiple=1.0,
57
+ net_pnl_eur=1.0,
58
+ outcome_label="target_hit",
59
+ created_at=datetime.utcnow(),
60
+ )
61
+ )
62
+ db.commit()
63
+ TradingGameRuntimeSnapshotService().produce_ledger_snapshot(db, game_id=game.id, limit=10)
64
+ TradingGameRuntimeSnapshotService().produce_equity_snapshot(db, game_id=game.id, limit=10)
65
+
66
+ payload = TradingGameReadinessService().readiness(db)
67
+
68
+ assert payload["status"] == "READY"
69
+ assert payload["eligible_trade_count"] == 3
70
+ assert payload["ledger_snapshot_status"] == "ready"
71
+ assert payload["equity_snapshot_status"] == "ready"
72
+
73
+
74
+ def test_alpha_readiness_is_capped_without_live_and_benchmark_depth():
75
+ with setup_db() as db:
76
+ game = TradingGame(game_id="alpha-test", current_capital=150.0)
77
+ db.add(game)
78
+ db.flush()
79
+ for index in range(12):
80
+ db.add(
81
+ TradingGameTrade(
82
+ game_id=game.id,
83
+ ticker=f"A{index}",
84
+ setup_type="pullback_to_trend",
85
+ realized_r_multiple=2.0,
86
+ excess_return_vs_benchmark=4.0,
87
+ outcome_label="target_hit",
88
+ created_at=datetime.utcnow(),
89
+ )
90
+ )
91
+ db.commit()
92
+
93
+ payload = AlphaReadinessEngine().readiness(db)
94
+
95
+ assert payload["status"] == "INSUFFICIENT_EVIDENCE"
96
+ assert payload["alpha_readiness_score"] <= 60
97
+ assert "benchmark_comparison_missing" in payload["warnings"]
98
+
99
+
100
+ def test_alpha_gates_require_benchmark_and_live_samples():
101
+ with setup_db() as db:
102
+ payload = AlphaGateService().gates(db)
103
+
104
+ blocked = [row for row in payload["rows"] if not row["passed"]]
105
+ assert blocked
106
+ assert payload["all_required_gates_passed"] is False
107
+
108
+
109
+ def test_edge_map_uses_stored_trade_outcomes_without_recalculation():
110
+ with setup_db() as db:
111
+ game = TradingGame(game_id="edge-test")
112
+ db.add(game)
113
+ db.flush()
114
+ db.add_all(
115
+ [
116
+ TradingGameTrade(game_id=game.id, ticker="NVDA", setup_type="momentum_breakout", sector="Technology", realized_r_multiple=2.0, excess_return_vs_benchmark=5.0, outcome_label="target_hit"),
117
+ TradingGameTrade(game_id=game.id, ticker="AAPL", setup_type="momentum_breakout", sector="Technology", realized_r_multiple=-1.0, excess_return_vs_benchmark=-2.0, outcome_label="stopped_out"),
118
+ ]
119
+ )
120
+ db.commit()
121
+
122
+ payload = EdgeMapService().edge_map(db)
123
+
124
+ assert payload["status"] == "ready"
125
+ assert payload["best_setups"][0]["entity"] == "momentum_breakout"
126
+ assert payload["sample_size"] == 2
127
+
128
+
129
+ def test_paper_copy_summary_is_paper_only_and_reuses_trade_plan_evidence():
130
+ with setup_db() as db:
131
+ db.add(
132
+ TradePlan(
133
+ ticker="NVDA",
134
+ setup_type="momentum_breakout",
135
+ actionability="actionable_if_confirmed",
136
+ entry_trigger="close above resistance with relative volume > 1.5x",
137
+ invalidation_level=120.0,
138
+ target_1=140.0,
139
+ confidence=72.0,
140
+ historical_setup_reliability=60.0,
141
+ )
142
+ )
143
+ db.commit()
144
+
145
+ payload = PaperCopyTradingService().summary(db, limit=5)
146
+
147
+ assert payload["paper_only"] is True
148
+ assert payload["no_broker_execution"] is True
149
+ assert payload["readiness"]["status"] == "READY_FOR_PAPER_MONITORING"
150
+ assert payload["rows"][0]["ticker"] == "NVDA"
151
+
152
+
153
+ def test_brain_command_summary_is_compact_and_truth_first():
154
+ with setup_db() as db:
155
+ db.add(LearningBenchmarkComparison(benchmark_name="SPY", result_label="underperforming", excess_return=-4.2, sample_size=12, statistical_confidence="low evidence"))
156
+ db.add(SniperScore(ticker="AMD", setup_type="pullback_to_trend", actionability="wait_for_trigger", sniper_score=68.0, confidence=60.0))
157
+ db.commit()
158
+
159
+ payload = BrainCommandSummaryService().summary(db)
160
+
161
+ assert payload["feature_set"] == "alpha-operating-system"
162
+ assert "capability_matrix" in payload
163
+ assert payload["policy"].startswith("Command reads compact evidence")
frontend/app/copy-trading/page.tsx CHANGED
@@ -15,7 +15,7 @@ export default function CopyTradingPage() {
15
  useEffect(() => {
16
  let mounted = true;
17
  setLoading(true);
18
- api.copyTradingDashboard(25)
19
  .then((payload) => {
20
  if (mounted) setData(payload);
21
  })
@@ -47,7 +47,7 @@ export default function CopyTradingPage() {
47
  { label: "Mode", value: data?.mode ?? "paper_copy_intelligence", tone: "info" },
48
  { label: "Guardrail", value: data?.paper_only ? "paper only" : "blocked", tone: data?.paper_only ? "positive" : "negative" },
49
  { label: "Candidates", value: String(summary?.candidate_count ?? rows.length), tone: rows.length ? "positive" : "attention" },
50
- { label: "Execution", value: data?.no_broker_execution ? "disabled" : "unknown", tone: "attention" },
51
  ]}
52
  />
53
 
@@ -58,6 +58,26 @@ export default function CopyTradingPage() {
58
  <MetricCard label="Blocked" value={summary?.blocked ?? 0} subvalue="Avoid/reduce/insufficient risk plan" icon={<ShieldAlert size={15} />} tone="negative" />
59
  </section>
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  <section className="grid-2" style={{ marginBottom: 12 }}>
62
  <BloombergPanel title="Truth Layer" value="research only" subtitle="What BLUM is allowed to say">
63
  <div className="brain-list dense">
@@ -156,3 +176,8 @@ function formatPrice(value: any) {
156
  if (value === null || value === undefined || Number.isNaN(Number(value))) return "n/a";
157
  return Number(value).toFixed(2);
158
  }
 
 
 
 
 
 
15
  useEffect(() => {
16
  let mounted = true;
17
  setLoading(true);
18
+ api.paperCopySummary(25)
19
  .then((payload) => {
20
  if (mounted) setData(payload);
21
  })
 
47
  { label: "Mode", value: data?.mode ?? "paper_copy_intelligence", tone: "info" },
48
  { label: "Guardrail", value: data?.paper_only ? "paper only" : "blocked", tone: data?.paper_only ? "positive" : "negative" },
49
  { label: "Candidates", value: String(summary?.candidate_count ?? rows.length), tone: rows.length ? "positive" : "attention" },
50
+ { label: "Readiness", value: data?.readiness?.status ?? "building", tone: "attention" },
51
  ]}
52
  />
53
 
 
58
  <MetricCard label="Blocked" value={summary?.blocked ?? 0} subvalue="Avoid/reduce/insufficient risk plan" icon={<ShieldAlert size={15} />} tone="negative" />
59
  </section>
60
 
61
+ <section className="grid-2" style={{ marginBottom: 12 }}>
62
+ <BloombergPanel title="Paper Copy Operating State" value={data?.readiness?.status ?? "pending"} subtitle="Durable state remains paper-only and brokerless">
63
+ <div className="professional-grid-3">
64
+ <MetricCard label="Strategies" value={data?.readiness?.strategy_count ?? 0} />
65
+ <MetricCard label="Portfolios" value={data?.readiness?.portfolio_count ?? 0} />
66
+ <MetricCard label="Open Positions" value={data?.readiness?.open_position_count ?? 0} />
67
+ </div>
68
+ <p>{data?.policy ?? "Paper copy intelligence is informational only."}</p>
69
+ </BloombergPanel>
70
+
71
+ <BloombergPanel title="Latest Paper Portfolio" value={data?.portfolio?.portfolio_id ?? "not created"} subtitle="Paper capital state is shown only when stored">
72
+ <div className="professional-grid-3">
73
+ <MetricCard label="Capital" value={formatCurrency(data?.portfolio?.current_capital)} />
74
+ <MetricCard label="Exposure" value={formatCurrency(data?.portfolio?.exposure)} />
75
+ <MetricCard label="Risk State" value={data?.portfolio?.risk_state ?? "n/a"} />
76
+ </div>
77
+ {!!data?.warnings?.length && <p>{data.warnings.slice(0, 3).join(" | ")}</p>}
78
+ </BloombergPanel>
79
+ </section>
80
+
81
  <section className="grid-2" style={{ marginBottom: 12 }}>
82
  <BloombergPanel title="Truth Layer" value="research only" subtitle="What BLUM is allowed to say">
83
  <div className="brain-list dense">
 
176
  if (value === null || value === undefined || Number.isNaN(Number(value))) return "n/a";
177
  return Number(value).toFixed(2);
178
  }
179
+
180
+ function formatCurrency(value: any) {
181
+ if (value === null || value === undefined || Number.isNaN(Number(value))) return "n/a";
182
+ return `${Number(value).toFixed(2)} EUR`;
183
+ }
frontend/app/dashboard/page.tsx CHANGED
@@ -41,7 +41,7 @@ export default function DashboardPage() {
41
  const [pipelineStatus, setPipelineStatus] = useState<PipelineStatus | null>(null);
42
  const [systemStatus, setSystemStatus] = useState<SystemStatus | null>(null);
43
  const [brainStatus, setBrainStatus] = useState<BrainStatus | null>(null);
44
- const [learningSummary, setLearningSummary] = useState<any | null>(null);
45
  const [executive, setExecutive] = useState<ExecutiveDashboardPayload | null>(null);
46
  const [error, setError] = useState("");
47
  const [liveError, setLiveError] = useState("");
@@ -53,7 +53,7 @@ export default function DashboardPage() {
53
  const overview = await api.overview();
54
  setData(overview);
55
 
56
- const [newsResult, sentimentResult, statusResult, systemResult, executiveResult, brainResult, learningResult] =
57
  await Promise.allSettled([
58
  api.liveNews(60),
59
  api.marketSentiment(48),
@@ -61,7 +61,7 @@ export default function DashboardPage() {
61
  api.systemStatus(),
62
  api.executiveDashboard(),
63
  api.brainStatus(),
64
- api.learningSummary()
65
  ] as const);
66
 
67
  if (newsResult.status === "fulfilled") setLiveNews(newsResult.value);
@@ -70,7 +70,7 @@ export default function DashboardPage() {
70
  if (systemResult.status === "fulfilled") setSystemStatus(systemResult.value);
71
  if (executiveResult.status === "fulfilled") setExecutive(executiveResult.value);
72
  if (brainResult.status === "fulfilled") setBrainStatus(brainResult.value);
73
- if (learningResult.status === "fulfilled") setLearningSummary(learningResult.value);
74
 
75
  setLiveError(
76
  [
@@ -80,7 +80,7 @@ export default function DashboardPage() {
80
  systemResult.status === "rejected" ? `system ${errorMessage(systemResult.reason)}` : "",
81
  executiveResult.status === "rejected" ? `executive ${errorMessage(executiveResult.reason)}` : "",
82
  brainResult.status === "rejected" ? `brain ${errorMessage(brainResult.reason)}` : "",
83
- learningResult.status === "rejected" ? `learning ${errorMessage(learningResult.reason)}` : ""
84
  ]
85
  .filter(Boolean)
86
  .join(" | ")
@@ -234,7 +234,7 @@ export default function DashboardPage() {
234
  />
235
  </section>
236
 
237
- <BrainLevelPanel summary={learningSummary} accuracy={data.accuracy} brainStatus={brainStatus} />
238
 
239
  <section className="market-command-layout">
240
  <div className="market-pulse-grid">
@@ -340,13 +340,16 @@ export default function DashboardPage() {
340
  }
341
 
342
  function BrainLevelPanel({ summary, accuracy, brainStatus }: { summary: any; accuracy?: AccuracyOverview; brainStatus: BrainStatus | null }) {
343
- const benchmarks = Object.entries(summary?.benchmark_summary?.major_benchmarks ?? {}).slice(0, 4) as Array<[string, any]>;
344
- const whatNext = summary?.what_blum_should_learn_next ?? {};
345
- const topWeakness = summary?.top_weakness;
346
- const latestLesson = summary?.latest_lesson_learned;
347
- const tradingPower = summary?.trading_power_score ?? accuracy?.blum_confidence_score;
348
- const powerLabel = summary?.trading_power_classification ?? accuracy?.confidence_label ?? "evidence building";
 
 
349
  const evidenceStatus = evidenceLabel(summary);
 
350
  return (
351
  <BloombergPanel
352
  title="BLUM Brain Level"
@@ -361,10 +364,10 @@ function BrainLevelPanel({ summary, accuracy, brainStatus }: { summary: any; acc
361
  <span>{powerLabel}</span>
362
  </div>
363
  <div className="brain-level-facts">
364
- <MetricCard label="Learning status" value={summary?.learning_loop_status ?? brainStatus?.learning_state ?? "pending"} subvalue={`latest ${formatDateTime(summary?.latest_learning_run_at)}`} tone="info" />
365
- <MetricCard label="Win / Expectancy" value={`${formatPct(summary?.win_rate)} / ${formatR(summary?.expectancy_r)}`} subvalue="paper Trading Game evidence" tone="attention" />
366
- <MetricCard label="Capital progress" value={formatPct(summary?.target_progress)} subvalue={`${summary?.completed_target_cycles ?? 0} target cycles | ${summary?.bankrupt_cycles ?? 0} bankrupt`} tone="positive" />
367
- <MetricCard label="Evidence level" value={evidenceStatus} subvalue={summary?.live_vs_historical_status ?? "live evidence pending"} tone={evidenceStatus.includes("weak") ? "attention" : "info"} />
368
  </div>
369
  </div>
370
 
@@ -390,8 +393,8 @@ function BrainLevelPanel({ summary, accuracy, brainStatus }: { summary: any; acc
390
  </div>
391
  <div>
392
  <span className="regime-badge tone-positive">next focus</span>
393
- <strong>{whatNext?.next_learning_focus?.target ?? whatNext?.conclusion?.summary ?? "No active focus priority"}</strong>
394
- <p>{whatNext?.next_learning_focus?.reason ?? "When evidence is sufficient, BLUM will show which factor/module should be studied next."}</p>
395
  </div>
396
  </div>
397
  </div>
@@ -616,9 +619,11 @@ function formatR(value: unknown) {
616
  function evidenceLabel(summary: any) {
617
  if (!summary) return "loading";
618
  const warnings = summary?.warnings ?? [];
 
 
619
  if (summary?.live_vs_historical_status === "missing") return "weak evidence";
620
  if (warnings.length > 2) return "limited evidence";
621
- if (summary?.trading_power_score == null) return "initializing";
622
  return "tracked evidence";
623
  }
624
 
 
41
  const [pipelineStatus, setPipelineStatus] = useState<PipelineStatus | null>(null);
42
  const [systemStatus, setSystemStatus] = useState<SystemStatus | null>(null);
43
  const [brainStatus, setBrainStatus] = useState<BrainStatus | null>(null);
44
+ const [commandSummary, setCommandSummary] = useState<any | null>(null);
45
  const [executive, setExecutive] = useState<ExecutiveDashboardPayload | null>(null);
46
  const [error, setError] = useState("");
47
  const [liveError, setLiveError] = useState("");
 
53
  const overview = await api.overview();
54
  setData(overview);
55
 
56
+ const [newsResult, sentimentResult, statusResult, systemResult, executiveResult, brainResult, commandResult] =
57
  await Promise.allSettled([
58
  api.liveNews(60),
59
  api.marketSentiment(48),
 
61
  api.systemStatus(),
62
  api.executiveDashboard(),
63
  api.brainStatus(),
64
+ api.brainCommandSummary()
65
  ] as const);
66
 
67
  if (newsResult.status === "fulfilled") setLiveNews(newsResult.value);
 
70
  if (systemResult.status === "fulfilled") setSystemStatus(systemResult.value);
71
  if (executiveResult.status === "fulfilled") setExecutive(executiveResult.value);
72
  if (brainResult.status === "fulfilled") setBrainStatus(brainResult.value);
73
+ if (commandResult.status === "fulfilled") setCommandSummary(commandResult.value);
74
 
75
  setLiveError(
76
  [
 
80
  systemResult.status === "rejected" ? `system ${errorMessage(systemResult.reason)}` : "",
81
  executiveResult.status === "rejected" ? `executive ${errorMessage(executiveResult.reason)}` : "",
82
  brainResult.status === "rejected" ? `brain ${errorMessage(brainResult.reason)}` : "",
83
+ commandResult.status === "rejected" ? `command ${errorMessage(commandResult.reason)}` : ""
84
  ]
85
  .filter(Boolean)
86
  .join(" | ")
 
234
  />
235
  </section>
236
 
237
+ <BrainLevelPanel summary={commandSummary} accuracy={data.accuracy} brainStatus={brainStatus} />
238
 
239
  <section className="market-command-layout">
240
  <div className="market-pulse-grid">
 
340
  }
341
 
342
  function BrainLevelPanel({ summary, accuracy, brainStatus }: { summary: any; accuracy?: AccuracyOverview; brainStatus: BrainStatus | null }) {
343
+ const learning = summary?.learning_evolution ?? summary ?? {};
344
+ const improvement = summary?.improvement_regression ?? {};
345
+ const benchmarks = Object.entries(summary?.benchmark_truth?.major_benchmarks ?? summary?.benchmark_summary?.major_benchmarks ?? {}).slice(0, 4) as Array<[string, any]>;
346
+ const whatNext = summary?.what_blum_should_learn_next ?? improvement;
347
+ const topWeakness = improvement?.top_weakness ?? summary?.top_weakness;
348
+ const latestLesson = improvement?.latest_lesson ?? summary?.latest_lesson_learned;
349
+ const tradingPower = summary?.brain_capability_score ?? learning?.trading_power_score ?? accuracy?.blum_confidence_score;
350
+ const powerLabel = summary?.brain_classification ?? learning?.trading_power_classification ?? accuracy?.confidence_label ?? "evidence building";
351
  const evidenceStatus = evidenceLabel(summary);
352
+ const statusStrip = summary?.brain_status_strip ?? {};
353
  return (
354
  <BloombergPanel
355
  title="BLUM Brain Level"
 
364
  <span>{powerLabel}</span>
365
  </div>
366
  <div className="brain-level-facts">
367
+ <MetricCard label="Learning status" value={statusStrip?.learning_status ?? learning?.latest_run_status ?? brainStatus?.learning_state ?? "pending"} subvalue={`latest ${formatDateTime(statusStrip?.latest_run_at ?? learning?.latest_run_at)}`} tone="info" />
368
+ <MetricCard label="Win / Expectancy" value={`${formatPct(learning?.win_rate)} / ${formatR(learning?.expectancy_r)}`} subvalue="paper Trading Game evidence" tone="attention" />
369
+ <MetricCard label="Capital progress" value={formatPct(learning?.target_progress)} subvalue={`copy ${statusStrip?.paper_copy_readiness ?? "pending"} | alpha ${statusStrip?.alpha_readiness ?? "pending"}`} tone="positive" />
370
+ <MetricCard label="Evidence level" value={evidenceStatus} subvalue={statusStrip?.trading_game_readiness ?? summary?.live_vs_historical_status ?? "live evidence pending"} tone={evidenceStatus.includes("weak") ? "attention" : "info"} />
371
  </div>
372
  </div>
373
 
 
393
  </div>
394
  <div>
395
  <span className="regime-badge tone-positive">next focus</span>
396
+ <strong>{whatNext?.next_learning_focus?.target ?? whatNext?.target ?? whatNext?.conclusion?.summary ?? "No active focus priority"}</strong>
397
+ <p>{whatNext?.next_learning_focus?.reason ?? whatNext?.reason ?? "When evidence is sufficient, BLUM will show which factor/module should be studied next."}</p>
398
  </div>
399
  </div>
400
  </div>
 
619
  function evidenceLabel(summary: any) {
620
  if (!summary) return "loading";
621
  const warnings = summary?.warnings ?? [];
622
+ if (summary?.brain_status_strip?.alpha_readiness === "INSUFFICIENT_EVIDENCE") return "weak evidence";
623
+ if (summary?.brain_capability_score == null && summary?.trading_power_score == null) return "initializing";
624
  if (summary?.live_vs_historical_status === "missing") return "weak evidence";
625
  if (warnings.length > 2) return "limited evidence";
626
+ if (summary?.brain_capability_score == null && summary?.trading_power_score == null) return "initializing";
627
  return "tracked evidence";
628
  }
629
 
frontend/app/learning/page.tsx CHANGED
@@ -59,6 +59,14 @@ export default function LearningPage() {
59
  async function loadTradingGame() {
60
  setTradingLoading(true);
61
  setTradingError("");
 
 
 
 
 
 
 
 
62
  const [equityResult, ledgerResult, ledgerSummaryResult, realityCheckResult, benchmarkResult] = await Promise.allSettled([
63
  api.tradingGameAnnotatedEquity(240),
64
  api.tradingGameLedger(25),
@@ -68,6 +76,7 @@ export default function LearningPage() {
68
  ] as const);
69
  if (!mounted) return;
70
  setTrading({
 
71
  equity: equityResult.status === "fulfilled" ? equityResult.value : null,
72
  ledger: ledgerResult.status === "fulfilled" ? ledgerResult.value : null,
73
  ledgerSummary: ledgerSummaryResult.status === "fulfilled" ? ledgerSummaryResult.value : null,
@@ -141,6 +150,7 @@ export default function LearningPage() {
141
  const ledgerSummary = trading?.ledgerSummary?.summary ?? trading?.ledger?.summary ?? {};
142
  const realityCheck = trading?.realityCheck ?? {};
143
  const benchmark = trading?.benchmark ?? summary?.benchmark_summary ?? {};
 
144
  const truthPanel = summary?.truth_panel ?? [];
145
 
146
  return (
@@ -192,6 +202,7 @@ export default function LearningPage() {
192
  realityCheck={realityCheck}
193
  benchmark={benchmark}
194
  equityPayload={trading?.equity}
 
195
  onRetry={() => setTrading(null)}
196
  onLoadMore={loadMoreLedger}
197
  />
@@ -325,10 +336,46 @@ function OverviewTab({ summary, loading, error, truthPanel, onRefresh }: { summa
325
  );
326
  }
327
 
328
- function TradingGameTab({ summary, cycle, loading, error, equityChart, equityPayload, ledgerRows, ledgerSummary, realityCheck, benchmark, onRetry, onLoadMore }: any) {
329
  const equityInfo = equityPayloadSummary(equityPayload, equityChart);
 
 
330
  return (
331
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  <AsyncPanel title="Trading Game Evidence" loading={loading && !ledgerRows.length} error={error} updatedAt={summary?.data_freshness?.game_updated_at} onRetry={onRetry} fallback="Loading focused trading evidence">
333
  <section className="grid-3">
334
  <div className="panel">
@@ -399,6 +446,8 @@ function TradingGameTab({ summary, cycle, loading, error, equityChart, equityPay
399
  {ledgerRows.length === 0 && <div className="empty-state">No trade ledger rows loaded yet.</div>}
400
  </div>
401
  </section>
 
 
402
  </>
403
  );
404
  }
 
59
  async function loadTradingGame() {
60
  setTradingLoading(true);
61
  setTradingError("");
62
+ const readiness = await api.tradingGameReadiness();
63
+ if (!mounted) return;
64
+ const canLoadEvidence = ["READY", "STALE_BUT_USABLE", "INSUFFICIENT_EVIDENCE"].includes(readiness?.status);
65
+ if (!canLoadEvidence) {
66
+ setTrading({ readiness });
67
+ setTradingLoading(false);
68
+ return;
69
+ }
70
  const [equityResult, ledgerResult, ledgerSummaryResult, realityCheckResult, benchmarkResult] = await Promise.allSettled([
71
  api.tradingGameAnnotatedEquity(240),
72
  api.tradingGameLedger(25),
 
76
  ] as const);
77
  if (!mounted) return;
78
  setTrading({
79
+ readiness,
80
  equity: equityResult.status === "fulfilled" ? equityResult.value : null,
81
  ledger: ledgerResult.status === "fulfilled" ? ledgerResult.value : null,
82
  ledgerSummary: ledgerSummaryResult.status === "fulfilled" ? ledgerSummaryResult.value : null,
 
150
  const ledgerSummary = trading?.ledgerSummary?.summary ?? trading?.ledger?.summary ?? {};
151
  const realityCheck = trading?.realityCheck ?? {};
152
  const benchmark = trading?.benchmark ?? summary?.benchmark_summary ?? {};
153
+ const tradingReadiness = trading?.readiness ?? null;
154
  const truthPanel = summary?.truth_panel ?? [];
155
 
156
  return (
 
202
  realityCheck={realityCheck}
203
  benchmark={benchmark}
204
  equityPayload={trading?.equity}
205
+ readiness={tradingReadiness}
206
  onRetry={() => setTrading(null)}
207
  onLoadMore={loadMoreLedger}
208
  />
 
336
  );
337
  }
338
 
339
+ function TradingGameTab({ summary, cycle, loading, error, equityChart, equityPayload, readiness, ledgerRows, ledgerSummary, realityCheck, benchmark, onRetry, onLoadMore }: any) {
340
  const equityInfo = equityPayloadSummary(equityPayload, equityChart);
341
+ const readinessStatus = readiness?.status ?? (loading ? "BUILDING" : "UNKNOWN");
342
+ const canShowEvidence = ["READY", "STALE_BUT_USABLE", "INSUFFICIENT_EVIDENCE"].includes(readinessStatus);
343
  return (
344
  <>
345
+ <section className={`panel trading-readiness-panel readiness-${String(readinessStatus).toLowerCase()}`}>
346
+ <div className="panel-head">
347
+ <span>Trading Game Readiness</span>
348
+ <strong>{readinessStatus.replaceAll("_", " ")}</strong>
349
+ </div>
350
+ <div className="evidence-grid">
351
+ <SmallDatum label="Evidence Grade" value={readiness?.evidence_grade ?? "pending"} />
352
+ <SmallDatum label="Source Decisions" value={readiness?.source_decision_count ?? "n/a"} />
353
+ <SmallDatum label="Trades" value={readiness?.source_trade_count ?? "n/a"} />
354
+ <SmallDatum label="Eligible Trades" value={readiness?.eligible_trade_count ?? "n/a"} />
355
+ <SmallDatum label="Ledger Snapshot" value={readiness?.ledger_snapshot_status ?? "n/a"} />
356
+ <SmallDatum label="Equity Snapshot" value={readiness?.equity_snapshot_status ?? "n/a"} />
357
+ </div>
358
+ {(readiness?.blocker || readiness?.next_required_action) && (
359
+ <p>
360
+ {readiness?.blocker ? `${readiness.blocker} ` : ""}
361
+ {readiness?.next_required_action ?? ""}
362
+ </p>
363
+ )}
364
+ {(readiness?.warnings ?? []).length > 0 && (
365
+ <div className="status-row">
366
+ {(readiness?.warnings ?? []).slice(0, 4).map((warning: string) => <StatusBadge key={warning} label={warning} />)}
367
+ </div>
368
+ )}
369
+ </section>
370
+
371
+ {!canShowEvidence && (
372
+ <div className="empty-state" style={{ marginTop: 12 }}>
373
+ Trading evidence is not renderable yet. The backend can keep building it autonomously; the UI will not trigger heavy work from this page.
374
+ </div>
375
+ )}
376
+
377
+ {canShowEvidence && (
378
+ <>
379
  <AsyncPanel title="Trading Game Evidence" loading={loading && !ledgerRows.length} error={error} updatedAt={summary?.data_freshness?.game_updated_at} onRetry={onRetry} fallback="Loading focused trading evidence">
380
  <section className="grid-3">
381
  <div className="panel">
 
446
  {ledgerRows.length === 0 && <div className="empty-state">No trade ledger rows loaded yet.</div>}
447
  </div>
448
  </section>
449
+ </>
450
+ )}
451
  </>
452
  );
453
  }
frontend/lib/api.ts CHANGED
@@ -199,6 +199,9 @@ export const api = {
199
  postJson<any>("/performance/frontend-widget", payload),
200
  systemStatus: () => getJson<SystemStatus>("/system/status"),
201
  brainRuntimeState: () => getJson<any>("/brain/runtime-state"),
 
 
 
202
  snapshotsHealth: () => getJson<any>("/snapshots/health"),
203
  produceSnapshots: (snapshotType?: string, maxItems = 8) =>
204
  postJson<any>(`/snapshots/produce?max_items=${maxItems}${snapshotType ? `&snapshot_type=${encodeURIComponent(snapshotType)}` : ""}`, {}),
@@ -239,9 +242,18 @@ export const api = {
239
  sniperMetrics: () => getJson<any>("/api/sniper/metrics"),
240
  sniperLessons: (limit = 40) => getJson<any[]>(`/api/sniper/lessons?limit=${limit}`),
241
  tradingGameStatus: () => getJson<any>("/api/trading-game/status"),
 
242
  copyTradingStatus: () => getJson<any>("/api/copy-trading/status"),
243
  copyTradingCandidates: (limit = 25) => getJson<any>(`/api/copy-trading/candidates?limit=${limit}`),
244
  copyTradingDashboard: (limit = 25) => getJson<any>(`/api/copy-trading/dashboard?limit=${limit}`),
 
 
 
 
 
 
 
 
245
  tradingGameRun: (batchSize = 60) => postJson<any>(`/api/trading-game/run?batch_size=${batchSize}`, {}),
246
  tradingGameReset: () => postJson<any>("/api/trading-game/reset", {}),
247
  tradingGameEquity: (limit = 500, gameId?: number) => getJson<any[]>(`/api/trading-game/equity?limit=${limit}${gameId ? `&game_id=${gameId}` : ""}`),
 
199
  postJson<any>("/performance/frontend-widget", payload),
200
  systemStatus: () => getJson<SystemStatus>("/system/status"),
201
  brainRuntimeState: () => getJson<any>("/brain/runtime-state"),
202
+ brainCommandSummary: () => getJson<any>("/brain/command-summary"),
203
+ brainCapabilities: () => getJson<any>("/brain/capabilities"),
204
+ brainEvolution: () => getJson<any>("/brain/evolution"),
205
  snapshotsHealth: () => getJson<any>("/snapshots/health"),
206
  produceSnapshots: (snapshotType?: string, maxItems = 8) =>
207
  postJson<any>(`/snapshots/produce?max_items=${maxItems}${snapshotType ? `&snapshot_type=${encodeURIComponent(snapshotType)}` : ""}`, {}),
 
242
  sniperMetrics: () => getJson<any>("/api/sniper/metrics"),
243
  sniperLessons: (limit = 40) => getJson<any[]>(`/api/sniper/lessons?limit=${limit}`),
244
  tradingGameStatus: () => getJson<any>("/api/trading-game/status"),
245
+ tradingGameReadiness: () => getJson<any>("/api/trading-game/readiness"),
246
  copyTradingStatus: () => getJson<any>("/api/copy-trading/status"),
247
  copyTradingCandidates: (limit = 25) => getJson<any>(`/api/copy-trading/candidates?limit=${limit}`),
248
  copyTradingDashboard: (limit = 25) => getJson<any>(`/api/copy-trading/dashboard?limit=${limit}`),
249
+ alphaReadiness: () => getJson<any>("/api/alpha/readiness"),
250
+ alphaEdgeMap: (limit = 12) => getJson<any>(`/api/alpha/edge-map?limit=${limit}`),
251
+ alphaGates: () => getJson<any>("/api/alpha/gates"),
252
+ paperCopySummary: (limit = 12) => getJson<any>(`/api/paper-copy/summary?limit=${limit}`),
253
+ paperCopyReadiness: () => getJson<any>("/api/paper-copy/readiness"),
254
+ paperCopyStrategies: (limit = 40) => getJson<any>(`/api/paper-copy/strategies?limit=${limit}`),
255
+ paperCopyPositions: (limit = 80) => getJson<any>(`/api/paper-copy/positions?limit=${limit}`),
256
+ paperCopyPortfolio: (portfolioId: string) => getJson<any>(`/api/paper-copy/portfolio/${encodeURIComponent(portfolioId)}`),
257
  tradingGameRun: (batchSize = 60) => postJson<any>(`/api/trading-game/run?batch_size=${batchSize}`, {}),
258
  tradingGameReset: () => postJson<any>("/api/trading-game/reset", {}),
259
  tradingGameEquity: (limit = 500, gameId?: number) => getJson<any[]>(`/api/trading-game/equity?limit=${limit}${gameId ? `&game_id=${gameId}` : ""}`),
frontend/package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "blum-ai-financial-intelligence-frontend",
3
- "version": "0.20.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p 3000",
 
1
  {
2
  "name": "blum-ai-financial-intelligence-frontend",
3
+ "version": "1.0.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.20.0",
4
  "private": true,
5
  "scripts": {
6
  "frontend:dev": "npm --prefix frontend run dev",
 
1
  {
2
  "name": "blum-ai-financial-intelligence",
3
+ "version": "1.0.0",
4
  "private": true,
5
  "scripts": {
6
  "frontend:dev": "npm --prefix frontend run dev",