Italianhype commited on
Commit
4da65ca
·
verified ·
1 Parent(s): 141b71a

Add autonomous dataset intelligence engine

Browse files
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": "persistent-autonomous-blum-financial-model-v0.7.1",
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.7.1.",
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.7.1"
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=36, alias="BLUM_MAX_UPDATE_ASSETS")
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=36, alias="BLUM_STARTUP_PIPELINE_LIMIT")
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=36, alias="BLUM_MAX_DYNAMIC_ASSET_NEWS_FEEDS")
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
- if technical.get("fundamental_confirmation") is None:
 
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 / 5 * 100
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.6"
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
- score = build_score(indicators, narrative, ts, asset)
 
 
 
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.18
165
- + trend_score * 0.18
166
- + sentiment_score * 0.16
167
- + volatility_score * 0.12
168
- + anomaly_score * 0.10
169
- + semantic_trend_score * 0.12
170
- + etf_confirmation_score * 0.08
171
- + risk_adjustment * 0.06
 
 
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
- return round(avg(history_depth, indicator_completeness, news_support, sentiment_quality, time_series_depth), 1)
 
 
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.7.1",
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.7.1",
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",