Spaces:
Running
Running
Deploy BLUM v1.0.0 alpha operating system
Browse files- CHANGELOG.md +13 -0
- MIGRATION_v1.0.0.md +23 -0
- README.md +41 -5
- RELEASE_NOTES_v1.0.0.md +24 -0
- backend/alembic/versions/0026_alpha_operating_system.py +232 -0
- backend/app/api/routes.py +80 -2
- backend/app/core/config.py +1 -1
- backend/app/models.py +211 -0
- backend/app/services/alpha_operating_system.py +815 -0
- backend/app/services/central_brain_runtime.py +30 -0
- backend/tests/test_alpha_operating_system.py +163 -0
- frontend/app/copy-trading/page.tsx +27 -2
- frontend/app/dashboard/page.tsx +24 -19
- frontend/app/learning/page.tsx +50 -1
- frontend/lib/api.ts +12 -0
- frontend/package.json +1 -1
- package.json +1 -1
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 /
|
| 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
|
| 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":
|
| 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.
|
| 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.
|
| 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.
|
| 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: "
|
| 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 [
|
| 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,
|
| 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.
|
| 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 (
|
| 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 |
-
|
| 84 |
]
|
| 85 |
.filter(Boolean)
|
| 86 |
.join(" | ")
|
|
@@ -234,7 +234,7 @@ export default function DashboardPage() {
|
|
| 234 |
/>
|
| 235 |
</section>
|
| 236 |
|
| 237 |
-
<BrainLevelPanel summary={
|
| 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
|
| 344 |
-
const
|
| 345 |
-
const
|
| 346 |
-
const
|
| 347 |
-
const
|
| 348 |
-
const
|
|
|
|
|
|
|
| 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={
|
| 365 |
-
<MetricCard label="Win / Expectancy" value={`${formatPct(
|
| 366 |
-
<MetricCard label="Capital progress" value={formatPct(
|
| 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.
|
| 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.
|
| 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",
|