Spaces:
Running
Running
Add autonomous dataset intelligence engine
Browse files- README.md +56 -0
- backend/alembic/versions/0009_autonomous_dataset_intelligence.py +79 -0
- backend/app/api/routes.py +34 -2
- backend/app/core/config.py +9 -4
- backend/app/data/seed_assets.py +44 -0
- backend/app/models.py +50 -0
- backend/app/services/autonomous_engine.py +182 -0
- backend/app/services/fundamentals.py +31 -0
- backend/app/services/huggingface_datasets.py +263 -0
- backend/app/services/realtime.py +15 -0
- backend/app/services/thesis_engine.py +20 -2
- backend/app/signals/engine.py +97 -16
- frontend/lib/types.ts +4 -0
- frontend/package.json +1 -1
- package.json +1 -1
README.md
CHANGED
|
@@ -211,6 +211,59 @@ The objective is not to predict stock prices. The objective is to learn how Blum
|
|
| 211 |
|
| 212 |
The autonomous Blum Financial Model cycle is server-side and evidence-bound. When `BLUM_ENABLE_LEARNING_LOOP=true`, it runs on startup, during market refresh and on its own interval controlled by `BLUM_MODEL_CYCLE_MINUTES` and `BLUM_MODEL_CYCLE_LIMIT`. Each cycle captures recent signal reasoning, evaluates matured thesis outcomes, refreshes training examples and logs a `blum_model_autonomous_cycle` learning event. It updates database memory only; it does not self-modify source code and it does not execute trades.
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
## Chart Vision Technical Analyst
|
| 215 |
|
| 216 |
Blum includes a dedicated technical chart intelligence module. It is designed to read financial chart images when a vision model is configured, but it never relies only on visual interpretation.
|
|
@@ -444,6 +497,8 @@ export BLUM_ENABLE_LEARNING_LOOP=true
|
|
| 444 |
export BLUM_LEARNING_LOOP_MINUTES=360
|
| 445 |
export BLUM_MODEL_CYCLE_MINUTES=5
|
| 446 |
export BLUM_MODEL_CYCLE_LIMIT=120
|
|
|
|
|
|
|
| 447 |
```
|
| 448 |
|
| 449 |
The learning loops only update database memory, confidence adjustments, proprietary reasoning examples and reversible scoring-weight versions.
|
|
@@ -485,6 +540,7 @@ The UI exposes `/system/status` in the sidebar and dashboard. If the GUI looks u
|
|
| 485 |
- `app_version` must show the latest deployed version.
|
| 486 |
- `feature_set` must show the expected feature bundle.
|
| 487 |
- `persistence.mode` must be `external_postgres` for strict no-reset durability, or `embedded_postgres` with a populated backup file plus persistent `/data` storage for demo durability.
|
|
|
|
| 488 |
- `POST /system/persistence/backup` can force an immediate embedded PostgreSQL backup after a learning cycle or manual operations.
|
| 489 |
- `Financial Brain` shows `fallback mode` unless `BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true`.
|
| 490 |
- Hugging Face serves the previous Docker image until the new build finishes successfully.
|
|
|
|
| 211 |
|
| 212 |
The autonomous Blum Financial Model cycle is server-side and evidence-bound. When `BLUM_ENABLE_LEARNING_LOOP=true`, it runs on startup, during market refresh and on its own interval controlled by `BLUM_MODEL_CYCLE_MINUTES` and `BLUM_MODEL_CYCLE_LIMIT`. Each cycle captures recent signal reasoning, evaluates matured thesis outcomes, refreshes training examples and logs a `blum_model_autonomous_cycle` learning event. It updates database memory only; it does not self-modify source code and it does not execute trades.
|
| 213 |
|
| 214 |
+
## Autonomous Research Engine
|
| 215 |
+
|
| 216 |
+
Blum now runs a server-side autonomous research cycle by default. Manual refresh buttons are diagnostics; normal operation does not require user input. The default cycle is controlled by:
|
| 217 |
+
|
| 218 |
+
```bash
|
| 219 |
+
export BLUM_ENABLE_AUTONOMOUS_ENGINE=true
|
| 220 |
+
export BLUM_AUTONOMOUS_CYCLE_MINUTES=20
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
The autonomous cycle executes in this strict order:
|
| 224 |
+
|
| 225 |
+
1. Refresh Hugging Face financial dataset catalog.
|
| 226 |
+
2. Update macro context.
|
| 227 |
+
3. Update SEC companyfacts fundamentals.
|
| 228 |
+
4. Hydrate historical and recent OHLCV prices from public providers.
|
| 229 |
+
5. Ingest public news and sentiment.
|
| 230 |
+
6. Generate signal snapshots.
|
| 231 |
+
7. Update ETF intelligence.
|
| 232 |
+
8. Update IPO radar.
|
| 233 |
+
9. Run accuracy audit.
|
| 234 |
+
10. Run Blum Financial Model reasoning and outcome learning.
|
| 235 |
+
11. Persist embedded PostgreSQL backup when configured.
|
| 236 |
+
|
| 237 |
+
Every cycle creates an `autonomous_engine_runs` row and a `learning_events` audit entry. If a provider fails, the run is marked `degraded` with the failing stage and traceback, rather than hiding the issue.
|
| 238 |
+
|
| 239 |
+
New APIs:
|
| 240 |
+
|
| 241 |
+
- `GET /autonomous/status`
|
| 242 |
+
- `POST /autonomous/run`
|
| 243 |
+
- `GET /datasets/sources`
|
| 244 |
+
- `POST /datasets/refresh`
|
| 245 |
+
|
| 246 |
+
## Hugging Face Dataset Intelligence
|
| 247 |
+
|
| 248 |
+
Blum catalogs real Hugging Face datasets for historical prices, SEC filings, earnings transcripts, finance reasoning and benchmark evidence. The catalog is metadata-first and incremental by design: large corpora are validated through Dataset Viewer/parquet metadata before any targeted ingestion is attempted.
|
| 249 |
+
|
| 250 |
+
Initial curated sources include:
|
| 251 |
+
|
| 252 |
+
- `defeatbeta/yahoo-finance-data`
|
| 253 |
+
- `paperswithbacktest/Stocks-Daily-Price`
|
| 254 |
+
- `TeraflopAI/SEC-EDGAR`
|
| 255 |
+
- `kurry/sp500_earnings_transcripts`
|
| 256 |
+
- `glopardo/sp500-earnings-transcripts`
|
| 257 |
+
- `paperswithbacktest/Stocks-Quarterly-Earnings`
|
| 258 |
+
- `c3po-ai/edgar-corpus`
|
| 259 |
+
- `PatronusAI/financebench`
|
| 260 |
+
- `BUPT-Reasoning-Lab/FinanceReasoning`
|
| 261 |
+
- `jlh-ibm/earnings_call`
|
| 262 |
+
- `younginpiniti/us-stocks-daily-all`
|
| 263 |
+
- `sfd-anonymous/edgar-forecast-benchmark`
|
| 264 |
+
|
| 265 |
+
The catalog is stored in `external_dataset_sources` with dataset id, source domain, license, priority, ingestion mode, Dataset Viewer status, parquet metadata and usage policy. Blum does not copy massive datasets blindly and does not fabricate missing evidence.
|
| 266 |
+
|
| 267 |
## Chart Vision Technical Analyst
|
| 268 |
|
| 269 |
Blum includes a dedicated technical chart intelligence module. It is designed to read financial chart images when a vision model is configured, but it never relies only on visual interpretation.
|
|
|
|
| 497 |
export BLUM_LEARNING_LOOP_MINUTES=360
|
| 498 |
export BLUM_MODEL_CYCLE_MINUTES=5
|
| 499 |
export BLUM_MODEL_CYCLE_LIMIT=120
|
| 500 |
+
export BLUM_ENABLE_HF_DATASET_CATALOG=true
|
| 501 |
+
export BLUM_HF_DATASET_REFRESH_HOURS=24
|
| 502 |
```
|
| 503 |
|
| 504 |
The learning loops only update database memory, confidence adjustments, proprietary reasoning examples and reversible scoring-weight versions.
|
|
|
|
| 540 |
- `app_version` must show the latest deployed version.
|
| 541 |
- `feature_set` must show the expected feature bundle.
|
| 542 |
- `persistence.mode` must be `external_postgres` for strict no-reset durability, or `embedded_postgres` with a populated backup file plus persistent `/data` storage for demo durability.
|
| 543 |
+
- `GET /autonomous/status` shows the latest autonomous run, stage diagnostics, readiness score and dataset catalog status.
|
| 544 |
- `POST /system/persistence/backup` can force an immediate embedded PostgreSQL backup after a learning cycle or manual operations.
|
| 545 |
- `Financial Brain` shows `fallback mode` unless `BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true`.
|
| 546 |
- Hugging Face serves the previous Docker image until the new build finishes successfully.
|
backend/alembic/versions/0009_autonomous_dataset_intelligence.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from alembic import op
|
| 4 |
+
import sqlalchemy as sa
|
| 5 |
+
from sqlalchemy.dialects import postgresql
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
revision = "0009_autonomous_dataset_intelligence"
|
| 9 |
+
down_revision = "0008_blum_financial_model"
|
| 10 |
+
branch_labels = None
|
| 11 |
+
depends_on = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
json_type = postgresql.JSONB(astext_type=sa.Text())
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def upgrade() -> None:
|
| 18 |
+
op.create_table(
|
| 19 |
+
"external_dataset_sources",
|
| 20 |
+
sa.Column("id", sa.Integer(), nullable=False),
|
| 21 |
+
sa.Column("dataset_id", sa.String(length=220), nullable=False),
|
| 22 |
+
sa.Column("provider", sa.String(length=80), nullable=False),
|
| 23 |
+
sa.Column("title", sa.String(length=260), nullable=False),
|
| 24 |
+
sa.Column("primary_domain", sa.String(length=80), nullable=False),
|
| 25 |
+
sa.Column("data_domains", json_type, nullable=False),
|
| 26 |
+
sa.Column("license", sa.String(length=80), nullable=False),
|
| 27 |
+
sa.Column("priority", sa.Integer(), nullable=False),
|
| 28 |
+
sa.Column("ingestion_mode", sa.String(length=80), nullable=False),
|
| 29 |
+
sa.Column("status", sa.String(length=80), nullable=False),
|
| 30 |
+
sa.Column("dataset_url", sa.Text(), nullable=False),
|
| 31 |
+
sa.Column("viewer_status", json_type, nullable=False),
|
| 32 |
+
sa.Column("parquet_files", json_type, nullable=False),
|
| 33 |
+
sa.Column("size_summary", json_type, nullable=False),
|
| 34 |
+
sa.Column("usage_policy", json_type, nullable=False),
|
| 35 |
+
sa.Column("last_checked_at", sa.DateTime(), nullable=True),
|
| 36 |
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
| 37 |
+
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
| 38 |
+
sa.PrimaryKeyConstraint("id"),
|
| 39 |
+
sa.UniqueConstraint("dataset_id", name="uq_external_dataset_source_dataset_id"),
|
| 40 |
+
)
|
| 41 |
+
for column in ["dataset_id", "provider", "primary_domain", "license", "priority", "ingestion_mode", "status", "last_checked_at", "created_at", "updated_at"]:
|
| 42 |
+
op.create_index(f"ix_external_dataset_sources_{column}", "external_dataset_sources", [column])
|
| 43 |
+
op.create_index("ix_external_dataset_sources_status_priority", "external_dataset_sources", ["status", "priority"])
|
| 44 |
+
op.create_index("ix_external_dataset_sources_domain_updated", "external_dataset_sources", ["primary_domain", "updated_at"])
|
| 45 |
+
|
| 46 |
+
op.create_table(
|
| 47 |
+
"autonomous_engine_runs",
|
| 48 |
+
sa.Column("id", sa.Integer(), nullable=False),
|
| 49 |
+
sa.Column("run_id", sa.String(length=100), nullable=False),
|
| 50 |
+
sa.Column("trigger", sa.String(length=80), nullable=False),
|
| 51 |
+
sa.Column("status", sa.String(length=80), nullable=False),
|
| 52 |
+
sa.Column("started_at", sa.DateTime(), nullable=False),
|
| 53 |
+
sa.Column("completed_at", sa.DateTime(), nullable=True),
|
| 54 |
+
sa.Column("stage_results", json_type, nullable=False),
|
| 55 |
+
sa.Column("readiness_score", sa.Float(), nullable=False),
|
| 56 |
+
sa.Column("data_coverage_score", sa.Float(), nullable=False),
|
| 57 |
+
sa.Column("reasoning_memory_created", sa.Integer(), nullable=False),
|
| 58 |
+
sa.Column("warning_count", sa.Integer(), nullable=False),
|
| 59 |
+
sa.Column("error_payload", json_type, nullable=False),
|
| 60 |
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
| 61 |
+
sa.PrimaryKeyConstraint("id"),
|
| 62 |
+
sa.UniqueConstraint("run_id", name="uq_autonomous_engine_run_id"),
|
| 63 |
+
)
|
| 64 |
+
for column in ["run_id", "trigger", "status", "started_at", "completed_at", "readiness_score", "data_coverage_score", "created_at"]:
|
| 65 |
+
op.create_index(f"ix_autonomous_engine_runs_{column}", "autonomous_engine_runs", [column])
|
| 66 |
+
op.create_index("ix_autonomous_engine_runs_status_started", "autonomous_engine_runs", ["status", "started_at"])
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def downgrade() -> None:
|
| 70 |
+
op.drop_index("ix_autonomous_engine_runs_status_started", table_name="autonomous_engine_runs")
|
| 71 |
+
for column in ["created_at", "data_coverage_score", "readiness_score", "completed_at", "started_at", "status", "trigger", "run_id"]:
|
| 72 |
+
op.drop_index(f"ix_autonomous_engine_runs_{column}", table_name="autonomous_engine_runs")
|
| 73 |
+
op.drop_table("autonomous_engine_runs")
|
| 74 |
+
|
| 75 |
+
op.drop_index("ix_external_dataset_sources_domain_updated", table_name="external_dataset_sources")
|
| 76 |
+
op.drop_index("ix_external_dataset_sources_status_priority", table_name="external_dataset_sources")
|
| 77 |
+
for column in ["updated_at", "created_at", "last_checked_at", "status", "ingestion_mode", "priority", "license", "primary_domain", "provider", "dataset_id"]:
|
| 78 |
+
op.drop_index(f"ix_external_dataset_sources_{column}", table_name="external_dataset_sources")
|
| 79 |
+
op.drop_table("external_dataset_sources")
|
backend/app/api/routes.py
CHANGED
|
@@ -17,6 +17,7 @@ from app.models import (
|
|
| 17 |
AIInsight,
|
| 18 |
AccuracySnapshot,
|
| 19 |
Asset,
|
|
|
|
| 20 |
BlumDatasetExport,
|
| 21 |
BlumKnowledgeGraphEdge,
|
| 22 |
BlumKnowledgeGraphNode,
|
|
@@ -33,6 +34,7 @@ from app.models import (
|
|
| 33 |
ChartPatternMemory,
|
| 34 |
ConfidenceAdjustment,
|
| 35 |
EmbeddingVector,
|
|
|
|
| 36 |
FundamentalSnapshot,
|
| 37 |
HistoricalSimilarityCase,
|
| 38 |
IntelligenceReport,
|
|
@@ -57,6 +59,7 @@ from app.models import (
|
|
| 57 |
)
|
| 58 |
from app.schemas import AssetOut, MarketUpdateRequest, NewsOut, NewsUpdateRequest, SemanticSearchRequest, SignalRunRequest
|
| 59 |
from app.services.accuracy import asset_accuracy_profile, latest_accuracy_snapshot, market_accuracy_overview, run_accuracy_audit, signal_validation_report
|
|
|
|
| 60 |
from app.services.blum_financial_model import (
|
| 61 |
build_training_dataset,
|
| 62 |
capture_ai_insight_reasoning,
|
|
@@ -93,6 +96,7 @@ from app.services.financial_brain_learning import (
|
|
| 93 |
run_learning_cycle,
|
| 94 |
)
|
| 95 |
from app.services.hybrid_chart_intelligence import HybridChartIntelligence
|
|
|
|
| 96 |
from app.services.ipo import ipo_radar, sec_company_submissions, update_ipo_radar
|
| 97 |
from app.services.live import live_news, market_sentiment
|
| 98 |
from app.services.macro import macro_overview, update_macro_snapshots
|
|
@@ -134,7 +138,7 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 134 |
return {
|
| 135 |
"service": "blum-ai-financial-intelligence",
|
| 136 |
"app_version": settings.app_version,
|
| 137 |
-
"feature_set": "
|
| 138 |
"environment": settings.environment,
|
| 139 |
"generated_at": datetime.utcnow().isoformat(),
|
| 140 |
"hugging_face": {
|
|
@@ -147,6 +151,8 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 147 |
"model_loading_enabled": settings.enable_model_loading,
|
| 148 |
"financial_brain_model_enabled": settings.enable_financial_brain_model,
|
| 149 |
"live_startup_enabled": settings.enable_live_startup,
|
|
|
|
|
|
|
| 150 |
"yfinance_fallback_enabled": settings.enable_yfinance_fallback,
|
| 151 |
"historical_price_seed_enabled": settings.seed_historical_prices_on_startup,
|
| 152 |
"startup_signal_seed_enabled": settings.seed_signals_on_startup,
|
|
@@ -161,6 +167,8 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 161 |
"chart_vision_min_confidence": settings.chart_vision_min_confidence,
|
| 162 |
"fundamentals_refresh_minutes": settings.fundamentals_refresh_minutes,
|
| 163 |
"macro_refresh_minutes": settings.macro_refresh_minutes,
|
|
|
|
|
|
|
| 164 |
},
|
| 165 |
"persistence": database_persistence_status(),
|
| 166 |
"active_models": {
|
|
@@ -189,6 +197,8 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 189 |
"financial_knowledge_graph": True,
|
| 190 |
"portfolio_scenario": True,
|
| 191 |
"watchlist": True,
|
|
|
|
|
|
|
| 192 |
},
|
| 193 |
"database_counts": {
|
| 194 |
"assets": int(db.scalar(select(func.count(Asset.id))) or 0),
|
|
@@ -227,13 +237,15 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 227 |
"blum_graph_edges": int(db.scalar(select(func.count(BlumKnowledgeGraphEdge.id))) or 0),
|
| 228 |
"blum_dataset_exports": int(db.scalar(select(func.count(BlumDatasetExport.id))) or 0),
|
| 229 |
"blum_training_jobs": int(db.scalar(select(func.count(BlumModelTrainingJob.id))) or 0),
|
|
|
|
|
|
|
| 230 |
},
|
| 231 |
"latest_news_created_at": latest_brain,
|
| 232 |
"why_gui_can_look_unchanged": [
|
| 233 |
"Hugging Face serves the previous image until the Docker build finishes successfully.",
|
| 234 |
"The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
|
| 235 |
"Existing snapshots must be regenerated with Run brain or full pipeline after a new deployment.",
|
| 236 |
-
"Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.
|
| 237 |
],
|
| 238 |
}
|
| 239 |
|
|
@@ -243,6 +255,26 @@ def trigger_database_backup() -> dict:
|
|
| 243 |
return backup_embedded_postgres_if_configured(reason="manual_api_trigger")
|
| 244 |
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
@router.get("/brain/status")
|
| 247 |
def financial_brain_status(db: Session = Depends(get_db)) -> dict:
|
| 248 |
return brain_status(db)
|
|
|
|
| 17 |
AIInsight,
|
| 18 |
AccuracySnapshot,
|
| 19 |
Asset,
|
| 20 |
+
AutonomousEngineRun,
|
| 21 |
BlumDatasetExport,
|
| 22 |
BlumKnowledgeGraphEdge,
|
| 23 |
BlumKnowledgeGraphNode,
|
|
|
|
| 34 |
ChartPatternMemory,
|
| 35 |
ConfidenceAdjustment,
|
| 36 |
EmbeddingVector,
|
| 37 |
+
ExternalDatasetSource,
|
| 38 |
FundamentalSnapshot,
|
| 39 |
HistoricalSimilarityCase,
|
| 40 |
IntelligenceReport,
|
|
|
|
| 59 |
)
|
| 60 |
from app.schemas import AssetOut, MarketUpdateRequest, NewsOut, NewsUpdateRequest, SemanticSearchRequest, SignalRunRequest
|
| 61 |
from app.services.accuracy import asset_accuracy_profile, latest_accuracy_snapshot, market_accuracy_overview, run_accuracy_audit, signal_validation_report
|
| 62 |
+
from app.services.autonomous_engine import AutonomousResearchEngine, latest_autonomous_status
|
| 63 |
from app.services.blum_financial_model import (
|
| 64 |
build_training_dataset,
|
| 65 |
capture_ai_insight_reasoning,
|
|
|
|
| 96 |
run_learning_cycle,
|
| 97 |
)
|
| 98 |
from app.services.hybrid_chart_intelligence import HybridChartIntelligence
|
| 99 |
+
from app.services.huggingface_datasets import dataset_catalog_status, refresh_huggingface_dataset_catalog
|
| 100 |
from app.services.ipo import ipo_radar, sec_company_submissions, update_ipo_radar
|
| 101 |
from app.services.live import live_news, market_sentiment
|
| 102 |
from app.services.macro import macro_overview, update_macro_snapshots
|
|
|
|
| 138 |
return {
|
| 139 |
"service": "blum-ai-financial-intelligence",
|
| 140 |
"app_version": settings.app_version,
|
| 141 |
+
"feature_set": "autonomous-dataset-intelligence-engine-v0.8.0",
|
| 142 |
"environment": settings.environment,
|
| 143 |
"generated_at": datetime.utcnow().isoformat(),
|
| 144 |
"hugging_face": {
|
|
|
|
| 151 |
"model_loading_enabled": settings.enable_model_loading,
|
| 152 |
"financial_brain_model_enabled": settings.enable_financial_brain_model,
|
| 153 |
"live_startup_enabled": settings.enable_live_startup,
|
| 154 |
+
"autonomous_engine_enabled": settings.enable_autonomous_engine,
|
| 155 |
+
"autonomous_cycle_minutes": settings.autonomous_cycle_minutes,
|
| 156 |
"yfinance_fallback_enabled": settings.enable_yfinance_fallback,
|
| 157 |
"historical_price_seed_enabled": settings.seed_historical_prices_on_startup,
|
| 158 |
"startup_signal_seed_enabled": settings.seed_signals_on_startup,
|
|
|
|
| 167 |
"chart_vision_min_confidence": settings.chart_vision_min_confidence,
|
| 168 |
"fundamentals_refresh_minutes": settings.fundamentals_refresh_minutes,
|
| 169 |
"macro_refresh_minutes": settings.macro_refresh_minutes,
|
| 170 |
+
"hf_dataset_catalog_enabled": settings.enable_hf_dataset_catalog,
|
| 171 |
+
"hf_dataset_refresh_hours": settings.hf_dataset_refresh_hours,
|
| 172 |
},
|
| 173 |
"persistence": database_persistence_status(),
|
| 174 |
"active_models": {
|
|
|
|
| 197 |
"financial_knowledge_graph": True,
|
| 198 |
"portfolio_scenario": True,
|
| 199 |
"watchlist": True,
|
| 200 |
+
"autonomous_research_engine": True,
|
| 201 |
+
"huggingface_dataset_catalog": True,
|
| 202 |
},
|
| 203 |
"database_counts": {
|
| 204 |
"assets": int(db.scalar(select(func.count(Asset.id))) or 0),
|
|
|
|
| 237 |
"blum_graph_edges": int(db.scalar(select(func.count(BlumKnowledgeGraphEdge.id))) or 0),
|
| 238 |
"blum_dataset_exports": int(db.scalar(select(func.count(BlumDatasetExport.id))) or 0),
|
| 239 |
"blum_training_jobs": int(db.scalar(select(func.count(BlumModelTrainingJob.id))) or 0),
|
| 240 |
+
"external_dataset_sources": int(db.scalar(select(func.count(ExternalDatasetSource.id))) or 0),
|
| 241 |
+
"autonomous_engine_runs": int(db.scalar(select(func.count(AutonomousEngineRun.id))) or 0),
|
| 242 |
},
|
| 243 |
"latest_news_created_at": latest_brain,
|
| 244 |
"why_gui_can_look_unchanged": [
|
| 245 |
"Hugging Face serves the previous image until the Docker build finishes successfully.",
|
| 246 |
"The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
|
| 247 |
"Existing snapshots must be regenerated with Run brain or full pipeline after a new deployment.",
|
| 248 |
+
"Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.8.0.",
|
| 249 |
],
|
| 250 |
}
|
| 251 |
|
|
|
|
| 255 |
return backup_embedded_postgres_if_configured(reason="manual_api_trigger")
|
| 256 |
|
| 257 |
|
| 258 |
+
@router.get("/autonomous/status")
|
| 259 |
+
def autonomous_status(db: Session = Depends(get_db)) -> dict:
|
| 260 |
+
return latest_autonomous_status(db)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
@router.post("/autonomous/run")
|
| 264 |
+
def autonomous_run(db: Session = Depends(get_db)) -> dict:
|
| 265 |
+
return AutonomousResearchEngine().run_cycle(db, trigger="manual_diagnostic")
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@router.get("/datasets/sources")
|
| 269 |
+
def datasets_sources(limit: int = Query(default=80, ge=1, le=200), db: Session = Depends(get_db)) -> dict:
|
| 270 |
+
return dataset_catalog_status(db, limit=limit)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
@router.post("/datasets/refresh")
|
| 274 |
+
def datasets_refresh(db: Session = Depends(get_db)) -> dict:
|
| 275 |
+
return refresh_huggingface_dataset_catalog(db, validate=True)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
@router.get("/brain/status")
|
| 279 |
def financial_brain_status(db: Session = Depends(get_db)) -> dict:
|
| 280 |
return brain_status(db)
|
backend/app/core/config.py
CHANGED
|
@@ -5,7 +5,7 @@ from pydantic_settings import BaseSettings
|
|
| 5 |
|
| 6 |
class Settings(BaseSettings):
|
| 7 |
app_name: str = "Blum AI Financial Intelligence"
|
| 8 |
-
app_version: str = "0.
|
| 9 |
environment: str = Field(default="demo", alias="ENVIRONMENT")
|
| 10 |
database_url: str = Field(
|
| 11 |
default="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum",
|
|
@@ -28,12 +28,14 @@ class Settings(BaseSettings):
|
|
| 28 |
chart_vision_min_confidence: float = Field(default=0.70, alias="CHART_VISION_MIN_CONFIDENCE")
|
| 29 |
default_benchmark: str = Field(default="SPY", alias="BLUM_DEFAULT_BENCHMARK")
|
| 30 |
enable_yfinance_fallback: bool = Field(default=False, alias="BLUM_ENABLE_YFINANCE_FALLBACK")
|
| 31 |
-
max_update_assets: int = Field(default=
|
| 32 |
enable_live_startup: bool = Field(default=True, alias="BLUM_ENABLE_LIVE_STARTUP")
|
|
|
|
|
|
|
| 33 |
seed_historical_prices_on_startup: bool = Field(default=True, alias="BLUM_SEED_HISTORICAL_PRICES_ON_STARTUP")
|
| 34 |
seed_signals_on_startup: bool = Field(default=True, alias="BLUM_SEED_SIGNALS_ON_STARTUP")
|
| 35 |
seed_accuracy_on_startup: bool = Field(default=True, alias="BLUM_SEED_ACCURACY_ON_STARTUP")
|
| 36 |
-
startup_pipeline_limit: int = Field(default=
|
| 37 |
news_refresh_minutes: int = Field(default=10, alias="BLUM_NEWS_REFRESH_MINUTES")
|
| 38 |
market_refresh_minutes: int = Field(default=45, alias="BLUM_MARKET_REFRESH_MINUTES")
|
| 39 |
data_gap_repair_minutes: int = Field(default=180, alias="BLUM_DATA_GAP_REPAIR_MINUTES")
|
|
@@ -48,12 +50,15 @@ class Settings(BaseSettings):
|
|
| 48 |
minimum_history_rows: int = Field(default=220, alias="BLUM_MINIMUM_HISTORY_ROWS")
|
| 49 |
ipo_refresh_minutes: int = Field(default=120, alias="BLUM_IPO_REFRESH_MINUTES")
|
| 50 |
news_fetch_workers: int = Field(default=10, alias="BLUM_NEWS_FETCH_WORKERS")
|
| 51 |
-
max_dynamic_asset_news_feeds: int = Field(default=
|
| 52 |
historical_price_period: str = Field(default="max", alias="BLUM_HISTORICAL_PRICE_PERIOD")
|
| 53 |
refresh_price_period: str = Field(default="6mo", alias="BLUM_REFRESH_PRICE_PERIOD")
|
| 54 |
sec_user_agent: str = Field(default="Blum-AI-Financial-Intelligence research demo", alias="BLUM_SEC_USER_AGENT")
|
| 55 |
blum_model_repository: str = Field(default="Italianhype/Blum", alias="BLUM_MODEL_REPOSITORY")
|
| 56 |
training_export_dir: str = Field(default="/tmp/blum_training_exports", alias="BLUM_TRAINING_EXPORT_DIR")
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
class Config:
|
| 59 |
env_file = ".env"
|
|
|
|
| 5 |
|
| 6 |
class Settings(BaseSettings):
|
| 7 |
app_name: str = "Blum AI Financial Intelligence"
|
| 8 |
+
app_version: str = "0.8.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",
|
|
|
|
| 28 |
chart_vision_min_confidence: float = Field(default=0.70, alias="CHART_VISION_MIN_CONFIDENCE")
|
| 29 |
default_benchmark: str = Field(default="SPY", alias="BLUM_DEFAULT_BENCHMARK")
|
| 30 |
enable_yfinance_fallback: bool = Field(default=False, alias="BLUM_ENABLE_YFINANCE_FALLBACK")
|
| 31 |
+
max_update_assets: int = Field(default=80, 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 |
seed_historical_prices_on_startup: bool = Field(default=True, alias="BLUM_SEED_HISTORICAL_PRICES_ON_STARTUP")
|
| 36 |
seed_signals_on_startup: bool = Field(default=True, alias="BLUM_SEED_SIGNALS_ON_STARTUP")
|
| 37 |
seed_accuracy_on_startup: bool = Field(default=True, alias="BLUM_SEED_ACCURACY_ON_STARTUP")
|
| 38 |
+
startup_pipeline_limit: int = Field(default=80, alias="BLUM_STARTUP_PIPELINE_LIMIT")
|
| 39 |
news_refresh_minutes: int = Field(default=10, alias="BLUM_NEWS_REFRESH_MINUTES")
|
| 40 |
market_refresh_minutes: int = Field(default=45, alias="BLUM_MARKET_REFRESH_MINUTES")
|
| 41 |
data_gap_repair_minutes: int = Field(default=180, alias="BLUM_DATA_GAP_REPAIR_MINUTES")
|
|
|
|
| 50 |
minimum_history_rows: int = Field(default=220, alias="BLUM_MINIMUM_HISTORY_ROWS")
|
| 51 |
ipo_refresh_minutes: int = Field(default=120, alias="BLUM_IPO_REFRESH_MINUTES")
|
| 52 |
news_fetch_workers: int = Field(default=10, alias="BLUM_NEWS_FETCH_WORKERS")
|
| 53 |
+
max_dynamic_asset_news_feeds: int = Field(default=60, alias="BLUM_MAX_DYNAMIC_ASSET_NEWS_FEEDS")
|
| 54 |
historical_price_period: str = Field(default="max", alias="BLUM_HISTORICAL_PRICE_PERIOD")
|
| 55 |
refresh_price_period: str = Field(default="6mo", alias="BLUM_REFRESH_PRICE_PERIOD")
|
| 56 |
sec_user_agent: str = Field(default="Blum-AI-Financial-Intelligence research demo", alias="BLUM_SEC_USER_AGENT")
|
| 57 |
blum_model_repository: str = Field(default="Italianhype/Blum", alias="BLUM_MODEL_REPOSITORY")
|
| 58 |
training_export_dir: str = Field(default="/tmp/blum_training_exports", alias="BLUM_TRAINING_EXPORT_DIR")
|
| 59 |
+
enable_hf_dataset_catalog: bool = Field(default=True, alias="BLUM_ENABLE_HF_DATASET_CATALOG")
|
| 60 |
+
hf_dataset_refresh_hours: int = Field(default=24, alias="BLUM_HF_DATASET_REFRESH_HOURS")
|
| 61 |
+
hf_dataset_max_sources: int = Field(default=40, alias="BLUM_HF_DATASET_MAX_SOURCES")
|
| 62 |
|
| 63 |
class Config:
|
| 64 |
env_file = ".env"
|
backend/app/data/seed_assets.py
CHANGED
|
@@ -69,3 +69,47 @@ SEED_ASSETS = [
|
|
| 69 |
{"ticker": "ENR.DE", "name": "Siemens Energy AG", "category": "Clean Energy", "sector": "Industrials", "industry": "Power Infrastructure", "country": "Germany", "asset_type": "Stock", "currency": "EUR", "exchange": "XETRA", "description": "Grid, turbine and power infrastructure transition exposure."},
|
| 70 |
]
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
{"ticker": "ENR.DE", "name": "Siemens Energy AG", "category": "Clean Energy", "sector": "Industrials", "industry": "Power Infrastructure", "country": "Germany", "asset_type": "Stock", "currency": "EUR", "exchange": "XETRA", "description": "Grid, turbine and power infrastructure transition exposure."},
|
| 70 |
]
|
| 71 |
|
| 72 |
+
SEED_ASSETS.extend([
|
| 73 |
+
{"ticker": "VOO", "name": "Vanguard S&P 500 ETF", "category": "Broad Market", "sector": "US Equity", "industry": "Large Cap Blend", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Low-cost S&P 500 benchmark exposure."},
|
| 74 |
+
{"ticker": "VTI", "name": "Vanguard Total Stock Market ETF", "category": "Broad Market", "sector": "US Equity", "industry": "Total Market", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Total US equity market breadth proxy."},
|
| 75 |
+
{"ticker": "XLI", "name": "Industrial Select Sector SPDR Fund", "category": "Sector", "sector": "Industrials", "industry": "Industrials", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Industrial cyclicality and capex proxy."},
|
| 76 |
+
{"ticker": "XLY", "name": "Consumer Discretionary Select Sector SPDR Fund", "category": "Sector", "sector": "Consumer Discretionary", "industry": "Consumer Discretionary", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Consumer discretionary and risk appetite proxy."},
|
| 77 |
+
{"ticker": "XLP", "name": "Consumer Staples Select Sector SPDR Fund", "category": "Sector", "sector": "Consumer Staples", "industry": "Consumer Staples", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Defensive consumer staples proxy."},
|
| 78 |
+
{"ticker": "XLU", "name": "Utilities Select Sector SPDR Fund", "category": "Sector", "sector": "Utilities", "industry": "Utilities", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Utilities, rates sensitivity and power demand proxy."},
|
| 79 |
+
{"ticker": "XLC", "name": "Communication Services Select Sector SPDR Fund", "category": "Sector", "sector": "Communication Services", "industry": "Media and Platforms", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Communication services sector rotation proxy."},
|
| 80 |
+
{"ticker": "SOXX", "name": "iShares Semiconductor ETF", "category": "Semiconductors", "sector": "Technology", "industry": "Semiconductors", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NASDAQ", "description": "Semiconductor ETF confirmation source."},
|
| 81 |
+
{"ticker": "IGV", "name": "iShares Expanded Tech-Software Sector ETF", "category": "Software", "sector": "Technology", "industry": "Software", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "Cboe", "description": "Software sector confirmation source."},
|
| 82 |
+
{"ticker": "CIBR", "name": "First Trust Nasdaq Cybersecurity ETF", "category": "Cyber Security", "sector": "Technology", "industry": "Cyber Security", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NASDAQ", "description": "Cybersecurity thematic confirmation ETF."},
|
| 83 |
+
{"ticker": "URA", "name": "Global X Uranium ETF", "category": "Nuclear Energy", "sector": "Energy", "industry": "Uranium and Nuclear", "country": "United States", "asset_type": "ETF", "currency": "USD", "exchange": "NYSE Arca", "description": "Uranium and nuclear power thematic proxy."},
|
| 84 |
+
{"ticker": "ORCL", "name": "Oracle Corporation", "category": "AI Infrastructure", "sector": "Technology", "industry": "Cloud Software", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Cloud infrastructure, database and enterprise AI workload exposure."},
|
| 85 |
+
{"ticker": "CRM", "name": "Salesforce Inc.", "category": "Software", "sector": "Technology", "industry": "Enterprise Software", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Enterprise software and AI-enabled workflow platform."},
|
| 86 |
+
{"ticker": "NOW", "name": "ServiceNow Inc.", "category": "Software", "sector": "Technology", "industry": "Workflow Software", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Enterprise workflow automation and AI software exposure."},
|
| 87 |
+
{"ticker": "PLTR", "name": "Palantir Technologies Inc.", "category": "AI", "sector": "Technology", "industry": "Data Analytics Software", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "AI data platform, defense and commercial analytics exposure."},
|
| 88 |
+
{"ticker": "NFLX", "name": "Netflix Inc.", "category": "Growth", "sector": "Communication Services", "industry": "Streaming Media", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Streaming media, advertising tier and global content platform."},
|
| 89 |
+
{"ticker": "SMCI", "name": "Super Micro Computer Inc.", "category": "AI Infrastructure", "sector": "Technology", "industry": "AI Servers", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "AI server infrastructure and data center buildout exposure."},
|
| 90 |
+
{"ticker": "MU", "name": "Micron Technology Inc.", "category": "Semiconductors", "sector": "Technology", "industry": "Memory Semiconductors", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Memory cycle and high-bandwidth memory AI exposure."},
|
| 91 |
+
{"ticker": "QCOM", "name": "Qualcomm Inc.", "category": "Semiconductors", "sector": "Technology", "industry": "Wireless Semiconductors", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Mobile, edge AI and wireless semiconductor exposure."},
|
| 92 |
+
{"ticker": "AMAT", "name": "Applied Materials Inc.", "category": "Semiconductors", "sector": "Technology", "industry": "Semiconductor Equipment", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Wafer fabrication equipment and semiconductor capex proxy."},
|
| 93 |
+
{"ticker": "LRCX", "name": "Lam Research Corporation", "category": "Semiconductors", "sector": "Technology", "industry": "Semiconductor Equipment", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Etch and deposition equipment exposure."},
|
| 94 |
+
{"ticker": "KLAC", "name": "KLA Corporation", "category": "Semiconductors", "sector": "Technology", "industry": "Semiconductor Equipment", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Process control and inspection equipment exposure."},
|
| 95 |
+
{"ticker": "INTC", "name": "Intel Corporation", "category": "Semiconductors", "sector": "Technology", "industry": "Integrated Semiconductors", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Foundry turnaround, CPUs and domestic semiconductor policy exposure."},
|
| 96 |
+
{"ticker": "IBM", "name": "International Business Machines Corporation", "category": "AI Infrastructure", "sector": "Technology", "industry": "Hybrid Cloud and Consulting", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Hybrid cloud, enterprise AI and consulting exposure."},
|
| 97 |
+
{"ticker": "PANW", "name": "Palo Alto Networks Inc.", "category": "Cyber Security", "sector": "Technology", "industry": "Cyber Security", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Enterprise cybersecurity platform leader."},
|
| 98 |
+
{"ticker": "CRWD", "name": "CrowdStrike Holdings Inc.", "category": "Cyber Security", "sector": "Technology", "industry": "Endpoint Security", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Cloud-native endpoint and security operations platform."},
|
| 99 |
+
{"ticker": "UBER", "name": "Uber Technologies Inc.", "category": "Growth", "sector": "Industrials", "industry": "Mobility and Delivery", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Mobility, delivery and platform operating leverage exposure."},
|
| 100 |
+
{"ticker": "COST", "name": "Costco Wholesale Corporation", "category": "Quality", "sector": "Consumer Staples", "industry": "Warehouse Retail", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "High-quality consumer staples and membership retail proxy."},
|
| 101 |
+
{"ticker": "WMT", "name": "Walmart Inc.", "category": "Quality", "sector": "Consumer Staples", "industry": "Retail", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Defensive retail, consumer health and scale logistics proxy."},
|
| 102 |
+
{"ticker": "HD", "name": "Home Depot Inc.", "category": "Housing", "sector": "Consumer Discretionary", "industry": "Home Improvement Retail", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Housing, repair/remodel and consumer discretionary proxy."},
|
| 103 |
+
{"ticker": "BA", "name": "Boeing Company", "category": "Aerospace", "sector": "Industrials", "industry": "Aerospace", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Commercial aerospace and defense manufacturing exposure."},
|
| 104 |
+
{"ticker": "CAT", "name": "Caterpillar Inc.", "category": "Industrials", "sector": "Industrials", "industry": "Machinery", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Global machinery, construction, mining and infrastructure proxy."},
|
| 105 |
+
{"ticker": "RTX", "name": "RTX Corporation", "category": "Defense", "sector": "Industrials", "industry": "Aerospace and Defense", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Defense systems and commercial aerospace supplier."},
|
| 106 |
+
{"ticker": "LMT", "name": "Lockheed Martin Corporation", "category": "Defense", "sector": "Industrials", "industry": "Defense", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Defense prime contractor and aerospace systems leader."},
|
| 107 |
+
{"ticker": "NOC", "name": "Northrop Grumman Corporation", "category": "Defense", "sector": "Industrials", "industry": "Defense", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Defense, space and strategic systems exposure."},
|
| 108 |
+
{"ticker": "UNH", "name": "UnitedHealth Group Incorporated", "category": "Healthcare", "sector": "Healthcare", "industry": "Managed Care", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Managed care and health services bellwether."},
|
| 109 |
+
{"ticker": "MRK", "name": "Merck & Co. Inc.", "category": "Healthcare", "sector": "Healthcare", "industry": "Pharmaceuticals", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Pharmaceutical innovation and oncology exposure."},
|
| 110 |
+
{"ticker": "ISRG", "name": "Intuitive Surgical Inc.", "category": "Healthcare Innovation", "sector": "Healthcare", "industry": "Medical Devices", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NASDAQ", "description": "Robotic surgery and medical device innovation exposure."},
|
| 111 |
+
{"ticker": "V", "name": "Visa Inc.", "category": "Quality", "sector": "Financials", "industry": "Payments", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Global card network and payments volume proxy."},
|
| 112 |
+
{"ticker": "MA", "name": "Mastercard Incorporated", "category": "Quality", "sector": "Financials", "industry": "Payments", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Global payments network and consumer spending proxy."},
|
| 113 |
+
{"ticker": "BAC", "name": "Bank of America Corporation", "category": "Value", "sector": "Financials", "industry": "Banks", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Rates, credit and US banking cycle exposure."},
|
| 114 |
+
{"ticker": "GS", "name": "Goldman Sachs Group Inc.", "category": "Capital Markets", "sector": "Financials", "industry": "Investment Banking", "country": "United States", "asset_type": "Stock", "currency": "USD", "exchange": "NYSE", "description": "Capital markets, deal activity and institutional risk appetite proxy."},
|
| 115 |
+
])
|
backend/app/models.py
CHANGED
|
@@ -955,3 +955,53 @@ class BlumModelTrainingJob(Base):
|
|
| 955 |
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)
|
| 956 |
|
| 957 |
dataset_export = relationship("BlumDatasetExport")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 955 |
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)
|
| 956 |
|
| 957 |
dataset_export = relationship("BlumDatasetExport")
|
| 958 |
+
|
| 959 |
+
|
| 960 |
+
class ExternalDatasetSource(Base):
|
| 961 |
+
__tablename__ = "external_dataset_sources"
|
| 962 |
+
__table_args__ = (
|
| 963 |
+
UniqueConstraint("dataset_id", name="uq_external_dataset_source_dataset_id"),
|
| 964 |
+
Index("ix_external_dataset_sources_status_priority", "status", "priority"),
|
| 965 |
+
Index("ix_external_dataset_sources_domain_updated", "primary_domain", "updated_at"),
|
| 966 |
+
)
|
| 967 |
+
|
| 968 |
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
| 969 |
+
dataset_id: Mapped[str] = mapped_column(String(220), unique=True, index=True)
|
| 970 |
+
provider: Mapped[str] = mapped_column(String(80), default="hugging_face", index=True)
|
| 971 |
+
title: Mapped[str] = mapped_column(String(260), default="")
|
| 972 |
+
primary_domain: Mapped[str] = mapped_column(String(80), default="market_data", index=True)
|
| 973 |
+
data_domains: Mapped[dict] = mapped_column(JsonType, default=dict)
|
| 974 |
+
license: Mapped[str] = mapped_column(String(80), default="unknown", index=True)
|
| 975 |
+
priority: Mapped[int] = mapped_column(Integer, default=50, index=True)
|
| 976 |
+
ingestion_mode: Mapped[str] = mapped_column(String(80), default="catalog_only", index=True)
|
| 977 |
+
status: Mapped[str] = mapped_column(String(80), default="discovered", index=True)
|
| 978 |
+
dataset_url: Mapped[str] = mapped_column(Text, default="")
|
| 979 |
+
viewer_status: Mapped[dict] = mapped_column(JsonType, default=dict)
|
| 980 |
+
parquet_files: Mapped[dict] = mapped_column(JsonType, default=dict)
|
| 981 |
+
size_summary: Mapped[dict] = mapped_column(JsonType, default=dict)
|
| 982 |
+
usage_policy: Mapped[dict] = mapped_column(JsonType, default=dict)
|
| 983 |
+
last_checked_at: Mapped[datetime | None] = mapped_column(DateTime, index=True)
|
| 984 |
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
|
| 985 |
+
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)
|
| 986 |
+
|
| 987 |
+
|
| 988 |
+
class AutonomousEngineRun(Base):
|
| 989 |
+
__tablename__ = "autonomous_engine_runs"
|
| 990 |
+
__table_args__ = (
|
| 991 |
+
UniqueConstraint("run_id", name="uq_autonomous_engine_run_id"),
|
| 992 |
+
Index("ix_autonomous_engine_runs_status_started", "status", "started_at"),
|
| 993 |
+
)
|
| 994 |
+
|
| 995 |
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
| 996 |
+
run_id: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
| 997 |
+
trigger: Mapped[str] = mapped_column(String(80), default="scheduled", index=True)
|
| 998 |
+
status: Mapped[str] = mapped_column(String(80), default="running", index=True)
|
| 999 |
+
started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
|
| 1000 |
+
completed_at: Mapped[datetime | None] = mapped_column(DateTime, index=True)
|
| 1001 |
+
stage_results: Mapped[dict] = mapped_column(JsonType, default=dict)
|
| 1002 |
+
readiness_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
|
| 1003 |
+
data_coverage_score: Mapped[float] = mapped_column(Float, default=0.0, index=True)
|
| 1004 |
+
reasoning_memory_created: Mapped[int] = mapped_column(Integer, default=0)
|
| 1005 |
+
warning_count: Mapped[int] = mapped_column(Integer, default=0)
|
| 1006 |
+
error_payload: Mapped[dict] = mapped_column(JsonType, default=dict)
|
| 1007 |
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
|
backend/app/services/autonomous_engine.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
import traceback
|
| 5 |
+
from uuid import uuid4
|
| 6 |
+
|
| 7 |
+
from sqlalchemy import func, select
|
| 8 |
+
from sqlalchemy.orm import Session
|
| 9 |
+
|
| 10 |
+
from app.core.config import get_settings
|
| 11 |
+
from app.ingestion.news_ingestor import NewsIngestor
|
| 12 |
+
from app.models import (
|
| 13 |
+
Asset,
|
| 14 |
+
AutonomousEngineRun,
|
| 15 |
+
BlumKnowledgeRecord,
|
| 16 |
+
ExternalDatasetSource,
|
| 17 |
+
LearningEvent,
|
| 18 |
+
NewsArticle,
|
| 19 |
+
PriceHistory,
|
| 20 |
+
SignalSnapshot,
|
| 21 |
+
)
|
| 22 |
+
from app.services.accuracy import run_accuracy_audit
|
| 23 |
+
from app.services.blum_financial_model import run_model_learning_cycle
|
| 24 |
+
from app.services.etf import update_etf_trends
|
| 25 |
+
from app.services.fundamentals import update_fundamentals
|
| 26 |
+
from app.services.huggingface_datasets import dataset_catalog_status, refresh_huggingface_dataset_catalog
|
| 27 |
+
from app.services.ipo import update_ipo_radar
|
| 28 |
+
from app.services.macro import update_macro_snapshots
|
| 29 |
+
from app.services.market_data import MarketDataService
|
| 30 |
+
from app.services.persistence import backup_embedded_postgres_if_configured
|
| 31 |
+
from app.signals.engine import SignalEngine
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
settings = get_settings()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class AutonomousResearchEngine:
|
| 38 |
+
"""Runs Blum's server-side intelligence cycle in a strict, auditable sequence."""
|
| 39 |
+
|
| 40 |
+
def run_cycle(self, db: Session, trigger: str = "scheduled") -> dict:
|
| 41 |
+
run_id = f"auto-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:8]}"
|
| 42 |
+
stage_results: dict[str, dict] = {}
|
| 43 |
+
started_at = datetime.utcnow()
|
| 44 |
+
|
| 45 |
+
def stage(name: str, work) -> dict:
|
| 46 |
+
stage_started = datetime.utcnow()
|
| 47 |
+
try:
|
| 48 |
+
result = work()
|
| 49 |
+
payload = {
|
| 50 |
+
"status": "ok",
|
| 51 |
+
"started_at": stage_started.isoformat(),
|
| 52 |
+
"completed_at": datetime.utcnow().isoformat(),
|
| 53 |
+
"result": result or {},
|
| 54 |
+
}
|
| 55 |
+
except Exception as exc:
|
| 56 |
+
payload = {
|
| 57 |
+
"status": "error",
|
| 58 |
+
"started_at": stage_started.isoformat(),
|
| 59 |
+
"completed_at": datetime.utcnow().isoformat(),
|
| 60 |
+
"error": f"{type(exc).__name__}: {exc}",
|
| 61 |
+
"traceback": traceback.format_exc(limit=4),
|
| 62 |
+
}
|
| 63 |
+
stage_results[name] = payload
|
| 64 |
+
return payload
|
| 65 |
+
|
| 66 |
+
stage("hf_dataset_catalog", lambda: self.refresh_dataset_catalog_if_needed(db))
|
| 67 |
+
stage("macro_context", lambda: update_macro_snapshots(db))
|
| 68 |
+
stage("fundamentals", lambda: update_fundamentals(db, limit=min(settings.max_update_assets, 32)))
|
| 69 |
+
stage("historical_prices", lambda: MarketDataService().update_prices(db, period=self.price_period(trigger), limit=settings.max_update_assets))
|
| 70 |
+
stage("news_sentiment", lambda: NewsIngestor().update_news(db, lookback_hours=96, limit_per_feed=45))
|
| 71 |
+
stage("signals", lambda: SignalEngine().run(db, limit=settings.max_update_assets))
|
| 72 |
+
stage("etf_intelligence", lambda: update_etf_trends(db))
|
| 73 |
+
stage("ipo_radar", lambda: update_ipo_radar(db, limit_per_form=55))
|
| 74 |
+
stage("accuracy_audit", lambda: run_accuracy_audit(db, limit=settings.max_update_assets))
|
| 75 |
+
if settings.enable_learning_loop:
|
| 76 |
+
stage("blum_financial_model", lambda: run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit))
|
| 77 |
+
stage("persistence_backup", lambda: backup_embedded_postgres_if_configured(reason="autonomous_engine_cycle"))
|
| 78 |
+
|
| 79 |
+
readiness = self.readiness(db, stage_results)
|
| 80 |
+
status = "ok" if readiness["warning_count"] == 0 else "degraded"
|
| 81 |
+
if any(item.get("status") == "error" for item in stage_results.values()):
|
| 82 |
+
status = "degraded"
|
| 83 |
+
run = AutonomousEngineRun(
|
| 84 |
+
run_id=run_id,
|
| 85 |
+
trigger=trigger,
|
| 86 |
+
status=status,
|
| 87 |
+
started_at=started_at,
|
| 88 |
+
completed_at=datetime.utcnow(),
|
| 89 |
+
stage_results=stage_results,
|
| 90 |
+
readiness_score=readiness["readiness_score"],
|
| 91 |
+
data_coverage_score=readiness["data_coverage_score"],
|
| 92 |
+
reasoning_memory_created=readiness["reasoning_memory_created"],
|
| 93 |
+
warning_count=readiness["warning_count"],
|
| 94 |
+
error_payload=readiness["errors"],
|
| 95 |
+
)
|
| 96 |
+
db.add(run)
|
| 97 |
+
db.add(
|
| 98 |
+
LearningEvent(
|
| 99 |
+
event_type="autonomous_research_engine_cycle",
|
| 100 |
+
severity="Info" if status == "ok" else "Warning",
|
| 101 |
+
title="Autonomous Blum research cycle completed",
|
| 102 |
+
description="Blum executed the full research pipeline without manual input.",
|
| 103 |
+
payload={"run_id": run_id, "trigger": trigger, "status": status, "readiness": readiness, "stage_results": stage_results},
|
| 104 |
+
)
|
| 105 |
+
)
|
| 106 |
+
db.commit()
|
| 107 |
+
return {"run_id": run_id, "trigger": trigger, "status": status, "readiness": readiness, "stage_results": stage_results}
|
| 108 |
+
|
| 109 |
+
def refresh_dataset_catalog_if_needed(self, db: Session) -> dict:
|
| 110 |
+
if not settings.enable_hf_dataset_catalog:
|
| 111 |
+
return {"status": "disabled"}
|
| 112 |
+
latest = db.scalar(select(func.max(ExternalDatasetSource.updated_at)))
|
| 113 |
+
if latest and latest >= datetime.utcnow() - timedelta(hours=settings.hf_dataset_refresh_hours):
|
| 114 |
+
return {"status": "fresh", "catalog": dataset_catalog_status(db, limit=settings.hf_dataset_max_sources)}
|
| 115 |
+
return refresh_huggingface_dataset_catalog(db, validate=True)
|
| 116 |
+
|
| 117 |
+
def price_period(self, trigger: str) -> str:
|
| 118 |
+
if trigger in {"startup", "manual"}:
|
| 119 |
+
return settings.historical_price_period
|
| 120 |
+
return settings.refresh_price_period
|
| 121 |
+
|
| 122 |
+
def readiness(self, db: Session, stage_results: dict) -> dict:
|
| 123 |
+
active_assets = int(db.scalar(select(func.count(Asset.id)).where(Asset.is_active.is_(True))) or 0)
|
| 124 |
+
priced_assets = int(db.scalar(select(func.count(func.distinct(PriceHistory.asset_id)))) or 0)
|
| 125 |
+
signal_count = int(db.scalar(select(func.count(SignalSnapshot.id))) or 0)
|
| 126 |
+
news_count = int(db.scalar(select(func.count(NewsArticle.id))) or 0)
|
| 127 |
+
reasoning_records = int(db.scalar(select(func.count(BlumKnowledgeRecord.id))) or 0)
|
| 128 |
+
dataset_sources = int(db.scalar(select(func.count(ExternalDatasetSource.id))) or 0)
|
| 129 |
+
coverage = (priced_assets / active_assets * 100) if active_assets else 0.0
|
| 130 |
+
evidence_components = [
|
| 131 |
+
min(100.0, coverage),
|
| 132 |
+
100.0 if signal_count > 0 else 0.0,
|
| 133 |
+
min(100.0, news_count / 100 * 100),
|
| 134 |
+
min(100.0, reasoning_records / 100 * 100),
|
| 135 |
+
min(100.0, dataset_sources / max(1, settings.hf_dataset_max_sources) * 100),
|
| 136 |
+
]
|
| 137 |
+
errors = {name: result for name, result in stage_results.items() if result.get("status") == "error"}
|
| 138 |
+
warnings = sum(1 for result in stage_results.values() if result.get("status") != "ok")
|
| 139 |
+
model_stage = stage_results.get("blum_financial_model", {}).get("result", {})
|
| 140 |
+
return {
|
| 141 |
+
"active_assets": active_assets,
|
| 142 |
+
"priced_assets": priced_assets,
|
| 143 |
+
"signal_count": signal_count,
|
| 144 |
+
"news_count": news_count,
|
| 145 |
+
"reasoning_records": reasoning_records,
|
| 146 |
+
"dataset_sources": dataset_sources,
|
| 147 |
+
"data_coverage_score": round(coverage, 2),
|
| 148 |
+
"readiness_score": round(sum(evidence_components) / len(evidence_components), 2),
|
| 149 |
+
"reasoning_memory_created": int(model_stage.get("knowledge_records_created", 0) or 0),
|
| 150 |
+
"warning_count": warnings,
|
| 151 |
+
"errors": errors,
|
| 152 |
+
"policy": "Autonomous research improves evidence quality and calibration; it does not guarantee market outperformance or execute trades.",
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def latest_autonomous_status(db: Session) -> dict:
|
| 157 |
+
latest = db.scalar(select(AutonomousEngineRun).order_by(AutonomousEngineRun.started_at.desc()).limit(1))
|
| 158 |
+
runs = db.scalars(select(AutonomousEngineRun).order_by(AutonomousEngineRun.started_at.desc()).limit(20)).all()
|
| 159 |
+
return {
|
| 160 |
+
"enabled": settings.enable_autonomous_engine,
|
| 161 |
+
"cycle_minutes": settings.autonomous_cycle_minutes,
|
| 162 |
+
"latest_run": serialize_run(latest) if latest else None,
|
| 163 |
+
"recent_runs": [serialize_run(run) for run in runs],
|
| 164 |
+
"dataset_catalog": dataset_catalog_status(db, limit=settings.hf_dataset_max_sources),
|
| 165 |
+
"policy": "Blum runs sequential server-side research cycles. Manual buttons are optional diagnostics, not required for normal operation.",
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def serialize_run(run: AutonomousEngineRun) -> dict:
|
| 170 |
+
return {
|
| 171 |
+
"run_id": run.run_id,
|
| 172 |
+
"trigger": run.trigger,
|
| 173 |
+
"status": run.status,
|
| 174 |
+
"started_at": run.started_at.isoformat() if run.started_at else None,
|
| 175 |
+
"completed_at": run.completed_at.isoformat() if run.completed_at else None,
|
| 176 |
+
"readiness_score": run.readiness_score,
|
| 177 |
+
"data_coverage_score": run.data_coverage_score,
|
| 178 |
+
"reasoning_memory_created": run.reasoning_memory_created,
|
| 179 |
+
"warning_count": run.warning_count,
|
| 180 |
+
"stage_results": run.stage_results,
|
| 181 |
+
"error_payload": run.error_payload,
|
| 182 |
+
}
|
backend/app/services/fundamentals.py
CHANGED
|
@@ -28,6 +28,37 @@ CIK_MAP = {
|
|
| 28 |
"NVO": "0000353278",
|
| 29 |
"ASML": "0000937966",
|
| 30 |
"SAP": "0001000184",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
SEC_METRICS = {
|
|
|
|
| 28 |
"NVO": "0000353278",
|
| 29 |
"ASML": "0000937966",
|
| 30 |
"SAP": "0001000184",
|
| 31 |
+
"ORCL": "0001341439",
|
| 32 |
+
"CRM": "0001108524",
|
| 33 |
+
"NOW": "0001373715",
|
| 34 |
+
"PLTR": "0001321655",
|
| 35 |
+
"NFLX": "0001065280",
|
| 36 |
+
"SMCI": "0001375365",
|
| 37 |
+
"MU": "0000723125",
|
| 38 |
+
"QCOM": "0000804328",
|
| 39 |
+
"AMAT": "0000006951",
|
| 40 |
+
"LRCX": "0000707549",
|
| 41 |
+
"KLAC": "0000319201",
|
| 42 |
+
"INTC": "0000050863",
|
| 43 |
+
"IBM": "0000051143",
|
| 44 |
+
"PANW": "0001327567",
|
| 45 |
+
"CRWD": "0001535527",
|
| 46 |
+
"UBER": "0001543151",
|
| 47 |
+
"COST": "0000909832",
|
| 48 |
+
"WMT": "0000104169",
|
| 49 |
+
"HD": "0000354950",
|
| 50 |
+
"BA": "0000012927",
|
| 51 |
+
"CAT": "0000018230",
|
| 52 |
+
"RTX": "0000101829",
|
| 53 |
+
"LMT": "0000936468",
|
| 54 |
+
"NOC": "0001133421",
|
| 55 |
+
"UNH": "0000731766",
|
| 56 |
+
"MRK": "0000310158",
|
| 57 |
+
"ISRG": "0001035267",
|
| 58 |
+
"V": "0001403161",
|
| 59 |
+
"MA": "0001141391",
|
| 60 |
+
"BAC": "0000070858",
|
| 61 |
+
"GS": "0000886982",
|
| 62 |
}
|
| 63 |
|
| 64 |
SEC_METRICS = {
|
backend/app/services/huggingface_datasets.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from urllib.parse import quote
|
| 5 |
+
|
| 6 |
+
import requests
|
| 7 |
+
from sqlalchemy import desc, select
|
| 8 |
+
from sqlalchemy.orm import Session
|
| 9 |
+
|
| 10 |
+
from app.models import ExternalDatasetSource, LearningEvent
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
DATASETS_SERVER = "https://datasets-server.huggingface.co"
|
| 14 |
+
HF_DATASET_URL = "https://huggingface.co/datasets"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
CURATED_HF_DATASETS = [
|
| 18 |
+
{
|
| 19 |
+
"dataset_id": "defeatbeta/yahoo-finance-data",
|
| 20 |
+
"title": "Yahoo Finance data, stock news and earnings call transcripts",
|
| 21 |
+
"primary_domain": "multi_source_finance",
|
| 22 |
+
"data_domains": ["market_data", "stock_news", "earnings_transcripts", "treasury_data"],
|
| 23 |
+
"license": "odc-by",
|
| 24 |
+
"priority": 98,
|
| 25 |
+
"ingestion_mode": "metadata_and_incremental_evidence",
|
| 26 |
+
"usage_policy": "Public dataset metadata is cataloged automatically. Large table ingestion must remain incremental and evidence-tracked.",
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"dataset_id": "paperswithbacktest/Stocks-Daily-Price",
|
| 30 |
+
"title": "Daily price data for 7000+ US stocks",
|
| 31 |
+
"primary_domain": "historical_prices",
|
| 32 |
+
"data_domains": ["daily_ohlcv", "us_equities", "backtest_history"],
|
| 33 |
+
"license": "other",
|
| 34 |
+
"priority": 94,
|
| 35 |
+
"ingestion_mode": "metadata_first_price_backfill_candidate",
|
| 36 |
+
"usage_policy": "Use as a candidate for historical OHLCV backfill after schema validation. No synthetic prices are created.",
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"dataset_id": "TeraflopAI/SEC-EDGAR",
|
| 40 |
+
"title": "Large-scale SEC EDGAR filings corpus",
|
| 41 |
+
"primary_domain": "filings",
|
| 42 |
+
"data_domains": ["sec_filings", "10k", "10q", "risk_factors", "management_discussion"],
|
| 43 |
+
"license": "apache-2.0",
|
| 44 |
+
"priority": 92,
|
| 45 |
+
"ingestion_mode": "catalog_and_targeted_retrieval",
|
| 46 |
+
"usage_policy": "Very large corpus. The Space should retrieve targeted company evidence rather than materializing the full corpus.",
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"dataset_id": "kurry/sp500_earnings_transcripts",
|
| 50 |
+
"title": "S&P 500 and US large-cap earnings transcripts from 2005 to 2025",
|
| 51 |
+
"primary_domain": "earnings_transcripts",
|
| 52 |
+
"data_domains": ["earnings_calls", "management_language", "guidance", "sentiment"],
|
| 53 |
+
"license": "mit",
|
| 54 |
+
"priority": 90,
|
| 55 |
+
"ingestion_mode": "metadata_first_transcript_retrieval",
|
| 56 |
+
"usage_policy": "Use for company narrative history and management tone. Respect dataset license and avoid fabricating missing transcripts.",
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"dataset_id": "glopardo/sp500-earnings-transcripts",
|
| 60 |
+
"title": "S&P 500 earnings call transcripts and quarterly context",
|
| 61 |
+
"primary_domain": "earnings_transcripts",
|
| 62 |
+
"data_domains": ["earnings_calls", "question_answering", "summarization", "retrieval"],
|
| 63 |
+
"license": "unknown",
|
| 64 |
+
"priority": 82,
|
| 65 |
+
"ingestion_mode": "metadata_first_transcript_retrieval",
|
| 66 |
+
"usage_policy": "Secondary transcript source for cross-source validation.",
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"dataset_id": "paperswithbacktest/Stocks-Quarterly-Earnings",
|
| 70 |
+
"title": "Quarterly earnings reports for 7000+ US stocks",
|
| 71 |
+
"primary_domain": "fundamentals",
|
| 72 |
+
"data_domains": ["quarterly_earnings", "fundamentals", "us_equities"],
|
| 73 |
+
"license": "other",
|
| 74 |
+
"priority": 78,
|
| 75 |
+
"ingestion_mode": "gated_metadata_only",
|
| 76 |
+
"usage_policy": "Gated source. Catalog only unless access is explicitly available.",
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"dataset_id": "c3po-ai/edgar-corpus",
|
| 80 |
+
"title": "Annual SEC 10-K filing corpus from 1993 to 2020",
|
| 81 |
+
"primary_domain": "filings",
|
| 82 |
+
"data_domains": ["10k", "sec_filings", "long_document_understanding"],
|
| 83 |
+
"license": "apache-2.0",
|
| 84 |
+
"priority": 75,
|
| 85 |
+
"ingestion_mode": "catalog_and_targeted_retrieval",
|
| 86 |
+
"usage_policy": "Historical filing text source for long-term company memory.",
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"dataset_id": "PatronusAI/financebench",
|
| 90 |
+
"title": "FinanceBench open-book financial QA benchmark",
|
| 91 |
+
"primary_domain": "reasoning_benchmark",
|
| 92 |
+
"data_domains": ["financial_qa", "reasoning_evaluation", "open_book_benchmark"],
|
| 93 |
+
"license": "cc-by-nc-4.0",
|
| 94 |
+
"priority": 68,
|
| 95 |
+
"ingestion_mode": "evaluation_only",
|
| 96 |
+
"usage_policy": "Use for evaluation ideas and benchmark structure, not for commercial training unless license allows it.",
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"dataset_id": "BUPT-Reasoning-Lab/FinanceReasoning",
|
| 100 |
+
"title": "Finance reasoning dataset",
|
| 101 |
+
"primary_domain": "reasoning_benchmark",
|
| 102 |
+
"data_domains": ["financial_reasoning", "thesis_quality", "reasoning_evaluation"],
|
| 103 |
+
"license": "cc-by-4.0",
|
| 104 |
+
"priority": 66,
|
| 105 |
+
"ingestion_mode": "evaluation_only",
|
| 106 |
+
"usage_policy": "Use to benchmark reasoning style; Blum proprietary reasoning remains separate.",
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"dataset_id": "jlh-ibm/earnings_call",
|
| 110 |
+
"title": "Earnings call transcripts with stock and sector price context",
|
| 111 |
+
"primary_domain": "earnings_transcripts",
|
| 112 |
+
"data_domains": ["earnings_calls", "stock_prices", "sector_index"],
|
| 113 |
+
"license": "cc0-1.0",
|
| 114 |
+
"priority": 64,
|
| 115 |
+
"ingestion_mode": "metadata_first_small_reference",
|
| 116 |
+
"usage_policy": "Useful as a small validation corpus for transcript and price-link logic.",
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"dataset_id": "younginpiniti/us-stocks-daily-all",
|
| 120 |
+
"title": "US stocks daily historical dataset",
|
| 121 |
+
"primary_domain": "historical_prices",
|
| 122 |
+
"data_domains": ["daily_ohlcv", "us_equities"],
|
| 123 |
+
"license": "unknown",
|
| 124 |
+
"priority": 63,
|
| 125 |
+
"ingestion_mode": "metadata_first_price_backfill_candidate",
|
| 126 |
+
"usage_policy": "Candidate source for redundant daily OHLCV validation after schema inspection.",
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"dataset_id": "sfd-anonymous/edgar-forecast-benchmark",
|
| 130 |
+
"title": "EDGAR grounded numerical forecasting benchmark",
|
| 131 |
+
"primary_domain": "reasoning_benchmark",
|
| 132 |
+
"data_domains": ["sec_filings", "forecasting_benchmark", "numerical_reasoning"],
|
| 133 |
+
"license": "cc-by-4.0",
|
| 134 |
+
"priority": 60,
|
| 135 |
+
"ingestion_mode": "evaluation_only",
|
| 136 |
+
"usage_policy": "Use to evaluate grounded reasoning, not as direct trading signal evidence.",
|
| 137 |
+
},
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def refresh_huggingface_dataset_catalog(db: Session, validate: bool = True) -> dict:
|
| 142 |
+
upserted = 0
|
| 143 |
+
validation = []
|
| 144 |
+
for item in CURATED_HF_DATASETS:
|
| 145 |
+
dataset_id = item["dataset_id"]
|
| 146 |
+
status_payload = validate_dataset(dataset_id) if validate else {"status": "not_checked"}
|
| 147 |
+
source = db.scalar(select(ExternalDatasetSource).where(ExternalDatasetSource.dataset_id == dataset_id))
|
| 148 |
+
payload = {
|
| 149 |
+
"provider": "hugging_face",
|
| 150 |
+
"title": item["title"],
|
| 151 |
+
"primary_domain": item["primary_domain"],
|
| 152 |
+
"data_domains": {"items": item["data_domains"]},
|
| 153 |
+
"license": item["license"],
|
| 154 |
+
"priority": item["priority"],
|
| 155 |
+
"ingestion_mode": item["ingestion_mode"],
|
| 156 |
+
"status": source_status(status_payload),
|
| 157 |
+
"dataset_url": f"{HF_DATASET_URL}/{dataset_id}",
|
| 158 |
+
"viewer_status": status_payload,
|
| 159 |
+
"parquet_files": status_payload.get("parquet_files", {}),
|
| 160 |
+
"size_summary": status_payload.get("size", {}),
|
| 161 |
+
"usage_policy": {"policy": item["usage_policy"], "no_synthetic_data": True},
|
| 162 |
+
"last_checked_at": datetime.utcnow(),
|
| 163 |
+
}
|
| 164 |
+
if source is None:
|
| 165 |
+
source = ExternalDatasetSource(dataset_id=dataset_id, **payload)
|
| 166 |
+
db.add(source)
|
| 167 |
+
else:
|
| 168 |
+
for key, value in payload.items():
|
| 169 |
+
setattr(source, key, value)
|
| 170 |
+
upserted += 1
|
| 171 |
+
validation.append({"dataset_id": dataset_id, "status": payload["status"], "priority": item["priority"]})
|
| 172 |
+
db.add(
|
| 173 |
+
LearningEvent(
|
| 174 |
+
event_type="huggingface_dataset_catalog_refresh",
|
| 175 |
+
severity="Info",
|
| 176 |
+
title="Hugging Face financial dataset catalog refreshed",
|
| 177 |
+
description="Blum refreshed its catalog of real public datasets for market data, filings, earnings and reasoning benchmarks.",
|
| 178 |
+
payload={"sources_seen": len(CURATED_HF_DATASETS), "validation": validation},
|
| 179 |
+
)
|
| 180 |
+
)
|
| 181 |
+
db.commit()
|
| 182 |
+
return {"status": "ok", "sources_upserted": upserted, "validation": validation}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def dataset_catalog_status(db: Session, limit: int = 80) -> dict:
|
| 186 |
+
rows = db.scalars(select(ExternalDatasetSource).order_by(ExternalDatasetSource.priority.desc(), desc(ExternalDatasetSource.updated_at)).limit(limit)).all()
|
| 187 |
+
ready = [row for row in rows if row.status in {"viewer_ready", "metadata_ready"}]
|
| 188 |
+
by_domain: dict[str, int] = {}
|
| 189 |
+
for row in rows:
|
| 190 |
+
by_domain[row.primary_domain] = by_domain.get(row.primary_domain, 0) + 1
|
| 191 |
+
return {
|
| 192 |
+
"status": "ready" if rows else "not_initialized",
|
| 193 |
+
"source_count": len(rows),
|
| 194 |
+
"ready_count": len(ready),
|
| 195 |
+
"domains": by_domain,
|
| 196 |
+
"sources": [serialize_source(row) for row in rows],
|
| 197 |
+
"policy": "Cataloged Hugging Face datasets are real public sources. Large datasets are validated and ingested incrementally, not copied blindly.",
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def validate_dataset(dataset_id: str) -> dict:
|
| 202 |
+
encoded = quote(dataset_id, safe="")
|
| 203 |
+
payload = {"dataset_id": dataset_id}
|
| 204 |
+
payload["is_valid"] = get_json(f"{DATASETS_SERVER}/is-valid?dataset={encoded}")
|
| 205 |
+
payload["splits"] = get_json(f"{DATASETS_SERVER}/splits?dataset={encoded}")
|
| 206 |
+
payload["parquet_files"] = slim_parquet(get_json(f"{DATASETS_SERVER}/parquet?dataset={encoded}"))
|
| 207 |
+
payload["size"] = get_json(f"{DATASETS_SERVER}/size?dataset={encoded}")
|
| 208 |
+
return payload
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def get_json(url: str) -> dict:
|
| 212 |
+
try:
|
| 213 |
+
response = requests.get(url, timeout=4)
|
| 214 |
+
if response.status_code >= 400:
|
| 215 |
+
return {"status": "error", "status_code": response.status_code, "detail": response.text[:240]}
|
| 216 |
+
return response.json()
|
| 217 |
+
except Exception as exc:
|
| 218 |
+
return {"status": "error", "error": f"{type(exc).__name__}: {exc}"}
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def slim_parquet(payload: dict) -> dict:
|
| 222 |
+
files = payload.get("parquet_files") if isinstance(payload, dict) else None
|
| 223 |
+
if not isinstance(files, list):
|
| 224 |
+
return payload
|
| 225 |
+
return {
|
| 226 |
+
"status": "ok",
|
| 227 |
+
"file_count": len(files),
|
| 228 |
+
"sample_files": files[:8],
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def source_status(payload: dict) -> str:
|
| 233 |
+
valid = payload.get("is_valid", {})
|
| 234 |
+
splits = payload.get("splits", {})
|
| 235 |
+
parquet = payload.get("parquet_files", {})
|
| 236 |
+
if isinstance(valid, dict) and valid.get("valid") is False:
|
| 237 |
+
return "unavailable"
|
| 238 |
+
if isinstance(parquet, dict) and parquet.get("file_count", 0) > 0:
|
| 239 |
+
return "viewer_ready"
|
| 240 |
+
if isinstance(splits, dict) and splits.get("splits"):
|
| 241 |
+
return "metadata_ready"
|
| 242 |
+
if any(isinstance(payload.get(key), dict) and payload[key].get("status") == "error" for key in ["is_valid", "splits", "parquet_files", "size"]):
|
| 243 |
+
return "metadata_partial"
|
| 244 |
+
return "discovered"
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def serialize_source(row: ExternalDatasetSource) -> dict:
|
| 248 |
+
return {
|
| 249 |
+
"dataset_id": row.dataset_id,
|
| 250 |
+
"title": row.title,
|
| 251 |
+
"primary_domain": row.primary_domain,
|
| 252 |
+
"data_domains": row.data_domains,
|
| 253 |
+
"license": row.license,
|
| 254 |
+
"priority": row.priority,
|
| 255 |
+
"ingestion_mode": row.ingestion_mode,
|
| 256 |
+
"status": row.status,
|
| 257 |
+
"dataset_url": row.dataset_url,
|
| 258 |
+
"viewer_status": row.viewer_status,
|
| 259 |
+
"parquet_files": row.parquet_files,
|
| 260 |
+
"size_summary": row.size_summary,
|
| 261 |
+
"usage_policy": row.usage_policy,
|
| 262 |
+
"last_checked_at": row.last_checked_at.isoformat() if row.last_checked_at else None,
|
| 263 |
+
}
|
backend/app/services/realtime.py
CHANGED
|
@@ -10,6 +10,7 @@ from app.core.config import get_settings
|
|
| 10 |
from app.core.database import SessionLocal
|
| 11 |
from app.ingestion.news_ingestor import NewsIngestor
|
| 12 |
from app.services.accuracy import run_accuracy_audit
|
|
|
|
| 13 |
from app.services.blum_financial_model import run_model_learning_cycle
|
| 14 |
from app.services.data_continuity import repair_data_gaps
|
| 15 |
from app.services.etf import update_etf_trends
|
|
@@ -44,6 +45,12 @@ def start_realtime_services() -> None:
|
|
| 44 |
_scheduler = BackgroundScheduler(timezone="UTC")
|
| 45 |
if settings.enable_live_startup:
|
| 46 |
threading.Thread(target=run_startup_pipeline, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
_scheduler.add_job(run_news_refresh, "interval", minutes=settings.news_refresh_minutes, id="news_refresh", replace_existing=True, max_instances=1)
|
| 48 |
_scheduler.add_job(run_market_refresh, "interval", minutes=settings.market_refresh_minutes, id="market_refresh", replace_existing=True, max_instances=1)
|
| 49 |
_scheduler.add_job(run_data_gap_repair, "interval", minutes=settings.data_gap_repair_minutes, id="data_gap_repair", replace_existing=True, max_instances=1)
|
|
@@ -72,6 +79,10 @@ def realtime_status() -> dict:
|
|
| 72 |
|
| 73 |
|
| 74 |
def run_startup_pipeline() -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
def work(db):
|
| 76 |
pipeline = PipelineService().run(db, limit=settings.startup_pipeline_limit, period=settings.historical_price_period)
|
| 77 |
learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
|
|
@@ -128,6 +139,10 @@ def run_blum_model_cycle_job() -> None:
|
|
| 128 |
_run_job("blum_financial_model_cycle", lambda db: run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit))
|
| 129 |
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
def _run_job(job_name: str, work):
|
| 132 |
with _state_lock:
|
| 133 |
if _state["running"]:
|
|
|
|
| 10 |
from app.core.database import SessionLocal
|
| 11 |
from app.ingestion.news_ingestor import NewsIngestor
|
| 12 |
from app.services.accuracy import run_accuracy_audit
|
| 13 |
+
from app.services.autonomous_engine import AutonomousResearchEngine
|
| 14 |
from app.services.blum_financial_model import run_model_learning_cycle
|
| 15 |
from app.services.data_continuity import repair_data_gaps
|
| 16 |
from app.services.etf import update_etf_trends
|
|
|
|
| 45 |
_scheduler = BackgroundScheduler(timezone="UTC")
|
| 46 |
if settings.enable_live_startup:
|
| 47 |
threading.Thread(target=run_startup_pipeline, daemon=True).start()
|
| 48 |
+
if settings.enable_autonomous_engine:
|
| 49 |
+
_scheduler.add_job(run_autonomous_engine_job, "interval", minutes=settings.autonomous_cycle_minutes, id="autonomous_research_engine", replace_existing=True, max_instances=1)
|
| 50 |
+
_scheduler.start()
|
| 51 |
+
with _state_lock:
|
| 52 |
+
_state["started"] = True
|
| 53 |
+
return
|
| 54 |
_scheduler.add_job(run_news_refresh, "interval", minutes=settings.news_refresh_minutes, id="news_refresh", replace_existing=True, max_instances=1)
|
| 55 |
_scheduler.add_job(run_market_refresh, "interval", minutes=settings.market_refresh_minutes, id="market_refresh", replace_existing=True, max_instances=1)
|
| 56 |
_scheduler.add_job(run_data_gap_repair, "interval", minutes=settings.data_gap_repair_minutes, id="data_gap_repair", replace_existing=True, max_instances=1)
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def run_startup_pipeline() -> None:
|
| 82 |
+
if settings.enable_autonomous_engine:
|
| 83 |
+
_run_job("autonomous_startup", lambda db: AutonomousResearchEngine().run_cycle(db, trigger="startup"))
|
| 84 |
+
return
|
| 85 |
+
|
| 86 |
def work(db):
|
| 87 |
pipeline = PipelineService().run(db, limit=settings.startup_pipeline_limit, period=settings.historical_price_period)
|
| 88 |
learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
|
|
|
|
| 139 |
_run_job("blum_financial_model_cycle", lambda db: run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit))
|
| 140 |
|
| 141 |
|
| 142 |
+
def run_autonomous_engine_job() -> None:
|
| 143 |
+
_run_job("autonomous_research_engine", lambda db: AutonomousResearchEngine().run_cycle(db, trigger="scheduled"))
|
| 144 |
+
|
| 145 |
+
|
| 146 |
def _run_job(job_name: str, work):
|
| 147 |
with _state_lock:
|
| 148 |
if _state["running"]:
|
backend/app/services/thesis_engine.py
CHANGED
|
@@ -134,6 +134,7 @@ def build_signal_thesis_payload(asset: Asset, score: dict, technical: dict, narr
|
|
| 134 |
narrative=narrative,
|
| 135 |
related_news=[],
|
| 136 |
market_context=market_context,
|
|
|
|
| 137 |
)
|
| 138 |
return {
|
| 139 |
"executive_thesis": thesis["executive_thesis"],
|
|
@@ -206,6 +207,7 @@ def enrich_theme_lifecycle(theme: dict, linked_assets: list[str] | None = None,
|
|
| 206 |
|
| 207 |
|
| 208 |
def observed_facts(ticker: str, signal: dict, technical: dict, narrative: dict, related_news: list[dict], historical: dict) -> list[str]:
|
|
|
|
| 209 |
facts = [
|
| 210 |
f"{ticker} classification is {signal.get('classification', 'Insufficient Evidence')}.",
|
| 211 |
f"Blum score is {display(signal.get('blum_score'))} and confidence is {display(signal.get('confidence_score'))}.",
|
|
@@ -213,6 +215,8 @@ def observed_facts(ticker: str, signal: dict, technical: dict, narrative: dict,
|
|
| 213 |
f"7D sentiment is {display(narrative.get('sentiment_7d'))} across {int(number(narrative.get('news_count_7d')))} linked recent news records.",
|
| 214 |
f"Stored related news items supplied to the thesis: {len(related_news)}.",
|
| 215 |
]
|
|
|
|
|
|
|
| 216 |
if historical:
|
| 217 |
facts.append(f"Historical similarity data mode is {historical.get('data_mode', historical.get('statistical_reliability', 'available'))}.")
|
| 218 |
return facts
|
|
@@ -275,6 +279,7 @@ def causal_reasoning(ticker: str, technical: dict, narrative: dict, related_news
|
|
| 275 |
def supporting_evidence(signal: dict, technical: dict, narrative: dict, related_news: list[dict], historical: dict) -> list[str]:
|
| 276 |
rows = []
|
| 277 |
breakdown = signal.get("score_breakdown") or {}
|
|
|
|
| 278 |
if number(breakdown.get("momentum_score")) >= 60:
|
| 279 |
rows.append(f"Momentum score is {number(breakdown.get('momentum_score')):.1f}.")
|
| 280 |
if number(breakdown.get("trend_score")) >= 60:
|
|
@@ -285,6 +290,8 @@ def supporting_evidence(signal: dict, technical: dict, narrative: dict, related_
|
|
| 285 |
rows.append("Price is above both SMA20 and SMA50.")
|
| 286 |
if number(narrative.get("news_count_7d")) >= 2:
|
| 287 |
rows.append(f"News flow is active with {int(number(narrative.get('news_count_7d')))} linked 7D records.")
|
|
|
|
|
|
|
| 288 |
if related_news:
|
| 289 |
best = max(related_news, key=lambda item: number(item.get("quality_score")))
|
| 290 |
rows.append(f"Highest-quality linked news source is {best.get('source', 'unknown')} with quality {display(best.get('quality_score'))}.")
|
|
@@ -295,6 +302,7 @@ def supporting_evidence(signal: dict, technical: dict, narrative: dict, related_
|
|
| 295 |
|
| 296 |
def contradicting_evidence(signal: dict, technical: dict, narrative: dict, related_news: list[dict], accuracy: dict, historical: dict) -> list[str]:
|
| 297 |
rows = []
|
|
|
|
| 298 |
perf_5d = number(technical.get("perf_5d"))
|
| 299 |
sentiment_7d = number(narrative.get("sentiment_7d"))
|
| 300 |
if perf_5d > 4 and sentiment_7d < -0.12:
|
|
@@ -305,6 +313,10 @@ def contradicting_evidence(signal: dict, technical: dict, narrative: dict, relat
|
|
| 305 |
rows.append("RSI is elevated, increasing the risk of crowded short-term momentum.")
|
| 306 |
if signal.get("risk_level") == "High":
|
| 307 |
rows.append("The latest signal is explicitly marked High risk.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
if number(accuracy.get("blum_confidence_score"), 100) < 55:
|
| 309 |
rows.append("Evidence confidence is limited by data quality checks.")
|
| 310 |
if historical.get("data_mode") == "demonstration_mode":
|
|
@@ -331,7 +343,8 @@ def uncertainty_points(signal: dict, technical: dict, narrative: dict, related_n
|
|
| 331 |
|
| 332 |
def missing_information(signal: dict, technical: dict, narrative: dict, related_news: list[dict], historical: dict, accuracy: dict) -> list[str]:
|
| 333 |
rows = []
|
| 334 |
-
|
|
|
|
| 335 |
rows.append("Fundamental confirmation is not fully connected to the signal.")
|
| 336 |
if number(narrative.get("news_count_7d")) == 0:
|
| 337 |
rows.append("Recent source-backed catalyst is missing.")
|
|
@@ -370,6 +383,7 @@ def invalidation_conditions(technical: dict, narrative: dict, risk_level: str) -
|
|
| 370 |
|
| 371 |
def thesis_risks(signal: dict, technical: dict, narrative: dict, accuracy: dict, regime: str) -> list[str]:
|
| 372 |
rows = []
|
|
|
|
| 373 |
if signal.get("risk_level") == "High":
|
| 374 |
rows.append("High signal risk can make the thesis unstable even when momentum is strong.")
|
| 375 |
if regime in {"Risk-Off", "Panic", "Bull Exhaustion"}:
|
|
@@ -380,6 +394,8 @@ def thesis_risks(signal: dict, technical: dict, narrative: dict, accuracy: dict,
|
|
| 380 |
rows.append("Data quality limits conviction.")
|
| 381 |
if number(narrative.get("sentiment_polarization")) > 0.8:
|
| 382 |
rows.append("Sentiment polarization is high; narrative interpretation may be unstable.")
|
|
|
|
|
|
|
| 383 |
return rows or ["No dominant single risk, but thesis quality depends on continued evidence alignment."]
|
| 384 |
|
| 385 |
|
|
@@ -422,10 +438,11 @@ def conviction_score(**kwargs) -> dict:
|
|
| 422 |
number(breakdown.get("trend_score")) >= 60,
|
| 423 |
number(breakdown.get("sentiment_score")) >= 58 or number(narrative.get("sentiment_7d")) > 0.12,
|
| 424 |
number(breakdown.get("etf_confirmation_score")) >= 55,
|
|
|
|
| 425 |
number(historical.get("case_count")) >= 12,
|
| 426 |
]
|
| 427 |
)
|
| 428 |
-
confirmation_score = independent_confirmations /
|
| 429 |
contradiction_score = clamp(100 - max(0, len(contradicting) - 1) * 18)
|
| 430 |
narrative_score = clamp(number(breakdown.get("semantic_trend_score"), number(narrative.get("semantic_trend_score"))))
|
| 431 |
technical_score = mean_safe([number(breakdown.get("momentum_score")), number(breakdown.get("trend_score"))])
|
|
@@ -455,6 +472,7 @@ def conviction_score(**kwargs) -> dict:
|
|
| 455 |
"technical_coherence": round(technical_score, 1),
|
| 456 |
"historical_support": round(historical_score, 1),
|
| 457 |
"market_context": round(context_score, 1),
|
|
|
|
| 458 |
},
|
| 459 |
"reducers": reducers or ["No major conviction reducer was detected, but the thesis remains conditional."],
|
| 460 |
}
|
|
|
|
| 134 |
narrative=narrative,
|
| 135 |
related_news=[],
|
| 136 |
market_context=market_context,
|
| 137 |
+
accuracy=narrative.get("accuracy_profile", {}),
|
| 138 |
)
|
| 139 |
return {
|
| 140 |
"executive_thesis": thesis["executive_thesis"],
|
|
|
|
| 207 |
|
| 208 |
|
| 209 |
def observed_facts(ticker: str, signal: dict, technical: dict, narrative: dict, related_news: list[dict], historical: dict) -> list[str]:
|
| 210 |
+
fundamentals = narrative.get("fundamentals") or {}
|
| 211 |
facts = [
|
| 212 |
f"{ticker} classification is {signal.get('classification', 'Insufficient Evidence')}.",
|
| 213 |
f"Blum score is {display(signal.get('blum_score'))} and confidence is {display(signal.get('confidence_score'))}.",
|
|
|
|
| 215 |
f"7D sentiment is {display(narrative.get('sentiment_7d'))} across {int(number(narrative.get('news_count_7d')))} linked recent news records.",
|
| 216 |
f"Stored related news items supplied to the thesis: {len(related_news)}.",
|
| 217 |
]
|
| 218 |
+
if fundamentals.get("status") == "ready":
|
| 219 |
+
facts.append(f"SEC fundamental score is {display(fundamentals.get('fundamental_score'))} from provider {fundamentals.get('provider', 'unknown')}.")
|
| 220 |
if historical:
|
| 221 |
facts.append(f"Historical similarity data mode is {historical.get('data_mode', historical.get('statistical_reliability', 'available'))}.")
|
| 222 |
return facts
|
|
|
|
| 279 |
def supporting_evidence(signal: dict, technical: dict, narrative: dict, related_news: list[dict], historical: dict) -> list[str]:
|
| 280 |
rows = []
|
| 281 |
breakdown = signal.get("score_breakdown") or {}
|
| 282 |
+
fundamentals = narrative.get("fundamentals") or {}
|
| 283 |
if number(breakdown.get("momentum_score")) >= 60:
|
| 284 |
rows.append(f"Momentum score is {number(breakdown.get('momentum_score')):.1f}.")
|
| 285 |
if number(breakdown.get("trend_score")) >= 60:
|
|
|
|
| 290 |
rows.append("Price is above both SMA20 and SMA50.")
|
| 291 |
if number(narrative.get("news_count_7d")) >= 2:
|
| 292 |
rows.append(f"News flow is active with {int(number(narrative.get('news_count_7d')))} linked 7D records.")
|
| 293 |
+
if number(breakdown.get("fundamental_score"), number(fundamentals.get("fundamental_score"))) >= 62:
|
| 294 |
+
rows.append(f"Fundamental evidence is supportive with score {number(breakdown.get('fundamental_score'), number(fundamentals.get('fundamental_score'))):.1f}.")
|
| 295 |
if related_news:
|
| 296 |
best = max(related_news, key=lambda item: number(item.get("quality_score")))
|
| 297 |
rows.append(f"Highest-quality linked news source is {best.get('source', 'unknown')} with quality {display(best.get('quality_score'))}.")
|
|
|
|
| 302 |
|
| 303 |
def contradicting_evidence(signal: dict, technical: dict, narrative: dict, related_news: list[dict], accuracy: dict, historical: dict) -> list[str]:
|
| 304 |
rows = []
|
| 305 |
+
fundamentals = narrative.get("fundamentals") or {}
|
| 306 |
perf_5d = number(technical.get("perf_5d"))
|
| 307 |
sentiment_7d = number(narrative.get("sentiment_7d"))
|
| 308 |
if perf_5d > 4 and sentiment_7d < -0.12:
|
|
|
|
| 313 |
rows.append("RSI is elevated, increasing the risk of crowded short-term momentum.")
|
| 314 |
if signal.get("risk_level") == "High":
|
| 315 |
rows.append("The latest signal is explicitly marked High risk.")
|
| 316 |
+
if fundamentals.get("status") == "missing":
|
| 317 |
+
rows.append("Fundamental evidence is missing, so the thesis is less complete.")
|
| 318 |
+
elif number(fundamentals.get("fundamental_score")) < 45:
|
| 319 |
+
rows.append("Fundamental score is weak relative to the technical or narrative setup.")
|
| 320 |
if number(accuracy.get("blum_confidence_score"), 100) < 55:
|
| 321 |
rows.append("Evidence confidence is limited by data quality checks.")
|
| 322 |
if historical.get("data_mode") == "demonstration_mode":
|
|
|
|
| 343 |
|
| 344 |
def missing_information(signal: dict, technical: dict, narrative: dict, related_news: list[dict], historical: dict, accuracy: dict) -> list[str]:
|
| 345 |
rows = []
|
| 346 |
+
fundamentals = narrative.get("fundamentals") or {}
|
| 347 |
+
if fundamentals.get("status") != "ready":
|
| 348 |
rows.append("Fundamental confirmation is not fully connected to the signal.")
|
| 349 |
if number(narrative.get("news_count_7d")) == 0:
|
| 350 |
rows.append("Recent source-backed catalyst is missing.")
|
|
|
|
| 383 |
|
| 384 |
def thesis_risks(signal: dict, technical: dict, narrative: dict, accuracy: dict, regime: str) -> list[str]:
|
| 385 |
rows = []
|
| 386 |
+
fundamentals = narrative.get("fundamentals") or {}
|
| 387 |
if signal.get("risk_level") == "High":
|
| 388 |
rows.append("High signal risk can make the thesis unstable even when momentum is strong.")
|
| 389 |
if regime in {"Risk-Off", "Panic", "Bull Exhaustion"}:
|
|
|
|
| 394 |
rows.append("Data quality limits conviction.")
|
| 395 |
if number(narrative.get("sentiment_polarization")) > 0.8:
|
| 396 |
rows.append("Sentiment polarization is high; narrative interpretation may be unstable.")
|
| 397 |
+
if fundamentals.get("status") == "missing":
|
| 398 |
+
rows.append("Fundamental coverage is missing or incomplete.")
|
| 399 |
return rows or ["No dominant single risk, but thesis quality depends on continued evidence alignment."]
|
| 400 |
|
| 401 |
|
|
|
|
| 438 |
number(breakdown.get("trend_score")) >= 60,
|
| 439 |
number(breakdown.get("sentiment_score")) >= 58 or number(narrative.get("sentiment_7d")) > 0.12,
|
| 440 |
number(breakdown.get("etf_confirmation_score")) >= 55,
|
| 441 |
+
number(breakdown.get("fundamental_score")) >= 62,
|
| 442 |
number(historical.get("case_count")) >= 12,
|
| 443 |
]
|
| 444 |
)
|
| 445 |
+
confirmation_score = independent_confirmations / 6 * 100
|
| 446 |
contradiction_score = clamp(100 - max(0, len(contradicting) - 1) * 18)
|
| 447 |
narrative_score = clamp(number(breakdown.get("semantic_trend_score"), number(narrative.get("semantic_trend_score"))))
|
| 448 |
technical_score = mean_safe([number(breakdown.get("momentum_score")), number(breakdown.get("trend_score"))])
|
|
|
|
| 472 |
"technical_coherence": round(technical_score, 1),
|
| 473 |
"historical_support": round(historical_score, 1),
|
| 474 |
"market_context": round(context_score, 1),
|
| 475 |
+
"fundamental_support": round(number(breakdown.get("fundamental_score")), 1),
|
| 476 |
},
|
| 477 |
"reducers": reducers or ["No major conviction reducer was detected, but the thesis remains conditional."],
|
| 478 |
}
|
backend/app/signals/engine.py
CHANGED
|
@@ -6,13 +6,13 @@ from sqlalchemy import delete, desc, select
|
|
| 6 |
from sqlalchemy.orm import Session
|
| 7 |
|
| 8 |
from app.ai.orchestrator import AIOrchestrator
|
| 9 |
-
from app.models import Asset, NewsAssetLink, PriceHistory, SentimentAnalysis, SignalSnapshot, TechnicalIndicator
|
| 10 |
from app.services.blum_financial_model import capture_signal_reasoning
|
| 11 |
from app.services.thesis_engine import build_signal_thesis_payload
|
| 12 |
from app.signals.indicators import compute_indicators
|
| 13 |
|
| 14 |
|
| 15 |
-
SCORE_VERSION = "blum-thesis-score-v0.
|
| 16 |
|
| 17 |
|
| 18 |
class SignalEngine:
|
|
@@ -34,9 +34,12 @@ class SignalEngine:
|
|
| 34 |
indicators = compute_indicators(frame, benchmark_frame)
|
| 35 |
ts = self.ai.time_series.analyze(frame)
|
| 36 |
narrative = self.narrative_features(db, asset)
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
previous = latest_signal_for_asset(db, asset.id)
|
| 39 |
-
confidence = confidence_score(frame, indicators, narrative, ts)
|
| 40 |
score["confidence_score"] = confidence
|
| 41 |
lifecycle = lifecycle_state(previous, score, confidence)
|
| 42 |
thesis = build_signal_thesis_payload(asset, score, indicators, narrative, ts)
|
|
@@ -127,7 +130,9 @@ def load_prices(db: Session, asset_id: int) -> pd.DataFrame:
|
|
| 127 |
return pd.DataFrame(rows, columns=["date", "open", "high", "low", "close", "volume"])
|
| 128 |
|
| 129 |
|
| 130 |
-
def build_score(indicators: dict, narrative: dict, ts: dict, asset: Asset) -> dict:
|
|
|
|
|
|
|
| 131 |
momentum_score = avg(
|
| 132 |
scale(indicators.get("perf_5d"), -8, 8),
|
| 133 |
scale(indicators.get("perf_1m"), -14, 16),
|
|
@@ -159,16 +164,20 @@ def build_score(indicators: dict, narrative: dict, ts: dict, asset: Asset) -> di
|
|
| 159 |
)
|
| 160 |
semantic_trend_score = narrative.get("semantic_trend_score", 0)
|
| 161 |
etf_confirmation_score = etf_confirmation_proxy(asset, indicators)
|
|
|
|
|
|
|
| 162 |
risk_adjustment = avg(volatility_score, scale(indicators.get("beta_vs_benchmark"), 2.1, 0.55), scale(indicators.get("recent_drawdown"), -30, 0))
|
| 163 |
blum_score = (
|
| 164 |
-
momentum_score * 0.
|
| 165 |
-
+ trend_score * 0.
|
| 166 |
-
+ sentiment_score * 0.
|
| 167 |
-
+ volatility_score * 0.
|
| 168 |
-
+ anomaly_score * 0.
|
| 169 |
-
+ semantic_trend_score * 0.
|
| 170 |
-
+ etf_confirmation_score * 0.
|
| 171 |
-
+
|
|
|
|
|
|
|
| 172 |
)
|
| 173 |
classification = classify_signal(blum_score, indicators, narrative, anomaly_score)
|
| 174 |
return {
|
|
@@ -184,6 +193,8 @@ def build_score(indicators: dict, narrative: dict, ts: dict, asset: Asset) -> di
|
|
| 184 |
"anomaly_score": round(anomaly_score, 1),
|
| 185 |
"semantic_trend_score": round(semantic_trend_score, 1),
|
| 186 |
"etf_confirmation_score": round(etf_confirmation_score, 1),
|
|
|
|
|
|
|
| 187 |
"risk_adjustment": round(risk_adjustment, 1),
|
| 188 |
},
|
| 189 |
}
|
|
@@ -250,7 +261,7 @@ def build_rule_explanation(asset: Asset, score: dict, indicators: dict, narrativ
|
|
| 250 |
return (
|
| 251 |
f"{asset.ticker} is classified as {score['classification']} with a Blum Intelligence Score of "
|
| 252 |
f"{score['blum_score']}. The engine combines momentum, trend quality, sentiment, volatility, "
|
| 253 |
-
f"semantic intensity, ETF confirmation and anomaly pressure. Current 5D performance is "
|
| 254 |
f"{indicators.get('perf_5d', 0):.2f}%, 1M performance is {indicators.get('perf_1m', 0):.2f}%, "
|
| 255 |
f"7D sentiment is {narrative.get('sentiment_7d', 0):.2f}, and the time-series regime is "
|
| 256 |
f"{ts.get('regime', 'unknown')}."
|
|
@@ -266,13 +277,17 @@ def latest_signal_for_asset(db: Session, asset_id: int) -> SignalSnapshot | None
|
|
| 266 |
)
|
| 267 |
|
| 268 |
|
| 269 |
-
def confidence_score(frame: pd.DataFrame, indicators: dict, narrative: dict, ts: dict) -> float:
|
|
|
|
|
|
|
| 270 |
history_depth = scale(len(frame), 60, 900)
|
| 271 |
indicator_completeness = sum(1 for key in ["sma20", "sma50", "sma200", "rsi", "macd_hist", "atr_percent", "support", "resistance"] if indicators.get(key) is not None) / 8 * 100
|
| 272 |
news_support = scale(narrative.get("news_count_30d", 0), 0, 12)
|
| 273 |
sentiment_quality = scale(abs(narrative.get("sentiment_30d", 0)), 0, 0.55)
|
| 274 |
time_series_depth = 80 if ts.get("regime") not in {"unknown", "insufficient_history"} else 35
|
| 275 |
-
|
|
|
|
|
|
|
| 276 |
|
| 277 |
|
| 278 |
def lifecycle_state(previous: SignalSnapshot | None, score: dict, confidence: float) -> str:
|
|
@@ -298,6 +313,72 @@ def etf_confirmation_proxy(asset: Asset, indicators: dict) -> float:
|
|
| 298 |
return base
|
| 299 |
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
def scale(value, low: float, high: float) -> float:
|
| 302 |
try:
|
| 303 |
number = float(value)
|
|
|
|
| 6 |
from sqlalchemy.orm import Session
|
| 7 |
|
| 8 |
from app.ai.orchestrator import AIOrchestrator
|
| 9 |
+
from app.models import AccuracySnapshot, Asset, FundamentalSnapshot, NewsAssetLink, PriceHistory, SentimentAnalysis, SignalSnapshot, TechnicalIndicator
|
| 10 |
from app.services.blum_financial_model import capture_signal_reasoning
|
| 11 |
from app.services.thesis_engine import build_signal_thesis_payload
|
| 12 |
from app.signals.indicators import compute_indicators
|
| 13 |
|
| 14 |
|
| 15 |
+
SCORE_VERSION = "blum-thesis-score-v0.8"
|
| 16 |
|
| 17 |
|
| 18 |
class SignalEngine:
|
|
|
|
| 34 |
indicators = compute_indicators(frame, benchmark_frame)
|
| 35 |
ts = self.ai.time_series.analyze(frame)
|
| 36 |
narrative = self.narrative_features(db, asset)
|
| 37 |
+
fundamentals = fundamental_features(db, asset)
|
| 38 |
+
accuracy = accuracy_features(db, asset)
|
| 39 |
+
narrative = {**narrative, "fundamentals": fundamentals, "accuracy_profile": accuracy}
|
| 40 |
+
score = build_score(indicators, narrative, ts, asset, fundamentals, accuracy)
|
| 41 |
previous = latest_signal_for_asset(db, asset.id)
|
| 42 |
+
confidence = confidence_score(frame, indicators, narrative, ts, fundamentals, accuracy)
|
| 43 |
score["confidence_score"] = confidence
|
| 44 |
lifecycle = lifecycle_state(previous, score, confidence)
|
| 45 |
thesis = build_signal_thesis_payload(asset, score, indicators, narrative, ts)
|
|
|
|
| 130 |
return pd.DataFrame(rows, columns=["date", "open", "high", "low", "close", "volume"])
|
| 131 |
|
| 132 |
|
| 133 |
+
def build_score(indicators: dict, narrative: dict, ts: dict, asset: Asset, fundamentals: dict | None = None, accuracy: dict | None = None) -> dict:
|
| 134 |
+
fundamentals = fundamentals or {}
|
| 135 |
+
accuracy = accuracy or {}
|
| 136 |
momentum_score = avg(
|
| 137 |
scale(indicators.get("perf_5d"), -8, 8),
|
| 138 |
scale(indicators.get("perf_1m"), -14, 16),
|
|
|
|
| 164 |
)
|
| 165 |
semantic_trend_score = narrative.get("semantic_trend_score", 0)
|
| 166 |
etf_confirmation_score = etf_confirmation_proxy(asset, indicators)
|
| 167 |
+
fundamental_score = float(fundamentals.get("fundamental_score", 45.0) or 45.0)
|
| 168 |
+
historical_accuracy_score = float(accuracy.get("accuracy_score", 50.0) or 50.0)
|
| 169 |
risk_adjustment = avg(volatility_score, scale(indicators.get("beta_vs_benchmark"), 2.1, 0.55), scale(indicators.get("recent_drawdown"), -30, 0))
|
| 170 |
blum_score = (
|
| 171 |
+
momentum_score * 0.16
|
| 172 |
+
+ trend_score * 0.16
|
| 173 |
+
+ sentiment_score * 0.13
|
| 174 |
+
+ volatility_score * 0.10
|
| 175 |
+
+ anomaly_score * 0.09
|
| 176 |
+
+ semantic_trend_score * 0.10
|
| 177 |
+
+ etf_confirmation_score * 0.07
|
| 178 |
+
+ fundamental_score * 0.10
|
| 179 |
+
+ historical_accuracy_score * 0.06
|
| 180 |
+
+ risk_adjustment * 0.03
|
| 181 |
)
|
| 182 |
classification = classify_signal(blum_score, indicators, narrative, anomaly_score)
|
| 183 |
return {
|
|
|
|
| 193 |
"anomaly_score": round(anomaly_score, 1),
|
| 194 |
"semantic_trend_score": round(semantic_trend_score, 1),
|
| 195 |
"etf_confirmation_score": round(etf_confirmation_score, 1),
|
| 196 |
+
"fundamental_score": round(fundamental_score, 1),
|
| 197 |
+
"historical_accuracy_score": round(historical_accuracy_score, 1),
|
| 198 |
"risk_adjustment": round(risk_adjustment, 1),
|
| 199 |
},
|
| 200 |
}
|
|
|
|
| 261 |
return (
|
| 262 |
f"{asset.ticker} is classified as {score['classification']} with a Blum Intelligence Score of "
|
| 263 |
f"{score['blum_score']}. The engine combines momentum, trend quality, sentiment, volatility, "
|
| 264 |
+
f"semantic intensity, ETF confirmation, fundamentals, historical accuracy and anomaly pressure. Current 5D performance is "
|
| 265 |
f"{indicators.get('perf_5d', 0):.2f}%, 1M performance is {indicators.get('perf_1m', 0):.2f}%, "
|
| 266 |
f"7D sentiment is {narrative.get('sentiment_7d', 0):.2f}, and the time-series regime is "
|
| 267 |
f"{ts.get('regime', 'unknown')}."
|
|
|
|
| 277 |
)
|
| 278 |
|
| 279 |
|
| 280 |
+
def confidence_score(frame: pd.DataFrame, indicators: dict, narrative: dict, ts: dict, fundamentals: dict | None = None, accuracy: dict | None = None) -> float:
|
| 281 |
+
fundamentals = fundamentals or {}
|
| 282 |
+
accuracy = accuracy or {}
|
| 283 |
history_depth = scale(len(frame), 60, 900)
|
| 284 |
indicator_completeness = sum(1 for key in ["sma20", "sma50", "sma200", "rsi", "macd_hist", "atr_percent", "support", "resistance"] if indicators.get(key) is not None) / 8 * 100
|
| 285 |
news_support = scale(narrative.get("news_count_30d", 0), 0, 12)
|
| 286 |
sentiment_quality = scale(abs(narrative.get("sentiment_30d", 0)), 0, 0.55)
|
| 287 |
time_series_depth = 80 if ts.get("regime") not in {"unknown", "insufficient_history"} else 35
|
| 288 |
+
fundamental_quality = float(fundamentals.get("quality_score", 0.0) or 0.0)
|
| 289 |
+
historical_accuracy = float(accuracy.get("accuracy_score", 50.0) or 50.0)
|
| 290 |
+
return round(avg(history_depth, indicator_completeness, news_support, sentiment_quality, time_series_depth, fundamental_quality, historical_accuracy), 1)
|
| 291 |
|
| 292 |
|
| 293 |
def lifecycle_state(previous: SignalSnapshot | None, score: dict, confidence: float) -> str:
|
|
|
|
| 313 |
return base
|
| 314 |
|
| 315 |
|
| 316 |
+
def fundamental_features(db: Session, asset: Asset) -> dict:
|
| 317 |
+
snapshot = db.scalar(
|
| 318 |
+
select(FundamentalSnapshot)
|
| 319 |
+
.where(FundamentalSnapshot.asset_id == asset.id)
|
| 320 |
+
.order_by(desc(FundamentalSnapshot.period_end), desc(FundamentalSnapshot.created_at))
|
| 321 |
+
.limit(1)
|
| 322 |
+
)
|
| 323 |
+
if snapshot is None:
|
| 324 |
+
return {"status": "missing", "quality_score": 0.0, "fundamental_score": 42.0, "issues": ["No SEC fundamental snapshot is stored."]}
|
| 325 |
+
metrics = snapshot.metrics or {}
|
| 326 |
+
revenue = metric_value(metrics, "revenue")
|
| 327 |
+
net_income = metric_value(metrics, "net_income")
|
| 328 |
+
assets = metric_value(metrics, "assets")
|
| 329 |
+
liabilities = metric_value(metrics, "liabilities")
|
| 330 |
+
operating_cash_flow = metric_value(metrics, "operating_cash_flow")
|
| 331 |
+
profit_margin = net_income / revenue if revenue else None
|
| 332 |
+
leverage = liabilities / assets if assets else None
|
| 333 |
+
cash_conversion = operating_cash_flow / net_income if operating_cash_flow is not None and net_income and net_income > 0 else None
|
| 334 |
+
score = avg(
|
| 335 |
+
float(snapshot.quality_score or 0.0),
|
| 336 |
+
scale(profit_margin, -0.18, 0.30) if profit_margin is not None else 45,
|
| 337 |
+
scale(1 - leverage, 0.05, 0.72) if leverage is not None else 45,
|
| 338 |
+
scale(cash_conversion, 0.25, 1.45) if cash_conversion is not None else 45,
|
| 339 |
+
)
|
| 340 |
+
return {
|
| 341 |
+
"status": "ready",
|
| 342 |
+
"provider": snapshot.provider,
|
| 343 |
+
"period_end": snapshot.period_end.isoformat() if snapshot.period_end else None,
|
| 344 |
+
"quality_score": float(snapshot.quality_score or 0.0),
|
| 345 |
+
"fundamental_score": round(score, 1),
|
| 346 |
+
"profit_margin": round(profit_margin, 4) if profit_margin is not None else None,
|
| 347 |
+
"liabilities_to_assets": round(leverage, 4) if leverage is not None else None,
|
| 348 |
+
"cash_conversion": round(cash_conversion, 4) if cash_conversion is not None else None,
|
| 349 |
+
"data_policy": "SEC companyfacts metrics only. Missing values are not estimated.",
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def accuracy_features(db: Session, asset: Asset) -> dict:
|
| 354 |
+
snapshot = db.scalar(
|
| 355 |
+
select(AccuracySnapshot)
|
| 356 |
+
.where(AccuracySnapshot.asset_id == asset.id, AccuracySnapshot.scope == "asset")
|
| 357 |
+
.order_by(desc(AccuracySnapshot.created_at))
|
| 358 |
+
.limit(1)
|
| 359 |
+
)
|
| 360 |
+
if snapshot is None:
|
| 361 |
+
return {"status": "missing", "accuracy_score": 50.0, "confidence_label": "Unknown"}
|
| 362 |
+
return {
|
| 363 |
+
"status": "ready",
|
| 364 |
+
"accuracy_score": float(snapshot.score or 50.0),
|
| 365 |
+
"confidence_label": snapshot.confidence_label,
|
| 366 |
+
"components": snapshot.components,
|
| 367 |
+
"issues": snapshot.issues,
|
| 368 |
+
"created_at": snapshot.created_at.isoformat() if snapshot.created_at else None,
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def metric_value(metrics: dict, key: str) -> float | None:
|
| 373 |
+
payload = metrics.get(key)
|
| 374 |
+
if not isinstance(payload, dict):
|
| 375 |
+
return None
|
| 376 |
+
try:
|
| 377 |
+
return float(payload.get("value"))
|
| 378 |
+
except Exception:
|
| 379 |
+
return None
|
| 380 |
+
|
| 381 |
+
|
| 382 |
def scale(value, low: float, high: float) -> float:
|
| 383 |
try:
|
| 384 |
number = float(value)
|
frontend/lib/types.ts
CHANGED
|
@@ -138,6 +138,8 @@ export type SystemStatus = {
|
|
| 138 |
model_loading_enabled: boolean;
|
| 139 |
financial_brain_model_enabled: boolean;
|
| 140 |
live_startup_enabled: boolean;
|
|
|
|
|
|
|
| 141 |
yfinance_fallback_enabled?: boolean;
|
| 142 |
historical_price_seed_enabled?: boolean;
|
| 143 |
startup_signal_seed_enabled?: boolean;
|
|
@@ -150,6 +152,8 @@ export type SystemStatus = {
|
|
| 150 |
blum_model_cycle_limit?: number;
|
| 151 |
fundamentals_refresh_minutes?: number;
|
| 152 |
macro_refresh_minutes?: number;
|
|
|
|
|
|
|
| 153 |
};
|
| 154 |
persistence?: {
|
| 155 |
mode: string;
|
|
|
|
| 138 |
model_loading_enabled: boolean;
|
| 139 |
financial_brain_model_enabled: boolean;
|
| 140 |
live_startup_enabled: boolean;
|
| 141 |
+
autonomous_engine_enabled?: boolean;
|
| 142 |
+
autonomous_cycle_minutes?: number;
|
| 143 |
yfinance_fallback_enabled?: boolean;
|
| 144 |
historical_price_seed_enabled?: boolean;
|
| 145 |
startup_signal_seed_enabled?: boolean;
|
|
|
|
| 152 |
blum_model_cycle_limit?: number;
|
| 153 |
fundamentals_refresh_minutes?: number;
|
| 154 |
macro_refresh_minutes?: number;
|
| 155 |
+
hf_dataset_catalog_enabled?: boolean;
|
| 156 |
+
hf_dataset_refresh_hours?: number;
|
| 157 |
};
|
| 158 |
persistence?: {
|
| 159 |
mode: string;
|
frontend/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "blum-ai-financial-intelligence-frontend",
|
| 3 |
-
"version": "0.
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"dev": "next dev -p 3000",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "blum-ai-financial-intelligence-frontend",
|
| 3 |
+
"version": "0.8.0",
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"dev": "next dev -p 3000",
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "blum-ai-financial-intelligence",
|
| 3 |
-
"version": "0.
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"frontend:dev": "npm --prefix frontend run dev",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "blum-ai-financial-intelligence",
|
| 3 |
+
"version": "0.8.0",
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"frontend:dev": "npm --prefix frontend run dev",
|