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