initial commit — cassandra v0.1

Containerised macro-strategy dashboard: 4-panel web UI (indicators,
portfolio, flash news, AI strategic log), MariaDB store, hourly
ingestion jobs, OpenRouter-backed AI analysis.

Ports the four prototype scripts in the parent dir (market_pulse,
flash_news, trading212, strategic_log) into async services backed by a
persistent DB and served via FastAPI + Jinja2 + HTMX. APScheduler runs
as a separate compose service for crash-safety and easier restarts.

Portfolio composition + position names come live from Trading 212;
news per-ticker headlines reuse those names. Tone (NOVICE/INTERMEDIATE/
PRO) and analysis style (DRY/SPECULATIVE) are env-configurable and
stored on each log row so historical entries show what produced them.

Default model is deepseek/deepseek-v4-flash (overridable via env).
Light/dark theme toggle, sans-serif for prose surfaces, monospace for
data. Bearer-token auth, OpenRouter monthly cost cap, RSS feeds auto-
disabled on consecutive failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-15 21:56:10 +01:00
commit a10409c02b
61 changed files with 4890 additions and 0 deletions

69
app/main.py Normal file
View file

@ -0,0 +1,69 @@
"""FastAPI entrypoint. Runs Alembic migrations on startup, bootstraps the
feeds table from TOML, mounts the API + HTML routers.
"""
from __future__ import annotations
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
from alembic import command
from alembic.config import Config as AlembicConfig
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app.config import get_settings
from app.db import get_session_factory
from app.logging import configure_logging, get_logger
from app.routers import api as api_router
from app.routers import pages as pages_router
from app.services.feeds_bootstrap import bootstrap_feeds
log = get_logger("cassandra")
APP_DIR = Path(__file__).resolve().parent
PROJECT_DIR = APP_DIR.parent
def _run_migrations() -> None:
"""Synchronous Alembic upgrade. Called once at lifespan startup."""
cfg = AlembicConfig(str(PROJECT_DIR / "alembic.ini"))
cfg.set_main_option("script_location", str(PROJECT_DIR / "alembic"))
cfg.set_main_option("sqlalchemy.url", get_settings().DATABASE_URL)
command.upgrade(cfg, "head")
@asynccontextmanager
async def lifespan(app: FastAPI):
configure_logging()
log.info("cassandra.startup")
try:
# Alembic's env.py uses asyncio.run() internally; offload it to a
# worker thread so it doesn't collide with FastAPI's running loop.
await asyncio.to_thread(_run_migrations)
log.info("cassandra.migrations.applied")
except Exception as e:
log.error("cassandra.migrations.failed", error=str(e))
raise
async with get_session_factory()() as session:
inserted = await bootstrap_feeds(session)
log.info("cassandra.feeds.bootstrap", inserted=inserted)
yield
log.info("cassandra.shutdown")
app = FastAPI(
title="Cassandra",
description="Macro-strategy dashboard",
version="0.1.0",
lifespan=lifespan,
)
app.mount(
"/static",
StaticFiles(directory=str(APP_DIR / "static")),
name="static",
)
app.include_router(api_router.router, prefix="/api", tags=["api"])
app.include_router(pages_router.router, tags=["pages"])