Spaces:
Running
Running
Fix live-forward paper runtime deploy
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +25 -0
- .next/trace +0 -1
- backend/app/core/config.py +12 -1
- backend/app/main.py +30 -2
- backend/app/services/autonomous_engine.py +2 -0
- backend/app/services/blum_financial_model.py +6 -2
- backend/app/services/bootstrap.py +68 -20
- backend/app/services/dashboard.py +43 -0
- backend/app/services/dashboard_snapshots.py +26 -4
- backend/app/services/financial_chat.py +106 -2
- backend/app/services/learning_loop.py +705 -48
- backend/app/services/learning_summary.py +304 -3
- backend/app/services/performance.py +7 -1
- backend/app/services/trade_transparency.py +166 -41
- backend/app/services/trading_game.py +21 -0
- backend/tests/test_learning_performance_architecture.py +73 -2
- backend/tests/test_performance_diagnostics.py +6 -3
- frontend/.next/BUILD_ID +1 -0
- frontend/.next/app-build-manifest.json +199 -0
- frontend/.next/app-path-routes-manifest.json +1 -0
- frontend/.next/build-manifest.json +32 -0
- frontend/.next/cache/.tsbuildinfo +0 -0
- frontend/.next/cache/webpack/client-production/0.pack +3 -0
- frontend/.next/cache/webpack/client-production/1.pack +3 -0
- frontend/.next/cache/webpack/client-production/10.pack +3 -0
- frontend/.next/cache/webpack/client-production/11.pack +3 -0
- frontend/.next/cache/webpack/client-production/12.pack +3 -0
- frontend/.next/cache/webpack/client-production/13.pack +3 -0
- frontend/.next/cache/webpack/client-production/14.pack +0 -0
- frontend/.next/cache/webpack/client-production/15.pack +0 -0
- frontend/.next/cache/webpack/client-production/2.pack +3 -0
- frontend/.next/cache/webpack/client-production/3.pack +0 -0
- frontend/.next/cache/webpack/client-production/4.pack +0 -0
- frontend/.next/cache/webpack/client-production/5.pack +0 -0
- frontend/.next/cache/webpack/client-production/6.pack +3 -0
- frontend/.next/cache/webpack/client-production/7.pack +3 -0
- frontend/.next/cache/webpack/client-production/8.pack +3 -0
- frontend/.next/cache/webpack/client-production/9.pack +3 -0
- frontend/.next/cache/webpack/client-production/index.pack +3 -0
- frontend/.next/cache/webpack/client-production/index.pack.old +3 -0
- frontend/.next/cache/webpack/edge-server-production/0.pack +0 -0
- frontend/.next/cache/webpack/edge-server-production/index.pack +0 -0
- frontend/.next/cache/webpack/server-production/0.pack +3 -0
- frontend/.next/cache/webpack/server-production/1.pack +3 -0
- frontend/.next/cache/webpack/server-production/10.pack +3 -0
- frontend/.next/cache/webpack/server-production/2.pack +3 -0
- frontend/.next/cache/webpack/server-production/3.pack +3 -0
- frontend/.next/cache/webpack/server-production/4.pack +3 -0
- frontend/.next/cache/webpack/server-production/5.pack +3 -0
- frontend/.next/cache/webpack/server-production/6.pack +3 -0
.gitattributes
CHANGED
|
@@ -39,3 +39,28 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 39 |
.upload-venv/lib/python3.13/site-packages/pip/_vendor/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 40 |
.upload-venv/lib/python3.13/site-packages/pip/_vendor/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text
|
| 41 |
.upload-venv/lib/python3.13/site-packages/yaml/_yaml.cpython-313-darwin.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
.upload-venv/lib/python3.13/site-packages/pip/_vendor/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 40 |
.upload-venv/lib/python3.13/site-packages/pip/_vendor/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text
|
| 41 |
.upload-venv/lib/python3.13/site-packages/yaml/_yaml.cpython-313-darwin.so filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
frontend/.next/cache/webpack/client-production/0.pack filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
frontend/.next/cache/webpack/client-production/1.pack filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
frontend/.next/cache/webpack/client-production/10.pack filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
frontend/.next/cache/webpack/client-production/11.pack filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
frontend/.next/cache/webpack/client-production/12.pack filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
frontend/.next/cache/webpack/client-production/13.pack filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
frontend/.next/cache/webpack/client-production/2.pack filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
frontend/.next/cache/webpack/client-production/6.pack filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
frontend/.next/cache/webpack/client-production/7.pack filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
frontend/.next/cache/webpack/client-production/8.pack filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
frontend/.next/cache/webpack/client-production/9.pack filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
frontend/.next/cache/webpack/client-production/index.pack filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
frontend/.next/cache/webpack/client-production/index.pack.old filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
frontend/.next/cache/webpack/server-production/0.pack filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
frontend/.next/cache/webpack/server-production/1.pack filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
frontend/.next/cache/webpack/server-production/10.pack filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
frontend/.next/cache/webpack/server-production/2.pack filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
frontend/.next/cache/webpack/server-production/3.pack filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
frontend/.next/cache/webpack/server-production/4.pack filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
frontend/.next/cache/webpack/server-production/5.pack filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
frontend/.next/cache/webpack/server-production/6.pack filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
frontend/.next/cache/webpack/server-production/7.pack filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
frontend/.next/cache/webpack/server-production/8.pack filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
frontend/.next/cache/webpack/server-production/index.pack filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
frontend/.next/cache/webpack/server-production/index.pack.old filter=lfs diff=lfs merge=lfs -text
|
.next/trace
CHANGED
|
@@ -1,2 +1 @@
|
|
| 1 |
[{"name":"generate-buildid","duration":134,"timestamp":87872497011,"id":4,"parentId":1,"tags":{},"startTime":1782192543160,"traceId":"74c71a2e401cebbb"},{"name":"load-custom-routes","duration":113,"timestamp":87872497220,"id":5,"parentId":1,"tags":{},"startTime":1782192543160,"traceId":"74c71a2e401cebbb"},{"name":"next-build","duration":48980,"timestamp":87872449545,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"14.2.35","isTurbopack":false},"startTime":1782192543112,"traceId":"74c71a2e401cebbb"}]
|
| 2 |
-
[{"name":"generate-buildid","duration":139,"timestamp":107902785130,"id":4,"parentId":1,"tags":{},"startTime":1782212573555,"traceId":"5254ca5a348315b0"},{"name":"load-custom-routes","duration":121,"timestamp":107902785349,"id":5,"parentId":1,"tags":{},"startTime":1782212573555,"traceId":"5254ca5a348315b0"},{"name":"next-build","duration":41482,"timestamp":107902744802,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"14.2.35","isTurbopack":false},"startTime":1782212573514,"traceId":"5254ca5a348315b0"}]
|
|
|
|
| 1 |
[{"name":"generate-buildid","duration":134,"timestamp":87872497011,"id":4,"parentId":1,"tags":{},"startTime":1782192543160,"traceId":"74c71a2e401cebbb"},{"name":"load-custom-routes","duration":113,"timestamp":87872497220,"id":5,"parentId":1,"tags":{},"startTime":1782192543160,"traceId":"74c71a2e401cebbb"},{"name":"next-build","duration":48980,"timestamp":87872449545,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"14.2.35","isTurbopack":false},"startTime":1782192543112,"traceId":"74c71a2e401cebbb"}]
|
|
|
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 = "
|
| 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",
|
|
@@ -31,6 +31,9 @@ class Settings(BaseSettings):
|
|
| 31 |
max_update_assets: int = Field(default=160, alias="BLUM_MAX_UPDATE_ASSETS")
|
| 32 |
enable_live_startup: bool = Field(default=True, alias="BLUM_ENABLE_LIVE_STARTUP")
|
| 33 |
enable_autonomous_engine: bool = Field(default=True, alias="BLUM_ENABLE_AUTONOMOUS_ENGINE")
|
|
|
|
|
|
|
|
|
|
| 34 |
autonomous_cycle_minutes: int = Field(default=20, alias="BLUM_AUTONOMOUS_CYCLE_MINUTES")
|
| 35 |
autonomous_repair_limit: int = Field(default=80, alias="BLUM_AUTONOMOUS_REPAIR_LIMIT")
|
| 36 |
seed_historical_prices_on_startup: bool = Field(default=True, alias="BLUM_SEED_HISTORICAL_PRICES_ON_STARTUP")
|
|
@@ -49,6 +52,13 @@ class Settings(BaseSettings):
|
|
| 49 |
learning_min_history_years: int = Field(default=3, alias="LEARNING_MIN_HISTORY_YEARS")
|
| 50 |
learning_asset_universe: str = Field(default="stocks,etfs", alias="LEARNING_ASSET_UNIVERSE")
|
| 51 |
learning_evaluation_mode: str = Field(default="walk_forward", alias="LEARNING_EVALUATION_MODE")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
blum_model_cycle_minutes: int = Field(default=5, alias="BLUM_MODEL_CYCLE_MINUTES")
|
| 53 |
blum_model_cycle_limit: int = Field(default=160, alias="BLUM_MODEL_CYCLE_LIMIT")
|
| 54 |
fundamentals_refresh_minutes: int = Field(default=720, alias="BLUM_FUNDAMENTALS_REFRESH_MINUTES")
|
|
@@ -62,6 +72,7 @@ class Settings(BaseSettings):
|
|
| 62 |
refresh_price_period: str = Field(default="6mo", alias="BLUM_REFRESH_PRICE_PERIOD")
|
| 63 |
sec_user_agent: str = Field(default="Blum-AI-Financial-Intelligence research demo", alias="BLUM_SEC_USER_AGENT")
|
| 64 |
blum_model_repository: str = Field(default="Italianhype/Blum", alias="BLUM_MODEL_REPOSITORY")
|
|
|
|
| 65 |
training_export_dir: str = Field(default="/tmp/blum_training_exports", alias="BLUM_TRAINING_EXPORT_DIR")
|
| 66 |
enable_hf_dataset_catalog: bool = Field(default=True, alias="BLUM_ENABLE_HF_DATASET_CATALOG")
|
| 67 |
hf_dataset_refresh_hours: int = Field(default=24, alias="BLUM_HF_DATASET_REFRESH_HOURS")
|
|
|
|
| 5 |
|
| 6 |
class Settings(BaseSettings):
|
| 7 |
app_name: str = "Blum AI Financial Intelligence"
|
| 8 |
+
app_version: str = "2.1.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",
|
|
|
|
| 31 |
max_update_assets: int = Field(default=160, alias="BLUM_MAX_UPDATE_ASSETS")
|
| 32 |
enable_live_startup: bool = Field(default=True, alias="BLUM_ENABLE_LIVE_STARTUP")
|
| 33 |
enable_autonomous_engine: bool = Field(default=True, alias="BLUM_ENABLE_AUTONOMOUS_ENGINE")
|
| 34 |
+
startup_run_full_autonomous: bool = Field(default=False, alias="BLUM_STARTUP_RUN_FULL_AUTONOMOUS")
|
| 35 |
+
blum_autonomous_max_seconds_per_job: int = Field(default=120, alias="BLUM_AUTONOMOUS_MAX_SECONDS_PER_JOB")
|
| 36 |
+
blum_autonomous_max_items_per_job: int = Field(default=50, alias="BLUM_AUTONOMOUS_MAX_ITEMS_PER_JOB")
|
| 37 |
autonomous_cycle_minutes: int = Field(default=20, alias="BLUM_AUTONOMOUS_CYCLE_MINUTES")
|
| 38 |
autonomous_repair_limit: int = Field(default=80, alias="BLUM_AUTONOMOUS_REPAIR_LIMIT")
|
| 39 |
seed_historical_prices_on_startup: bool = Field(default=True, alias="BLUM_SEED_HISTORICAL_PRICES_ON_STARTUP")
|
|
|
|
| 52 |
learning_min_history_years: int = Field(default=3, alias="LEARNING_MIN_HISTORY_YEARS")
|
| 53 |
learning_asset_universe: str = Field(default="stocks,etfs", alias="LEARNING_ASSET_UNIVERSE")
|
| 54 |
learning_evaluation_mode: str = Field(default="walk_forward", alias="LEARNING_EVALUATION_MODE")
|
| 55 |
+
learning_random_sample_ratio: float = Field(default=0.40, alias="LEARNING_RANDOM_SAMPLE_RATIO")
|
| 56 |
+
learning_alpha_loss_sample_ratio: float = Field(default=0.30, alias="LEARNING_ALPHA_LOSS_SAMPLE_RATIO")
|
| 57 |
+
learning_factor_focus_sample_ratio: float = Field(default=0.20, alias="LEARNING_FACTOR_FOCUS_SAMPLE_RATIO")
|
| 58 |
+
learning_capital_preservation_sample_ratio: float = Field(default=0.10, alias="LEARNING_CAPITAL_PRESERVATION_SAMPLE_RATIO")
|
| 59 |
+
professional_learning_enabled: bool = Field(default=True, alias="BLUM_PROFESSIONAL_LEARNING_ENABLED")
|
| 60 |
+
professional_learning_minutes: int = Field(default=30, alias="BLUM_PROFESSIONAL_LEARNING_MINUTES")
|
| 61 |
+
professional_learning_batch_size: int = Field(default=20, alias="BLUM_PROFESSIONAL_LEARNING_BATCH_SIZE")
|
| 62 |
blum_model_cycle_minutes: int = Field(default=5, alias="BLUM_MODEL_CYCLE_MINUTES")
|
| 63 |
blum_model_cycle_limit: int = Field(default=160, alias="BLUM_MODEL_CYCLE_LIMIT")
|
| 64 |
fundamentals_refresh_minutes: int = Field(default=720, alias="BLUM_FUNDAMENTALS_REFRESH_MINUTES")
|
|
|
|
| 72 |
refresh_price_period: str = Field(default="6mo", alias="BLUM_REFRESH_PRICE_PERIOD")
|
| 73 |
sec_user_agent: str = Field(default="Blum-AI-Financial-Intelligence research demo", alias="BLUM_SEC_USER_AGENT")
|
| 74 |
blum_model_repository: str = Field(default="Italianhype/Blum", alias="BLUM_MODEL_REPOSITORY")
|
| 75 |
+
blum_analyst_repository: str = Field(default="Italianhype/Blum-Analyst", alias="BLUM_ANALYST_REPOSITORY")
|
| 76 |
training_export_dir: str = Field(default="/tmp/blum_training_exports", alias="BLUM_TRAINING_EXPORT_DIR")
|
| 77 |
enable_hf_dataset_catalog: bool = Field(default=True, alias="BLUM_ENABLE_HF_DATASET_CATALOG")
|
| 78 |
hf_dataset_refresh_hours: int = Field(default=24, alias="BLUM_HF_DATASET_REFRESH_HOURS")
|
backend/app/main.py
CHANGED
|
@@ -9,7 +9,15 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 9 |
from fastapi.responses import FileResponse
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
|
| 12 |
-
from app.api.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from app.core.config import get_settings
|
| 14 |
from app.core.database import SessionLocal
|
| 15 |
from app.services.bootstrap import bootstrap_database
|
|
@@ -38,7 +46,13 @@ app.add_middleware(
|
|
| 38 |
allow_headers=["*"],
|
| 39 |
)
|
| 40 |
|
| 41 |
-
app.include_router(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
@app.middleware("http")
|
|
@@ -67,6 +81,13 @@ async def performance_timing_middleware(request: Request, call_next):
|
|
| 67 |
duration_ms,
|
| 68 |
{"method": request.method, "path": request.url.path, "referer": request.headers.get("referer", "")[:180]},
|
| 69 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
return response
|
| 71 |
finally:
|
| 72 |
duration_ms = (time.perf_counter() - started) * 1000
|
|
@@ -106,6 +127,13 @@ def is_heavy_recalculation_call(method: str, path: str) -> bool:
|
|
| 106 |
"/business-quality/recalculate",
|
| 107 |
"/decision-intelligence/superiority/recalculate",
|
| 108 |
"/learning-intelligence/self-improvement/generate",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
)
|
| 110 |
return any(fragment in path for fragment in heavy_fragments)
|
| 111 |
|
|
|
|
| 9 |
from fastapi.responses import FileResponse
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
|
| 12 |
+
from app.api.routers import (
|
| 13 |
+
alpha_router,
|
| 14 |
+
analyst_router,
|
| 15 |
+
brain_router,
|
| 16 |
+
legacy_router,
|
| 17 |
+
paper_trading_router,
|
| 18 |
+
runtime_router,
|
| 19 |
+
training_router,
|
| 20 |
+
)
|
| 21 |
from app.core.config import get_settings
|
| 22 |
from app.core.database import SessionLocal
|
| 23 |
from app.services.bootstrap import bootstrap_database
|
|
|
|
| 46 |
allow_headers=["*"],
|
| 47 |
)
|
| 48 |
|
| 49 |
+
app.include_router(brain_router)
|
| 50 |
+
app.include_router(training_router)
|
| 51 |
+
app.include_router(paper_trading_router)
|
| 52 |
+
app.include_router(alpha_router)
|
| 53 |
+
app.include_router(runtime_router)
|
| 54 |
+
app.include_router(analyst_router)
|
| 55 |
+
app.include_router(legacy_router)
|
| 56 |
|
| 57 |
|
| 58 |
@app.middleware("http")
|
|
|
|
| 81 |
duration_ms,
|
| 82 |
{"method": request.method, "path": request.url.path, "referer": request.headers.get("referer", "")[:180]},
|
| 83 |
)
|
| 84 |
+
if request.method.upper() == "GET" and "persist=true" in request.url.query.lower():
|
| 85 |
+
response.headers["X-BLUM-GET-SIDE-EFFECT-RISK"] = "true"
|
| 86 |
+
performance_recorder.record_dashboard_widget(
|
| 87 |
+
"performance.GET_ENDPOINT_SIDE_EFFECT_DETECTED",
|
| 88 |
+
duration_ms,
|
| 89 |
+
{"method": request.method, "path": request.url.path, "query": request.url.query[:180]},
|
| 90 |
+
)
|
| 91 |
return response
|
| 92 |
finally:
|
| 93 |
duration_ms = (time.perf_counter() - started) * 1000
|
|
|
|
| 127 |
"/business-quality/recalculate",
|
| 128 |
"/decision-intelligence/superiority/recalculate",
|
| 129 |
"/learning-intelligence/self-improvement/generate",
|
| 130 |
+
"/api/meta-cognition/recalculate",
|
| 131 |
+
"/api/meta-cognition/factor-importance/recalculate",
|
| 132 |
+
"/api/meta-cognition/evaluate",
|
| 133 |
+
"/api/meta-cognition/capital-preservation/evaluate",
|
| 134 |
+
"/api/meta-cognition/learning-focus/generate",
|
| 135 |
+
"/api/meta-cognition/noise/detect",
|
| 136 |
+
"/snapshots/produce",
|
| 137 |
)
|
| 138 |
return any(fragment in path for fragment in heavy_fragments)
|
| 139 |
|
backend/app/services/autonomous_engine.py
CHANGED
|
@@ -34,6 +34,7 @@ from app.services.macro import update_macro_snapshots
|
|
| 34 |
from app.services.market_data import MarketDataService
|
| 35 |
from app.services.market_sniper import MarketSniperEngine
|
| 36 |
from app.services.persistence import backup_embedded_postgres_if_configured
|
|
|
|
| 37 |
from app.services.trading_game import TradingGameSimulator
|
| 38 |
from app.signals.engine import SignalEngine
|
| 39 |
|
|
@@ -85,6 +86,7 @@ class AutonomousResearchEngine:
|
|
| 85 |
stage("etf_intelligence", lambda: update_etf_trends(db))
|
| 86 |
stage("ipo_radar", lambda: update_ipo_radar(db, limit_per_form=55))
|
| 87 |
stage("accuracy_audit", lambda: run_accuracy_audit(db, limit=settings.max_update_assets))
|
|
|
|
| 88 |
if settings.enable_learning_loop:
|
| 89 |
stage("blum_financial_model", lambda: run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit))
|
| 90 |
stage("blum_learning_loop", lambda: LearningLoopService().run_batch(db, batch_size=settings.learning_batch_size, trigger="autonomous_engine"))
|
|
|
|
| 34 |
from app.services.market_data import MarketDataService
|
| 35 |
from app.services.market_sniper import MarketSniperEngine
|
| 36 |
from app.services.persistence import backup_embedded_postgres_if_configured
|
| 37 |
+
from app.services.research_planner import AutonomousResearchPlanner
|
| 38 |
from app.services.trading_game import TradingGameSimulator
|
| 39 |
from app.signals.engine import SignalEngine
|
| 40 |
|
|
|
|
| 86 |
stage("etf_intelligence", lambda: update_etf_trends(db))
|
| 87 |
stage("ipo_radar", lambda: update_ipo_radar(db, limit_per_form=55))
|
| 88 |
stage("accuracy_audit", lambda: run_accuracy_audit(db, limit=settings.max_update_assets))
|
| 89 |
+
stage("research_planner", lambda: AutonomousResearchPlanner().generate(db, persist=True))
|
| 90 |
if settings.enable_learning_loop:
|
| 91 |
stage("blum_financial_model", lambda: run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit))
|
| 92 |
stage("blum_learning_loop", lambda: LearningLoopService().run_batch(db, batch_size=settings.learning_batch_size, trigger="autonomous_engine"))
|
backend/app/services/blum_financial_model.py
CHANGED
|
@@ -96,7 +96,7 @@ def model_status(db: Session) -> dict:
|
|
| 96 |
}
|
| 97 |
|
| 98 |
|
| 99 |
-
def run_model_learning_cycle(db: Session, limit: int = 120) -> dict:
|
| 100 |
signals = db.scalars(select(SignalSnapshot).order_by(desc(SignalSnapshot.created_at)).limit(limit)).all()
|
| 101 |
skipped = 0
|
| 102 |
before_count = count(db, BlumKnowledgeRecord.id)
|
|
@@ -127,7 +127,11 @@ def run_model_learning_cycle(db: Session, limit: int = 120) -> dict:
|
|
| 127 |
)
|
| 128 |
db.add(event)
|
| 129 |
db.commit()
|
| 130 |
-
backup_result =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
return {
|
| 132 |
"status": "ok",
|
| 133 |
"signals_seen": len(signals),
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
|
| 99 |
+
def run_model_learning_cycle(db: Session, limit: int = 120, backup: bool = True) -> dict:
|
| 100 |
signals = db.scalars(select(SignalSnapshot).order_by(desc(SignalSnapshot.created_at)).limit(limit)).all()
|
| 101 |
skipped = 0
|
| 102 |
before_count = count(db, BlumKnowledgeRecord.id)
|
|
|
|
| 127 |
)
|
| 128 |
db.add(event)
|
| 129 |
db.commit()
|
| 130 |
+
backup_result = (
|
| 131 |
+
backup_embedded_postgres_if_configured(reason="blum_model_autonomous_cycle")
|
| 132 |
+
if backup
|
| 133 |
+
else {"status": "skipped", "reason": "frequent professional learning cycles do not run full embedded database backups"}
|
| 134 |
+
)
|
| 135 |
return {
|
| 136 |
"status": "ok",
|
| 137 |
"signals_seen": len(signals),
|
backend/app/services/bootstrap.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
| 3 |
import csv
|
| 4 |
from datetime import date
|
| 5 |
import gzip
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from sqlalchemy import func, select
|
|
@@ -67,31 +68,49 @@ def seed_historical_prices(db: Session) -> dict:
|
|
| 67 |
"inserted_rows": 0,
|
| 68 |
"message": "Historical price cache is not packaged in this build.",
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
asset_ids = dict(db.execute(select(Asset.ticker, Asset.id)).all())
|
| 71 |
inserted = 0
|
| 72 |
skipped = 0
|
| 73 |
tickers: set[str] = set()
|
| 74 |
rows: list[dict] = []
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
return {
|
| 96 |
"enabled": True,
|
| 97 |
"cache_status": "loaded",
|
|
@@ -121,6 +140,35 @@ def seed_startup_accuracy(db: Session) -> dict:
|
|
| 121 |
return {"enabled": True, **run_accuracy_audit(db, limit=get_settings().max_update_assets)}
|
| 122 |
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
def price_row_from_cache(asset_id: int, item: dict) -> dict | None:
|
| 125 |
try:
|
| 126 |
close = float(item["close"])
|
|
|
|
| 3 |
import csv
|
| 4 |
from datetime import date
|
| 5 |
import gzip
|
| 6 |
+
from gzip import BadGzipFile
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
from sqlalchemy import func, select
|
|
|
|
| 68 |
"inserted_rows": 0,
|
| 69 |
"message": "Historical price cache is not packaged in this build.",
|
| 70 |
}
|
| 71 |
+
pointer = git_lfs_pointer_summary(HISTORICAL_PRICE_CACHE)
|
| 72 |
+
if pointer:
|
| 73 |
+
return invalid_historical_cache_response(
|
| 74 |
+
"git_lfs_pointer",
|
| 75 |
+
(
|
| 76 |
+
"Historical price cache is a Git LFS pointer, not the resolved gzip payload. "
|
| 77 |
+
"Run `git lfs pull` in development or ensure the Space resolves LFS assets. "
|
| 78 |
+
"Startup continues without synthetic market data."
|
| 79 |
+
),
|
| 80 |
+
pointer,
|
| 81 |
+
)
|
| 82 |
asset_ids = dict(db.execute(select(Asset.ticker, Asset.id)).all())
|
| 83 |
inserted = 0
|
| 84 |
skipped = 0
|
| 85 |
tickers: set[str] = set()
|
| 86 |
rows: list[dict] = []
|
| 87 |
+
try:
|
| 88 |
+
with gzip.open(HISTORICAL_PRICE_CACHE, "rt", newline="", encoding="utf-8") as handle:
|
| 89 |
+
reader = csv.DictReader(handle)
|
| 90 |
+
for item in reader:
|
| 91 |
+
ticker = (item.get("ticker") or "").upper()
|
| 92 |
+
asset_id = asset_ids.get(ticker)
|
| 93 |
+
if not asset_id:
|
| 94 |
+
skipped += 1
|
| 95 |
+
continue
|
| 96 |
+
row = price_row_from_cache(asset_id, item)
|
| 97 |
+
if not row:
|
| 98 |
+
skipped += 1
|
| 99 |
+
continue
|
| 100 |
+
tickers.add(ticker)
|
| 101 |
+
rows.append(row)
|
| 102 |
+
if len(rows) >= 5000:
|
| 103 |
+
inserted += insert_price_rows(db, rows)
|
| 104 |
+
rows.clear()
|
| 105 |
+
if rows:
|
| 106 |
+
inserted += insert_price_rows(db, rows)
|
| 107 |
+
db.commit()
|
| 108 |
+
except (BadGzipFile, EOFError, OSError, UnicodeDecodeError, csv.Error) as exc:
|
| 109 |
+
db.rollback()
|
| 110 |
+
return invalid_historical_cache_response(
|
| 111 |
+
"invalid",
|
| 112 |
+
f"Historical price cache could not be loaded ({type(exc).__name__}). Startup continues without synthetic market data.",
|
| 113 |
+
)
|
| 114 |
return {
|
| 115 |
"enabled": True,
|
| 116 |
"cache_status": "loaded",
|
|
|
|
| 140 |
return {"enabled": True, **run_accuracy_audit(db, limit=get_settings().max_update_assets)}
|
| 141 |
|
| 142 |
|
| 143 |
+
def git_lfs_pointer_summary(path: Path) -> dict | None:
|
| 144 |
+
try:
|
| 145 |
+
head = path.read_bytes()[:220]
|
| 146 |
+
except OSError:
|
| 147 |
+
return None
|
| 148 |
+
if not head.startswith(b"version https://git-lfs.github.com/spec/v1"):
|
| 149 |
+
return None
|
| 150 |
+
text = head.decode("utf-8", errors="replace")
|
| 151 |
+
summary = {"pointer_detected": True}
|
| 152 |
+
for line in text.splitlines():
|
| 153 |
+
if line.startswith("oid "):
|
| 154 |
+
summary["oid"] = line.split(" ", 1)[1]
|
| 155 |
+
elif line.startswith("size "):
|
| 156 |
+
summary["expected_size"] = line.split(" ", 1)[1]
|
| 157 |
+
return summary
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def invalid_historical_cache_response(cache_status: str, message: str, diagnostics: dict | None = None) -> dict:
|
| 161 |
+
return {
|
| 162 |
+
"enabled": True,
|
| 163 |
+
"cache_status": cache_status,
|
| 164 |
+
"cache_file": HISTORICAL_PRICE_CACHE.name,
|
| 165 |
+
"inserted_rows": 0,
|
| 166 |
+
"diagnostics": diagnostics or {},
|
| 167 |
+
"message": message,
|
| 168 |
+
"data_policy": "No synthetic prices are created when packaged OHLCV cache is unavailable.",
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
def price_row_from_cache(asset_id: int, item: dict) -> dict | None:
|
| 173 |
try:
|
| 174 |
close = float(item["close"])
|
backend/app/services/dashboard.py
CHANGED
|
@@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
|
|
| 6 |
from app.models import Asset, ETFTrend, NewsArticle, PriceHistory, SentimentAnalysis, SignalSnapshot
|
| 7 |
from app.services.accuracy import latest_accuracy_snapshot, market_accuracy_overview, signal_validation_report
|
| 8 |
from app.services.data_continuity import data_coverage_report
|
|
|
|
| 9 |
from app.services.macro import macro_overview
|
| 10 |
from app.services.market_data import market_snapshot_for_asset
|
| 11 |
from app.services.performance import performance_recorder
|
|
@@ -14,6 +15,19 @@ from app.services.realtime import realtime_status
|
|
| 14 |
|
| 15 |
|
| 16 |
def dashboard_overview(db: Session) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
with performance_recorder.dashboard_widget("dashboard.load_signal_candidates"):
|
| 18 |
signals = db.scalars(select(SignalSnapshot).order_by(desc(SignalSnapshot.created_at), desc(SignalSnapshot.blum_score)).limit(80)).all()
|
| 19 |
with performance_recorder.dashboard_widget("dashboard.rank_latest_signals", signal_count=len(signals)):
|
|
@@ -80,6 +94,35 @@ def dashboard_overview(db: Session) -> dict:
|
|
| 80 |
}
|
| 81 |
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def signal_payload(signal: SignalSnapshot, db: Session | None = None) -> dict:
|
| 84 |
with performance_recorder.dashboard_widget("dashboard.signal_payload", ticker=signal.ticker):
|
| 85 |
payload = {
|
|
|
|
| 6 |
from app.models import Asset, ETFTrend, NewsArticle, PriceHistory, SentimentAnalysis, SignalSnapshot
|
| 7 |
from app.services.accuracy import latest_accuracy_snapshot, market_accuracy_overview, signal_validation_report
|
| 8 |
from app.services.data_continuity import data_coverage_report
|
| 9 |
+
from app.services.dashboard_snapshots import DashboardSnapshotService
|
| 10 |
from app.services.macro import macro_overview
|
| 11 |
from app.services.market_data import market_snapshot_for_asset
|
| 12 |
from app.services.performance import performance_recorder
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
def dashboard_overview(db: Session) -> dict:
|
| 18 |
+
with performance_recorder.dashboard_widget("dashboard.snapshot_lookup", snapshot_type="dashboard_overview_summary"):
|
| 19 |
+
snapshot = DashboardSnapshotService().latest(db, "dashboard_overview_summary")
|
| 20 |
+
if snapshot["payload"]:
|
| 21 |
+
payload = dict(snapshot["payload"])
|
| 22 |
+
payload["snapshot_status"] = snapshot["status"]
|
| 23 |
+
payload["snapshot_created_at"] = snapshot.get("created_at")
|
| 24 |
+
payload["snapshot_warnings"] = snapshot.get("warnings", [])
|
| 25 |
+
payload["runtime_policy"] = "snapshot_first_no_live_recalculation"
|
| 26 |
+
return payload
|
| 27 |
+
return empty_dashboard_overview(snapshot)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def build_dashboard_overview_live(db: Session) -> dict:
|
| 31 |
with performance_recorder.dashboard_widget("dashboard.load_signal_candidates"):
|
| 32 |
signals = db.scalars(select(SignalSnapshot).order_by(desc(SignalSnapshot.created_at), desc(SignalSnapshot.blum_score)).limit(80)).all()
|
| 33 |
with performance_recorder.dashboard_widget("dashboard.rank_latest_signals", signal_count=len(signals)):
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
|
| 97 |
+
def empty_dashboard_overview(snapshot: dict) -> dict:
|
| 98 |
+
return {
|
| 99 |
+
"snapshot_status": snapshot.get("status", "missing"),
|
| 100 |
+
"snapshot_created_at": snapshot.get("created_at"),
|
| 101 |
+
"snapshot_warnings": [snapshot.get("warning") or "dashboard_overview_summary snapshot is not ready"],
|
| 102 |
+
"runtime_policy": "snapshot_first_no_live_recalculation",
|
| 103 |
+
"market_pulse": {
|
| 104 |
+
"asset_count": 0,
|
| 105 |
+
"article_count": 0,
|
| 106 |
+
"average_sentiment": 0,
|
| 107 |
+
"signal_count": 0,
|
| 108 |
+
"classification_mix": {},
|
| 109 |
+
"price_row_count": 0,
|
| 110 |
+
},
|
| 111 |
+
"data_coverage": {"status": "missing_snapshot"},
|
| 112 |
+
"accuracy": {"status": "missing_snapshot"},
|
| 113 |
+
"macro": {"status": "missing_snapshot"},
|
| 114 |
+
"validation": {"status": "missing_snapshot"},
|
| 115 |
+
"readiness": {"status": "missing_snapshot"},
|
| 116 |
+
"realtime": {"status": "unknown"},
|
| 117 |
+
"todays_strongest_signals": [],
|
| 118 |
+
"narrative_breakouts": [],
|
| 119 |
+
"technical_breakouts": [],
|
| 120 |
+
"sentiment_divergence": [],
|
| 121 |
+
"watchlist_candidates": [],
|
| 122 |
+
"etf_rotation_leaders": [],
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
def signal_payload(signal: SignalSnapshot, db: Session | None = None) -> dict:
|
| 127 |
with performance_recorder.dashboard_widget("dashboard.signal_payload", ticker=signal.ticker):
|
| 128 |
payload = {
|
backend/app/services/dashboard_snapshots.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
from datetime import datetime, timedelta
|
| 4 |
import time
|
| 5 |
|
| 6 |
from sqlalchemy import desc, select
|
|
@@ -38,6 +38,7 @@ class DashboardSnapshotService:
|
|
| 38 |
"is_stale": is_stale,
|
| 39 |
"payload": row.payload_json or {},
|
| 40 |
"source_modules": row.source_modules_json or {},
|
|
|
|
| 41 |
"computation_duration_ms": row.computation_duration_ms,
|
| 42 |
"warnings": row.warnings_json or [],
|
| 43 |
}
|
|
@@ -51,6 +52,7 @@ class DashboardSnapshotService:
|
|
| 51 |
source_modules: dict | None = None,
|
| 52 |
ttl_seconds: int = 300,
|
| 53 |
warnings: list[str] | None = None,
|
|
|
|
| 54 |
computation_duration_ms: float | None = None,
|
| 55 |
) -> dict:
|
| 56 |
started = time.perf_counter()
|
|
@@ -58,11 +60,12 @@ class DashboardSnapshotService:
|
|
| 58 |
snapshot_type=snapshot_type,
|
| 59 |
created_at=datetime.utcnow(),
|
| 60 |
expires_at=datetime.utcnow() + timedelta(seconds=max(1, ttl_seconds)),
|
| 61 |
-
payload_json=payload,
|
| 62 |
-
source_modules_json=source_modules or {},
|
|
|
|
| 63 |
is_stale=False,
|
| 64 |
computation_duration_ms=computation_duration_ms,
|
| 65 |
-
warnings_json=warnings or [],
|
| 66 |
)
|
| 67 |
if row.computation_duration_ms is None:
|
| 68 |
row.computation_duration_ms = round((time.perf_counter() - started) * 1000, 3)
|
|
@@ -82,3 +85,22 @@ def record_snapshot_cache(snapshot_type: str, *, hit: bool, status: str) -> None
|
|
| 82 |
)
|
| 83 |
except Exception:
|
| 84 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
from datetime import date, datetime, timedelta
|
| 4 |
import time
|
| 5 |
|
| 6 |
from sqlalchemy import desc, select
|
|
|
|
| 38 |
"is_stale": is_stale,
|
| 39 |
"payload": row.payload_json or {},
|
| 40 |
"source_modules": row.source_modules_json or {},
|
| 41 |
+
"missing_sections": getattr(row, "missing_sections_json", None) or [],
|
| 42 |
"computation_duration_ms": row.computation_duration_ms,
|
| 43 |
"warnings": row.warnings_json or [],
|
| 44 |
}
|
|
|
|
| 52 |
source_modules: dict | None = None,
|
| 53 |
ttl_seconds: int = 300,
|
| 54 |
warnings: list[str] | None = None,
|
| 55 |
+
missing_sections: list[str] | None = None,
|
| 56 |
computation_duration_ms: float | None = None,
|
| 57 |
) -> dict:
|
| 58 |
started = time.perf_counter()
|
|
|
|
| 60 |
snapshot_type=snapshot_type,
|
| 61 |
created_at=datetime.utcnow(),
|
| 62 |
expires_at=datetime.utcnow() + timedelta(seconds=max(1, ttl_seconds)),
|
| 63 |
+
payload_json=json_safe(payload),
|
| 64 |
+
source_modules_json=json_safe(source_modules or {}),
|
| 65 |
+
missing_sections_json=json_safe(missing_sections or []),
|
| 66 |
is_stale=False,
|
| 67 |
computation_duration_ms=computation_duration_ms,
|
| 68 |
+
warnings_json=json_safe(warnings or []),
|
| 69 |
)
|
| 70 |
if row.computation_duration_ms is None:
|
| 71 |
row.computation_duration_ms = round((time.perf_counter() - started) * 1000, 3)
|
|
|
|
| 85 |
)
|
| 86 |
except Exception:
|
| 87 |
pass
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def json_safe(value):
|
| 91 |
+
if isinstance(value, (datetime, date)):
|
| 92 |
+
return value.isoformat()
|
| 93 |
+
if isinstance(value, dict):
|
| 94 |
+
return {str(key): json_safe(item) for key, item in value.items()}
|
| 95 |
+
if isinstance(value, list):
|
| 96 |
+
return [json_safe(item) for item in value]
|
| 97 |
+
if isinstance(value, tuple):
|
| 98 |
+
return [json_safe(item) for item in value]
|
| 99 |
+
try:
|
| 100 |
+
import numpy as np
|
| 101 |
+
|
| 102 |
+
if isinstance(value, np.generic):
|
| 103 |
+
return json_safe(value.item())
|
| 104 |
+
except Exception:
|
| 105 |
+
pass
|
| 106 |
+
return value
|
backend/app/services/financial_chat.py
CHANGED
|
@@ -33,6 +33,8 @@ 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.capital_allocation import AdaptiveCapitalAllocationEngine
|
| 37 |
from app.services.decision_intelligence import DecisionIntelligenceDashboardService
|
| 38 |
from app.services.market_data import market_snapshot_for_asset
|
|
@@ -487,6 +489,8 @@ def trading_game_context_for_chat(db: Session) -> dict:
|
|
| 487 |
"learning_intelligence": LearningIntelligenceDashboardService().dashboard(db),
|
| 488 |
"decision_intelligence": DecisionIntelligenceDashboardService().dashboard(db),
|
| 489 |
"capital_allocation": AdaptiveCapitalAllocationEngine().dashboard(db),
|
|
|
|
|
|
|
| 490 |
"pnl_breakdown": PnLBreakdownService().game_breakdown(db),
|
| 491 |
"reality_check": TradingGameRealityCheckService().evaluate(db),
|
| 492 |
"failures": engine.failures(db, limit=12),
|
|
@@ -1477,6 +1481,8 @@ def build_trading_game_response(language: str, context: dict) -> dict:
|
|
| 1477 |
learning_intelligence = context.get("learning_intelligence") or {}
|
| 1478 |
decision_intelligence = context.get("decision_intelligence") or {}
|
| 1479 |
capital_allocation = context.get("capital_allocation") or {}
|
|
|
|
|
|
|
| 1480 |
if not game:
|
| 1481 |
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."
|
| 1482 |
return build_error_response(language, message, [])
|
|
@@ -1505,6 +1511,8 @@ def build_trading_game_response(language: str, context: dict) -> dict:
|
|
| 1505 |
{"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
|
| 1506 |
{"key": "decision_intelligence", "title": "Decision Intelligence", "bullets": decision_intelligence_lines(decision_intelligence, language)},
|
| 1507 |
{"key": "capital_allocation", "title": "Capital Allocation Intelligence", "bullets": capital_allocation_lines(capital_allocation, language)},
|
|
|
|
|
|
|
| 1508 |
{"key": "trade_ledger", "title": "Trade ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
|
| 1509 |
{"key": "intelligence_metrics", "title": "Trading intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
|
| 1510 |
{"key": "live_forward", "title": "Storico vs live paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
|
|
@@ -1545,6 +1553,8 @@ def build_trading_game_response(language: str, context: dict) -> dict:
|
|
| 1545 |
{"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
|
| 1546 |
{"key": "decision_intelligence", "title": "Decision Intelligence", "bullets": decision_intelligence_lines(decision_intelligence, language)},
|
| 1547 |
{"key": "capital_allocation", "title": "Capital Allocation Intelligence", "bullets": capital_allocation_lines(capital_allocation, language)},
|
|
|
|
|
|
|
| 1548 |
{"key": "trade_ledger", "title": "Trade Ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
|
| 1549 |
{"key": "intelligence_metrics", "title": "Trading Intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
|
| 1550 |
{"key": "live_forward", "title": "Historical vs Live Paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
|
|
@@ -1574,7 +1584,7 @@ def build_trading_game_response(language: str, context: dict) -> dict:
|
|
| 1574 |
"executive_view": summary,
|
| 1575 |
"risk_reward_view": f"Expectancy {format_number(game.get('expectancy_r'))}R, drawdown {format_signed(game.get('max_drawdown'))}%.",
|
| 1576 |
"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)},
|
| 1577 |
-
"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, "capital_allocation": capital_allocation},
|
| 1578 |
"answer_to_user": summary,
|
| 1579 |
}
|
| 1580 |
|
|
@@ -1691,6 +1701,100 @@ def capital_allocation_lines(payload: dict, language: str) -> list[str]:
|
|
| 1691 |
return dedupe_warnings(lines)
|
| 1692 |
|
| 1693 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1694 |
def trade_ledger_lines(rows: list[dict], language: str) -> list[str]:
|
| 1695 |
if not rows:
|
| 1696 |
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."]
|
|
@@ -2484,7 +2588,7 @@ def infer_intent(message: str, mode: str | None = None) -> str:
|
|
| 2484 |
return "fundamental_analysis"
|
| 2485 |
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"]):
|
| 2486 |
return "reasoning_memory_question"
|
| 2487 |
-
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", "capital allocation", "cash allocation", "cash reserve", "quanto capitale", "quanto allocare", "peso capitale", "sizing logic", "logica di sizing"]):
|
| 2488 |
return "trading_game"
|
| 2489 |
if any(term in normalized for term in ["sniper", "entrabile", "meglio aspettare", "ingresso", "entry", "risk/reward", "uscita", "exit", "target", "invalidazione", "invalidation", "profitto", "take profit"]):
|
| 2490 |
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.alpha_recovery import AlphaRecoveryDashboardService
|
| 37 |
+
from app.services.meta_cognition import MetaCognitionEngine
|
| 38 |
from app.services.capital_allocation import AdaptiveCapitalAllocationEngine
|
| 39 |
from app.services.decision_intelligence import DecisionIntelligenceDashboardService
|
| 40 |
from app.services.market_data import market_snapshot_for_asset
|
|
|
|
| 489 |
"learning_intelligence": LearningIntelligenceDashboardService().dashboard(db),
|
| 490 |
"decision_intelligence": DecisionIntelligenceDashboardService().dashboard(db),
|
| 491 |
"capital_allocation": AdaptiveCapitalAllocationEngine().dashboard(db),
|
| 492 |
+
"alpha_recovery": AlphaRecoveryDashboardService().dashboard(db),
|
| 493 |
+
"meta_cognition": MetaCognitionEngine().summary(db),
|
| 494 |
"pnl_breakdown": PnLBreakdownService().game_breakdown(db),
|
| 495 |
"reality_check": TradingGameRealityCheckService().evaluate(db),
|
| 496 |
"failures": engine.failures(db, limit=12),
|
|
|
|
| 1481 |
learning_intelligence = context.get("learning_intelligence") or {}
|
| 1482 |
decision_intelligence = context.get("decision_intelligence") or {}
|
| 1483 |
capital_allocation = context.get("capital_allocation") or {}
|
| 1484 |
+
alpha_recovery = context.get("alpha_recovery") or {}
|
| 1485 |
+
meta_cognition = context.get("meta_cognition") or {}
|
| 1486 |
if not game:
|
| 1487 |
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."
|
| 1488 |
return build_error_response(language, message, [])
|
|
|
|
| 1511 |
{"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
|
| 1512 |
{"key": "decision_intelligence", "title": "Decision Intelligence", "bullets": decision_intelligence_lines(decision_intelligence, language)},
|
| 1513 |
{"key": "capital_allocation", "title": "Capital Allocation Intelligence", "bullets": capital_allocation_lines(capital_allocation, language)},
|
| 1514 |
+
{"key": "alpha_recovery", "title": "Alpha Recovery", "bullets": alpha_recovery_lines(alpha_recovery, language)},
|
| 1515 |
+
{"key": "meta_cognition", "title": "Meta-Cognition", "bullets": meta_cognition_lines(meta_cognition, language)},
|
| 1516 |
{"key": "trade_ledger", "title": "Trade ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
|
| 1517 |
{"key": "intelligence_metrics", "title": "Trading intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
|
| 1518 |
{"key": "live_forward", "title": "Storico vs live paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
|
|
|
|
| 1553 |
{"key": "learning_intelligence", "title": "Learning Intelligence", "bullets": learning_intelligence_lines(learning_intelligence, language)},
|
| 1554 |
{"key": "decision_intelligence", "title": "Decision Intelligence", "bullets": decision_intelligence_lines(decision_intelligence, language)},
|
| 1555 |
{"key": "capital_allocation", "title": "Capital Allocation Intelligence", "bullets": capital_allocation_lines(capital_allocation, language)},
|
| 1556 |
+
{"key": "alpha_recovery", "title": "Alpha Recovery", "bullets": alpha_recovery_lines(alpha_recovery, language)},
|
| 1557 |
+
{"key": "meta_cognition", "title": "Meta-Cognition", "bullets": meta_cognition_lines(meta_cognition, language)},
|
| 1558 |
{"key": "trade_ledger", "title": "Trade Ledger", "bullets": trade_ledger_lines(ledger_rows, language)},
|
| 1559 |
{"key": "intelligence_metrics", "title": "Trading Intelligence", "bullets": intelligence_metric_lines(intelligence_metrics, rolling_metrics, metrics_by_setup, language)},
|
| 1560 |
{"key": "live_forward", "title": "Historical vs Live Paper", "bullets": historical_live_lines(live_forward, historical_vs_live, language)},
|
|
|
|
| 1584 |
"executive_view": summary,
|
| 1585 |
"risk_reward_view": f"Expectancy {format_number(game.get('expectancy_r'))}R, drawdown {format_signed(game.get('max_drawdown'))}%.",
|
| 1586 |
"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)},
|
| 1587 |
+
"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, "capital_allocation": capital_allocation, "alpha_recovery": alpha_recovery, "meta_cognition": meta_cognition},
|
| 1588 |
"answer_to_user": summary,
|
| 1589 |
}
|
| 1590 |
|
|
|
|
| 1701 |
return dedupe_warnings(lines)
|
| 1702 |
|
| 1703 |
|
| 1704 |
+
def alpha_recovery_lines(payload: dict, language: str) -> list[str]:
|
| 1705 |
+
if not payload or payload.get("status") == "unavailable":
|
| 1706 |
+
return ["Alpha Recovery non disponibile: non invento perche BLUM perde o vince alpha." if language == "it" else "Alpha Recovery is unavailable; I will not invent why BLUM won or lost alpha."]
|
| 1707 |
+
truth = (payload.get("truth_layer") or {}).get("lines") or []
|
| 1708 |
+
attribution = payload.get("latest_attribution") or {}
|
| 1709 |
+
attribution_summary = attribution.get("summary") or {}
|
| 1710 |
+
top_category = attribution_summary.get("top_category")
|
| 1711 |
+
missed = ((payload.get("missed_winners") or {}).get("rows") or [])[:3]
|
| 1712 |
+
actions = ((payload.get("recovery_actions") or {}).get("rows") or [])[:3]
|
| 1713 |
+
methodology_rows = ((payload.get("latest_methodology") or {}).get("rows") or [])[:3]
|
| 1714 |
+
invalid = [row for row in methodology_rows if not row.get("methodology_valid")]
|
| 1715 |
+
if language == "it":
|
| 1716 |
+
lines = list(truth[:3]) or ["Evidenza insufficiente: servono benchmark validati e trade ledger prima di attribuire perdita alpha."]
|
| 1717 |
+
if invalid:
|
| 1718 |
+
lines.append("Alcuni benchmark non sono validi per apprendimento: BLUM blocca conclusioni di recovery su quei confronti.")
|
| 1719 |
+
if top_category:
|
| 1720 |
+
lines.append(f"Causa misurata principale: {str(top_category).replace('_', ' ')}.")
|
| 1721 |
+
if missed:
|
| 1722 |
+
lines.append("Missed winners da replay: " + ", ".join(f"{row.get('ticker')} ({format_signed(row.get('benchmark_relative_return'))}%)" for row in missed))
|
| 1723 |
+
if actions:
|
| 1724 |
+
lines.append(f"Azione recovery proposta: {actions[0].get('recommended_action')} | stato {actions[0].get('status')}.")
|
| 1725 |
+
return dedupe_warnings(lines)
|
| 1726 |
+
lines = list(truth[:3]) or ["Insufficient evidence: validated benchmarks and trade ledger data are required before attributing alpha loss."]
|
| 1727 |
+
if invalid:
|
| 1728 |
+
lines.append("Some benchmarks are not valid for learning; BLUM blocks recovery conclusions on those comparisons.")
|
| 1729 |
+
if top_category:
|
| 1730 |
+
lines.append(f"Main measured cause: {str(top_category).replace('_', ' ')}.")
|
| 1731 |
+
if missed:
|
| 1732 |
+
lines.append("Missed winners for replay: " + ", ".join(f"{row.get('ticker')} ({format_signed(row.get('benchmark_relative_return'))}%)" for row in missed))
|
| 1733 |
+
if actions:
|
| 1734 |
+
lines.append(f"Recovery action proposed: {actions[0].get('recommended_action')} | status {actions[0].get('status')}.")
|
| 1735 |
+
return dedupe_warnings(lines)
|
| 1736 |
+
|
| 1737 |
+
|
| 1738 |
+
def meta_cognition_lines(payload: dict, language: str) -> list[str]:
|
| 1739 |
+
if not payload or payload.get("status") == "unavailable":
|
| 1740 |
+
return ["Meta-Cognition non disponibile: non invento quali fattori creano o distruggono alpha." if language == "it" else "Meta-Cognition is unavailable; I will not invent which factors create or destroy alpha."]
|
| 1741 |
+
snapshot_payload = ((payload.get("snapshot") or {}).get("payload") or {}) if isinstance(payload.get("snapshot"), dict) else {}
|
| 1742 |
+
factor_summary = ((payload.get("factor_importance") or {}).get("summary") or {})
|
| 1743 |
+
focus_rows = ((payload.get("learning_focus") or {}).get("rows") or [])
|
| 1744 |
+
noise_rows = ((payload.get("noise") or {}).get("rows") or [])
|
| 1745 |
+
preservation_summary = ((payload.get("capital_preservation") or {}).get("summary") or {})
|
| 1746 |
+
events_summary = ((payload.get("events") or {}).get("summary") or {})
|
| 1747 |
+
conclusion = payload.get("conclusion") or snapshot_payload.get("meta_cognition_conclusion") or {}
|
| 1748 |
+
|
| 1749 |
+
top_alpha = snapshot_payload.get("top_alpha_factor") or first_payload(factor_summary.get("top_alpha_creators"))
|
| 1750 |
+
top_destroyer = snapshot_payload.get("top_alpha_destroyer") or first_payload(factor_summary.get("top_alpha_destroyers"))
|
| 1751 |
+
noisiest = snapshot_payload.get("noisiest_factor") or first_payload(factor_summary.get("noisiest_factors")) or first_payload(noise_rows)
|
| 1752 |
+
focus = snapshot_payload.get("next_learning_focus") or first_payload(focus_rows)
|
| 1753 |
+
preservation = snapshot_payload.get("strongest_capital_preservation_rule") or first_payload(preservation_summary.get("top_preservers"))
|
| 1754 |
+
|
| 1755 |
+
if language == "it":
|
| 1756 |
+
lines = []
|
| 1757 |
+
if top_alpha:
|
| 1758 |
+
lines.append(f"Fattore che sta creando piu alpha misurabile: {top_alpha.get('factor_name', 'n/a')} | contributo {format_number(top_alpha.get('alpha_contribution'))} | campione {top_alpha.get('sample_size', 'n/a')}.")
|
| 1759 |
+
if top_destroyer:
|
| 1760 |
+
lines.append(f"Fattore che sta distruggendo piu alpha: {top_destroyer.get('factor_name', 'n/a')} | perdita {format_number(top_destroyer.get('alpha_loss_contribution'))} | azione {top_destroyer.get('recommended_weight_action', 'n/a')}.")
|
| 1761 |
+
if noisiest:
|
| 1762 |
+
lines.append(f"Fattore/modulo piu rumoroso: {noisiest.get('factor_name') or noisiest.get('module_name', 'n/a')} | noise {format_number(noisiest.get('noise_score'))}/100 | severita {noisiest.get('severity', 'n/a')}.")
|
| 1763 |
+
if preservation:
|
| 1764 |
+
lines.append(f"Regola no-trade piu utile: {preservation.get('ticker', preservation.get('factor_name', 'n/a'))} | capitale preservato {format_money(preservation.get('capital_preserved'))} | quality {format_number(preservation.get('quality_score'))}/100.")
|
| 1765 |
+
if focus:
|
| 1766 |
+
lines.append(f"Prossimo focus Learning Loop: {focus.get('priority_type', 'focus')} su {focus.get('target', 'n/a')} | valore atteso {format_number(focus.get('expected_learning_value'))}/100.")
|
| 1767 |
+
if conclusion.get("summary"):
|
| 1768 |
+
lines.append(str(conclusion.get("summary")))
|
| 1769 |
+
if events_summary.get("events"):
|
| 1770 |
+
lines.append(f"Azioni di learning valutate: {events_summary.get('events')} | miglioramenti {events_summary.get('improvements', 0)} | peggioramenti {events_summary.get('degradations', 0)}.")
|
| 1771 |
+
return dedupe_warnings(lines or ["Evidenza meta-cognitiva insufficiente: serve ledger, benchmark validi e piu campioni prima di attribuire alpha ai fattori."])
|
| 1772 |
+
|
| 1773 |
+
lines = []
|
| 1774 |
+
if top_alpha:
|
| 1775 |
+
lines.append(f"Top measured alpha-creating factor: {top_alpha.get('factor_name', 'n/a')} | contribution {format_number(top_alpha.get('alpha_contribution'))} | sample {top_alpha.get('sample_size', 'n/a')}.")
|
| 1776 |
+
if top_destroyer:
|
| 1777 |
+
lines.append(f"Top measured alpha-destroying factor: {top_destroyer.get('factor_name', 'n/a')} | loss {format_number(top_destroyer.get('alpha_loss_contribution'))} | action {top_destroyer.get('recommended_weight_action', 'n/a')}.")
|
| 1778 |
+
if noisiest:
|
| 1779 |
+
lines.append(f"Noisiest factor/module: {noisiest.get('factor_name') or noisiest.get('module_name', 'n/a')} | noise {format_number(noisiest.get('noise_score'))}/100 | severity {noisiest.get('severity', 'n/a')}.")
|
| 1780 |
+
if preservation:
|
| 1781 |
+
lines.append(f"Strongest no-trade preservation rule: {preservation.get('ticker', preservation.get('factor_name', 'n/a'))} | preserved {format_money(preservation.get('capital_preserved'))} | quality {format_number(preservation.get('quality_score'))}/100.")
|
| 1782 |
+
if focus:
|
| 1783 |
+
lines.append(f"Next Learning Loop focus: {focus.get('priority_type', 'focus')} on {focus.get('target', 'n/a')} | expected value {format_number(focus.get('expected_learning_value'))}/100.")
|
| 1784 |
+
if conclusion.get("summary"):
|
| 1785 |
+
lines.append(str(conclusion.get("summary")))
|
| 1786 |
+
if events_summary.get("events"):
|
| 1787 |
+
lines.append(f"Learning actions evaluated: {events_summary.get('events')} | improvements {events_summary.get('improvements', 0)} | degradations {events_summary.get('degradations', 0)}.")
|
| 1788 |
+
return dedupe_warnings(lines or ["Insufficient meta-cognition evidence: BLUM needs a ledger, valid benchmarks and more samples before attributing alpha to factors."])
|
| 1789 |
+
|
| 1790 |
+
|
| 1791 |
+
def first_payload(value: object) -> dict:
|
| 1792 |
+
if isinstance(value, list) and value:
|
| 1793 |
+
first = value[0]
|
| 1794 |
+
return first if isinstance(first, dict) else {}
|
| 1795 |
+
return value if isinstance(value, dict) else {}
|
| 1796 |
+
|
| 1797 |
+
|
| 1798 |
def trade_ledger_lines(rows: list[dict], language: str) -> list[str]:
|
| 1799 |
if not rows:
|
| 1800 |
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."]
|
|
|
|
| 2588 |
return "fundamental_analysis"
|
| 2589 |
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"]):
|
| 2590 |
return "reasoning_memory_question"
|
| 2591 |
+
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", "capital allocation", "cash allocation", "cash reserve", "quanto capitale", "quanto allocare", "peso capitale", "sizing logic", "logica di sizing", "alpha loss", "alpha recovery", "perche perdiamo", "perché perdiamo", "perdiamo contro", "why are we losing", "why losing", "qqq", "spy", "missed winners", "winners mancati", "opportunita mancate", "opportunità mancate", "recovery action", "azioni recovery", "perdita alpha", "alpha perso", "lose alpha", "meta cognition", "meta-cognition", "meta cognizione", "meta-cognizione", "fattore alpha", "factor alpha", "quale fattore", "which factor", "fattore rumoroso", "noisy factor", "modulo rumoroso", "cosa deve imparare", "what should blum learn", "imparare adesso", "learn next", "trust technical", "fidarsi dei tecnici", "capital preservation", "preservazione capitale", "no-trade rule", "regola no trade"]):
|
| 2592 |
return "trading_game"
|
| 2593 |
if any(term in normalized for term in ["sniper", "entrabile", "meglio aspettare", "ingresso", "entry", "risk/reward", "uscita", "exit", "target", "invalidazione", "invalidation", "profitto", "take profit"]):
|
| 2594 |
return "market_sniper"
|
backend/app/services/learning_loop.py
CHANGED
|
@@ -17,11 +17,15 @@ from app.core.config import get_settings
|
|
| 17 |
from app.models import (
|
| 18 |
Asset,
|
| 19 |
FundamentalSnapshot,
|
|
|
|
| 20 |
HistoricalPrediction,
|
|
|
|
|
|
|
| 21 |
LearningEvent,
|
| 22 |
LearningMetric,
|
| 23 |
LearningRun,
|
| 24 |
MacroSnapshot,
|
|
|
|
| 25 |
MistakeAnalysis,
|
| 26 |
ModelVersion,
|
| 27 |
NewsArticle,
|
|
@@ -30,6 +34,7 @@ from app.models import (
|
|
| 30 |
PriceHistory,
|
| 31 |
SignalPerformance,
|
| 32 |
StrategyMemory,
|
|
|
|
| 33 |
)
|
| 34 |
from app.services.technical_analysis_engine import TechnicalAnalysisEngine
|
| 35 |
|
|
@@ -54,6 +59,10 @@ BASE_SIGNAL_WEIGHTS = {
|
|
| 54 |
"regime": 0.05,
|
| 55 |
}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
ERROR_TYPES = {
|
| 58 |
"technical_false_breakout",
|
| 59 |
"overbought_signal_ignored",
|
|
@@ -86,10 +95,17 @@ class LearningLoopService:
|
|
| 86 |
self.memory = StrategyMemoryService()
|
| 87 |
self.model_scores = ModelScoreService()
|
| 88 |
|
| 89 |
-
def run_batch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
requested_batch = int(batch_size or settings.learning_batch_size)
|
| 91 |
-
|
| 92 |
-
daily_guard = self.daily_guard(db,
|
|
|
|
| 93 |
run_id = f"learn-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:8]}"
|
| 94 |
run = LearningRun(
|
| 95 |
run_id=run_id,
|
|
@@ -104,26 +120,26 @@ class LearningLoopService:
|
|
| 104 |
db.flush()
|
| 105 |
|
| 106 |
if not daily_guard["allowed"]:
|
| 107 |
-
run.status = "
|
| 108 |
run.completed_at = datetime.utcnow()
|
| 109 |
-
run.summary = {"reason": daily_guard["reason"], "requested_batch_size": requested_batch}
|
| 110 |
event = LearningEvent(
|
| 111 |
-
event_type="
|
| 112 |
severity="Warning",
|
| 113 |
-
title="BLUM Learning Loop
|
| 114 |
description=daily_guard["reason"],
|
| 115 |
payload={"run_id": run_id, "guard": daily_guard},
|
| 116 |
)
|
| 117 |
db.add(event)
|
| 118 |
db.commit()
|
| 119 |
-
return {"status": "
|
| 120 |
|
| 121 |
reports: list[dict] = []
|
| 122 |
errors: list[dict] = []
|
| 123 |
seen_samples: set[tuple[str, str]] = set()
|
| 124 |
for _ in range(batch):
|
| 125 |
try:
|
| 126 |
-
sample = self.sampler.
|
| 127 |
if not sample:
|
| 128 |
errors.append({"stage": "sample", "error": "No eligible historical sample found."})
|
| 129 |
continue
|
|
@@ -135,9 +151,16 @@ class LearningLoopService:
|
|
| 135 |
sniper_learning = {"status": "skipped", "reason": "No reports created."}
|
| 136 |
if reports:
|
| 137 |
try:
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
except Exception as exc:
|
| 142 |
sniper_learning = {"status": "degraded", "error": f"{type(exc).__name__}: {exc}"}
|
| 143 |
|
|
@@ -156,6 +179,7 @@ class LearningLoopService:
|
|
| 156 |
"latest_reports": reports[-5:],
|
| 157 |
"dashboard_metrics": metrics,
|
| 158 |
"model_version": model_version,
|
|
|
|
| 159 |
"market_sniper_learning": sniper_learning,
|
| 160 |
}
|
| 161 |
run.anti_overfitting_report = anti_overfitting
|
|
@@ -174,10 +198,13 @@ class LearningLoopService:
|
|
| 174 |
"status": run.status,
|
| 175 |
"run_id": run_id,
|
| 176 |
"batch_size": batch,
|
|
|
|
|
|
|
| 177 |
"reports_created": len(reports),
|
| 178 |
"errors": errors[:8],
|
| 179 |
"metrics": metrics,
|
| 180 |
"model_version": model_version,
|
|
|
|
| 181 |
"market_sniper_learning": sniper_learning,
|
| 182 |
"anti_overfitting": anti_overfitting,
|
| 183 |
"disclaimer": "Research learning loop only. It improves calibration and robustness; it does not create guaranteed market predictions.",
|
|
@@ -187,7 +214,12 @@ class LearningLoopService:
|
|
| 187 |
context = self.point_in_time.context_for(db, sample["asset"], sample["analysis_date"])
|
| 188 |
prediction_context = dict(context)
|
| 189 |
prediction_context.pop("future_prices", None)
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
prediction = HistoricalPrediction(
|
| 192 |
learning_run_id=run.id,
|
| 193 |
asset_id=sample["asset"].id,
|
|
@@ -203,7 +235,12 @@ class LearningLoopService:
|
|
| 203 |
point_in_time_context=json_safe(context),
|
| 204 |
expected_direction=prediction_payload["prediction"]["dominant_direction"],
|
| 205 |
confidence=prediction_payload["prediction"]["aggregate_confidence"],
|
| 206 |
-
model_version="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
data_quality_score=context["data_quality_score"],
|
| 208 |
)
|
| 209 |
db.add(prediction)
|
|
@@ -264,6 +301,7 @@ class LearningLoopService:
|
|
| 264 |
memory_updates.extend(self.memory.update_from_outcome(db, context, frame_prediction, outcome_payload, analysis))
|
| 265 |
|
| 266 |
self.memory.update_signal_performance(db, context, prediction_payload, outcomes)
|
|
|
|
| 267 |
report = {
|
| 268 |
"asset": prediction.ticker,
|
| 269 |
"analysis_date": prediction.analysis_date.isoformat(),
|
|
@@ -280,20 +318,35 @@ class LearningLoopService:
|
|
| 280 |
"mistakes": mistakes,
|
| 281 |
"lessons_learned": [item["lesson"] for item in memory_updates],
|
| 282 |
"memory_updates": memory_updates,
|
|
|
|
| 283 |
}
|
| 284 |
return json_safe(report)
|
| 285 |
|
| 286 |
def daily_guard(self, db: Session, requested_batch: int) -> dict:
|
| 287 |
start = datetime.combine(datetime.utcnow().date(), time.min)
|
| 288 |
today_predictions = int(db.scalar(select(func.coalesce(func.sum(LearningRun.predictions_created), 0)).where(LearningRun.started_at >= start)) or 0)
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
return {
|
| 292 |
"allowed": allowed,
|
| 293 |
"today_predictions": today_predictions,
|
| 294 |
"requested_batch": requested_batch,
|
| 295 |
-
"
|
| 296 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
}
|
| 298 |
|
| 299 |
|
|
@@ -301,6 +354,92 @@ class HistoricalSamplerService:
|
|
| 301 |
def __init__(self) -> None:
|
| 302 |
self.rng = random.Random(self.seed())
|
| 303 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
def random_sample(self, db: Session, seen_samples: set[tuple[str, str]] | None = None) -> dict | None:
|
| 305 |
universe = {item.strip().lower() for item in settings.learning_asset_universe.split(",") if item.strip()}
|
| 306 |
asset_types = []
|
|
@@ -313,32 +452,37 @@ class HistoricalSamplerService:
|
|
| 313 |
|
| 314 |
candidates = db.scalars(select(Asset).where(Asset.is_active.is_(True), Asset.asset_type.in_(asset_types))).all()
|
| 315 |
self.rng.shuffle(candidates)
|
| 316 |
-
min_rows = max(252, settings.learning_min_history_years * 252)
|
| 317 |
for asset in candidates:
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
continue
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
if earliest >= latest:
|
| 327 |
continue
|
| 328 |
-
|
| 329 |
-
for _ in range(8):
|
| 330 |
-
analysis_date = nearest_trading_date(
|
| 331 |
-
db,
|
| 332 |
-
asset,
|
| 333 |
-
earliest + timedelta(days=self.rng.randint(0, max(1, span))),
|
| 334 |
-
latest,
|
| 335 |
-
)
|
| 336 |
-
if not analysis_date:
|
| 337 |
-
continue
|
| 338 |
-
key = (asset.ticker, analysis_date.isoformat())
|
| 339 |
-
if seen_samples and key in seen_samples:
|
| 340 |
-
continue
|
| 341 |
-
return {"asset": asset, "analysis_date": analysis_date, "first_price_date": first_date, "last_price_date": last_date, "sample_rows": count}
|
| 342 |
return None
|
| 343 |
|
| 344 |
def seed(self) -> int:
|
|
@@ -480,12 +624,18 @@ class PointInTimeDataService:
|
|
| 480 |
|
| 481 |
|
| 482 |
class PredictionEngine:
|
| 483 |
-
def predict(self, context: dict) -> dict:
|
| 484 |
technical = context["technical"]
|
| 485 |
signal_scores = self.signal_scores(context)
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
| 487 |
dominant_direction = direction_from_score(aggregate_score, technical)
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
| 489 |
timeframes = {
|
| 490 |
timeframe: self.timeframe_prediction(timeframe, config, context, signal_scores, aggregate_score, dominant_direction, confidence)
|
| 491 |
for timeframe, config in TIMEFRAMES.items()
|
|
@@ -502,9 +652,68 @@ class PredictionEngine:
|
|
| 502 |
"reasoning": self.reasoning(context, signal_scores, dominant_direction),
|
| 503 |
},
|
| 504 |
"timeframes": timeframes,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
"anti_leakage": context["point_in_time_policy"],
|
| 506 |
}
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
def signal_scores(self, context: dict) -> dict:
|
| 509 |
technical = context["technical"]
|
| 510 |
indicators = technical.get("technical_indicators") or {}
|
|
@@ -775,19 +984,43 @@ class StrategyMemoryService:
|
|
| 775 |
class ModelScoreService:
|
| 776 |
def recalculate(self, db: Session) -> dict:
|
| 777 |
rows = db.scalars(select(SignalPerformance).order_by(desc(SignalPerformance.updated_at)).limit(600)).all()
|
| 778 |
-
if not rows:
|
| 779 |
-
return {"status": "insufficient_sample", "version": None}
|
| 780 |
previous = active_model_version(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
previous_weights = previous.weights if previous else BASE_SIGNAL_WEIGHTS
|
| 782 |
new_weights = dict(BASE_SIGNAL_WEIGHTS)
|
| 783 |
for signal_name in new_weights:
|
| 784 |
-
matching = [row for row in rows if row.signal_name == signal_name and row.sample_count >=
|
| 785 |
if not matching:
|
| 786 |
continue
|
| 787 |
avg_reliability = mean(row.reliability_score for row in matching)
|
| 788 |
new_weights[signal_name] = max(0.03, new_weights[signal_name] + (avg_reliability - 50) / 1000)
|
| 789 |
new_weights = normalize_weights(new_weights)
|
| 790 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
version = f"learning-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
|
| 792 |
row = ModelVersion(
|
| 793 |
version=version,
|
|
@@ -844,6 +1077,181 @@ class ModelScoreService:
|
|
| 844 |
}
|
| 845 |
|
| 846 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
class LearningDashboardService:
|
| 848 |
def dashboard(self, db: Session) -> dict:
|
| 849 |
latest_run = db.scalar(select(LearningRun).order_by(desc(LearningRun.started_at)).limit(1))
|
|
@@ -866,6 +1274,7 @@ class LearningDashboardService:
|
|
| 866 |
"strategy_memory": self.strategy_memory(db),
|
| 867 |
"mistakes": self.mistake_summary(db),
|
| 868 |
"model_versions": [serialize_model_version(row) for row in db.scalars(select(ModelVersion).order_by(desc(ModelVersion.created_at)).limit(8)).all()],
|
|
|
|
| 869 |
"trading_game": trading_game,
|
| 870 |
"policy": "BLUM Learning Loop optimizes calibration and robustness, not artificial 100% winrate.",
|
| 871 |
}
|
|
@@ -1302,6 +1711,227 @@ def active_model_version(db: Session) -> ModelVersion | None:
|
|
| 1302 |
return db.scalar(select(ModelVersion).where(ModelVersion.is_active.is_(True)).order_by(desc(ModelVersion.created_at)).limit(1))
|
| 1303 |
|
| 1304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
def normalize_weights(weights: dict[str, float]) -> dict[str, float]:
|
| 1306 |
total = sum(max(0.0, float(value)) for value in weights.values()) or 1.0
|
| 1307 |
return {key: round(max(0.0, float(value)) / total, 4) for key, value in weights.items()}
|
|
@@ -1372,6 +2002,11 @@ def serialize_prediction(row: HistoricalPrediction) -> dict:
|
|
| 1372 |
"market_regime": row.market_regime,
|
| 1373 |
"volatility_regime": row.volatility_regime,
|
| 1374 |
"data_quality_score": row.data_quality_score,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1375 |
"prediction": row.prediction_payload.get("prediction", {}) if row.prediction_payload else {},
|
| 1376 |
"timeframes": row.prediction_payload.get("timeframes", {}) if row.prediction_payload else {},
|
| 1377 |
"created_at": iso(row.created_at),
|
|
@@ -1423,6 +2058,24 @@ def serialize_model_version(row: ModelVersion) -> dict:
|
|
| 1423 |
}
|
| 1424 |
|
| 1425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1426 |
def fundamental_reason(fundamentals: dict) -> str:
|
| 1427 |
if fundamentals.get("status") == "ready":
|
| 1428 |
return f"Point-in-time verified fundamentals available with quality score {fundamentals.get('quality_score')}."
|
|
@@ -1509,6 +2162,10 @@ def clamp(value: float, low: float = 0.0, high: float = 100.0) -> float:
|
|
| 1509 |
return max(low, min(high, float(value)))
|
| 1510 |
|
| 1511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1512 |
def round_float(value) -> float | None:
|
| 1513 |
return round(safe_float(value), 4) if value is not None else None
|
| 1514 |
|
|
|
|
| 17 |
from app.models import (
|
| 18 |
Asset,
|
| 19 |
FundamentalSnapshot,
|
| 20 |
+
FeedbackLoopAudit,
|
| 21 |
HistoricalPrediction,
|
| 22 |
+
CapitalPreservationAlpha,
|
| 23 |
+
LearningFocusPriority,
|
| 24 |
LearningEvent,
|
| 25 |
LearningMetric,
|
| 26 |
LearningRun,
|
| 27 |
MacroSnapshot,
|
| 28 |
+
MissedWinner,
|
| 29 |
MistakeAnalysis,
|
| 30 |
ModelVersion,
|
| 31 |
NewsArticle,
|
|
|
|
| 34 |
PriceHistory,
|
| 35 |
SignalPerformance,
|
| 36 |
StrategyMemory,
|
| 37 |
+
TradingGameTrade,
|
| 38 |
)
|
| 39 |
from app.services.technical_analysis_engine import TechnicalAnalysisEngine
|
| 40 |
|
|
|
|
| 59 |
"regime": 0.05,
|
| 60 |
}
|
| 61 |
|
| 62 |
+
MIN_MODEL_VERSION_OUTCOMES = 30
|
| 63 |
+
MIN_MODEL_VERSION_SIGNAL_ROWS = 3
|
| 64 |
+
MIN_MODEL_VERSION_SIGNAL_SAMPLE = 3
|
| 65 |
+
|
| 66 |
ERROR_TYPES = {
|
| 67 |
"technical_false_breakout",
|
| 68 |
"overbought_signal_ignored",
|
|
|
|
| 95 |
self.memory = StrategyMemoryService()
|
| 96 |
self.model_scores = ModelScoreService()
|
| 97 |
|
| 98 |
+
def run_batch(
|
| 99 |
+
self,
|
| 100 |
+
db: Session,
|
| 101 |
+
batch_size: int | None = None,
|
| 102 |
+
trigger: str = "manual",
|
| 103 |
+
sniper_simulation_limit: int | None = None,
|
| 104 |
+
) -> dict:
|
| 105 |
requested_batch = int(batch_size or settings.learning_batch_size)
|
| 106 |
+
configured_batch = max(1, min(requested_batch, settings.learning_batch_size, 500))
|
| 107 |
+
daily_guard = self.daily_guard(db, configured_batch)
|
| 108 |
+
batch = max(1, int(daily_guard.get("effective_batch", configured_batch) or configured_batch))
|
| 109 |
run_id = f"learn-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:8]}"
|
| 110 |
run = LearningRun(
|
| 111 |
run_id=run_id,
|
|
|
|
| 120 |
db.flush()
|
| 121 |
|
| 122 |
if not daily_guard["allowed"]:
|
| 123 |
+
run.status = "budget_wait"
|
| 124 |
run.completed_at = datetime.utcnow()
|
| 125 |
+
run.summary = {"reason": daily_guard["reason"], "requested_batch_size": requested_batch, "daily_guard": daily_guard}
|
| 126 |
event = LearningEvent(
|
| 127 |
+
event_type="blum_learning_loop_budget_wait",
|
| 128 |
severity="Warning",
|
| 129 |
+
title="BLUM Learning Loop waiting for daily budget",
|
| 130 |
description=daily_guard["reason"],
|
| 131 |
payload={"run_id": run_id, "guard": daily_guard},
|
| 132 |
)
|
| 133 |
db.add(event)
|
| 134 |
db.commit()
|
| 135 |
+
return {"status": "budget_wait", "run_id": run_id, "guard": daily_guard}
|
| 136 |
|
| 137 |
reports: list[dict] = []
|
| 138 |
errors: list[dict] = []
|
| 139 |
seen_samples: set[tuple[str, str]] = set()
|
| 140 |
for _ in range(batch):
|
| 141 |
try:
|
| 142 |
+
sample = self.sampler.blended_sample(db, seen_samples=seen_samples, trigger=trigger)
|
| 143 |
if not sample:
|
| 144 |
errors.append({"stage": "sample", "error": "No eligible historical sample found."})
|
| 145 |
continue
|
|
|
|
| 151 |
sniper_learning = {"status": "skipped", "reason": "No reports created."}
|
| 152 |
if reports:
|
| 153 |
try:
|
| 154 |
+
limit = sniper_simulation_limit if sniper_simulation_limit is not None else min(300, max(60, len(reports) * len(TIMEFRAMES) * 6))
|
| 155 |
+
if limit > 0:
|
| 156 |
+
from app.services.market_sniper import MarketSniperEngine
|
| 157 |
+
|
| 158 |
+
sniper_learning = MarketSniperEngine().simulate(db, limit=limit)
|
| 159 |
+
else:
|
| 160 |
+
sniper_learning = {
|
| 161 |
+
"status": "deferred",
|
| 162 |
+
"reason": "Sniper R-multiple simulation is deferred for this bounded learning lane.",
|
| 163 |
+
}
|
| 164 |
except Exception as exc:
|
| 165 |
sniper_learning = {"status": "degraded", "error": f"{type(exc).__name__}: {exc}"}
|
| 166 |
|
|
|
|
| 179 |
"latest_reports": reports[-5:],
|
| 180 |
"dashboard_metrics": metrics,
|
| 181 |
"model_version": model_version,
|
| 182 |
+
"learning_mode": "alpha_loss_replay" if trigger == "alpha_loss_replay" else "random_point_in_time",
|
| 183 |
"market_sniper_learning": sniper_learning,
|
| 184 |
}
|
| 185 |
run.anti_overfitting_report = anti_overfitting
|
|
|
|
| 198 |
"status": run.status,
|
| 199 |
"run_id": run_id,
|
| 200 |
"batch_size": batch,
|
| 201 |
+
"requested_batch_size": requested_batch,
|
| 202 |
+
"daily_guard": daily_guard,
|
| 203 |
"reports_created": len(reports),
|
| 204 |
"errors": errors[:8],
|
| 205 |
"metrics": metrics,
|
| 206 |
"model_version": model_version,
|
| 207 |
+
"learning_mode": "alpha_loss_replay" if trigger == "alpha_loss_replay" else "random_point_in_time",
|
| 208 |
"market_sniper_learning": sniper_learning,
|
| 209 |
"anti_overfitting": anti_overfitting,
|
| 210 |
"disclaimer": "Research learning loop only. It improves calibration and robustness; it does not create guaranteed market predictions.",
|
|
|
|
| 214 |
context = self.point_in_time.context_for(db, sample["asset"], sample["analysis_date"])
|
| 215 |
prediction_context = dict(context)
|
| 216 |
prediction_context.pop("future_prices", None)
|
| 217 |
+
sample_metadata = dict(sample)
|
| 218 |
+
sample_metadata["run_trigger"] = run.trigger
|
| 219 |
+
sample_metadata["evaluation_mode"] = run.evaluation_mode
|
| 220 |
+
sample_metadata["learning_mode_metadata"] = learning_mode_metadata(run.trigger, sample_metadata)
|
| 221 |
+
prediction_payload = self.predictor.predict(prediction_context, db=db, sample_metadata=sample_metadata)
|
| 222 |
+
feedback_payload = prediction_payload.get("feedback_loop", {})
|
| 223 |
prediction = HistoricalPrediction(
|
| 224 |
learning_run_id=run.id,
|
| 225 |
asset_id=sample["asset"].id,
|
|
|
|
| 235 |
point_in_time_context=json_safe(context),
|
| 236 |
expected_direction=prediction_payload["prediction"]["dominant_direction"],
|
| 237 |
confidence=prediction_payload["prediction"]["aggregate_confidence"],
|
| 238 |
+
model_version=feedback_payload.get("model_version_used") or "base-static",
|
| 239 |
+
model_version_used=feedback_payload.get("model_version_used") or "base-static",
|
| 240 |
+
weights_used=json_safe(feedback_payload.get("weights_used") or {}),
|
| 241 |
+
learning_memory_used=json_safe(feedback_payload.get("learning_memory_used") or {}),
|
| 242 |
+
strategy_memory_used=json_safe(feedback_payload.get("strategy_memory_used") or {}),
|
| 243 |
+
research_priority_used=json_safe(feedback_payload.get("research_priority_used") or {}),
|
| 244 |
data_quality_score=context["data_quality_score"],
|
| 245 |
)
|
| 246 |
db.add(prediction)
|
|
|
|
| 301 |
memory_updates.extend(self.memory.update_from_outcome(db, context, frame_prediction, outcome_payload, analysis))
|
| 302 |
|
| 303 |
self.memory.update_signal_performance(db, context, prediction_payload, outcomes)
|
| 304 |
+
feedback_audit = FeedbackLoopAuditService().record(db, prediction, prediction_payload, outcomes, memory_updates)
|
| 305 |
report = {
|
| 306 |
"asset": prediction.ticker,
|
| 307 |
"analysis_date": prediction.analysis_date.isoformat(),
|
|
|
|
| 318 |
"mistakes": mistakes,
|
| 319 |
"lessons_learned": [item["lesson"] for item in memory_updates],
|
| 320 |
"memory_updates": memory_updates,
|
| 321 |
+
"feedback_loop_audit": feedback_audit,
|
| 322 |
}
|
| 323 |
return json_safe(report)
|
| 324 |
|
| 325 |
def daily_guard(self, db: Session, requested_batch: int) -> dict:
|
| 326 |
start = datetime.combine(datetime.utcnow().date(), time.min)
|
| 327 |
today_predictions = int(db.scalar(select(func.coalesce(func.sum(LearningRun.predictions_created), 0)).where(LearningRun.started_at >= start)) or 0)
|
| 328 |
+
max_daily = max(0, int(settings.learning_max_daily_runs))
|
| 329 |
+
remaining = max(0, max_daily - today_predictions)
|
| 330 |
+
effective_batch = min(max(1, requested_batch), remaining) if remaining else 0
|
| 331 |
+
projected = today_predictions + effective_batch
|
| 332 |
+
allowed = effective_batch > 0
|
| 333 |
+
partial = allowed and effective_batch < requested_batch
|
| 334 |
return {
|
| 335 |
"allowed": allowed,
|
| 336 |
"today_predictions": today_predictions,
|
| 337 |
"requested_batch": requested_batch,
|
| 338 |
+
"effective_batch": effective_batch,
|
| 339 |
+
"remaining_daily_budget": remaining,
|
| 340 |
+
"projected_predictions": projected,
|
| 341 |
+
"max_daily_runs": max_daily,
|
| 342 |
+
"partial_batch": partial,
|
| 343 |
+
"reason": (
|
| 344 |
+
"within daily limit"
|
| 345 |
+
if allowed and not partial
|
| 346 |
+
else "partial batch: using remaining daily learning budget to keep training without overfitting"
|
| 347 |
+
if partial
|
| 348 |
+
else "daily learning budget exhausted; waiting for next UTC window instead of oversampling the same day"
|
| 349 |
+
),
|
| 350 |
}
|
| 351 |
|
| 352 |
|
|
|
|
| 354 |
def __init__(self) -> None:
|
| 355 |
self.rng = random.Random(self.seed())
|
| 356 |
|
| 357 |
+
def blended_sample(self, db: Session, seen_samples: set[tuple[str, str]] | None = None, trigger: str = "manual") -> dict | None:
|
| 358 |
+
if trigger == "alpha_loss_replay":
|
| 359 |
+
return self.alpha_loss_sample(db, seen_samples=seen_samples) or self.random_sample(db, seen_samples=seen_samples)
|
| 360 |
+
roll = self.rng.random()
|
| 361 |
+
random_ratio = clamp_ratio(settings.learning_random_sample_ratio)
|
| 362 |
+
alpha_ratio = clamp_ratio(settings.learning_alpha_loss_sample_ratio)
|
| 363 |
+
factor_ratio = clamp_ratio(settings.learning_factor_focus_sample_ratio)
|
| 364 |
+
preservation_ratio = clamp_ratio(settings.learning_capital_preservation_sample_ratio)
|
| 365 |
+
total = max(0.01, random_ratio + alpha_ratio + factor_ratio + preservation_ratio)
|
| 366 |
+
random_cut = random_ratio / total
|
| 367 |
+
alpha_cut = random_cut + alpha_ratio / total
|
| 368 |
+
factor_cut = alpha_cut + factor_ratio / total
|
| 369 |
+
if roll < random_cut:
|
| 370 |
+
return self.random_sample(db, seen_samples=seen_samples)
|
| 371 |
+
if roll < alpha_cut:
|
| 372 |
+
return self.alpha_loss_sample(db, seen_samples=seen_samples) or self.random_sample(db, seen_samples=seen_samples)
|
| 373 |
+
if roll < factor_cut:
|
| 374 |
+
return self.focus_priority_sample(db, seen_samples=seen_samples) or self.random_sample(db, seen_samples=seen_samples)
|
| 375 |
+
return self.capital_preservation_sample(db, seen_samples=seen_samples) or self.random_sample(db, seen_samples=seen_samples)
|
| 376 |
+
|
| 377 |
+
def alpha_loss_sample(self, db: Session, seen_samples: set[tuple[str, str]] | None = None) -> dict | None:
|
| 378 |
+
missed = db.scalars(
|
| 379 |
+
select(MissedWinner)
|
| 380 |
+
.order_by(desc(MissedWinner.benchmark_relative_return), desc(MissedWinner.created_at))
|
| 381 |
+
.limit(120)
|
| 382 |
+
).all()
|
| 383 |
+
for row in missed:
|
| 384 |
+
asset = db.scalar(select(Asset).where(Asset.ticker == row.ticker, Asset.is_active.is_(True)).limit(1))
|
| 385 |
+
if not asset:
|
| 386 |
+
continue
|
| 387 |
+
sample = self.sample_for_asset(db, asset, preferred_date=as_date(row.decision_date), seen_samples=seen_samples)
|
| 388 |
+
if sample:
|
| 389 |
+
sample["sampling_reason"] = "alpha_loss_replay"
|
| 390 |
+
sample["missed_winner_id"] = row.id
|
| 391 |
+
sample["benchmark_relative_return"] = row.benchmark_relative_return
|
| 392 |
+
return sample
|
| 393 |
+
return None
|
| 394 |
+
|
| 395 |
+
def focus_priority_sample(self, db: Session, seen_samples: set[tuple[str, str]] | None = None) -> dict | None:
|
| 396 |
+
priorities = db.scalars(
|
| 397 |
+
select(LearningFocusPriority)
|
| 398 |
+
.where(LearningFocusPriority.status.in_(["proposed", "active"]))
|
| 399 |
+
.order_by(desc(LearningFocusPriority.expected_learning_value), desc(LearningFocusPriority.created_at))
|
| 400 |
+
.limit(80)
|
| 401 |
+
).all()
|
| 402 |
+
for priority in priorities:
|
| 403 |
+
target = str(priority.target or "").upper()
|
| 404 |
+
asset = db.scalar(select(Asset).where(Asset.ticker == target, Asset.is_active.is_(True)).limit(1))
|
| 405 |
+
if not asset:
|
| 406 |
+
asset = db.scalar(select(Asset).where(Asset.sector.ilike(priority.target), Asset.is_active.is_(True)).limit(1))
|
| 407 |
+
if not asset:
|
| 408 |
+
linked_trade = db.scalar(
|
| 409 |
+
select(TradingGameTrade)
|
| 410 |
+
.where(TradingGameTrade.setup_type == priority.target)
|
| 411 |
+
.order_by(desc(TradingGameTrade.created_at))
|
| 412 |
+
.limit(1)
|
| 413 |
+
)
|
| 414 |
+
asset = db.scalar(select(Asset).where(Asset.ticker == linked_trade.ticker, Asset.is_active.is_(True)).limit(1)) if linked_trade else None
|
| 415 |
+
if not asset:
|
| 416 |
+
continue
|
| 417 |
+
sample = self.sample_for_asset(db, asset, preferred_date=None, seen_samples=seen_samples)
|
| 418 |
+
if sample:
|
| 419 |
+
sample["sampling_reason"] = "learning_focus_priority"
|
| 420 |
+
sample["learning_focus_priority_id"] = priority.id
|
| 421 |
+
sample["priority_type"] = priority.priority_type
|
| 422 |
+
return sample
|
| 423 |
+
return None
|
| 424 |
+
|
| 425 |
+
def capital_preservation_sample(self, db: Session, seen_samples: set[tuple[str, str]] | None = None) -> dict | None:
|
| 426 |
+
rows = db.scalars(
|
| 427 |
+
select(CapitalPreservationAlpha)
|
| 428 |
+
.where(CapitalPreservationAlpha.missed_gain > CapitalPreservationAlpha.avoided_loss)
|
| 429 |
+
.order_by(desc(CapitalPreservationAlpha.missed_gain), desc(CapitalPreservationAlpha.created_at))
|
| 430 |
+
.limit(80)
|
| 431 |
+
).all()
|
| 432 |
+
for row in rows:
|
| 433 |
+
asset = db.scalar(select(Asset).where(Asset.ticker == row.ticker, Asset.is_active.is_(True)).limit(1))
|
| 434 |
+
if not asset:
|
| 435 |
+
continue
|
| 436 |
+
sample = self.sample_for_asset(db, asset, preferred_date=as_date(row.decision_date), seen_samples=seen_samples)
|
| 437 |
+
if sample:
|
| 438 |
+
sample["sampling_reason"] = "capital_preservation_replay"
|
| 439 |
+
sample["capital_preservation_alpha_id"] = row.id
|
| 440 |
+
return sample
|
| 441 |
+
return None
|
| 442 |
+
|
| 443 |
def random_sample(self, db: Session, seen_samples: set[tuple[str, str]] | None = None) -> dict | None:
|
| 444 |
universe = {item.strip().lower() for item in settings.learning_asset_universe.split(",") if item.strip()}
|
| 445 |
asset_types = []
|
|
|
|
| 452 |
|
| 453 |
candidates = db.scalars(select(Asset).where(Asset.is_active.is_(True), Asset.asset_type.in_(asset_types))).all()
|
| 454 |
self.rng.shuffle(candidates)
|
|
|
|
| 455 |
for asset in candidates:
|
| 456 |
+
sample = self.sample_for_asset(db, asset, preferred_date=None, seen_samples=seen_samples)
|
| 457 |
+
if sample:
|
| 458 |
+
return sample
|
| 459 |
+
return None
|
| 460 |
+
|
| 461 |
+
def sample_for_asset(self, db: Session, asset: Asset, preferred_date: date | None, seen_samples: set[tuple[str, str]] | None = None) -> dict | None:
|
| 462 |
+
min_rows = max(252, settings.learning_min_history_years * 252)
|
| 463 |
+
stats = db.execute(
|
| 464 |
+
select(func.count(PriceHistory.id), func.min(PriceHistory.date), func.max(PriceHistory.date)).where(PriceHistory.asset_id == asset.id)
|
| 465 |
+
).one()
|
| 466 |
+
count, first_date, last_date = int(stats[0] or 0), as_date(stats[1]), as_date(stats[2])
|
| 467 |
+
if count < min_rows or not first_date or not last_date:
|
| 468 |
+
return None
|
| 469 |
+
earliest = first_date + timedelta(days=max(365, settings.learning_min_history_years * 365))
|
| 470 |
+
latest = last_date - timedelta(days=TIMEFRAMES["long"]["horizon_days"] * 2 + 30)
|
| 471 |
+
if earliest >= latest:
|
| 472 |
+
return None
|
| 473 |
+
preferred_candidates: list[date] = []
|
| 474 |
+
if preferred_date and earliest <= preferred_date <= latest:
|
| 475 |
+
preferred_candidates.extend([preferred_date, preferred_date - timedelta(days=3), preferred_date + timedelta(days=3)])
|
| 476 |
+
span = (latest - earliest).days
|
| 477 |
+
preferred_candidates.extend(earliest + timedelta(days=self.rng.randint(0, max(1, span))) for _ in range(8))
|
| 478 |
+
for candidate_date in preferred_candidates:
|
| 479 |
+
analysis_date = nearest_trading_date(db, asset, max(earliest, min(candidate_date, latest)), latest)
|
| 480 |
+
if not analysis_date:
|
| 481 |
continue
|
| 482 |
+
key = (asset.ticker, analysis_date.isoformat())
|
| 483 |
+
if seen_samples and key in seen_samples:
|
|
|
|
| 484 |
continue
|
| 485 |
+
return {"asset": asset, "analysis_date": analysis_date, "first_price_date": first_date, "last_price_date": last_date, "sample_rows": count}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
return None
|
| 487 |
|
| 488 |
def seed(self) -> int:
|
|
|
|
| 624 |
|
| 625 |
|
| 626 |
class PredictionEngine:
|
| 627 |
+
def predict(self, context: dict, db: Session | None = None, sample_metadata: dict | None = None) -> dict:
|
| 628 |
technical = context["technical"]
|
| 629 |
signal_scores = self.signal_scores(context)
|
| 630 |
+
sample_metadata = sample_metadata or {}
|
| 631 |
+
feedback = self.feedback_context(db, context, signal_scores, sample_metadata)
|
| 632 |
+
weights_used = feedback["weights_used"]
|
| 633 |
+
aggregate_score = weighted_score(signal_scores, weights_used)
|
| 634 |
dominant_direction = direction_from_score(aggregate_score, technical)
|
| 635 |
+
base_confidence = confidence_from_evidence(aggregate_score, context["data_quality_score"], context["market_context"], technical)
|
| 636 |
+
confidence = round(clamp(base_confidence + feedback["confidence_adjustment"], 15, 88), 1)
|
| 637 |
+
feedback["base_confidence"] = base_confidence
|
| 638 |
+
feedback["final_confidence"] = confidence
|
| 639 |
timeframes = {
|
| 640 |
timeframe: self.timeframe_prediction(timeframe, config, context, signal_scores, aggregate_score, dominant_direction, confidence)
|
| 641 |
for timeframe, config in TIMEFRAMES.items()
|
|
|
|
| 652 |
"reasoning": self.reasoning(context, signal_scores, dominant_direction),
|
| 653 |
},
|
| 654 |
"timeframes": timeframes,
|
| 655 |
+
"feedback_loop": feedback,
|
| 656 |
+
"model_version_used": feedback["model_version_used"],
|
| 657 |
+
"weights_used": weights_used,
|
| 658 |
+
"learning_memory_used": feedback["learning_memory_used"],
|
| 659 |
+
"strategy_memory_used": feedback["strategy_memory_used"],
|
| 660 |
+
"research_priority_used": feedback["research_priority_used"],
|
| 661 |
+
"learning_mode_metadata": feedback["learning_mode_metadata"],
|
| 662 |
"anti_leakage": context["point_in_time_policy"],
|
| 663 |
}
|
| 664 |
|
| 665 |
+
def feedback_context(self, db: Session | None, context: dict, signal_scores: dict, sample_metadata: dict) -> dict:
|
| 666 |
+
model_version_used, weights_used, weight_source = active_weight_context(db)
|
| 667 |
+
signal_memory = signal_performance_context(db, context, signal_scores)
|
| 668 |
+
strategy_memory = strategy_memory_context(db, context)
|
| 669 |
+
research_priority = research_priority_context(db, sample_metadata)
|
| 670 |
+
mode_metadata = sample_metadata.get("learning_mode_metadata") or learning_mode_metadata(sample_metadata.get("run_trigger"), sample_metadata)
|
| 671 |
+
confidence_adjustment = round(
|
| 672 |
+
clamp(
|
| 673 |
+
signal_memory["confidence_delta"] + strategy_memory["confidence_delta"] + research_priority.get("confidence_delta", 0.0),
|
| 674 |
+
-14.0,
|
| 675 |
+
14.0,
|
| 676 |
+
),
|
| 677 |
+
2,
|
| 678 |
+
)
|
| 679 |
+
return {
|
| 680 |
+
"model_version_used": model_version_used,
|
| 681 |
+
"weight_source": weight_source,
|
| 682 |
+
"weights_used": weights_used,
|
| 683 |
+
"learning_memory_used": {
|
| 684 |
+
"signal_performance": signal_memory["rows"],
|
| 685 |
+
"confidence_delta": signal_memory["confidence_delta"],
|
| 686 |
+
"policy": "SignalPerformance reliability changes confidence only when enough outcome evidence exists.",
|
| 687 |
+
},
|
| 688 |
+
"strategy_memory_used": {
|
| 689 |
+
"rows": strategy_memory["rows"],
|
| 690 |
+
"confidence_delta": strategy_memory["confidence_delta"],
|
| 691 |
+
"policy": "StrategyMemory lessons modify confidence when their stored conditions match the current point-in-time setup.",
|
| 692 |
+
},
|
| 693 |
+
"research_priority_used": research_priority,
|
| 694 |
+
"learning_mode_metadata": mode_metadata,
|
| 695 |
+
"confidence_adjustment": confidence_adjustment,
|
| 696 |
+
"policy": "PredictionEngine uses active learned weights when available; otherwise BASE_SIGNAL_WEIGHTS. Learning memory changes confidence, not source code.",
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
def baseline_prediction(self, context: dict) -> dict:
|
| 700 |
+
signal_scores = self.signal_scores(context)
|
| 701 |
+
weights = normalize_weights(BASE_SIGNAL_WEIGHTS)
|
| 702 |
+
aggregate_score = weighted_score(signal_scores, weights)
|
| 703 |
+
direction = direction_from_score(aggregate_score, context["technical"])
|
| 704 |
+
confidence = confidence_from_evidence(aggregate_score, context["data_quality_score"], context["market_context"], context["technical"])
|
| 705 |
+
return {
|
| 706 |
+
"model_version_used": "base-static",
|
| 707 |
+
"weights_used": weights,
|
| 708 |
+
"aggregate_score": round(aggregate_score, 2),
|
| 709 |
+
"aggregate_confidence": confidence,
|
| 710 |
+
"dominant_direction": direction,
|
| 711 |
+
"actionability": feedback_actionability(direction, confidence, aggregate_score),
|
| 712 |
+
"confidence_adjustment": 0.0,
|
| 713 |
+
"memory_adjustment_used": False,
|
| 714 |
+
"policy": "Counterfactual baseline uses BASE_SIGNAL_WEIGHTS and ignores learned memory/confidence adjustments.",
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
def signal_scores(self, context: dict) -> dict:
|
| 718 |
technical = context["technical"]
|
| 719 |
indicators = technical.get("technical_indicators") or {}
|
|
|
|
| 984 |
class ModelScoreService:
|
| 985 |
def recalculate(self, db: Session) -> dict:
|
| 986 |
rows = db.scalars(select(SignalPerformance).order_by(desc(SignalPerformance.updated_at)).limit(600)).all()
|
|
|
|
|
|
|
| 987 |
previous = active_model_version(db)
|
| 988 |
+
anti = self.anti_overfitting_report(db)
|
| 989 |
+
eligible_rows = [row for row in rows if int(row.sample_count or 0) >= MIN_MODEL_VERSION_SIGNAL_SAMPLE]
|
| 990 |
+
if (
|
| 991 |
+
int(anti.get("sample_count", 0) or 0) < MIN_MODEL_VERSION_OUTCOMES
|
| 992 |
+
or len(eligible_rows) < MIN_MODEL_VERSION_SIGNAL_ROWS
|
| 993 |
+
):
|
| 994 |
+
return {
|
| 995 |
+
"status": "insufficient_evidence",
|
| 996 |
+
"version": previous.version if previous else None,
|
| 997 |
+
"active_version": previous.version if previous else None,
|
| 998 |
+
"thresholds": {
|
| 999 |
+
"min_outcomes": MIN_MODEL_VERSION_OUTCOMES,
|
| 1000 |
+
"min_signal_rows": MIN_MODEL_VERSION_SIGNAL_ROWS,
|
| 1001 |
+
"min_signal_sample": MIN_MODEL_VERSION_SIGNAL_SAMPLE,
|
| 1002 |
+
},
|
| 1003 |
+
"evidence": {"outcomes": anti.get("sample_count", 0), "eligible_signal_rows": len(eligible_rows)},
|
| 1004 |
+
"anti_overfitting": anti,
|
| 1005 |
+
"policy": "No ModelVersion is created until enough outcome and signal reliability evidence exists.",
|
| 1006 |
+
}
|
| 1007 |
previous_weights = previous.weights if previous else BASE_SIGNAL_WEIGHTS
|
| 1008 |
new_weights = dict(BASE_SIGNAL_WEIGHTS)
|
| 1009 |
for signal_name in new_weights:
|
| 1010 |
+
matching = [row for row in rows if row.signal_name == signal_name and row.sample_count >= MIN_MODEL_VERSION_SIGNAL_SAMPLE]
|
| 1011 |
if not matching:
|
| 1012 |
continue
|
| 1013 |
avg_reliability = mean(row.reliability_score for row in matching)
|
| 1014 |
new_weights[signal_name] = max(0.03, new_weights[signal_name] + (avg_reliability - 50) / 1000)
|
| 1015 |
new_weights = normalize_weights(new_weights)
|
| 1016 |
+
if previous and max(abs(safe_float(new_weights.get(key)) - safe_float((previous.weights or {}).get(key))) for key in BASE_SIGNAL_WEIGHTS) < 0.001:
|
| 1017 |
+
return {
|
| 1018 |
+
"status": "stable",
|
| 1019 |
+
"version": previous.version,
|
| 1020 |
+
"weights": previous.weights,
|
| 1021 |
+
"anti_overfitting": anti,
|
| 1022 |
+
"policy": "No new ModelVersion created because learned weights did not materially change.",
|
| 1023 |
+
}
|
| 1024 |
version = f"learning-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
|
| 1025 |
row = ModelVersion(
|
| 1026 |
version=version,
|
|
|
|
| 1077 |
}
|
| 1078 |
|
| 1079 |
|
| 1080 |
+
class FeedbackLoopAuditService:
|
| 1081 |
+
def record(
|
| 1082 |
+
self,
|
| 1083 |
+
db: Session,
|
| 1084 |
+
prediction: HistoricalPrediction,
|
| 1085 |
+
prediction_payload: dict,
|
| 1086 |
+
outcomes: list[dict],
|
| 1087 |
+
memory_updates: list[dict],
|
| 1088 |
+
) -> dict:
|
| 1089 |
+
feedback = prediction_payload.get("feedback_loop", {})
|
| 1090 |
+
usable = [row for row in outcomes if row.get("outcome_label") in {"correct", "wrong", "neutral"}]
|
| 1091 |
+
correct = sum(1 for row in usable if row.get("outcome_label") == "correct")
|
| 1092 |
+
wrong = sum(1 for row in usable if row.get("outcome_label") == "wrong")
|
| 1093 |
+
returns = [safe_float(row.get("realized_return")) for row in usable if row.get("realized_return") is not None]
|
| 1094 |
+
evidence_grade = "medium" if len(usable) >= 3 else "low" if usable else "insufficient"
|
| 1095 |
+
learned = {
|
| 1096 |
+
"memory_updates": memory_updates[:12],
|
| 1097 |
+
"signal_performance_used": (feedback.get("learning_memory_used") or {}).get("signal_performance", []),
|
| 1098 |
+
"strategy_memory_used": (feedback.get("strategy_memory_used") or {}).get("rows", []),
|
| 1099 |
+
}
|
| 1100 |
+
counterfactual = self.counterfactual_audit(prediction, prediction_payload, outcomes)
|
| 1101 |
+
comparison = counterfactual.get("outcome_comparison", {})
|
| 1102 |
+
changed_decision = bool(
|
| 1103 |
+
(counterfactual.get("differences") or {}).get("direction_changed")
|
| 1104 |
+
or (counterfactual.get("differences") or {}).get("actionability_changed")
|
| 1105 |
+
or safe_float((counterfactual.get("differences") or {}).get("score_delta")) != 0.0
|
| 1106 |
+
or safe_float((counterfactual.get("differences") or {}).get("confidence_delta")) != 0.0
|
| 1107 |
+
)
|
| 1108 |
+
improved = bool(comparison.get("improvement_detected"))
|
| 1109 |
+
changes = {
|
| 1110 |
+
"model_version_used": feedback.get("model_version_used"),
|
| 1111 |
+
"weights_used": feedback.get("weights_used"),
|
| 1112 |
+
"confidence_adjustment": feedback.get("confidence_adjustment"),
|
| 1113 |
+
"base_confidence": feedback.get("base_confidence"),
|
| 1114 |
+
"final_confidence": feedback.get("final_confidence"),
|
| 1115 |
+
"research_priority_used": feedback.get("research_priority_used"),
|
| 1116 |
+
"learning_mode_metadata": feedback.get("learning_mode_metadata"),
|
| 1117 |
+
"counterfactual_audit": counterfactual,
|
| 1118 |
+
}
|
| 1119 |
+
decision = {
|
| 1120 |
+
"prediction_id": prediction.id,
|
| 1121 |
+
"ticker": prediction.ticker,
|
| 1122 |
+
"analysis_date": iso(prediction.analysis_date),
|
| 1123 |
+
"direction": prediction.expected_direction,
|
| 1124 |
+
"confidence": prediction.confidence,
|
| 1125 |
+
"aggregate_score": (prediction_payload.get("prediction") or {}).get("aggregate_score"),
|
| 1126 |
+
"actionability": counterfactual.get("learned_prediction", {}).get("actionability"),
|
| 1127 |
+
}
|
| 1128 |
+
outcome_payload = {
|
| 1129 |
+
"correct": correct,
|
| 1130 |
+
"wrong": wrong,
|
| 1131 |
+
"neutral": sum(1 for row in usable if row.get("outcome_label") == "neutral"),
|
| 1132 |
+
"average_realized_return": round(mean(returns), 4) if returns else None,
|
| 1133 |
+
"outcomes": {row.get("timeframe"): row.get("outcome_label") for row in outcomes},
|
| 1134 |
+
"baseline_direction_correct": comparison.get("baseline_direction_correct"),
|
| 1135 |
+
"learned_direction_correct": comparison.get("learned_direction_correct"),
|
| 1136 |
+
"baseline_actionability": comparison.get("baseline_actionability"),
|
| 1137 |
+
"learned_actionability": comparison.get("learned_actionability"),
|
| 1138 |
+
"baseline_would_trade": comparison.get("baseline_would_trade"),
|
| 1139 |
+
"learned_would_trade": comparison.get("learned_would_trade"),
|
| 1140 |
+
"avoided_loss": comparison.get("avoided_loss"),
|
| 1141 |
+
"missed_gain": comparison.get("missed_gain"),
|
| 1142 |
+
"improvement_reason": comparison.get("improvement_reason"),
|
| 1143 |
+
}
|
| 1144 |
+
summary = (
|
| 1145 |
+
f"{prediction.ticker} used {feedback.get('model_version_used', 'base-static')} with "
|
| 1146 |
+
f"confidence adjustment {safe_float(feedback.get('confidence_adjustment')):.2f}; "
|
| 1147 |
+
f"changed_decision={changed_decision}, improved={improved}. "
|
| 1148 |
+
f"reason={comparison.get('improvement_reason', 'counterfactual_not_available')}."
|
| 1149 |
+
)
|
| 1150 |
+
row = FeedbackLoopAudit(
|
| 1151 |
+
prediction_id=prediction.id,
|
| 1152 |
+
ticker=prediction.ticker,
|
| 1153 |
+
model_version_used=feedback.get("model_version_used") or "base-static",
|
| 1154 |
+
learned_knowledge_json=json_safe(learned),
|
| 1155 |
+
changes_applied_json=json_safe(changes),
|
| 1156 |
+
future_decision_json=json_safe(decision),
|
| 1157 |
+
outcome_json=json_safe(outcome_payload),
|
| 1158 |
+
improvement_detected=improved,
|
| 1159 |
+
evidence_grade=evidence_grade,
|
| 1160 |
+
summary=summary,
|
| 1161 |
+
)
|
| 1162 |
+
db.add(row)
|
| 1163 |
+
return {
|
| 1164 |
+
"status": "recorded",
|
| 1165 |
+
"prediction_id": prediction.id,
|
| 1166 |
+
"model_version_used": row.model_version_used,
|
| 1167 |
+
"what_was_learned": learned,
|
| 1168 |
+
"what_changed": changes,
|
| 1169 |
+
"future_decision_used_change": decision,
|
| 1170 |
+
"counterfactual_audit": counterfactual,
|
| 1171 |
+
"outcome": outcome_payload,
|
| 1172 |
+
"improvement_detected": improved,
|
| 1173 |
+
"evidence_grade": evidence_grade,
|
| 1174 |
+
"summary": summary,
|
| 1175 |
+
}
|
| 1176 |
+
|
| 1177 |
+
def counterfactual_audit(self, prediction: HistoricalPrediction, prediction_payload: dict, outcomes: list[dict]) -> dict:
|
| 1178 |
+
context = dict(prediction.point_in_time_context or {})
|
| 1179 |
+
context.pop("future_prices", None)
|
| 1180 |
+
baseline = PredictionEngine().baseline_prediction(context) if context else {}
|
| 1181 |
+
learned_prediction = prediction_payload.get("prediction") or {}
|
| 1182 |
+
baseline_actionability = baseline.get("actionability")
|
| 1183 |
+
learned_actionability = feedback_actionability(
|
| 1184 |
+
learned_prediction.get("dominant_direction"),
|
| 1185 |
+
learned_prediction.get("aggregate_confidence"),
|
| 1186 |
+
learned_prediction.get("aggregate_score"),
|
| 1187 |
+
)
|
| 1188 |
+
learned = {
|
| 1189 |
+
"model_version_used": (prediction_payload.get("feedback_loop") or {}).get("model_version_used") or "base-static",
|
| 1190 |
+
"weights_used": prediction_payload.get("weights_used") or {},
|
| 1191 |
+
"aggregate_score": learned_prediction.get("aggregate_score"),
|
| 1192 |
+
"aggregate_confidence": learned_prediction.get("aggregate_confidence"),
|
| 1193 |
+
"dominant_direction": learned_prediction.get("dominant_direction"),
|
| 1194 |
+
"actionability": learned_actionability,
|
| 1195 |
+
"confidence_adjustment": (prediction_payload.get("feedback_loop") or {}).get("confidence_adjustment", 0.0),
|
| 1196 |
+
"memory_adjustment_used": bool((prediction_payload.get("feedback_loop") or {}).get("confidence_adjustment")),
|
| 1197 |
+
}
|
| 1198 |
+
baseline_correct = direction_correctness_summary(baseline.get("dominant_direction"), outcomes)
|
| 1199 |
+
learned_correct = direction_correctness_summary(learned.get("dominant_direction"), outcomes)
|
| 1200 |
+
baseline_would_trade = feedback_would_trade(baseline_actionability)
|
| 1201 |
+
learned_would_trade = feedback_would_trade(learned_actionability)
|
| 1202 |
+
returns = [safe_float(row.get("realized_return")) for row in outcomes if row.get("realized_return") is not None]
|
| 1203 |
+
average_return = mean(returns) if returns else 0.0
|
| 1204 |
+
avoided_loss = round(abs(average_return), 4) if baseline_would_trade and not learned_would_trade and average_return < 0 else 0.0
|
| 1205 |
+
missed_gain = round(average_return, 4) if baseline_would_trade and not learned_would_trade and average_return > 0 else 0.0
|
| 1206 |
+
improvement_detected, improvement_reason = counterfactual_improvement_reason(
|
| 1207 |
+
baseline_correct,
|
| 1208 |
+
learned_correct,
|
| 1209 |
+
baseline_would_trade,
|
| 1210 |
+
learned_would_trade,
|
| 1211 |
+
average_return,
|
| 1212 |
+
avoided_loss,
|
| 1213 |
+
missed_gain,
|
| 1214 |
+
)
|
| 1215 |
+
comparison = {
|
| 1216 |
+
"score_delta": round(safe_float(learned.get("aggregate_score")) - safe_float(baseline.get("aggregate_score")), 4),
|
| 1217 |
+
"confidence_delta": round(safe_float(learned.get("aggregate_confidence")) - safe_float(baseline.get("aggregate_confidence")), 4),
|
| 1218 |
+
"direction_changed": baseline.get("dominant_direction") != learned.get("dominant_direction"),
|
| 1219 |
+
"actionability_changed": baseline_actionability != learned_actionability,
|
| 1220 |
+
}
|
| 1221 |
+
return {
|
| 1222 |
+
"baseline_prediction": baseline,
|
| 1223 |
+
"learned_prediction": learned,
|
| 1224 |
+
"differences": comparison,
|
| 1225 |
+
"outcome_comparison": {
|
| 1226 |
+
"outcome_labels": {row.get("timeframe"): row.get("outcome_label") for row in outcomes},
|
| 1227 |
+
"realized_returns": {row.get("timeframe"): row.get("realized_return") for row in outcomes},
|
| 1228 |
+
"learned_direction": learned.get("dominant_direction"),
|
| 1229 |
+
"baseline_direction": baseline.get("dominant_direction"),
|
| 1230 |
+
"baseline_direction_correct": baseline_correct["direction_correct"],
|
| 1231 |
+
"learned_direction_correct": learned_correct["direction_correct"],
|
| 1232 |
+
"baseline_correct_count": baseline_correct["correct_count"],
|
| 1233 |
+
"learned_correct_count": learned_correct["correct_count"],
|
| 1234 |
+
"baseline_actionability": baseline_actionability,
|
| 1235 |
+
"learned_actionability": learned_actionability,
|
| 1236 |
+
"baseline_would_trade": baseline_would_trade,
|
| 1237 |
+
"learned_would_trade": learned_would_trade,
|
| 1238 |
+
"avoided_loss": avoided_loss,
|
| 1239 |
+
"missed_gain": missed_gain,
|
| 1240 |
+
"improvement_detected": improvement_detected,
|
| 1241 |
+
"improvement_reason": improvement_reason,
|
| 1242 |
+
"policy": "Outcome is observed after prediction persistence; baseline is recomputed only from point-in-time context.",
|
| 1243 |
+
},
|
| 1244 |
+
}
|
| 1245 |
+
|
| 1246 |
+
def report(self, db: Session, limit: int = 20) -> dict:
|
| 1247 |
+
rows = db.scalars(select(FeedbackLoopAudit).order_by(desc(FeedbackLoopAudit.created_at)).limit(limit)).all()
|
| 1248 |
+
return {
|
| 1249 |
+
"status": "ready" if rows else "insufficient_evidence",
|
| 1250 |
+
"rows": [serialize_feedback_audit(row) for row in rows],
|
| 1251 |
+
"policy": "FeedbackLoopAudit is persisted by background learning runs and never computed by GET page render.",
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
|
| 1255 |
class LearningDashboardService:
|
| 1256 |
def dashboard(self, db: Session) -> dict:
|
| 1257 |
latest_run = db.scalar(select(LearningRun).order_by(desc(LearningRun.started_at)).limit(1))
|
|
|
|
| 1274 |
"strategy_memory": self.strategy_memory(db),
|
| 1275 |
"mistakes": self.mistake_summary(db),
|
| 1276 |
"model_versions": [serialize_model_version(row) for row in db.scalars(select(ModelVersion).order_by(desc(ModelVersion.created_at)).limit(8)).all()],
|
| 1277 |
+
"feedback_loop_audit": FeedbackLoopAuditService().report(db, limit=8),
|
| 1278 |
"trading_game": trading_game,
|
| 1279 |
"policy": "BLUM Learning Loop optimizes calibration and robustness, not artificial 100% winrate.",
|
| 1280 |
}
|
|
|
|
| 1711 |
return db.scalar(select(ModelVersion).where(ModelVersion.is_active.is_(True)).order_by(desc(ModelVersion.created_at)).limit(1))
|
| 1712 |
|
| 1713 |
|
| 1714 |
+
def active_weight_context(db: Session | None) -> tuple[str, dict, str]:
|
| 1715 |
+
if db is not None:
|
| 1716 |
+
row = active_model_version(db)
|
| 1717 |
+
if row and isinstance(row.weights, dict) and row.weights:
|
| 1718 |
+
return row.version, model_weights_with_fallback(row.weights), "active_model_version"
|
| 1719 |
+
return "base-static", normalize_weights(BASE_SIGNAL_WEIGHTS), "base_signal_weights"
|
| 1720 |
+
|
| 1721 |
+
|
| 1722 |
+
def learning_mode_metadata(trigger: str | None, sample_metadata: dict | None = None) -> dict:
|
| 1723 |
+
sample_metadata = sample_metadata or {}
|
| 1724 |
+
sampling_reason = sample_metadata.get("sampling_reason") or "random_point_in_time"
|
| 1725 |
+
mode = "training_replay" if sampling_reason in {"alpha_loss_replay", "learning_focus_priority", "capital_preservation_replay"} or trigger == "alpha_loss_replay" else "walk_forward_validation"
|
| 1726 |
+
if trigger == "paper_forward" or sample_metadata.get("mode") == "paper_forward":
|
| 1727 |
+
mode = "paper_forward"
|
| 1728 |
+
return {
|
| 1729 |
+
"mode": mode,
|
| 1730 |
+
"training_replay": mode == "training_replay",
|
| 1731 |
+
"walk_forward_validation": mode == "walk_forward_validation",
|
| 1732 |
+
"paper_forward": mode == "paper_forward",
|
| 1733 |
+
"trigger": trigger or sample_metadata.get("run_trigger") or "unknown",
|
| 1734 |
+
"sampling_reason": sampling_reason,
|
| 1735 |
+
"evaluation_mode": sample_metadata.get("evaluation_mode") or settings.learning_evaluation_mode,
|
| 1736 |
+
"policy": "Mode metadata is descriptive. It changes audit traceability, not source code or frontend execution.",
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
+
|
| 1740 |
+
def feedback_actionability(direction: str | None, confidence: float | None, score: float | None = None) -> str:
|
| 1741 |
+
confidence_value = safe_float(confidence)
|
| 1742 |
+
score_value = safe_float(score)
|
| 1743 |
+
if direction == "neutral" or confidence_value < 35:
|
| 1744 |
+
return "watch"
|
| 1745 |
+
if confidence_value >= 68 and direction in {"bullish", "bearish"} and abs(score_value - 50) >= 10:
|
| 1746 |
+
return "active_setup"
|
| 1747 |
+
if confidence_value >= 54 and direction in {"bullish", "bearish"}:
|
| 1748 |
+
return "wait_for_trigger"
|
| 1749 |
+
return "watch"
|
| 1750 |
+
|
| 1751 |
+
|
| 1752 |
+
def feedback_would_trade(actionability: str | None) -> bool:
|
| 1753 |
+
return actionability in {"active_setup", "wait_for_trigger", "actionable_if_confirmed"}
|
| 1754 |
+
|
| 1755 |
+
|
| 1756 |
+
def direction_correctness_summary(direction: str | None, outcomes: list[dict]) -> dict:
|
| 1757 |
+
rows = [row for row in outcomes if row.get("realized_return") is not None]
|
| 1758 |
+
if not rows:
|
| 1759 |
+
return {"direction_correct": None, "correct_count": 0, "wrong_count": 0, "sample_count": 0}
|
| 1760 |
+
correct = sum(1 for row in rows if direction_matches_return(direction, safe_float(row.get("realized_return"))))
|
| 1761 |
+
wrong = len(rows) - correct
|
| 1762 |
+
return {
|
| 1763 |
+
"direction_correct": correct > wrong,
|
| 1764 |
+
"correct_count": correct,
|
| 1765 |
+
"wrong_count": wrong,
|
| 1766 |
+
"sample_count": len(rows),
|
| 1767 |
+
}
|
| 1768 |
+
|
| 1769 |
+
|
| 1770 |
+
def direction_matches_return(direction: str | None, realized_return: float) -> bool:
|
| 1771 |
+
if direction == "bullish":
|
| 1772 |
+
return realized_return > 0
|
| 1773 |
+
if direction == "bearish":
|
| 1774 |
+
return realized_return < 0
|
| 1775 |
+
if direction == "neutral":
|
| 1776 |
+
return abs(realized_return) <= 1.0
|
| 1777 |
+
return False
|
| 1778 |
+
|
| 1779 |
+
|
| 1780 |
+
def counterfactual_improvement_reason(
|
| 1781 |
+
baseline_correct: dict,
|
| 1782 |
+
learned_correct: dict,
|
| 1783 |
+
baseline_would_trade: bool,
|
| 1784 |
+
learned_would_trade: bool,
|
| 1785 |
+
average_return: float,
|
| 1786 |
+
avoided_loss: float,
|
| 1787 |
+
missed_gain: float,
|
| 1788 |
+
) -> tuple[bool, str]:
|
| 1789 |
+
baseline_count = int(baseline_correct.get("correct_count") or 0)
|
| 1790 |
+
learned_count = int(learned_correct.get("correct_count") or 0)
|
| 1791 |
+
if learned_count > baseline_count:
|
| 1792 |
+
return True, "learned_direction_more_correct_than_baseline"
|
| 1793 |
+
if learned_count < baseline_count:
|
| 1794 |
+
return False, "learned_direction_less_correct_than_baseline"
|
| 1795 |
+
if avoided_loss > 0:
|
| 1796 |
+
return True, "learned_actionability_avoided_baseline_loss"
|
| 1797 |
+
if missed_gain > 0:
|
| 1798 |
+
return False, "learned_actionability_missed_baseline_gain"
|
| 1799 |
+
if learned_would_trade and not baseline_would_trade and average_return > 0:
|
| 1800 |
+
return True, "learned_actionability_captured_gain_baseline_would_skip"
|
| 1801 |
+
if learned_would_trade and not baseline_would_trade and average_return < 0:
|
| 1802 |
+
return False, "learned_actionability_entered_losing_trade_baseline_would_skip"
|
| 1803 |
+
return False, "no_counterfactual_improvement_detected"
|
| 1804 |
+
|
| 1805 |
+
|
| 1806 |
+
def model_weights_with_fallback(weights: dict) -> dict:
|
| 1807 |
+
merged = dict(BASE_SIGNAL_WEIGHTS)
|
| 1808 |
+
for key in BASE_SIGNAL_WEIGHTS:
|
| 1809 |
+
if key in weights:
|
| 1810 |
+
merged[key] = safe_float(weights.get(key)) if weights.get(key) is not None else BASE_SIGNAL_WEIGHTS[key]
|
| 1811 |
+
return normalize_weights(merged)
|
| 1812 |
+
|
| 1813 |
+
|
| 1814 |
+
def signal_performance_context(db: Session | None, context: dict, signal_scores: dict) -> dict:
|
| 1815 |
+
if db is None or not signal_scores:
|
| 1816 |
+
return {"rows": [], "confidence_delta": 0.0}
|
| 1817 |
+
regime = context.get("market_context", {}).get("market_regime", "Unknown")
|
| 1818 |
+
rows = db.scalars(
|
| 1819 |
+
select(SignalPerformance)
|
| 1820 |
+
.where(SignalPerformance.signal_name.in_(list(signal_scores.keys())), SignalPerformance.market_regime == regime)
|
| 1821 |
+
.order_by(desc(SignalPerformance.sample_count), desc(SignalPerformance.updated_at))
|
| 1822 |
+
.limit(80)
|
| 1823 |
+
).all()
|
| 1824 |
+
grouped: dict[str, list[SignalPerformance]] = defaultdict(list)
|
| 1825 |
+
for row in rows:
|
| 1826 |
+
grouped[row.signal_name].append(row)
|
| 1827 |
+
payloads = []
|
| 1828 |
+
deltas = []
|
| 1829 |
+
for signal_name, signal_rows in grouped.items():
|
| 1830 |
+
sample_count = sum(int(row.sample_count or 0) for row in signal_rows)
|
| 1831 |
+
reliability = mean(float(row.reliability_score or 50.0) for row in signal_rows)
|
| 1832 |
+
false_positives = sum(int(row.false_positive_count or 0) for row in signal_rows)
|
| 1833 |
+
false_positive_rate = false_positives / max(1, sample_count)
|
| 1834 |
+
enough_evidence = sample_count >= MIN_MODEL_VERSION_SIGNAL_SAMPLE
|
| 1835 |
+
delta = clamp((reliability - 50.0) / 8.0 - false_positive_rate * 4.0, -5.0, 5.0) if enough_evidence else 0.0
|
| 1836 |
+
payloads.append(
|
| 1837 |
+
{
|
| 1838 |
+
"signal_name": signal_name,
|
| 1839 |
+
"market_regime": regime,
|
| 1840 |
+
"sample_count": sample_count,
|
| 1841 |
+
"reliability_score": round(reliability, 2),
|
| 1842 |
+
"false_positive_rate": round(false_positive_rate, 4),
|
| 1843 |
+
"signal_score": signal_scores.get(signal_name),
|
| 1844 |
+
"confidence_delta": round(delta, 3),
|
| 1845 |
+
"used": enough_evidence,
|
| 1846 |
+
}
|
| 1847 |
+
)
|
| 1848 |
+
if enough_evidence:
|
| 1849 |
+
deltas.append(delta)
|
| 1850 |
+
total_delta = clamp(sum(deltas) / max(1, len(deltas)) * 1.4, -8.0, 8.0) if deltas else 0.0
|
| 1851 |
+
return {"rows": payloads[:16], "confidence_delta": round(total_delta, 3)}
|
| 1852 |
+
|
| 1853 |
+
|
| 1854 |
+
def strategy_memory_context(db: Session | None, context: dict) -> dict:
|
| 1855 |
+
if db is None:
|
| 1856 |
+
return {"rows": [], "confidence_delta": 0.0}
|
| 1857 |
+
rows = db.scalars(select(StrategyMemory).order_by(desc(StrategyMemory.reliability_score), desc(StrategyMemory.updated_at)).limit(120)).all()
|
| 1858 |
+
applicable = []
|
| 1859 |
+
deltas = []
|
| 1860 |
+
for row in rows:
|
| 1861 |
+
if not strategy_memory_matches(row, context):
|
| 1862 |
+
continue
|
| 1863 |
+
sample_count = int(row.sample_count or 0)
|
| 1864 |
+
enough_evidence = sample_count >= 3
|
| 1865 |
+
positive = int(row.positive_count or 0)
|
| 1866 |
+
negative = int(row.negative_count or 0)
|
| 1867 |
+
reliability = safe_float(row.reliability_score) if row.reliability_score is not None else 50.0
|
| 1868 |
+
delta = clamp((reliability - 50.0) / 10.0, -4.0, 4.0) if enough_evidence else 0.0
|
| 1869 |
+
if negative > positive and enough_evidence:
|
| 1870 |
+
delta = min(delta, -1.5)
|
| 1871 |
+
applicable.append(
|
| 1872 |
+
{
|
| 1873 |
+
"memory_key": row.memory_key,
|
| 1874 |
+
"category": row.category,
|
| 1875 |
+
"lesson": row.lesson,
|
| 1876 |
+
"sample_count": sample_count,
|
| 1877 |
+
"positive_count": positive,
|
| 1878 |
+
"negative_count": negative,
|
| 1879 |
+
"reliability_score": reliability,
|
| 1880 |
+
"confidence_delta": round(delta, 3),
|
| 1881 |
+
"used": enough_evidence,
|
| 1882 |
+
}
|
| 1883 |
+
)
|
| 1884 |
+
if enough_evidence:
|
| 1885 |
+
deltas.append(delta)
|
| 1886 |
+
total_delta = clamp(sum(deltas), -6.0, 6.0) if deltas else 0.0
|
| 1887 |
+
return {"rows": applicable[:12], "confidence_delta": round(total_delta, 3)}
|
| 1888 |
+
|
| 1889 |
+
|
| 1890 |
+
def strategy_memory_matches(row: StrategyMemory, context: dict) -> bool:
|
| 1891 |
+
conditions = row.conditions or {}
|
| 1892 |
+
technical = context.get("technical") or {}
|
| 1893 |
+
indicators = technical.get("technical_indicators") or {}
|
| 1894 |
+
volume = technical.get("volume") or {}
|
| 1895 |
+
fundamentals = context.get("fundamentals") or {}
|
| 1896 |
+
if "rsi_gt" in conditions and safe_float(indicators.get("rsi")) > safe_float(conditions.get("rsi_gt")):
|
| 1897 |
+
return True
|
| 1898 |
+
if "relative_volume_gt" in conditions and safe_float(volume.get("relative_volume")) > safe_float(conditions.get("relative_volume_gt")):
|
| 1899 |
+
return True
|
| 1900 |
+
if conditions.get("fundamentals") == "not_point_in_time_verified" and fundamentals.get("status") != "ready":
|
| 1901 |
+
return True
|
| 1902 |
+
if row.category in {"general_calibration", "volume_confirmation"}:
|
| 1903 |
+
return True
|
| 1904 |
+
return False
|
| 1905 |
+
|
| 1906 |
+
|
| 1907 |
+
def research_priority_context(db: Session | None, sample_metadata: dict) -> dict:
|
| 1908 |
+
priority_id = sample_metadata.get("learning_focus_priority_id")
|
| 1909 |
+
if db is not None and priority_id:
|
| 1910 |
+
row = db.get(LearningFocusPriority, priority_id)
|
| 1911 |
+
if row:
|
| 1912 |
+
return {
|
| 1913 |
+
"status": "used",
|
| 1914 |
+
"id": row.id,
|
| 1915 |
+
"priority_type": row.priority_type,
|
| 1916 |
+
"target": row.target,
|
| 1917 |
+
"reason": row.reason,
|
| 1918 |
+
"expected_learning_value": row.expected_learning_value,
|
| 1919 |
+
"urgency": row.urgency,
|
| 1920 |
+
"sample_gap": row.sample_gap,
|
| 1921 |
+
"sampling_reason": sample_metadata.get("sampling_reason"),
|
| 1922 |
+
"confidence_delta": 0.0,
|
| 1923 |
+
}
|
| 1924 |
+
if sample_metadata.get("sampling_reason"):
|
| 1925 |
+
return {
|
| 1926 |
+
"status": "used",
|
| 1927 |
+
"sampling_reason": sample_metadata.get("sampling_reason"),
|
| 1928 |
+
"priority_type": sample_metadata.get("priority_type"),
|
| 1929 |
+
"missed_winner_id": sample_metadata.get("missed_winner_id"),
|
| 1930 |
+
"confidence_delta": 0.0,
|
| 1931 |
+
}
|
| 1932 |
+
return {"status": "not_applicable", "confidence_delta": 0.0}
|
| 1933 |
+
|
| 1934 |
+
|
| 1935 |
def normalize_weights(weights: dict[str, float]) -> dict[str, float]:
|
| 1936 |
total = sum(max(0.0, float(value)) for value in weights.values()) or 1.0
|
| 1937 |
return {key: round(max(0.0, float(value)) / total, 4) for key, value in weights.items()}
|
|
|
|
| 2002 |
"market_regime": row.market_regime,
|
| 2003 |
"volatility_regime": row.volatility_regime,
|
| 2004 |
"data_quality_score": row.data_quality_score,
|
| 2005 |
+
"model_version_used": row.model_version_used,
|
| 2006 |
+
"weights_used": row.weights_used,
|
| 2007 |
+
"learning_memory_used": row.learning_memory_used,
|
| 2008 |
+
"strategy_memory_used": row.strategy_memory_used,
|
| 2009 |
+
"research_priority_used": row.research_priority_used,
|
| 2010 |
"prediction": row.prediction_payload.get("prediction", {}) if row.prediction_payload else {},
|
| 2011 |
"timeframes": row.prediction_payload.get("timeframes", {}) if row.prediction_payload else {},
|
| 2012 |
"created_at": iso(row.created_at),
|
|
|
|
| 2058 |
}
|
| 2059 |
|
| 2060 |
|
| 2061 |
+
def serialize_feedback_audit(row: FeedbackLoopAudit) -> dict:
|
| 2062 |
+
return {
|
| 2063 |
+
"id": row.id,
|
| 2064 |
+
"prediction_id": row.prediction_id,
|
| 2065 |
+
"ticker": row.ticker,
|
| 2066 |
+
"model_version_used": row.model_version_used,
|
| 2067 |
+
"what_was_learned": row.learned_knowledge_json,
|
| 2068 |
+
"what_changed": row.changes_applied_json,
|
| 2069 |
+
"counterfactual_audit": (row.changes_applied_json or {}).get("counterfactual_audit"),
|
| 2070 |
+
"future_decision_used_change": row.future_decision_json,
|
| 2071 |
+
"outcome": row.outcome_json,
|
| 2072 |
+
"improvement_detected": row.improvement_detected,
|
| 2073 |
+
"evidence_grade": row.evidence_grade,
|
| 2074 |
+
"summary": row.summary,
|
| 2075 |
+
"created_at": iso(row.created_at),
|
| 2076 |
+
}
|
| 2077 |
+
|
| 2078 |
+
|
| 2079 |
def fundamental_reason(fundamentals: dict) -> str:
|
| 2080 |
if fundamentals.get("status") == "ready":
|
| 2081 |
return f"Point-in-time verified fundamentals available with quality score {fundamentals.get('quality_score')}."
|
|
|
|
| 2162 |
return max(low, min(high, float(value)))
|
| 2163 |
|
| 2164 |
|
| 2165 |
+
def clamp_ratio(value: float) -> float:
|
| 2166 |
+
return max(0.0, min(1.0, safe_float(value)))
|
| 2167 |
+
|
| 2168 |
+
|
| 2169 |
def round_float(value) -> float | None:
|
| 2170 |
return round(safe_float(value), 4) if value is not None else None
|
| 2171 |
|
backend/app/services/learning_summary.py
CHANGED
|
@@ -3,16 +3,23 @@ from __future__ import annotations
|
|
| 3 |
from datetime import datetime
|
| 4 |
import time
|
| 5 |
|
| 6 |
-
from sqlalchemy import desc, select
|
| 7 |
from sqlalchemy.orm import Session
|
| 8 |
|
| 9 |
from app.models import (
|
| 10 |
BlumTradingPowerScore,
|
| 11 |
DashboardSnapshot,
|
|
|
|
| 12 |
LearningBenchmarkComparison,
|
|
|
|
|
|
|
| 13 |
LearningRun,
|
|
|
|
|
|
|
| 14 |
TradingCapitalCycle,
|
| 15 |
TradingGame,
|
|
|
|
|
|
|
| 16 |
)
|
| 17 |
from app.services.dashboard_snapshots import DashboardSnapshotService
|
| 18 |
from app.services.performance import performance_recorder
|
|
@@ -30,6 +37,21 @@ class LearningSummaryService:
|
|
| 30 |
started = time.perf_counter()
|
| 31 |
missing_sections: list[str] = []
|
| 32 |
warnings: list[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
power = db.scalar(select(BlumTradingPowerScore).order_by(desc(BlumTradingPowerScore.calculated_at)).limit(1))
|
| 35 |
if power is None:
|
|
@@ -54,35 +76,63 @@ class LearningSummaryService:
|
|
| 54 |
if latest_run is None:
|
| 55 |
missing_sections.append("latest_learning_run")
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
benchmarks = list(db.scalars(select(LearningBenchmarkComparison).order_by(desc(LearningBenchmarkComparison.calculated_at)).limit(24)).all())
|
| 58 |
benchmark_summary = summarize_benchmarks(benchmarks)
|
| 59 |
if not benchmarks:
|
| 60 |
missing_sections.append("benchmark_summary")
|
| 61 |
|
| 62 |
live_snapshot = DashboardSnapshotService().latest(db, "live_vs_historical_summary")
|
|
|
|
| 63 |
if live_snapshot["status"] == "missing":
|
| 64 |
warnings.append("Live vs historical summary snapshot is missing.")
|
|
|
|
|
|
|
| 65 |
|
| 66 |
current_capital = getattr(current_cycle, "final_capital", None) if current_cycle else getattr(game, "current_capital", None)
|
| 67 |
target_capital = getattr(current_cycle, "target_capital", None) if current_cycle else getattr(game, "target_capital", None)
|
| 68 |
target_progress = safe_progress(current_capital, target_capital)
|
|
|
|
| 69 |
truth_panel_lines = build_truth_panel(power, benchmark_summary, warnings, benchmarks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
payload = {
|
| 71 |
"status": "initializing" if missing_sections else "ready",
|
| 72 |
"generated_at": datetime.utcnow().isoformat(),
|
| 73 |
"summary_duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
|
|
|
| 74 |
"trading_power_score": power.score if power else None,
|
| 75 |
"trading_power_classification": power.classification if power else "initializing",
|
|
|
|
| 76 |
"current_capital": current_capital,
|
| 77 |
"target_capital": target_capital,
|
| 78 |
"target_progress": target_progress,
|
|
|
|
|
|
|
| 79 |
"completed_target_cycles": getattr(game, "target_cycles_completed", 0) if game else 0,
|
| 80 |
"bankrupt_cycles": getattr(game, "bankrupt_cycles", 0) if game else 0,
|
| 81 |
"latest_learning_run_status": getattr(latest_run, "status", None) or "not_started",
|
| 82 |
"latest_learning_run_at": latest_run.started_at.isoformat() if latest_run and latest_run.started_at else None,
|
| 83 |
"benchmark_summary": benchmark_summary,
|
| 84 |
"live_vs_historical_summary": live_snapshot,
|
|
|
|
|
|
|
|
|
|
| 85 |
"truth_panel": truth_panel_lines,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
"warnings": warnings,
|
| 87 |
"missing_sections": missing_sections,
|
| 88 |
"data_freshness": {
|
|
@@ -90,13 +140,15 @@ class LearningSummaryService:
|
|
| 90 |
"game_updated_at": game.updated_at.isoformat() if game and game.updated_at else None,
|
| 91 |
"learning_run_started_at": latest_run.started_at.isoformat() if latest_run and latest_run.started_at else None,
|
| 92 |
"benchmark_calculated_at": max((row.calculated_at for row in benchmarks if row.calculated_at), default=None).isoformat() if benchmarks else None,
|
|
|
|
|
|
|
| 93 |
},
|
| 94 |
-
"is_recalculation_running":
|
| 95 |
"suggested_next_step": "Use background workers or explicit recalculation buttons to refresh missing summaries." if missing_sections else "Summary is ready.",
|
| 96 |
"performance": {
|
| 97 |
"budget_ms": 300,
|
| 98 |
"duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
| 99 |
-
"source": "
|
| 100 |
},
|
| 101 |
}
|
| 102 |
performance_recorder.record_dashboard_widget(
|
|
@@ -150,6 +202,160 @@ def build_truth_panel(power: BlumTradingPowerScore | None, benchmark_summary: di
|
|
| 150 |
return output[:7]
|
| 151 |
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
def safe_progress(capital: float | None, target: float | None) -> float | None:
|
| 154 |
if capital is None or target is None or target <= 0:
|
| 155 |
return None
|
|
@@ -158,3 +364,98 @@ def safe_progress(capital: float | None, target: float | None) -> float | None:
|
|
| 158 |
|
| 159 |
def format_number(value: float | None) -> str:
|
| 160 |
return "n/a" if value is None else f"{value:.2f}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from datetime import datetime
|
| 4 |
import time
|
| 5 |
|
| 6 |
+
from sqlalchemy import desc, inspect, select
|
| 7 |
from sqlalchemy.orm import Session
|
| 8 |
|
| 9 |
from app.models import (
|
| 10 |
BlumTradingPowerScore,
|
| 11 |
DashboardSnapshot,
|
| 12 |
+
CapitalPreservationAlpha,
|
| 13 |
LearningBenchmarkComparison,
|
| 14 |
+
LearningFactorImportance,
|
| 15 |
+
LearningFocusPriority,
|
| 16 |
LearningRun,
|
| 17 |
+
LearningStrengthWeaknessMap,
|
| 18 |
+
ReasoningNoiseFlag,
|
| 19 |
TradingCapitalCycle,
|
| 20 |
TradingGame,
|
| 21 |
+
TradingIntelligenceMetric,
|
| 22 |
+
TradeLearningEvidence,
|
| 23 |
)
|
| 24 |
from app.services.dashboard_snapshots import DashboardSnapshotService
|
| 25 |
from app.services.performance import performance_recorder
|
|
|
|
| 37 |
started = time.perf_counter()
|
| 38 |
missing_sections: list[str] = []
|
| 39 |
warnings: list[str] = []
|
| 40 |
+
snapshot_types = [
|
| 41 |
+
"learning_summary",
|
| 42 |
+
"trading_game_summary",
|
| 43 |
+
"benchmark_summary",
|
| 44 |
+
"intelligence_growth_summary",
|
| 45 |
+
"truth_panel_summary",
|
| 46 |
+
"meta_cognition_summary",
|
| 47 |
+
]
|
| 48 |
+
snapshots = {snapshot_type: DashboardSnapshotService().latest(db, snapshot_type) for snapshot_type in snapshot_types}
|
| 49 |
+
stale_snapshots = [snapshot_type for snapshot_type, payload in snapshots.items() if payload.get("status") == "stale"]
|
| 50 |
+
missing_snapshots = [snapshot_type for snapshot_type, payload in snapshots.items() if payload.get("status") == "missing"]
|
| 51 |
+
if stale_snapshots:
|
| 52 |
+
warnings.append(f"Using stale dashboard snapshots for: {', '.join(stale_snapshots)}.")
|
| 53 |
+
if missing_snapshots:
|
| 54 |
+
warnings.append(f"Dashboard snapshots not available yet for: {', '.join(missing_snapshots)}.")
|
| 55 |
|
| 56 |
power = db.scalar(select(BlumTradingPowerScore).order_by(desc(BlumTradingPowerScore.calculated_at)).limit(1))
|
| 57 |
if power is None:
|
|
|
|
| 76 |
if latest_run is None:
|
| 77 |
missing_sections.append("latest_learning_run")
|
| 78 |
|
| 79 |
+
latest_metric = db.scalar(select(TradingIntelligenceMetric).order_by(desc(TradingIntelligenceMetric.calculated_at)).limit(1))
|
| 80 |
+
if latest_metric is None:
|
| 81 |
+
missing_sections.append("trading_intelligence_metrics")
|
| 82 |
+
|
| 83 |
+
top_weakness = db.scalar(select(LearningStrengthWeaknessMap).order_by(desc(LearningStrengthWeaknessMap.weakness_score), desc(LearningStrengthWeaknessMap.calculated_at)).limit(1))
|
| 84 |
+
latest_lesson = db.scalar(select(TradeLearningEvidence).order_by(desc(TradeLearningEvidence.created_at)).limit(1))
|
| 85 |
+
|
| 86 |
benchmarks = list(db.scalars(select(LearningBenchmarkComparison).order_by(desc(LearningBenchmarkComparison.calculated_at)).limit(24)).all())
|
| 87 |
benchmark_summary = summarize_benchmarks(benchmarks)
|
| 88 |
if not benchmarks:
|
| 89 |
missing_sections.append("benchmark_summary")
|
| 90 |
|
| 91 |
live_snapshot = DashboardSnapshotService().latest(db, "live_vs_historical_summary")
|
| 92 |
+
meta_snapshot = snapshots.get("meta_cognition_summary") or {}
|
| 93 |
if live_snapshot["status"] == "missing":
|
| 94 |
warnings.append("Live vs historical summary snapshot is missing.")
|
| 95 |
+
if meta_snapshot.get("status") == "missing":
|
| 96 |
+
warnings.append("Meta-Cognition summary snapshot is missing.")
|
| 97 |
|
| 98 |
current_capital = getattr(current_cycle, "final_capital", None) if current_cycle else getattr(game, "current_capital", None)
|
| 99 |
target_capital = getattr(current_cycle, "target_capital", None) if current_cycle else getattr(game, "target_capital", None)
|
| 100 |
target_progress = safe_progress(current_capital, target_capital)
|
| 101 |
+
current_cycle_payload = serialize_cycle(current_cycle)
|
| 102 |
truth_panel_lines = build_truth_panel(power, benchmark_summary, warnings, benchmarks)
|
| 103 |
+
truth_snapshot = snapshots.get("truth_panel_summary", {})
|
| 104 |
+
truth_snapshot_payload = truth_snapshot.get("payload") or {}
|
| 105 |
+
if truth_snapshot_payload.get("truth_panel"):
|
| 106 |
+
truth_panel_lines = truth_snapshot_payload["truth_panel"]
|
| 107 |
+
last_snapshot_timestamp = latest_snapshot_timestamp(snapshots)
|
| 108 |
+
backend_training_status = training_status(latest_run, snapshots)
|
| 109 |
payload = {
|
| 110 |
"status": "initializing" if missing_sections else "ready",
|
| 111 |
"generated_at": datetime.utcnow().isoformat(),
|
| 112 |
"summary_duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
| 113 |
+
"learning_loop_status": getattr(latest_run, "status", None) or "not_started",
|
| 114 |
"trading_power_score": power.score if power else None,
|
| 115 |
"trading_power_classification": power.classification if power else "initializing",
|
| 116 |
+
"current_capital_cycle": current_cycle_payload,
|
| 117 |
"current_capital": current_capital,
|
| 118 |
"target_capital": target_capital,
|
| 119 |
"target_progress": target_progress,
|
| 120 |
+
"win_rate": first_not_none(getattr(latest_metric, "win_rate", None), getattr(game, "win_rate", None)),
|
| 121 |
+
"expectancy_r": first_not_none(getattr(latest_metric, "expectancy_r", None), getattr(current_cycle, "expectancy_r", None), getattr(game, "expectancy_r", None)),
|
| 122 |
"completed_target_cycles": getattr(game, "target_cycles_completed", 0) if game else 0,
|
| 123 |
"bankrupt_cycles": getattr(game, "bankrupt_cycles", 0) if game else 0,
|
| 124 |
"latest_learning_run_status": getattr(latest_run, "status", None) or "not_started",
|
| 125 |
"latest_learning_run_at": latest_run.started_at.isoformat() if latest_run and latest_run.started_at else None,
|
| 126 |
"benchmark_summary": benchmark_summary,
|
| 127 |
"live_vs_historical_summary": live_snapshot,
|
| 128 |
+
"live_vs_historical_status": live_snapshot.get("status", "missing"),
|
| 129 |
+
"top_weakness": serialize_weakness(top_weakness),
|
| 130 |
+
"latest_lesson_learned": serialize_lesson(latest_lesson),
|
| 131 |
"truth_panel": truth_panel_lines,
|
| 132 |
+
"what_blum_should_learn_next": what_blum_should_learn_next(db, meta_snapshot),
|
| 133 |
+
"backend_training_status": backend_training_status,
|
| 134 |
+
"last_snapshot_timestamp": last_snapshot_timestamp,
|
| 135 |
+
"snapshots": summarize_snapshots(snapshots),
|
| 136 |
"warnings": warnings,
|
| 137 |
"missing_sections": missing_sections,
|
| 138 |
"data_freshness": {
|
|
|
|
| 140 |
"game_updated_at": game.updated_at.isoformat() if game and game.updated_at else None,
|
| 141 |
"learning_run_started_at": latest_run.started_at.isoformat() if latest_run and latest_run.started_at else None,
|
| 142 |
"benchmark_calculated_at": max((row.calculated_at for row in benchmarks if row.calculated_at), default=None).isoformat() if benchmarks else None,
|
| 143 |
+
"trading_intelligence_calculated_at": latest_metric.calculated_at.isoformat() if latest_metric and latest_metric.calculated_at else None,
|
| 144 |
+
"last_snapshot_timestamp": last_snapshot_timestamp,
|
| 145 |
},
|
| 146 |
+
"is_recalculation_running": backend_training_status["is_recalculation_running"],
|
| 147 |
"suggested_next_step": "Use background workers or explicit recalculation buttons to refresh missing summaries." if missing_sections else "Summary is ready.",
|
| 148 |
"performance": {
|
| 149 |
"budget_ms": 300,
|
| 150 |
"duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
| 151 |
+
"source": "snapshots_first_latest_precomputed_rows_only",
|
| 152 |
},
|
| 153 |
}
|
| 154 |
performance_recorder.record_dashboard_widget(
|
|
|
|
| 202 |
return output[:7]
|
| 203 |
|
| 204 |
|
| 205 |
+
def what_blum_should_learn_next(db: Session, meta_snapshot: dict) -> dict:
|
| 206 |
+
snapshot_payload = meta_snapshot.get("payload") or {}
|
| 207 |
+
if snapshot_payload:
|
| 208 |
+
return {
|
| 209 |
+
"status": meta_snapshot.get("status", "ready"),
|
| 210 |
+
"top_alpha_creating_factor": compact_factor(snapshot_payload.get("top_alpha_factor")),
|
| 211 |
+
"top_alpha_destroying_factor": compact_factor(snapshot_payload.get("top_alpha_destroyer")),
|
| 212 |
+
"noisiest_factor": compact_factor(snapshot_payload.get("noisiest_factor")),
|
| 213 |
+
"most_undervalued_factor": compact_factor(snapshot_payload.get("most_undervalued_factor")),
|
| 214 |
+
"strongest_capital_preservation_rule": compact_preservation(snapshot_payload.get("strongest_capital_preservation_rule")),
|
| 215 |
+
"weakest_current_module": compact_noise(snapshot_payload.get("weakest_current_module")),
|
| 216 |
+
"next_learning_focus": compact_focus(snapshot_payload.get("next_learning_focus")),
|
| 217 |
+
"conclusion": snapshot_payload.get("meta_cognition_conclusion", {}).get("summary") or "Meta-Cognition snapshot available.",
|
| 218 |
+
"source": "snapshot",
|
| 219 |
+
}
|
| 220 |
+
if not all(table_exists(db, model.__tablename__) for model in [LearningFactorImportance, LearningFocusPriority, ReasoningNoiseFlag, CapitalPreservationAlpha]):
|
| 221 |
+
return {
|
| 222 |
+
"status": "missing",
|
| 223 |
+
"top_alpha_creating_factor": None,
|
| 224 |
+
"top_alpha_destroying_factor": None,
|
| 225 |
+
"noisiest_factor": None,
|
| 226 |
+
"most_undervalued_factor": None,
|
| 227 |
+
"strongest_capital_preservation_rule": None,
|
| 228 |
+
"weakest_current_module": None,
|
| 229 |
+
"next_learning_focus": None,
|
| 230 |
+
"conclusion": "Meta-Cognition tables are not available yet; showing the rest of the Learning snapshot without recalculation.",
|
| 231 |
+
"source": "schema_not_ready",
|
| 232 |
+
}
|
| 233 |
+
factor_rows = db.scalars(select(LearningFactorImportance).order_by(desc(LearningFactorImportance.calculated_at)).limit(40)).all()
|
| 234 |
+
focus = db.scalar(
|
| 235 |
+
select(LearningFocusPriority)
|
| 236 |
+
.where(LearningFocusPriority.status.in_(["proposed", "active"]))
|
| 237 |
+
.order_by(desc(LearningFocusPriority.expected_learning_value), desc(LearningFocusPriority.created_at))
|
| 238 |
+
.limit(1)
|
| 239 |
+
)
|
| 240 |
+
noise = db.scalar(select(ReasoningNoiseFlag).where(ReasoningNoiseFlag.status == "open").order_by(desc(ReasoningNoiseFlag.created_at)).limit(1))
|
| 241 |
+
preservation = db.scalar(select(CapitalPreservationAlpha).order_by(desc(CapitalPreservationAlpha.capital_preserved), desc(CapitalPreservationAlpha.created_at)).limit(1))
|
| 242 |
+
top_alpha = max(factor_rows, key=lambda row: row.alpha_contribution, default=None)
|
| 243 |
+
top_loss = max(factor_rows, key=lambda row: row.alpha_loss_contribution, default=None)
|
| 244 |
+
noisiest = max(factor_rows, key=lambda row: row.noise_score, default=None)
|
| 245 |
+
undervalued = max(factor_rows, key=lambda row: row.undervaluation_score, default=None)
|
| 246 |
+
return {
|
| 247 |
+
"status": "ready" if factor_rows or focus or noise or preservation else "missing",
|
| 248 |
+
"top_alpha_creating_factor": compact_factor_obj(top_alpha),
|
| 249 |
+
"top_alpha_destroying_factor": compact_factor_obj(top_loss),
|
| 250 |
+
"noisiest_factor": compact_factor_obj(noisiest),
|
| 251 |
+
"most_undervalued_factor": compact_factor_obj(undervalued),
|
| 252 |
+
"strongest_capital_preservation_rule": compact_preservation_obj(preservation),
|
| 253 |
+
"weakest_current_module": compact_noise_obj(noise),
|
| 254 |
+
"next_learning_focus": compact_focus_obj(focus),
|
| 255 |
+
"conclusion": "Meta-Cognition snapshot missing; showing latest stored evidence only." if factor_rows else "Meta-Cognition evidence is not available yet.",
|
| 256 |
+
"source": "latest_rows_no_recalculation",
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def table_exists(db: Session, table_name: str) -> bool:
|
| 261 |
+
try:
|
| 262 |
+
return inspect(db.get_bind()).has_table(table_name)
|
| 263 |
+
except Exception:
|
| 264 |
+
return False
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def compact_factor(item: dict | None) -> dict | None:
|
| 268 |
+
if not item:
|
| 269 |
+
return None
|
| 270 |
+
return {
|
| 271 |
+
"factor_name": item.get("factor_name"),
|
| 272 |
+
"sample_size": item.get("sample_size"),
|
| 273 |
+
"confidence": item.get("confidence"),
|
| 274 |
+
"recommended_weight_action": item.get("recommended_weight_action"),
|
| 275 |
+
"alpha_contribution": item.get("alpha_contribution"),
|
| 276 |
+
"alpha_loss_contribution": item.get("alpha_loss_contribution"),
|
| 277 |
+
"noise_score": item.get("noise_score"),
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def compact_factor_obj(row: LearningFactorImportance | None) -> dict | None:
|
| 282 |
+
return compact_factor({
|
| 283 |
+
"factor_name": row.factor_name,
|
| 284 |
+
"sample_size": row.sample_size,
|
| 285 |
+
"confidence": row.confidence,
|
| 286 |
+
"recommended_weight_action": row.recommended_weight_action,
|
| 287 |
+
"alpha_contribution": row.alpha_contribution,
|
| 288 |
+
"alpha_loss_contribution": row.alpha_loss_contribution,
|
| 289 |
+
"noise_score": row.noise_score,
|
| 290 |
+
}) if row else None
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def compact_focus(item: dict | None) -> dict | None:
|
| 294 |
+
if not item:
|
| 295 |
+
return None
|
| 296 |
+
return {
|
| 297 |
+
"priority_type": item.get("priority_type"),
|
| 298 |
+
"target": item.get("target"),
|
| 299 |
+
"expected_learning_value": item.get("expected_learning_value"),
|
| 300 |
+
"urgency": item.get("urgency"),
|
| 301 |
+
"reason": item.get("reason"),
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def compact_focus_obj(row: LearningFocusPriority | None) -> dict | None:
|
| 306 |
+
return compact_focus({
|
| 307 |
+
"priority_type": row.priority_type,
|
| 308 |
+
"target": row.target,
|
| 309 |
+
"expected_learning_value": row.expected_learning_value,
|
| 310 |
+
"urgency": row.urgency,
|
| 311 |
+
"reason": row.reason,
|
| 312 |
+
}) if row else None
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def compact_preservation(item: dict | None) -> dict | None:
|
| 316 |
+
if not item:
|
| 317 |
+
return None
|
| 318 |
+
return {
|
| 319 |
+
"ticker": item.get("ticker"),
|
| 320 |
+
"setup_type": item.get("setup_type"),
|
| 321 |
+
"capital_preserved": item.get("capital_preserved"),
|
| 322 |
+
"opportunity_cost": item.get("opportunity_cost"),
|
| 323 |
+
"quality_score": item.get("quality_score"),
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def compact_preservation_obj(row: CapitalPreservationAlpha | None) -> dict | None:
|
| 328 |
+
return compact_preservation({
|
| 329 |
+
"ticker": row.ticker,
|
| 330 |
+
"setup_type": row.setup_type,
|
| 331 |
+
"capital_preserved": row.capital_preserved,
|
| 332 |
+
"opportunity_cost": row.opportunity_cost,
|
| 333 |
+
"quality_score": row.quality_score,
|
| 334 |
+
}) if row else None
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def compact_noise(item: dict | None) -> dict | None:
|
| 338 |
+
if not item:
|
| 339 |
+
return None
|
| 340 |
+
return {
|
| 341 |
+
"factor_name": item.get("factor_name"),
|
| 342 |
+
"module_name": item.get("module_name"),
|
| 343 |
+
"noise_type": item.get("noise_type"),
|
| 344 |
+
"severity": item.get("severity"),
|
| 345 |
+
"recommended_action": item.get("recommended_action"),
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def compact_noise_obj(row: ReasoningNoiseFlag | None) -> dict | None:
|
| 350 |
+
return compact_noise({
|
| 351 |
+
"factor_name": row.factor_name,
|
| 352 |
+
"module_name": row.module_name,
|
| 353 |
+
"noise_type": row.noise_type,
|
| 354 |
+
"severity": row.severity,
|
| 355 |
+
"recommended_action": row.recommended_action,
|
| 356 |
+
}) if row else None
|
| 357 |
+
|
| 358 |
+
|
| 359 |
def safe_progress(capital: float | None, target: float | None) -> float | None:
|
| 360 |
if capital is None or target is None or target <= 0:
|
| 361 |
return None
|
|
|
|
| 364 |
|
| 365 |
def format_number(value: float | None) -> str:
|
| 366 |
return "n/a" if value is None else f"{value:.2f}"
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def first_not_none(*values):
|
| 370 |
+
for value in values:
|
| 371 |
+
if value is not None:
|
| 372 |
+
return value
|
| 373 |
+
return None
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def serialize_cycle(row: TradingCapitalCycle | None) -> dict | None:
|
| 377 |
+
if row is None:
|
| 378 |
+
return None
|
| 379 |
+
return {
|
| 380 |
+
"id": row.id,
|
| 381 |
+
"cycle_number": row.cycle_number,
|
| 382 |
+
"status": row.status,
|
| 383 |
+
"started_at": row.started_at.isoformat() if row.started_at else None,
|
| 384 |
+
"ended_at": row.ended_at.isoformat() if row.ended_at else None,
|
| 385 |
+
"start_capital": row.start_capital,
|
| 386 |
+
"target_capital": row.target_capital,
|
| 387 |
+
"final_capital": row.final_capital,
|
| 388 |
+
"trades_count": row.trades_count,
|
| 389 |
+
"wins": row.wins,
|
| 390 |
+
"losses": row.losses,
|
| 391 |
+
"missed_entries": row.missed_entries,
|
| 392 |
+
"target_hits": row.target_hits,
|
| 393 |
+
"stop_hits": row.stop_hits,
|
| 394 |
+
"expectancy_r": row.expectancy_r,
|
| 395 |
+
"excess_return_vs_benchmark": row.excess_return_vs_benchmark,
|
| 396 |
+
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def serialize_weakness(row: LearningStrengthWeaknessMap | None) -> dict | None:
|
| 401 |
+
if row is None:
|
| 402 |
+
return None
|
| 403 |
+
return {
|
| 404 |
+
"dimension": row.dimension,
|
| 405 |
+
"entity": row.entity,
|
| 406 |
+
"weakness_score": row.weakness_score,
|
| 407 |
+
"strength_score": row.strength_score,
|
| 408 |
+
"sample_size": row.sample_size,
|
| 409 |
+
"main_problem": row.main_problem,
|
| 410 |
+
"recommended_action": row.recommended_action,
|
| 411 |
+
"priority": row.priority,
|
| 412 |
+
"calculated_at": row.calculated_at.isoformat() if row.calculated_at else None,
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def serialize_lesson(row: TradeLearningEvidence | None) -> dict | None:
|
| 417 |
+
if row is None:
|
| 418 |
+
return None
|
| 419 |
+
return {
|
| 420 |
+
"ticker": row.ticker,
|
| 421 |
+
"setup_type": row.setup_type,
|
| 422 |
+
"lesson_type": row.lesson_type,
|
| 423 |
+
"observation": row.observation,
|
| 424 |
+
"sample_size": row.sample_size,
|
| 425 |
+
"affected_module": row.affected_module,
|
| 426 |
+
"confidence": row.confidence,
|
| 427 |
+
"created_at": row.created_at.isoformat() if row.created_at else None,
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def summarize_snapshots(snapshots: dict[str, dict]) -> dict:
|
| 432 |
+
output = {}
|
| 433 |
+
for snapshot_type, payload in snapshots.items():
|
| 434 |
+
output[snapshot_type] = {
|
| 435 |
+
"status": payload.get("status"),
|
| 436 |
+
"created_at": payload.get("created_at"),
|
| 437 |
+
"expires_at": payload.get("expires_at"),
|
| 438 |
+
"is_stale": payload.get("is_stale"),
|
| 439 |
+
"warnings": payload.get("warnings") or ([payload.get("warning")] if payload.get("warning") else []),
|
| 440 |
+
}
|
| 441 |
+
return output
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def latest_snapshot_timestamp(snapshots: dict[str, dict]) -> str | None:
|
| 445 |
+
timestamps = [payload.get("created_at") for payload in snapshots.values() if payload.get("created_at")]
|
| 446 |
+
return max(timestamps) if timestamps else None
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def training_status(latest_run: LearningRun | None, snapshots: dict[str, dict]) -> dict:
|
| 450 |
+
running_statuses = {"running", "queued", "processing", "in_progress"}
|
| 451 |
+
status = getattr(latest_run, "status", None) or "not_started"
|
| 452 |
+
running = status in running_statuses
|
| 453 |
+
stale_count = sum(1 for payload in snapshots.values() if payload.get("status") == "stale")
|
| 454 |
+
return {
|
| 455 |
+
"mode": "backend_scheduler_independent",
|
| 456 |
+
"status": "running" if running else status,
|
| 457 |
+
"is_recalculation_running": running,
|
| 458 |
+
"frontend_policy": "read_only_snapshot_observer",
|
| 459 |
+
"stale_snapshot_count": stale_count,
|
| 460 |
+
"latest_run_id": getattr(latest_run, "run_id", None),
|
| 461 |
+
}
|
backend/app/services/performance.py
CHANGED
|
@@ -424,7 +424,12 @@ def top_bottlenecks(api_events: list[dict[str, Any]], db_events: list[dict[str,
|
|
| 424 |
|
| 425 |
|
| 426 |
def learning_page_load_summary(api_events: list[dict[str, Any]], widget_events: list[dict[str, Any]], frontend_widget_events: list[dict[str, Any]]) -> dict[str, Any]:
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
status_counts: dict[str, int] = defaultdict(int)
|
| 429 |
for event in frontend_api_events:
|
| 430 |
status_counts[str((event.get("metadata") or {}).get("status") or "unknown")] += 1
|
|
@@ -450,6 +455,7 @@ def learning_page_load_summary(api_events: list[dict[str, Any]], widget_events:
|
|
| 450 |
]
|
| 451 |
return {
|
| 452 |
"frontend_request_count": len(frontend_api_events),
|
|
|
|
| 453 |
"duplicate_request_count": status_counts.get("deduped", 0),
|
| 454 |
"cache_hit_count": status_counts.get("cache_hit", 0) + status_counts.get("cache", 0),
|
| 455 |
"status_counts": dict(status_counts),
|
|
|
|
| 424 |
|
| 425 |
|
| 426 |
def learning_page_load_summary(api_events: list[dict[str, Any]], widget_events: list[dict[str, Any]], frontend_widget_events: list[dict[str, Any]]) -> dict[str, Any]:
|
| 427 |
+
all_frontend_api_events = [event for event in frontend_widget_events if str(event.get("name", "")).startswith("frontend.api.")]
|
| 428 |
+
frontend_api_events = [
|
| 429 |
+
event for event in all_frontend_api_events
|
| 430 |
+
if str((event.get("metadata") or {}).get("route") or "").startswith("/learning")
|
| 431 |
+
and bool((event.get("metadata") or {}).get("initial_learning_window"))
|
| 432 |
+
]
|
| 433 |
status_counts: dict[str, int] = defaultdict(int)
|
| 434 |
for event in frontend_api_events:
|
| 435 |
status_counts[str((event.get("metadata") or {}).get("status") or "unknown")] += 1
|
|
|
|
| 455 |
]
|
| 456 |
return {
|
| 457 |
"frontend_request_count": len(frontend_api_events),
|
| 458 |
+
"unscoped_frontend_request_count": len(all_frontend_api_events),
|
| 459 |
"duplicate_request_count": status_counts.get("deduped", 0),
|
| 460 |
"cache_hit_count": status_counts.get("cache_hit", 0) + status_counts.get("cache", 0),
|
| 461 |
"status_counts": dict(status_counts),
|
backend/app/services/trade_transparency.py
CHANGED
|
@@ -51,41 +51,79 @@ class TradeLedgerService:
|
|
| 51 |
limit: int = 200,
|
| 52 |
offset: int = 0,
|
| 53 |
refresh: bool = True,
|
|
|
|
|
|
|
| 54 |
) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
game = self.game(db, game_id)
|
| 56 |
if not game:
|
| 57 |
return {"status": "no_game", "rows": [], "summary": {}, "policy": TRANSPARENCY_POLICY}
|
|
|
|
|
|
|
|
|
|
| 58 |
if refresh:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
query =
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
"status": "ok",
|
|
|
|
| 86 |
"game": serialize_game_header(game),
|
| 87 |
-
"summary":
|
| 88 |
-
"rows":
|
| 89 |
"total": total,
|
| 90 |
"limit": limit,
|
| 91 |
"offset": offset,
|
|
@@ -104,6 +142,27 @@ class TradeLedgerService:
|
|
| 104 |
},
|
| 105 |
"policy": TRANSPARENCY_POLICY,
|
| 106 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
def detail(self, db: Session, trade_id: int) -> dict:
|
| 109 |
trade = db.get(TradingGameTrade, trade_id)
|
|
@@ -233,6 +292,33 @@ class TradeLedgerService:
|
|
| 233 |
return db.get(TradingGame, game_id)
|
| 234 |
return db.scalar(select(TradingGame).where(TradingGame.status == "active").order_by(desc(TradingGame.started_at)).limit(1)) or db.scalar(select(TradingGame).order_by(desc(TradingGame.started_at)).limit(1))
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
def serialize_trade(self, db: Session, trade: TradingGameTrade) -> dict:
|
| 237 |
return {
|
| 238 |
"trade_id": trade.id,
|
|
@@ -430,22 +516,61 @@ class TradeQualityEvaluator:
|
|
| 430 |
|
| 431 |
|
| 432 |
class EquityCurveAnnotationService:
|
| 433 |
-
def annotated_equity(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
game = TradeLedgerService().game(db, game_id)
|
| 435 |
if not game:
|
| 436 |
return {"status": "no_game", "equity_curve_points": [], "benchmark_curve_points": [], "annotations": []}
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
def refresh(self, db: Session, game: TradingGame, trades: list[TradingGameTrade]) -> None:
|
| 451 |
for trade in trades:
|
|
|
|
| 51 |
limit: int = 200,
|
| 52 |
offset: int = 0,
|
| 53 |
refresh: bool = True,
|
| 54 |
+
use_snapshot: bool = True,
|
| 55 |
+
include_trace: bool = False,
|
| 56 |
) -> dict:
|
| 57 |
+
if use_snapshot and self._can_use_ledger_snapshot(
|
| 58 |
+
ticker=ticker,
|
| 59 |
+
setup_type=setup_type,
|
| 60 |
+
outcome_label=outcome_label,
|
| 61 |
+
start_date=start_date,
|
| 62 |
+
end_date=end_date,
|
| 63 |
+
min_r=min_r,
|
| 64 |
+
max_r=max_r,
|
| 65 |
+
only_open=only_open,
|
| 66 |
+
only_closed=only_closed,
|
| 67 |
+
sort_by=sort_by,
|
| 68 |
+
):
|
| 69 |
+
from app.services.trading_game_runtime import TradingGameRuntimeSnapshotService
|
| 70 |
+
|
| 71 |
+
snapshot_payload = TradingGameRuntimeSnapshotService().ledger_from_snapshot(db, game_id=game_id, limit=limit, offset=offset)
|
| 72 |
+
if snapshot_payload is not None:
|
| 73 |
+
return snapshot_payload
|
| 74 |
game = self.game(db, game_id)
|
| 75 |
if not game:
|
| 76 |
return {"status": "no_game", "rows": [], "summary": {}, "policy": TRANSPARENCY_POLICY}
|
| 77 |
+
from app.services.trading_game_runtime import RuntimeTrace, payload_size_bytes
|
| 78 |
+
|
| 79 |
+
trace = RuntimeTrace("trading_game_ledger_live_read")
|
| 80 |
if refresh:
|
| 81 |
+
with trace.phase("refresh_transparency"):
|
| 82 |
+
self.refresh_game_transparency(db, game, commit=False, persist_reality=False)
|
| 83 |
+
with trace.phase("base_trade_query"):
|
| 84 |
+
query = select(TradingGameTrade).where(TradingGameTrade.game_id == game.id)
|
| 85 |
+
if ticker:
|
| 86 |
+
query = query.where(TradingGameTrade.ticker == ticker.upper())
|
| 87 |
+
if setup_type:
|
| 88 |
+
query = query.where(TradingGameTrade.setup_type == setup_type)
|
| 89 |
+
if outcome_label:
|
| 90 |
+
query = query.where(TradingGameTrade.outcome_label == outcome_label)
|
| 91 |
+
parsed_start = parse_date(start_date)
|
| 92 |
+
parsed_end = parse_date(end_date)
|
| 93 |
+
if parsed_start:
|
| 94 |
+
query = query.where(TradingGameTrade.entry_date >= parsed_start)
|
| 95 |
+
if parsed_end:
|
| 96 |
+
query = query.where(TradingGameTrade.entry_date <= parsed_end)
|
| 97 |
+
if min_r is not None:
|
| 98 |
+
query = query.where(TradingGameTrade.realized_r_multiple >= min_r)
|
| 99 |
+
if max_r is not None:
|
| 100 |
+
query = query.where(TradingGameTrade.realized_r_multiple <= max_r)
|
| 101 |
+
if only_open:
|
| 102 |
+
query = query.where(TradingGameTrade.exit_date.is_(None))
|
| 103 |
+
if only_closed:
|
| 104 |
+
query = query.where(TradingGameTrade.exit_date.is_not(None))
|
| 105 |
+
total = int(db.scalar(select(func.count()).select_from(query.subquery())) or 0)
|
| 106 |
+
query = order_trade_query(query, sort_by).limit(limit).offset(offset)
|
| 107 |
+
rows = db.scalars(query).all()
|
| 108 |
+
with trace.phase("attribution_loading"):
|
| 109 |
+
attribution_count = 0
|
| 110 |
+
with trace.phase("evidence_loading"):
|
| 111 |
+
evidence_count = 0
|
| 112 |
+
with trace.phase("benchmark_loading"):
|
| 113 |
+
benchmark_count = sum(1 for row in rows if row.benchmark_return_same_period is not None or row.benchmark_return is not None)
|
| 114 |
+
with trace.phase("quality_loading"):
|
| 115 |
+
quality_count = sum(1 for row in rows if row.trade_quality_score is not None)
|
| 116 |
+
with trace.phase("prediction_loading"):
|
| 117 |
+
prediction_count = sum(1 for row in rows if row.thesis_id)
|
| 118 |
+
with trace.phase("serialization"):
|
| 119 |
+
serialized_rows = [self.serialize_trade(db, row) for row in rows]
|
| 120 |
+
summary = self.summary_for_game(db, game)
|
| 121 |
+
payload = {
|
| 122 |
"status": "ok",
|
| 123 |
+
"snapshot_status": "miss",
|
| 124 |
"game": serialize_game_header(game),
|
| 125 |
+
"summary": summary,
|
| 126 |
+
"rows": serialized_rows,
|
| 127 |
"total": total,
|
| 128 |
"limit": limit,
|
| 129 |
"offset": offset,
|
|
|
|
| 142 |
},
|
| 143 |
"policy": TRANSPARENCY_POLICY,
|
| 144 |
}
|
| 145 |
+
with trace.phase("json_generation"):
|
| 146 |
+
response_size = payload_size_bytes(payload)
|
| 147 |
+
trace.add(
|
| 148 |
+
snapshot_miss=True,
|
| 149 |
+
base_trade_query_count=2,
|
| 150 |
+
attribution_loading_queries=0,
|
| 151 |
+
evidence_loading_queries=0,
|
| 152 |
+
benchmark_loading_queries=0,
|
| 153 |
+
quality_loading_queries=0,
|
| 154 |
+
prediction_loading_queries=0,
|
| 155 |
+
attribution_rows=attribution_count,
|
| 156 |
+
evidence_rows=evidence_count,
|
| 157 |
+
benchmark_rows=benchmark_count,
|
| 158 |
+
quality_rows=quality_count,
|
| 159 |
+
prediction_links=prediction_count,
|
| 160 |
+
row_count=len(rows),
|
| 161 |
+
total_trades=total,
|
| 162 |
+
response_size_bytes=response_size,
|
| 163 |
+
)
|
| 164 |
+
payload["runtime_trace"] = trace.payload()
|
| 165 |
+
return payload
|
| 166 |
|
| 167 |
def detail(self, db: Session, trade_id: int) -> dict:
|
| 168 |
trade = db.get(TradingGameTrade, trade_id)
|
|
|
|
| 292 |
return db.get(TradingGame, game_id)
|
| 293 |
return db.scalar(select(TradingGame).where(TradingGame.status == "active").order_by(desc(TradingGame.started_at)).limit(1)) or db.scalar(select(TradingGame).order_by(desc(TradingGame.started_at)).limit(1))
|
| 294 |
|
| 295 |
+
def _can_use_ledger_snapshot(
|
| 296 |
+
self,
|
| 297 |
+
*,
|
| 298 |
+
ticker: str | None,
|
| 299 |
+
setup_type: str | None,
|
| 300 |
+
outcome_label: str | None,
|
| 301 |
+
start_date: str | None,
|
| 302 |
+
end_date: str | None,
|
| 303 |
+
min_r: float | None,
|
| 304 |
+
max_r: float | None,
|
| 305 |
+
only_open: bool,
|
| 306 |
+
only_closed: bool,
|
| 307 |
+
sort_by: str,
|
| 308 |
+
) -> bool:
|
| 309 |
+
return (
|
| 310 |
+
ticker is None
|
| 311 |
+
and setup_type is None
|
| 312 |
+
and outcome_label is None
|
| 313 |
+
and start_date is None
|
| 314 |
+
and end_date is None
|
| 315 |
+
and min_r is None
|
| 316 |
+
and max_r is None
|
| 317 |
+
and not only_open
|
| 318 |
+
and not only_closed
|
| 319 |
+
and sort_by == "created_at_desc"
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
def serialize_trade(self, db: Session, trade: TradingGameTrade) -> dict:
|
| 323 |
return {
|
| 324 |
"trade_id": trade.id,
|
|
|
|
| 516 |
|
| 517 |
|
| 518 |
class EquityCurveAnnotationService:
|
| 519 |
+
def annotated_equity(
|
| 520 |
+
self,
|
| 521 |
+
db: Session,
|
| 522 |
+
game_id: int | None = None,
|
| 523 |
+
limit: int = 800,
|
| 524 |
+
*,
|
| 525 |
+
refresh: bool = False,
|
| 526 |
+
use_snapshot: bool = True,
|
| 527 |
+
include_trace: bool = False,
|
| 528 |
+
) -> dict:
|
| 529 |
+
if use_snapshot:
|
| 530 |
+
from app.services.trading_game_runtime import TradingGameRuntimeSnapshotService
|
| 531 |
+
|
| 532 |
+
snapshot_payload = TradingGameRuntimeSnapshotService().equity_from_snapshot(db, game_id=game_id, limit=limit)
|
| 533 |
+
if snapshot_payload is not None:
|
| 534 |
+
return snapshot_payload
|
| 535 |
game = TradeLedgerService().game(db, game_id)
|
| 536 |
if not game:
|
| 537 |
return {"status": "no_game", "equity_curve_points": [], "benchmark_curve_points": [], "annotations": []}
|
| 538 |
+
from app.services.trading_game_runtime import RuntimeTrace, payload_size_bytes
|
| 539 |
+
|
| 540 |
+
trace = RuntimeTrace("equity_curve_annotated_live_read")
|
| 541 |
+
if refresh:
|
| 542 |
+
with trace.phase("annotation_refresh"):
|
| 543 |
+
trades = db.scalars(select(TradingGameTrade).where(TradingGameTrade.game_id == game.id).order_by(TradingGameTrade.created_at)).all()
|
| 544 |
+
self.refresh(db, game, trades)
|
| 545 |
+
with trace.phase("equity_points_loading"):
|
| 546 |
+
points = db.scalars(select(TradingGameEquityCurve).where(TradingGameEquityCurve.game_id == game.id).order_by(TradingGameEquityCurve.created_at).limit(limit)).all()
|
| 547 |
+
with trace.phase("annotations_loading"):
|
| 548 |
+
annotations = db.scalars(select(EquityCurveAnnotation).where(EquityCurveAnnotation.game_id == game.id).order_by(EquityCurveAnnotation.timestamp).limit(limit)).all()
|
| 549 |
+
with trace.phase("benchmark_loading"):
|
| 550 |
+
benchmark_curve_points = [{"timestamp": iso(row.equity_date or row.created_at), "value": row.benchmark_equity, "return": row.benchmark_return} for row in points]
|
| 551 |
+
with trace.phase("serialization"):
|
| 552 |
+
payload = {
|
| 553 |
+
"status": "ok",
|
| 554 |
+
"snapshot_status": "miss",
|
| 555 |
+
"game": serialize_game_header(game),
|
| 556 |
+
"equity_curve_points": [serialize_equity_point(row) for row in points],
|
| 557 |
+
"benchmark_curve_points": benchmark_curve_points,
|
| 558 |
+
"annotations": [serialize_annotation(row) for row in annotations],
|
| 559 |
+
"policy": "Markers connect equity movement to trade entries, exits, drawdowns, rule events and benchmark divergence when those events exist.",
|
| 560 |
+
}
|
| 561 |
+
with trace.phase("json_generation"):
|
| 562 |
+
size = payload_size_bytes(payload)
|
| 563 |
+
trace.add(
|
| 564 |
+
snapshot_miss=True,
|
| 565 |
+
equity_points_queries=1,
|
| 566 |
+
annotations_queries=1,
|
| 567 |
+
benchmark_loading_queries=0,
|
| 568 |
+
point_count=len(points),
|
| 569 |
+
annotation_count=len(annotations),
|
| 570 |
+
response_size_bytes=size,
|
| 571 |
+
)
|
| 572 |
+
payload["runtime_trace"] = trace.payload()
|
| 573 |
+
return payload
|
| 574 |
|
| 575 |
def refresh(self, db: Session, game: TradingGame, trades: list[TradingGameTrade]) -> None:
|
| 576 |
for trade in trades:
|
backend/app/services/trading_game.py
CHANGED
|
@@ -428,6 +428,7 @@ class TradingGameSimulator:
|
|
| 428 |
"simulation": simulation.simulation_payload,
|
| 429 |
"prediction_id": prediction.id if prediction else None,
|
| 430 |
"market_regime": prediction.market_regime if prediction else "unknown",
|
|
|
|
| 431 |
"guardrails": guardrails(),
|
| 432 |
},
|
| 433 |
)
|
|
@@ -624,6 +625,26 @@ def trade_reproducibility_score(simulation: ExecutionSimulation, prediction: His
|
|
| 624 |
return round(clamp(score), 2)
|
| 625 |
|
| 626 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
def decision_state_for(simulation: ExecutionSimulation, reproducibility: float) -> str:
|
| 628 |
if reproducibility < 42:
|
| 629 |
return "avoid"
|
|
|
|
| 428 |
"simulation": simulation.simulation_payload,
|
| 429 |
"prediction_id": prediction.id if prediction else None,
|
| 430 |
"market_regime": prediction.market_regime if prediction else "unknown",
|
| 431 |
+
"feedback_loop": prediction_feedback_payload(prediction),
|
| 432 |
"guardrails": guardrails(),
|
| 433 |
},
|
| 434 |
)
|
|
|
|
| 625 |
return round(clamp(score), 2)
|
| 626 |
|
| 627 |
|
| 628 |
+
def prediction_feedback_payload(prediction: HistoricalPrediction | None) -> dict:
|
| 629 |
+
if not prediction:
|
| 630 |
+
return {
|
| 631 |
+
"status": "missing_prediction",
|
| 632 |
+
"policy": "No HistoricalPrediction was linked to this simulated trade.",
|
| 633 |
+
}
|
| 634 |
+
feedback = (prediction.prediction_payload or {}).get("feedback_loop") or {}
|
| 635 |
+
return {
|
| 636 |
+
"status": "ready",
|
| 637 |
+
"model_version_used": prediction.model_version_used or prediction.model_version or "base-static",
|
| 638 |
+
"weights_used": prediction.weights_used or feedback.get("weights_used") or {},
|
| 639 |
+
"confidence_adjustment": feedback.get("confidence_adjustment", 0.0),
|
| 640 |
+
"learning_memory_used": prediction.learning_memory_used or feedback.get("learning_memory_used") or {},
|
| 641 |
+
"strategy_memory_used": prediction.strategy_memory_used or feedback.get("strategy_memory_used") or {},
|
| 642 |
+
"research_priority_used": prediction.research_priority_used or feedback.get("research_priority_used") or {},
|
| 643 |
+
"learning_mode_metadata": feedback.get("learning_mode_metadata") or (prediction.prediction_payload or {}).get("learning_mode_metadata") or {},
|
| 644 |
+
"policy": "Paper trade payload records the prediction feedback loop metadata; it does not recalculate learning during trade rendering.",
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
|
| 648 |
def decision_state_for(simulation: ExecutionSimulation, reproducibility: float) -> str:
|
| 649 |
if reproducibility < 42:
|
| 650 |
return "avoid"
|
backend/tests/test_learning_performance_architecture.py
CHANGED
|
@@ -9,8 +9,12 @@ from app.models import (
|
|
| 9 |
DashboardSnapshot,
|
| 10 |
LearningBenchmarkComparison,
|
| 11 |
LearningRun,
|
|
|
|
| 12 |
TradingCapitalCycle,
|
| 13 |
TradingGame,
|
|
|
|
|
|
|
|
|
|
| 14 |
)
|
| 15 |
from app.services.dashboard_snapshots import DashboardSnapshotService
|
| 16 |
from app.services.learning_summary import LearningSummaryService, build_truth_panel, safe_progress, summarize_benchmarks
|
|
@@ -41,8 +45,12 @@ def test_learning_summary_returns_partial_payload_without_precomputed_rows():
|
|
| 41 |
DashboardSnapshot,
|
| 42 |
LearningBenchmarkComparison,
|
| 43 |
LearningRun,
|
|
|
|
| 44 |
TradingCapitalCycle,
|
| 45 |
TradingGame,
|
|
|
|
|
|
|
|
|
|
| 46 |
]:
|
| 47 |
model.__table__.create(bind=engine)
|
| 48 |
|
|
@@ -51,6 +59,9 @@ def test_learning_summary_returns_partial_payload_without_precomputed_rows():
|
|
| 51 |
|
| 52 |
assert payload["status"] == "initializing"
|
| 53 |
assert "trading_power_score" in payload["missing_sections"]
|
|
|
|
|
|
|
|
|
|
| 54 |
assert payload["truth_panel"][0].startswith("Not enough precomputed evidence")
|
| 55 |
|
| 56 |
|
|
@@ -74,9 +85,69 @@ def test_learning_page_keeps_heavy_work_out_of_initial_render():
|
|
| 74 |
page = Path(__file__).resolve().parents[2] / "frontend" / "app" / "learning" / "page.tsx"
|
| 75 |
text = page.read_text()
|
| 76 |
|
| 77 |
-
assert
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
assert "recalculateLearningTradingPower" not in text
|
| 80 |
assert "recalculateDecisionSuperiority" not in text
|
| 81 |
assert "recalculateBusinessQuality" not in text
|
| 82 |
assert "recalculatePortfolioIntelligence" not in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
DashboardSnapshot,
|
| 10 |
LearningBenchmarkComparison,
|
| 11 |
LearningRun,
|
| 12 |
+
LearningStrengthWeaknessMap,
|
| 13 |
TradingCapitalCycle,
|
| 14 |
TradingGame,
|
| 15 |
+
TradingGameTrade,
|
| 16 |
+
TradingIntelligenceMetric,
|
| 17 |
+
TradeLearningEvidence,
|
| 18 |
)
|
| 19 |
from app.services.dashboard_snapshots import DashboardSnapshotService
|
| 20 |
from app.services.learning_summary import LearningSummaryService, build_truth_panel, safe_progress, summarize_benchmarks
|
|
|
|
| 45 |
DashboardSnapshot,
|
| 46 |
LearningBenchmarkComparison,
|
| 47 |
LearningRun,
|
| 48 |
+
LearningStrengthWeaknessMap,
|
| 49 |
TradingCapitalCycle,
|
| 50 |
TradingGame,
|
| 51 |
+
TradingGameTrade,
|
| 52 |
+
TradingIntelligenceMetric,
|
| 53 |
+
TradeLearningEvidence,
|
| 54 |
]:
|
| 55 |
model.__table__.create(bind=engine)
|
| 56 |
|
|
|
|
| 59 |
|
| 60 |
assert payload["status"] == "initializing"
|
| 61 |
assert "trading_power_score" in payload["missing_sections"]
|
| 62 |
+
assert "trading_intelligence_metrics" in payload["missing_sections"]
|
| 63 |
+
assert payload["backend_training_status"]["frontend_policy"] == "read_only_snapshot_observer"
|
| 64 |
+
assert "snapshots" in payload
|
| 65 |
assert payload["truth_panel"][0].startswith("Not enough precomputed evidence")
|
| 66 |
|
| 67 |
|
|
|
|
| 85 |
page = Path(__file__).resolve().parents[2] / "frontend" / "app" / "learning" / "page.tsx"
|
| 86 |
text = page.read_text()
|
| 87 |
|
| 88 |
+
assert text.strip() == 'export { default } from "../training-ground/page";'
|
| 89 |
+
|
| 90 |
+
training_page = Path(__file__).resolve().parents[2] / "frontend" / "app" / "training-ground" / "page.tsx"
|
| 91 |
+
training_text = training_page.read_text()
|
| 92 |
+
initial_effect = training_text.split("useEffect(() => {", 1)[1].split("}, []);", 1)[0]
|
| 93 |
+
|
| 94 |
+
assert "api.traderTrainingGround()" in initial_effect
|
| 95 |
+
assert initial_effect.count("api.") == 1
|
| 96 |
+
assert "api.tradingGameLedger" not in training_text
|
| 97 |
+
assert "api.alphaRecoveryDashboard()" not in training_text
|
| 98 |
+
assert "api.metaCognitionSummary()" not in training_text
|
| 99 |
assert "recalculateLearningTradingPower" not in text
|
| 100 |
assert "recalculateDecisionSuperiority" not in text
|
| 101 |
assert "recalculateBusinessQuality" not in text
|
| 102 |
assert "recalculatePortfolioIntelligence" not in text
|
| 103 |
+
assert "recalculateAlphaRecovery" not in text
|
| 104 |
+
assert "metaCognitionRecalculate" not in text
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def test_deep_diagnostics_uses_human_readable_renderer_not_raw_json_dump():
|
| 108 |
+
page = Path(__file__).resolve().parents[2] / "frontend" / "components" / "DiagnosticPanelRenderer.tsx"
|
| 109 |
+
text = page.read_text()
|
| 110 |
+
|
| 111 |
+
assert "compactPreview" not in text
|
| 112 |
+
assert "JSON.stringify(compactPreview" not in text
|
| 113 |
+
assert "DiagnosticPanelRenderer" in text
|
| 114 |
+
assert "Show raw JSON" in text
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def test_diagnostic_panel_renderer_hides_raw_json_by_default():
|
| 118 |
+
renderer = Path(__file__).resolve().parents[2] / "frontend" / "components" / "DiagnosticPanelRenderer.tsx"
|
| 119 |
+
text = renderer.read_text()
|
| 120 |
+
|
| 121 |
+
assert "export function DiagnosticPanelRenderer" in text
|
| 122 |
+
assert "function ReliabilityByRegimeRenderer" in text
|
| 123 |
+
assert "function EnsembleStatusRenderer" in text
|
| 124 |
+
assert "export function SampleSizeWarning" in text
|
| 125 |
+
assert "export function WeightDistributionTable" in text
|
| 126 |
+
assert "Show raw JSON" in text
|
| 127 |
+
assert "Hide raw JSON" in text
|
| 128 |
+
assert "open && <pre" in text
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def test_diagnostic_renderer_exposes_evidence_warnings_and_tables():
|
| 132 |
+
renderer = Path(__file__).resolve().parents[2] / "frontend" / "components" / "DiagnosticPanelRenderer.tsx"
|
| 133 |
+
text = renderer.read_text()
|
| 134 |
+
|
| 135 |
+
assert "Small sample size: treat as weak evidence." in text
|
| 136 |
+
assert "Reliability is promising but not durable without more samples." in text
|
| 137 |
+
assert "Negative average return with positive excess return needs review." in text
|
| 138 |
+
assert "DiagnosticTable" in text
|
| 139 |
+
assert "Engine" in text
|
| 140 |
+
assert "Hit Rate" in text
|
| 141 |
+
assert "Excess vs Benchmark" in text
|
| 142 |
+
assert "Confidence Penalty" in text
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_frontend_blocks_heavy_learning_post_during_initial_render():
|
| 146 |
+
api_file = Path(__file__).resolve().parents[2] / "frontend" / "lib" / "api.ts"
|
| 147 |
+
text = api_file.read_text()
|
| 148 |
+
|
| 149 |
+
assert "blocked_heavy_frontend_recalculation" in text
|
| 150 |
+
assert "LEARNING_INITIAL_RENDER_GUARD_MS" in text
|
| 151 |
+
assert '"/api/capital-allocation/recalculate"' in text
|
| 152 |
+
assert '"/api/alpha-recovery/recalculate"' in text
|
| 153 |
+
assert '"/api/meta-cognition/recalculate"' in text
|
backend/tests/test_performance_diagnostics.py
CHANGED
|
@@ -35,9 +35,11 @@ def test_performance_recorder_builds_diagnostics():
|
|
| 35 |
def test_performance_recorder_exposes_learning_page_load_diagnostics():
|
| 36 |
recorder = PerformanceRecorder()
|
| 37 |
now = datetime.utcnow()
|
| 38 |
-
|
| 39 |
-
recorder.record_frontend_widget("frontend.api.GET./api/
|
| 40 |
-
recorder.record_frontend_widget("frontend.api.GET./api/trading-game/status", 0.
|
|
|
|
|
|
|
| 41 |
recorder.record_dashboard_widget(
|
| 42 |
"performance.heavy_recalculation_triggered_during_page_load",
|
| 43 |
3400.0,
|
|
@@ -49,6 +51,7 @@ def test_performance_recorder_exposes_learning_page_load_diagnostics():
|
|
| 49 |
summary = payload["initial_learning_page_load"]
|
| 50 |
|
| 51 |
assert summary["frontend_request_count"] == 3
|
|
|
|
| 52 |
assert summary["cache_hit_count"] == 1
|
| 53 |
assert summary["duplicate_request_count"] == 1
|
| 54 |
assert summary["heavy_post_calls_during_page_load"][0]["path"] == "/api/business-quality/recalculate"
|
|
|
|
| 35 |
def test_performance_recorder_exposes_learning_page_load_diagnostics():
|
| 36 |
recorder = PerformanceRecorder()
|
| 37 |
now = datetime.utcnow()
|
| 38 |
+
learning_meta = {"source": "fetchBlum", "route": "/learning", "initial_learning_window": True}
|
| 39 |
+
recorder.record_frontend_widget("frontend.api.GET./api/learning-intelligence/summary", 120.0, {"status": "ok", **learning_meta})
|
| 40 |
+
recorder.record_frontend_widget("frontend.api.GET./api/trading-game/status", 0.4, {"status": "cache_hit", **learning_meta})
|
| 41 |
+
recorder.record_frontend_widget("frontend.api.GET./api/trading-game/status", 0.2, {"status": "deduped", **learning_meta})
|
| 42 |
+
recorder.record_frontend_widget("frontend.api.GET./dashboard/overview", 800.0, {"status": "ok", "source": "fetchBlum", "route": "/", "initial_learning_window": False})
|
| 43 |
recorder.record_dashboard_widget(
|
| 44 |
"performance.heavy_recalculation_triggered_during_page_load",
|
| 45 |
3400.0,
|
|
|
|
| 51 |
summary = payload["initial_learning_page_load"]
|
| 52 |
|
| 53 |
assert summary["frontend_request_count"] == 3
|
| 54 |
+
assert summary["unscoped_frontend_request_count"] == 4
|
| 55 |
assert summary["cache_hit_count"] == 1
|
| 56 |
assert summary["duplicate_request_count"] == 1
|
| 57 |
assert summary["heavy_post_calls_during_page_load"][0]["path"] == "/api/business-quality/recalculate"
|
frontend/.next/BUILD_ID
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
wZUVLKXl_CiOxUlt_FdXC
|
frontend/.next/app-build-manifest.json
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"pages": {
|
| 3 |
+
"/_not-found/page": [
|
| 4 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 5 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 6 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 7 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 8 |
+
"static/chunks/app/_not-found/page-8194a34fe9d5ebe2.js"
|
| 9 |
+
],
|
| 10 |
+
"/layout": [
|
| 11 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 12 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 13 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 14 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 15 |
+
"static/css/02bd12e03544ea91.css",
|
| 16 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 17 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 18 |
+
"static/chunks/app/layout-c302ea2b37a37766.js"
|
| 19 |
+
],
|
| 20 |
+
"/backtest/page": [
|
| 21 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 22 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 23 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 24 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 25 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 26 |
+
"static/chunks/app/backtest/page-60b494c3f5caeab7.js"
|
| 27 |
+
],
|
| 28 |
+
"/chart-analyst/page": [
|
| 29 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 30 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 31 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 32 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 33 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 34 |
+
"static/chunks/75-8c48549bc3f12b00.js",
|
| 35 |
+
"static/chunks/app/chart-analyst/page-422122938e79d615.js"
|
| 36 |
+
],
|
| 37 |
+
"/chat/page": [
|
| 38 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 39 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 40 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 41 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 42 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 43 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 44 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 45 |
+
"static/chunks/145-04bdfe555d73caf0.js",
|
| 46 |
+
"static/chunks/app/chat/page-01bb723eea532598.js"
|
| 47 |
+
],
|
| 48 |
+
"/chatbot/page": [
|
| 49 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 50 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 51 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 52 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 53 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 54 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 55 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 56 |
+
"static/chunks/145-04bdfe555d73caf0.js",
|
| 57 |
+
"static/chunks/app/chatbot/page-ec86bf53c052af5b.js"
|
| 58 |
+
],
|
| 59 |
+
"/learning/page": [
|
| 60 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 61 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 62 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 63 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 64 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 65 |
+
"static/chunks/app/learning/page-fb9d9a3de58b413d.js"
|
| 66 |
+
],
|
| 67 |
+
"/methodology/page": [
|
| 68 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 69 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 70 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 71 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 72 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 73 |
+
"static/chunks/app/methodology/page-f0ef96f8ef7d3450.js"
|
| 74 |
+
],
|
| 75 |
+
"/ipo-radar/page": [
|
| 76 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 77 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 78 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 79 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 80 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 81 |
+
"static/chunks/app/ipo-radar/page-37bfbd97573d6050.js"
|
| 82 |
+
],
|
| 83 |
+
"/market-brain/page": [
|
| 84 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 85 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 86 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 87 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 88 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 89 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 90 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 91 |
+
"static/chunks/app/market-brain/page-a3c75d13701833bc.js"
|
| 92 |
+
],
|
| 93 |
+
"/narratives/page": [
|
| 94 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 95 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 96 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 97 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 98 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 99 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 100 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 101 |
+
"static/chunks/433-89ff72e62f774508.js",
|
| 102 |
+
"static/chunks/app/narratives/page-f858506c5d5d372d.js"
|
| 103 |
+
],
|
| 104 |
+
"/page": [
|
| 105 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 106 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 107 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 108 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 109 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 110 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 111 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 112 |
+
"static/chunks/96-ce8c57cd15d50941.js",
|
| 113 |
+
"static/chunks/app/page-d0db9a93fb4af9a5.js"
|
| 114 |
+
],
|
| 115 |
+
"/etf-radar/page": [
|
| 116 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 117 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 118 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 119 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 120 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 121 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 122 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 123 |
+
"static/chunks/app/etf-radar/page-cb504f6343d399db.js"
|
| 124 |
+
],
|
| 125 |
+
"/performance/page": [
|
| 126 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 127 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 128 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 129 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 130 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 131 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 132 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 133 |
+
"static/chunks/app/performance/page-72d89cf8fa4bd0b1.js"
|
| 134 |
+
],
|
| 135 |
+
"/signal-lab/page": [
|
| 136 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 137 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 138 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 139 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 140 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 141 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 142 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 143 |
+
"static/chunks/app/signal-lab/page-a2f85a5c0d0847cd.js"
|
| 144 |
+
],
|
| 145 |
+
"/sniper/page": [
|
| 146 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 147 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 148 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 149 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 150 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 151 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 152 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 153 |
+
"static/chunks/app/sniper/page-66e545665c88da6b.js"
|
| 154 |
+
],
|
| 155 |
+
"/stock-radar/page": [
|
| 156 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 157 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 158 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 159 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 160 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 161 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 162 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 163 |
+
"static/chunks/app/stock-radar/page-6e7f483858d9a39e.js"
|
| 164 |
+
],
|
| 165 |
+
"/themes/page": [
|
| 166 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 167 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 168 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 169 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 170 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 171 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 172 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 173 |
+
"static/chunks/433-89ff72e62f774508.js",
|
| 174 |
+
"static/chunks/app/themes/page-9af78316fbb208f8.js"
|
| 175 |
+
],
|
| 176 |
+
"/dashboard/page": [
|
| 177 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 178 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 179 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 180 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 181 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 182 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 183 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 184 |
+
"static/chunks/96-ce8c57cd15d50941.js",
|
| 185 |
+
"static/chunks/app/dashboard/page-d9577bde4b3301c9.js"
|
| 186 |
+
],
|
| 187 |
+
"/assets/[ticker]/page": [
|
| 188 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 189 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 190 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 191 |
+
"static/chunks/main-app-80b65e8c4f383000.js",
|
| 192 |
+
"static/chunks/183-66f1efcf12b29a02.js",
|
| 193 |
+
"static/chunks/651-5689242ae9b0ffc6.js",
|
| 194 |
+
"static/chunks/908-fe3c8e03f00ace4e.js",
|
| 195 |
+
"static/chunks/75-8c48549bc3f12b00.js",
|
| 196 |
+
"static/chunks/app/assets/[ticker]/page-3687f8c7fa1ef7c8.js"
|
| 197 |
+
]
|
| 198 |
+
}
|
| 199 |
+
}
|
frontend/.next/app-path-routes-manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"/_not-found/page":"/_not-found","/backtest/page":"/backtest","/chart-analyst/page":"/chart-analyst","/chat/page":"/chat","/chatbot/page":"/chatbot","/learning/page":"/learning","/methodology/page":"/methodology","/ipo-radar/page":"/ipo-radar","/market-brain/page":"/market-brain","/narratives/page":"/narratives","/page":"/","/etf-radar/page":"/etf-radar","/performance/page":"/performance","/signal-lab/page":"/signal-lab","/sniper/page":"/sniper","/stock-radar/page":"/stock-radar","/themes/page":"/themes","/dashboard/page":"/dashboard","/assets/[ticker]/page":"/assets/[ticker]"}
|
frontend/.next/build-manifest.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"polyfillFiles": [
|
| 3 |
+
"static/chunks/polyfills-42372ed130431b0a.js"
|
| 4 |
+
],
|
| 5 |
+
"devFiles": [],
|
| 6 |
+
"ampDevFiles": [],
|
| 7 |
+
"lowPriorityFiles": [
|
| 8 |
+
"static/wZUVLKXl_CiOxUlt_FdXC/_buildManifest.js",
|
| 9 |
+
"static/wZUVLKXl_CiOxUlt_FdXC/_ssgManifest.js"
|
| 10 |
+
],
|
| 11 |
+
"rootMainFiles": [
|
| 12 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 13 |
+
"static/chunks/2200cc46-bb2e82a2a422da49.js",
|
| 14 |
+
"static/chunks/945-dfa5efbc7dfb2860.js",
|
| 15 |
+
"static/chunks/main-app-80b65e8c4f383000.js"
|
| 16 |
+
],
|
| 17 |
+
"pages": {
|
| 18 |
+
"/_app": [
|
| 19 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 20 |
+
"static/chunks/framework-6e06c675866dc992.js",
|
| 21 |
+
"static/chunks/main-d4dac7885e92ed7d.js",
|
| 22 |
+
"static/chunks/pages/_app-0c3037849002a4aa.js"
|
| 23 |
+
],
|
| 24 |
+
"/_error": [
|
| 25 |
+
"static/chunks/webpack-aeb5c49f88887156.js",
|
| 26 |
+
"static/chunks/framework-6e06c675866dc992.js",
|
| 27 |
+
"static/chunks/main-d4dac7885e92ed7d.js",
|
| 28 |
+
"static/chunks/pages/_error-a647cd2c75dc4dc7.js"
|
| 29 |
+
]
|
| 30 |
+
},
|
| 31 |
+
"ampFirstPages": []
|
| 32 |
+
}
|
frontend/.next/cache/.tsbuildinfo
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/.next/cache/webpack/client-production/0.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:64c20a49f6c3005f093ae1cf65d33529c0a59bbd35ba5ad023bca0d3cb434afa
|
| 3 |
+
size 118293334
|
frontend/.next/cache/webpack/client-production/1.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5e03aa28adc6a768648f515402a900fe14c6752847d7d964a1929cb3004a9b6c
|
| 3 |
+
size 868911
|
frontend/.next/cache/webpack/client-production/10.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:71fce4cef0b4b1f91d826885a2588f7734eb1eed4dd69e70ba1700cac0783b0a
|
| 3 |
+
size 8511085
|
frontend/.next/cache/webpack/client-production/11.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d59a7fd7a8c5256381e9719bc1e090967de13243d63add7c753a130867cc3927
|
| 3 |
+
size 1294746
|
frontend/.next/cache/webpack/client-production/12.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5d140048b49b2aa71f6dcca9123f6e4fde88898eef94c98d37246bf2188adc8e
|
| 3 |
+
size 3857837
|
frontend/.next/cache/webpack/client-production/13.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f6e722d889d1adf3361080549c4442a68aa9b575dbdb84465f319c8f06f86c74
|
| 3 |
+
size 9983794
|
frontend/.next/cache/webpack/client-production/14.pack
ADDED
|
Binary file (64 kB). View file
|
|
|
frontend/.next/cache/webpack/client-production/15.pack
ADDED
|
Binary file (79.3 kB). View file
|
|
|
frontend/.next/cache/webpack/client-production/2.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1bda4f125628c87fda9af6581649974e2231693f2fbb1a0989920194d4b3daa6
|
| 3 |
+
size 1249964
|
frontend/.next/cache/webpack/client-production/3.pack
ADDED
|
Binary file (4.89 kB). View file
|
|
|
frontend/.next/cache/webpack/client-production/4.pack
ADDED
|
Binary file (14.7 kB). View file
|
|
|
frontend/.next/cache/webpack/client-production/5.pack
ADDED
|
Binary file (792 Bytes). View file
|
|
|
frontend/.next/cache/webpack/client-production/6.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:57c5d14b0f12b9dd3ec358cf64e42e813c43ef4e62aedae8f38b4e6372cbae52
|
| 3 |
+
size 6614985
|
frontend/.next/cache/webpack/client-production/7.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aaaa8c24f9ecde0e1428c5de84f15d7950323267d01fa36f5fc56859043395c2
|
| 3 |
+
size 108202
|
frontend/.next/cache/webpack/client-production/8.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2944bcbd152b577f7950f35c27cfbdd36f4d52134dff689a8b78f17ad4104cce
|
| 3 |
+
size 1112198
|
frontend/.next/cache/webpack/client-production/9.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c1e183c17cb5b8d257c1dbe1dcc04e84ce501d2d1a7ca7b66d2e0125327079e4
|
| 3 |
+
size 1062275
|
frontend/.next/cache/webpack/client-production/index.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:943c09107fb7bebabb49c02a0e84e94e938d4a94071f3c7746b3bab30ce503ba
|
| 3 |
+
size 5360825
|
frontend/.next/cache/webpack/client-production/index.pack.old
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b801d39452a502fce5ff9ea86dc9f12dd0c7acf45995880e368d15d8a2aa687b
|
| 3 |
+
size 5359015
|
frontend/.next/cache/webpack/edge-server-production/0.pack
ADDED
|
Binary file (274 Bytes). View file
|
|
|
frontend/.next/cache/webpack/edge-server-production/index.pack
ADDED
|
Binary file (28.6 kB). View file
|
|
|
frontend/.next/cache/webpack/server-production/0.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fbc928f3fe56a2511ce7ada8c42eabadf0939456e4e986640298317ff1701e12
|
| 3 |
+
size 19157446
|
frontend/.next/cache/webpack/server-production/1.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c6e9570f82bbe9b380b8f3fb99a5696f151db001f6f69084595c8f908f9918e5
|
| 3 |
+
size 1227512
|
frontend/.next/cache/webpack/server-production/10.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b562e5e3f5cb6d15b73a0ab664a9282aae91e6e728a8147a9ff433aca27c7aa7
|
| 3 |
+
size 484820
|
frontend/.next/cache/webpack/server-production/2.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:565d9a97b0e7643692ad87c36fe861e2bb92e0f9562b73008fdd7449c3d8d306
|
| 3 |
+
size 1378163
|
frontend/.next/cache/webpack/server-production/3.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:92873e01aee48539364ba76c413082cf9ac240cad900f375e6237cc1ff8ab900
|
| 3 |
+
size 1467753
|
frontend/.next/cache/webpack/server-production/4.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:80f2429d58fd2de77209e8d1d9a1411721a9a394bd4ea35456d453ecd82941b1
|
| 3 |
+
size 11907910
|
frontend/.next/cache/webpack/server-production/5.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee5ee084edc5d07c06db017a493f328ff249612ec351a5aae1f25edcbcbe8aea
|
| 3 |
+
size 1497242
|
frontend/.next/cache/webpack/server-production/6.pack
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:09db955bc8f95822127420242b6a8baab0b2bb169b92c1d505db64cd1d770e66
|
| 3 |
+
size 6031704
|