feat: isolated cycle action — Check New Market Events
Décompose le cycle en 8 actions appelables individuellement. Action 1 implémentée : scan de 4 sources (news RSS, surprises FRED, MA crossovers yfinance, rapports institutionnels) → création de market_events avec déduplication. UI CycleActions page + sidebar link. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
194
backend/routers/cycle_actions.py
Normal file
194
backend/routers/cycle_actions.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Isolated Cycle Actions — callable individually from UI or cron.
|
||||
|
||||
Decomposition of the full auto_cycle into 8 independent actions:
|
||||
|
||||
ACTION 1 — check-market-events ← IMPLEMENTED HERE
|
||||
Sources: RSS news + FRED eco surprises + MA crossovers + institutional reports
|
||||
Output : new market_events created in DB
|
||||
|
||||
ACTION 2 — refresh-price-data (planned)
|
||||
Sources: yfinance OHLCV for all watchlist instruments
|
||||
Output : price_data table updated
|
||||
|
||||
ACTION 3 — compute-indicators (planned)
|
||||
Sources: price_data in DB
|
||||
Output : ma20/50/100/200 + RSI + BB + ATR stored in instrument_indicators
|
||||
|
||||
ACTION 4 — evaluate-impacts (planned)
|
||||
Sources: market_events without impact scores
|
||||
Output : GPT-evaluated impact_score + affected_assets stored
|
||||
|
||||
ACTION 5 — generate-signals (planned)
|
||||
Sources: indicators + market_events + portfolio
|
||||
Output : trading signals for each instrument
|
||||
|
||||
ACTION 6 — update-regime (planned)
|
||||
Sources: signals + macro context
|
||||
Output : current macro_regime classification
|
||||
|
||||
ACTION 7 — snapshot-portfolio (planned)
|
||||
Sources: portfolio positions + current prices
|
||||
Output : risk metrics (VaR, PnL, delta) snapshot stored
|
||||
|
||||
ACTION 8 — generate-report (planned)
|
||||
Sources: all of the above
|
||||
Output : daily cycle report PDF/JSON
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/actions", tags=["Cycle Actions"])
|
||||
|
||||
|
||||
# ── Request / Response models ─────────────────────────────────────────────────
|
||||
|
||||
class CheckEventsRequest(BaseModel):
|
||||
sources: Optional[List[str]] = None # ['news','eco','technical','reports']
|
||||
news_impact_min: float = 0.55
|
||||
news_lookback_hours: int = 48
|
||||
eco_z_threshold: float = 1.5
|
||||
eco_days: int = 7
|
||||
technical_lookback_days: int = 7
|
||||
report_days: int = 7
|
||||
report_min_importance: int = 3
|
||||
|
||||
|
||||
class ActionResult(BaseModel):
|
||||
action: str
|
||||
status: str
|
||||
started_at: str
|
||||
finished_at: str
|
||||
duration_s: float
|
||||
total_created: int
|
||||
detail: Dict[str, Any]
|
||||
|
||||
|
||||
# ── In-memory run state (prevents concurrent duplicate runs) ──────────────────
|
||||
_running: Dict[str, bool] = {}
|
||||
|
||||
|
||||
def _guard(action: str) -> None:
|
||||
if _running.get(action):
|
||||
raise HTTPException(status_code=409, detail=f"Action '{action}' is already running")
|
||||
_running[action] = True
|
||||
|
||||
|
||||
def _release(action: str) -> None:
|
||||
_running.pop(action, None)
|
||||
|
||||
|
||||
# ── GET /api/actions — list all actions with status ───────────────────────────
|
||||
|
||||
@router.get("")
|
||||
def list_actions():
|
||||
"""Return the catalogue of available cycle actions and their run status."""
|
||||
actions = [
|
||||
{
|
||||
"id": "check-market-events",
|
||||
"label": "Check New Market Events",
|
||||
"description": "Scans RSS news, FRED eco surprises, MA crossovers, and institutional reports to create new market_events.",
|
||||
"status": "running" if _running.get("check-market-events") else "idle",
|
||||
"implemented": True,
|
||||
},
|
||||
{"id": "refresh-price-data", "label": "Refresh Price Data", "description": "Download fresh OHLCV for all watchlist instruments.", "status": "idle", "implemented": False},
|
||||
{"id": "compute-indicators", "label": "Compute Indicators", "description": "Recalculate MA/RSI/BB/ATR for all instruments.", "status": "idle", "implemented": False},
|
||||
{"id": "evaluate-impacts", "label": "Evaluate Impacts", "description": "AI-score market_events that lack impact_score.", "status": "idle", "implemented": False},
|
||||
{"id": "generate-signals", "label": "Generate Signals", "description": "Compute trading signals from indicators + events.", "status": "idle", "implemented": False},
|
||||
{"id": "update-regime", "label": "Update Regime", "description": "Re-classify the current macro regime.", "status": "idle", "implemented": False},
|
||||
{"id": "snapshot-portfolio", "label": "Snapshot Portfolio", "description": "Store a risk snapshot (VaR, PnL, delta).", "status": "idle", "implemented": False},
|
||||
{"id": "generate-report", "label": "Generate Report", "description": "Build and persist the daily cycle report.", "status": "idle", "implemented": False},
|
||||
]
|
||||
return {"actions": actions}
|
||||
|
||||
|
||||
# ── POST /api/actions/check-market-events ─────────────────────────────────────
|
||||
|
||||
@router.post("/check-market-events", response_model=ActionResult)
|
||||
def check_market_events(req: CheckEventsRequest):
|
||||
"""
|
||||
ACTION 1 — Check New Market Events.
|
||||
|
||||
Runs synchronously (up to ~60 s with yfinance + OpenAI calls).
|
||||
Returns counts of created events per source.
|
||||
"""
|
||||
_guard("check-market-events")
|
||||
started_at = datetime.utcnow()
|
||||
|
||||
try:
|
||||
from services.market_event_detector import check_new_market_events
|
||||
result = check_new_market_events(
|
||||
sources=req.sources,
|
||||
news_impact_min=req.news_impact_min,
|
||||
news_lookback_hours=req.news_lookback_hours,
|
||||
eco_z_threshold=req.eco_z_threshold,
|
||||
eco_days=req.eco_days,
|
||||
technical_lookback_days=req.technical_lookback_days,
|
||||
report_days=req.report_days,
|
||||
report_min_importance=req.report_min_importance,
|
||||
)
|
||||
except Exception as e:
|
||||
_release("check-market-events")
|
||||
logger.error(f"[action/check-market-events] {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
_release("check-market-events")
|
||||
finished_at = datetime.utcnow()
|
||||
duration_s = (finished_at - started_at).total_seconds()
|
||||
|
||||
return ActionResult(
|
||||
action="check-market-events",
|
||||
status="success",
|
||||
started_at=started_at.isoformat(),
|
||||
finished_at=finished_at.isoformat(),
|
||||
duration_s=round(duration_s, 2),
|
||||
total_created=result.get("total_created", 0),
|
||||
detail={
|
||||
"news": result.get("news", []),
|
||||
"eco": result.get("eco", []),
|
||||
"technical": result.get("technical", []),
|
||||
"reports": result.get("reports", []),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── POST /api/actions/check-market-events/background ─────────────────────────
|
||||
|
||||
@router.post("/check-market-events/background", status_code=202)
|
||||
def check_market_events_background(
|
||||
req: CheckEventsRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
):
|
||||
"""
|
||||
Fire-and-forget version — returns 202 immediately.
|
||||
Results are only visible via logs (system_logs table or backend console).
|
||||
"""
|
||||
if _running.get("check-market-events"):
|
||||
raise HTTPException(status_code=409, detail="Action 'check-market-events' is already running")
|
||||
|
||||
def _run():
|
||||
_guard("check-market-events")
|
||||
try:
|
||||
from services.market_event_detector import check_new_market_events
|
||||
check_new_market_events(
|
||||
sources=req.sources,
|
||||
news_impact_min=req.news_impact_min,
|
||||
news_lookback_hours=req.news_lookback_hours,
|
||||
eco_z_threshold=req.eco_z_threshold,
|
||||
eco_days=req.eco_days,
|
||||
technical_lookback_days=req.technical_lookback_days,
|
||||
report_days=req.report_days,
|
||||
report_min_importance=req.report_min_importance,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[action/check-market-events/bg] {e}")
|
||||
finally:
|
||||
_release("check-market-events")
|
||||
|
||||
background_tasks.add_task(_run)
|
||||
return {"status": "accepted", "message": "check-market-events started in background"}
|
||||
Reference in New Issue
Block a user