Spaces:
Running
Running
Harden Blum autonomous learning persistence
Browse files- README.md +9 -1
- backend/app/api/routes.py +33 -2
- backend/app/core/config.py +3 -1
- backend/app/services/blum_financial_model.py +41 -0
- backend/app/services/realtime.py +10 -2
- frontend/lib/types.ts +15 -0
- frontend/package.json +1 -1
- package.json +1 -1
- scripts/start.sh +38 -0
README.md
CHANGED
|
@@ -191,6 +191,7 @@ The reasoning model APIs are backend-only:
|
|
| 191 |
- `POST /model/capture/{ticker}`
|
| 192 |
- `POST /model/capture-all`
|
| 193 |
- `POST /model/evaluate-outcomes`
|
|
|
|
| 194 |
- `GET /model/knowledge`
|
| 195 |
- `GET /model/knowledge/{record_id}`
|
| 196 |
- `GET /model/memory/search?q=...`
|
|
@@ -208,6 +209,8 @@ Training export uses JSONL and targets future Hugging Face training workflows fo
|
|
| 208 |
|
| 209 |
The objective is not to predict stock prices. The objective is to learn how Blum reasons: explain, contextualize, compare, critique, calibrate confidence and improve future thesis quality.
|
| 210 |
|
|
|
|
|
|
|
| 211 |
## Chart Vision Technical Analyst
|
| 212 |
|
| 213 |
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.
|
|
@@ -439,9 +442,11 @@ Optional self-learning cadence:
|
|
| 439 |
```bash
|
| 440 |
export BLUM_ENABLE_LEARNING_LOOP=true
|
| 441 |
export BLUM_LEARNING_LOOP_MINUTES=360
|
|
|
|
|
|
|
| 442 |
```
|
| 443 |
|
| 444 |
-
The learning
|
| 445 |
|
| 446 |
## Docker
|
| 447 |
|
|
@@ -459,6 +464,8 @@ docker run --rm -p 7860:7860 \
|
|
| 459 |
blum-ai-financial-intelligence
|
| 460 |
```
|
| 461 |
|
|
|
|
|
|
|
| 462 |
## Hugging Face Spaces Deployment
|
| 463 |
|
| 464 |
Use a Docker Space. Upload the repository with:
|
|
@@ -477,6 +484,7 @@ The UI exposes `/system/status` in the sidebar and dashboard. If the GUI looks u
|
|
| 477 |
|
| 478 |
- `app_version` must show the latest deployed version.
|
| 479 |
- `feature_set` must show the expected feature bundle.
|
|
|
|
| 480 |
- `Financial Brain` shows `fallback mode` unless `BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true`.
|
| 481 |
- Hugging Face serves the previous Docker image until the new build finishes successfully.
|
| 482 |
- Existing Market Brain snapshots should be regenerated with `Run brain` after a deployment.
|
|
|
|
| 191 |
- `POST /model/capture/{ticker}`
|
| 192 |
- `POST /model/capture-all`
|
| 193 |
- `POST /model/evaluate-outcomes`
|
| 194 |
+
- `POST /model/run-learning-cycle`
|
| 195 |
- `GET /model/knowledge`
|
| 196 |
- `GET /model/knowledge/{record_id}`
|
| 197 |
- `GET /model/memory/search?q=...`
|
|
|
|
| 209 |
|
| 210 |
The objective is not to predict stock prices. The objective is to learn how Blum reasons: explain, contextualize, compare, critique, calibrate confidence and improve future thesis quality.
|
| 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.
|
|
|
|
| 442 |
```bash
|
| 443 |
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.
|
| 450 |
|
| 451 |
## Docker
|
| 452 |
|
|
|
|
| 464 |
blum-ai-financial-intelligence
|
| 465 |
```
|
| 466 |
|
| 467 |
+
For Hugging Face Docker demos without an external database, the startup script writes periodic embedded PostgreSQL backups to `/data/blum/embedded_postgres_blum.sql` and restores them on startup when the public schema is empty. This protects the learning memory only when Hugging Face persistent storage is enabled for the `/data` mount. The strict no-reset configuration is still an external `DATABASE_URL`.
|
| 468 |
+
|
| 469 |
## Hugging Face Spaces Deployment
|
| 470 |
|
| 471 |
Use a Docker Space. Upload the repository with:
|
|
|
|
| 484 |
|
| 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 |
- `Financial Brain` shows `fallback mode` unless `BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true`.
|
| 489 |
- Hugging Face serves the previous Docker image until the new build finishes successfully.
|
| 490 |
- Existing Market Brain snapshots should be regenerated with `Run brain` after a deployment.
|
backend/app/api/routes.py
CHANGED
|
@@ -71,6 +71,7 @@ from app.services.blum_financial_model import (
|
|
| 71 |
narrative_memory,
|
| 72 |
quality_overview,
|
| 73 |
regime_memory,
|
|
|
|
| 74 |
self_critique_for_record,
|
| 75 |
semantic_reasoning_search,
|
| 76 |
training_manifest,
|
|
@@ -132,7 +133,7 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 132 |
return {
|
| 133 |
"service": "blum-ai-financial-intelligence",
|
| 134 |
"app_version": settings.app_version,
|
| 135 |
-
"feature_set": "
|
| 136 |
"environment": settings.environment,
|
| 137 |
"generated_at": datetime.utcnow().isoformat(),
|
| 138 |
"hugging_face": {
|
|
@@ -153,11 +154,14 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 153 |
"accuracy_audit_minutes": settings.accuracy_audit_minutes,
|
| 154 |
"learning_loop_enabled": settings.enable_learning_loop,
|
| 155 |
"learning_loop_minutes": settings.learning_loop_minutes,
|
|
|
|
|
|
|
| 156 |
"chart_vision_mode": settings.chart_vision_mode,
|
| 157 |
"chart_vision_min_confidence": settings.chart_vision_min_confidence,
|
| 158 |
"fundamentals_refresh_minutes": settings.fundamentals_refresh_minutes,
|
| 159 |
"macro_refresh_minutes": settings.macro_refresh_minutes,
|
| 160 |
},
|
|
|
|
| 161 |
"active_models": {
|
| 162 |
"finbert": settings.finbert_model,
|
| 163 |
"embeddings": settings.embedding_model,
|
|
@@ -228,11 +232,33 @@ def system_status(db: Session = Depends(get_db)) -> dict:
|
|
| 228 |
"Hugging Face serves the previous image until the Docker build finishes successfully.",
|
| 229 |
"The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
|
| 230 |
"Existing snapshots must be regenerated with Run brain or full pipeline after a new deployment.",
|
| 231 |
-
"Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.7.
|
| 232 |
],
|
| 233 |
}
|
| 234 |
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
@router.get("/brain/status")
|
| 237 |
def financial_brain_status(db: Session = Depends(get_db)) -> dict:
|
| 238 |
return brain_status(db)
|
|
@@ -309,6 +335,11 @@ def blum_model_evaluate_outcomes(limit: int = Query(default=250, ge=1, le=2000),
|
|
| 309 |
return evaluate_thesis_outcomes(db, limit=limit)
|
| 310 |
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
@router.get("/model/knowledge")
|
| 313 |
def blum_model_knowledge_records(
|
| 314 |
ticker: str | None = Query(default=None),
|
|
|
|
| 71 |
narrative_memory,
|
| 72 |
quality_overview,
|
| 73 |
regime_memory,
|
| 74 |
+
run_model_learning_cycle,
|
| 75 |
self_critique_for_record,
|
| 76 |
semantic_reasoning_search,
|
| 77 |
training_manifest,
|
|
|
|
| 133 |
return {
|
| 134 |
"service": "blum-ai-financial-intelligence",
|
| 135 |
"app_version": settings.app_version,
|
| 136 |
+
"feature_set": "persistent-autonomous-blum-financial-model-v0.7.1",
|
| 137 |
"environment": settings.environment,
|
| 138 |
"generated_at": datetime.utcnow().isoformat(),
|
| 139 |
"hugging_face": {
|
|
|
|
| 154 |
"accuracy_audit_minutes": settings.accuracy_audit_minutes,
|
| 155 |
"learning_loop_enabled": settings.enable_learning_loop,
|
| 156 |
"learning_loop_minutes": settings.learning_loop_minutes,
|
| 157 |
+
"blum_model_cycle_minutes": settings.blum_model_cycle_minutes,
|
| 158 |
+
"blum_model_cycle_limit": settings.blum_model_cycle_limit,
|
| 159 |
"chart_vision_mode": settings.chart_vision_mode,
|
| 160 |
"chart_vision_min_confidence": settings.chart_vision_min_confidence,
|
| 161 |
"fundamentals_refresh_minutes": settings.fundamentals_refresh_minutes,
|
| 162 |
"macro_refresh_minutes": settings.macro_refresh_minutes,
|
| 163 |
},
|
| 164 |
+
"persistence": database_persistence_status(),
|
| 165 |
"active_models": {
|
| 166 |
"finbert": settings.finbert_model,
|
| 167 |
"embeddings": settings.embedding_model,
|
|
|
|
| 232 |
"Hugging Face serves the previous image until the Docker build finishes successfully.",
|
| 233 |
"The finance-domain 7B model is disabled by default unless BLUM_ENABLE_FINANCIAL_BRAIN_MODEL=true.",
|
| 234 |
"Existing snapshots must be regenerated with Run brain or full pipeline after a new deployment.",
|
| 235 |
+
"Browser cache can keep old static Next.js chunks; hard refresh if app_version is not 0.7.1.",
|
| 236 |
],
|
| 237 |
}
|
| 238 |
|
| 239 |
|
| 240 |
+
def database_persistence_status() -> dict:
|
| 241 |
+
backup_file = os.getenv("BLUM_EMBEDDED_POSTGRES_BACKUP_FILE")
|
| 242 |
+
backup_exists = bool(backup_file and os.path.exists(backup_file))
|
| 243 |
+
backup_size = os.path.getsize(backup_file) if backup_exists and backup_file else 0
|
| 244 |
+
uses_external_database = bool(os.getenv("DATABASE_URL")) and not backup_file
|
| 245 |
+
mode = "external_postgres" if uses_external_database else "embedded_postgres"
|
| 246 |
+
return {
|
| 247 |
+
"mode": mode,
|
| 248 |
+
"external_database_configured": uses_external_database,
|
| 249 |
+
"embedded_backup_file": backup_file,
|
| 250 |
+
"embedded_backup_exists": backup_exists,
|
| 251 |
+
"embedded_backup_size_bytes": backup_size,
|
| 252 |
+
"embedded_backup_interval_seconds": int(os.getenv("BLUM_DB_BACKUP_SECONDS", "300")),
|
| 253 |
+
"persistent_dir": os.getenv("BLUM_PERSIST_DIR", "/data/blum"),
|
| 254 |
+
"strict_no_reset_mode": uses_external_database,
|
| 255 |
+
"durability_note": (
|
| 256 |
+
"External DATABASE_URL is the strict no-reset mode. Embedded PostgreSQL backup can recover learning state only "
|
| 257 |
+
"when Hugging Face persistent storage is enabled for the /data mount."
|
| 258 |
+
),
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
|
| 262 |
@router.get("/brain/status")
|
| 263 |
def financial_brain_status(db: Session = Depends(get_db)) -> dict:
|
| 264 |
return brain_status(db)
|
|
|
|
| 335 |
return evaluate_thesis_outcomes(db, limit=limit)
|
| 336 |
|
| 337 |
|
| 338 |
+
@router.post("/model/run-learning-cycle")
|
| 339 |
+
def blum_model_run_learning_cycle(limit: int = Query(default=120, ge=1, le=2000), db: Session = Depends(get_db)) -> dict:
|
| 340 |
+
return run_model_learning_cycle(db, limit=limit)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
@router.get("/model/knowledge")
|
| 344 |
def blum_model_knowledge_records(
|
| 345 |
ticker: str | None = Query(default=None),
|
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.
|
| 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",
|
|
@@ -40,6 +40,8 @@ class Settings(BaseSettings):
|
|
| 40 |
accuracy_audit_minutes: int = Field(default=240, alias="BLUM_ACCURACY_AUDIT_MINUTES")
|
| 41 |
enable_learning_loop: bool = Field(default=True, alias="BLUM_ENABLE_LEARNING_LOOP")
|
| 42 |
learning_loop_minutes: int = Field(default=360, alias="BLUM_LEARNING_LOOP_MINUTES")
|
|
|
|
|
|
|
| 43 |
fundamentals_refresh_minutes: int = Field(default=720, alias="BLUM_FUNDAMENTALS_REFRESH_MINUTES")
|
| 44 |
macro_refresh_minutes: int = Field(default=240, alias="BLUM_MACRO_REFRESH_MINUTES")
|
| 45 |
stale_price_max_age_days: int = Field(default=7, alias="BLUM_STALE_PRICE_MAX_AGE_DAYS")
|
|
|
|
| 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",
|
|
|
|
| 40 |
accuracy_audit_minutes: int = Field(default=240, alias="BLUM_ACCURACY_AUDIT_MINUTES")
|
| 41 |
enable_learning_loop: bool = Field(default=True, alias="BLUM_ENABLE_LEARNING_LOOP")
|
| 42 |
learning_loop_minutes: int = Field(default=360, alias="BLUM_LEARNING_LOOP_MINUTES")
|
| 43 |
+
blum_model_cycle_minutes: int = Field(default=5, alias="BLUM_MODEL_CYCLE_MINUTES")
|
| 44 |
+
blum_model_cycle_limit: int = Field(default=120, alias="BLUM_MODEL_CYCLE_LIMIT")
|
| 45 |
fundamentals_refresh_minutes: int = Field(default=720, alias="BLUM_FUNDAMENTALS_REFRESH_MINUTES")
|
| 46 |
macro_refresh_minutes: int = Field(default=240, alias="BLUM_MACRO_REFRESH_MINUTES")
|
| 47 |
stale_price_max_age_days: int = Field(default=7, alias="BLUM_STALE_PRICE_MAX_AGE_DAYS")
|
backend/app/services/blum_financial_model.py
CHANGED
|
@@ -72,6 +72,47 @@ def model_status(db: Session) -> dict:
|
|
| 72 |
}
|
| 73 |
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
def capture_latest_asset_reasoning(db: Session, asset: Asset, source_type: str = "manual_capture") -> dict:
|
| 76 |
signal = db.scalar(
|
| 77 |
select(SignalSnapshot)
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
|
| 75 |
+
def run_model_learning_cycle(db: Session, limit: int = 120) -> dict:
|
| 76 |
+
signals = db.scalars(select(SignalSnapshot).order_by(desc(SignalSnapshot.created_at)).limit(limit)).all()
|
| 77 |
+
skipped = 0
|
| 78 |
+
before_count = count(db, BlumKnowledgeRecord.id)
|
| 79 |
+
for signal in signals:
|
| 80 |
+
asset = signal.asset or db.get(Asset, signal.asset_id)
|
| 81 |
+
if asset is None:
|
| 82 |
+
skipped += 1
|
| 83 |
+
continue
|
| 84 |
+
capture_signal_reasoning(db, signal, asset)
|
| 85 |
+
db.flush()
|
| 86 |
+
captured = max(0, count(db, BlumKnowledgeRecord.id) - before_count)
|
| 87 |
+
outcome_result = evaluate_thesis_outcomes(db, limit=limit)
|
| 88 |
+
dataset_result = build_training_dataset(db, limit=limit, min_quality=55.0)
|
| 89 |
+
event = LearningEvent(
|
| 90 |
+
event_type="blum_model_autonomous_cycle",
|
| 91 |
+
severity="Info",
|
| 92 |
+
title="Blum Financial Model autonomous cycle completed",
|
| 93 |
+
description="Captured latest reasoning, evaluated matured thesis outcomes and refreshed proprietary training examples.",
|
| 94 |
+
payload={
|
| 95 |
+
"signals_seen": len(signals),
|
| 96 |
+
"knowledge_records_created": captured,
|
| 97 |
+
"signals_skipped": skipped,
|
| 98 |
+
"outcomes": outcome_result,
|
| 99 |
+
"dataset": dataset_result,
|
| 100 |
+
},
|
| 101 |
+
)
|
| 102 |
+
db.add(event)
|
| 103 |
+
db.commit()
|
| 104 |
+
return {
|
| 105 |
+
"status": "ok",
|
| 106 |
+
"signals_seen": len(signals),
|
| 107 |
+
"knowledge_records_created": captured,
|
| 108 |
+
"signals_skipped": skipped,
|
| 109 |
+
"outcomes": outcome_result,
|
| 110 |
+
"dataset": dataset_result,
|
| 111 |
+
"learning_event_id": event.id,
|
| 112 |
+
"disclaimer": DISCLAIMER,
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
def capture_latest_asset_reasoning(db: Session, asset: Asset, source_type: str = "manual_capture") -> dict:
|
| 117 |
signal = db.scalar(
|
| 118 |
select(SignalSnapshot)
|
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.data_continuity import repair_data_gaps
|
| 14 |
from app.services.etf import update_etf_trends
|
| 15 |
from app.services.fundamentals import update_fundamentals
|
|
@@ -52,6 +53,7 @@ def start_realtime_services() -> None:
|
|
| 52 |
_scheduler.add_job(run_ipo_refresh, "interval", minutes=settings.ipo_refresh_minutes, id="ipo_refresh", replace_existing=True, max_instances=1)
|
| 53 |
if settings.enable_learning_loop:
|
| 54 |
_scheduler.add_job(run_learning_cycle_job, "interval", minutes=settings.learning_loop_minutes, id="financial_brain_learning", replace_existing=True, max_instances=1)
|
|
|
|
| 55 |
_scheduler.start()
|
| 56 |
with _state_lock:
|
| 57 |
_state["started"] = True
|
|
@@ -73,7 +75,8 @@ def run_startup_pipeline() -> None:
|
|
| 73 |
def work(db):
|
| 74 |
pipeline = PipelineService().run(db, limit=settings.startup_pipeline_limit, period=settings.historical_price_period)
|
| 75 |
learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
_run_job("startup_pipeline", work)
|
| 79 |
|
|
@@ -88,7 +91,8 @@ def run_market_refresh() -> None:
|
|
| 88 |
signals = SignalEngine().run(db, limit=settings.max_update_assets)
|
| 89 |
etf = update_etf_trends(db)
|
| 90 |
learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
|
| 91 |
-
|
|
|
|
| 92 |
|
| 93 |
_run_job("market_refresh", work)
|
| 94 |
|
|
@@ -120,6 +124,10 @@ def run_learning_cycle_job() -> None:
|
|
| 120 |
_run_job("financial_brain_learning", lambda db: run_learning_cycle(db, limit=settings.max_update_assets * 6))
|
| 121 |
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
def _run_job(job_name: str, work):
|
| 124 |
with _state_lock:
|
| 125 |
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.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
|
| 16 |
from app.services.fundamentals import update_fundamentals
|
|
|
|
| 53 |
_scheduler.add_job(run_ipo_refresh, "interval", minutes=settings.ipo_refresh_minutes, id="ipo_refresh", replace_existing=True, max_instances=1)
|
| 54 |
if settings.enable_learning_loop:
|
| 55 |
_scheduler.add_job(run_learning_cycle_job, "interval", minutes=settings.learning_loop_minutes, id="financial_brain_learning", replace_existing=True, max_instances=1)
|
| 56 |
+
_scheduler.add_job(run_blum_model_cycle_job, "interval", minutes=settings.blum_model_cycle_minutes, id="blum_financial_model_cycle", replace_existing=True, max_instances=1)
|
| 57 |
_scheduler.start()
|
| 58 |
with _state_lock:
|
| 59 |
_state["started"] = True
|
|
|
|
| 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 {}
|
| 78 |
+
model_learning = run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit) if settings.enable_learning_loop else {}
|
| 79 |
+
return {"pipeline": pipeline, "financial_brain_learning": learning, "blum_financial_model": model_learning}
|
| 80 |
|
| 81 |
_run_job("startup_pipeline", work)
|
| 82 |
|
|
|
|
| 91 |
signals = SignalEngine().run(db, limit=settings.max_update_assets)
|
| 92 |
etf = update_etf_trends(db)
|
| 93 |
learning = run_learning_cycle(db, limit=settings.max_update_assets * 6) if settings.enable_learning_loop else {}
|
| 94 |
+
model_learning = run_model_learning_cycle(db, limit=settings.blum_model_cycle_limit) if settings.enable_learning_loop else {}
|
| 95 |
+
return {"market_update": market, "signal_run": signals, "etf_update": etf, "financial_brain_learning": learning, "blum_financial_model": model_learning}
|
| 96 |
|
| 97 |
_run_job("market_refresh", work)
|
| 98 |
|
|
|
|
| 124 |
_run_job("financial_brain_learning", lambda db: run_learning_cycle(db, limit=settings.max_update_assets * 6))
|
| 125 |
|
| 126 |
|
| 127 |
+
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"]:
|
frontend/lib/types.ts
CHANGED
|
@@ -144,9 +144,24 @@ export type SystemStatus = {
|
|
| 144 |
startup_accuracy_seed_enabled?: boolean;
|
| 145 |
data_gap_repair_minutes?: number;
|
| 146 |
accuracy_audit_minutes?: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
fundamentals_refresh_minutes?: number;
|
| 148 |
macro_refresh_minutes?: number;
|
| 149 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
active_models: {
|
| 151 |
finbert: string;
|
| 152 |
embeddings: string;
|
|
|
|
| 144 |
startup_accuracy_seed_enabled?: boolean;
|
| 145 |
data_gap_repair_minutes?: number;
|
| 146 |
accuracy_audit_minutes?: number;
|
| 147 |
+
learning_loop_enabled?: boolean;
|
| 148 |
+
learning_loop_minutes?: number;
|
| 149 |
+
blum_model_cycle_minutes?: number;
|
| 150 |
+
blum_model_cycle_limit?: number;
|
| 151 |
fundamentals_refresh_minutes?: number;
|
| 152 |
macro_refresh_minutes?: number;
|
| 153 |
};
|
| 154 |
+
persistence?: {
|
| 155 |
+
mode: string;
|
| 156 |
+
external_database_configured: boolean;
|
| 157 |
+
embedded_backup_file?: string | null;
|
| 158 |
+
embedded_backup_exists: boolean;
|
| 159 |
+
embedded_backup_size_bytes: number;
|
| 160 |
+
embedded_backup_interval_seconds: number;
|
| 161 |
+
persistent_dir: string;
|
| 162 |
+
strict_no_reset_mode: boolean;
|
| 163 |
+
durability_note: string;
|
| 164 |
+
};
|
| 165 |
active_models: {
|
| 166 |
finbert: string;
|
| 167 |
embeddings: string;
|
frontend/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "blum-ai-financial-intelligence-frontend",
|
| 3 |
-
"version": "0.7.
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"dev": "next dev -p 3000",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "blum-ai-financial-intelligence-frontend",
|
| 3 |
+
"version": "0.7.1",
|
| 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.
|
| 4 |
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
"frontend:dev": "npm --prefix frontend run dev",
|
|
|
|
| 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",
|
scripts/start.sh
CHANGED
|
@@ -2,13 +2,51 @@
|
|
| 2 |
set -euo pipefail
|
| 3 |
|
| 4 |
export PORT="${PORT:-7860}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
if [[ -z "${DATABASE_URL:-}" ]]; then
|
| 7 |
echo "No DATABASE_URL provided. Starting embedded PostgreSQL for the Hugging Face Docker demo."
|
|
|
|
| 8 |
service postgresql start
|
| 9 |
su postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='blum'\" | grep -q 1 || createdb blum"
|
| 10 |
su postgres -c "psql -c \"ALTER USER postgres PASSWORD 'postgres';\""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
export DATABASE_URL="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum"
|
|
|
|
| 12 |
else
|
| 13 |
echo "Using external PostgreSQL DATABASE_URL."
|
| 14 |
fi
|
|
|
|
| 2 |
set -euo pipefail
|
| 3 |
|
| 4 |
export PORT="${PORT:-7860}"
|
| 5 |
+
export BLUM_PERSIST_DIR="${BLUM_PERSIST_DIR:-/data/blum}"
|
| 6 |
+
export BLUM_DB_BACKUP_SECONDS="${BLUM_DB_BACKUP_SECONDS:-300}"
|
| 7 |
+
|
| 8 |
+
backup_embedded_postgres() {
|
| 9 |
+
if [[ -n "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE:-}" ]]; then
|
| 10 |
+
mkdir -p "$(dirname "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}")" || true
|
| 11 |
+
if su postgres -c "pg_dump blum" > "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}.tmp"; then
|
| 12 |
+
mv "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}.tmp" "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}"
|
| 13 |
+
echo "Embedded PostgreSQL backup updated at ${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}."
|
| 14 |
+
else
|
| 15 |
+
echo "Embedded PostgreSQL backup failed; keeping previous backup if present."
|
| 16 |
+
rm -f "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}.tmp"
|
| 17 |
+
fi
|
| 18 |
+
fi
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
start_backup_loop() {
|
| 22 |
+
if [[ -n "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE:-}" ]]; then
|
| 23 |
+
(
|
| 24 |
+
while true; do
|
| 25 |
+
sleep "${BLUM_DB_BACKUP_SECONDS}"
|
| 26 |
+
backup_embedded_postgres
|
| 27 |
+
done
|
| 28 |
+
) &
|
| 29 |
+
fi
|
| 30 |
+
}
|
| 31 |
|
| 32 |
if [[ -z "${DATABASE_URL:-}" ]]; then
|
| 33 |
echo "No DATABASE_URL provided. Starting embedded PostgreSQL for the Hugging Face Docker demo."
|
| 34 |
+
mkdir -p "${BLUM_PERSIST_DIR}" || true
|
| 35 |
service postgresql start
|
| 36 |
su postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='blum'\" | grep -q 1 || createdb blum"
|
| 37 |
su postgres -c "psql -c \"ALTER USER postgres PASSWORD 'postgres';\""
|
| 38 |
+
export BLUM_EMBEDDED_POSTGRES_BACKUP_FILE="${BLUM_PERSIST_DIR}/embedded_postgres_blum.sql"
|
| 39 |
+
TABLE_COUNT="$(su postgres -c "psql -d blum -tAc \"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';\"" | tr -d '[:space:]')"
|
| 40 |
+
if [[ "${TABLE_COUNT:-0}" == "0" && -s "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}" ]]; then
|
| 41 |
+
echo "Restoring embedded PostgreSQL backup from ${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}."
|
| 42 |
+
su postgres -c "psql -d blum" < "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}" || echo "Backup restore failed; continuing with migrations."
|
| 43 |
+
elif [[ ! -s "${BLUM_EMBEDDED_POSTGRES_BACKUP_FILE}" ]]; then
|
| 44 |
+
echo "No embedded PostgreSQL backup found yet. A new backup will be written periodically."
|
| 45 |
+
else
|
| 46 |
+
echo "Embedded PostgreSQL already contains tables; skipping restore."
|
| 47 |
+
fi
|
| 48 |
export DATABASE_URL="postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/blum"
|
| 49 |
+
start_backup_loop
|
| 50 |
else
|
| 51 |
echo "Using external PostgreSQL DATABASE_URL."
|
| 52 |
fi
|