feat: portfolio context injection + AI call log viewer

Portfolio context (portfolio_context.py):
- get_open_trades_with_moves(): fetches open trades + 1d/5d yfinance price moves
- get_portfolio_concentration(): counts by asset_class
- build_portfolio_context_block(): formatted prompt block with strict AI instructions
  (no double positions, flag contradictions, avoid overweight classes)

AI call logging:
- ai_call_logs table in DB (run_id, call_type, system/user prompt, response, tokens, ms)
- _chat() now accepts log_meta dict → saves call to DB non-blocking after each call
- suggest and score_batch calls pass run_id + call_type for full traceability

auto_cycle.py:
- Builds portfolio context before snapshot and both AI calls
- Context snapshot now includes portfolio_open_positions key

SystemLogs.tsx:
- "Contexte IA" tab gains sub-tabs: Contexte / Appels IA
- AiCallRow: expandable with 3 panes (user prompt / system prompt / response)
  shows model, tokens breakdown, duration, call type badge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-21 10:36:05 +02:00
parent 5d3ff19393
commit 4ad3a9a782
7 changed files with 457 additions and 17 deletions

View File

@@ -183,6 +183,9 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
suggest_patterns_from_market_context, score_patterns_with_context,
ai_score_news_batch, _chat, DEFAULT_ANALYSIS_TEMPLATE,
)
from services.portfolio_context import (
get_open_trades_with_moves, get_portfolio_concentration, build_portfolio_context_block,
)
# KB confidence decay (non-blocking)
try:
@@ -447,6 +450,18 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
except Exception as _te:
logger.warning(f"[Cycle] Tech indicators failed (non-blocking): {_te}")
# ── Portfolio context (open positions + recent moves) ──────────
_portfolio_block = ""
_open_trades_snap = []
_portfolio_conc = {}
try:
_open_trades_snap = get_open_trades_with_moves()
_portfolio_conc = get_portfolio_concentration(_open_trades_snap)
_portfolio_block = build_portfolio_context_block(_open_trades_snap, _portfolio_conc)
logger.info(f"[Cycle {run_id[:16]}] Portfolio context: {len(_open_trades_snap)} open trades")
except Exception as _pe:
logger.warning(f"[Cycle] Portfolio context failed (non-blocking): {_pe}")
# ── Save full context snapshot ─────────────────────────────────
try:
from services.ai_analyzer import apply_news_decay as _apply_decay2, partition_news_by_age as _part
@@ -473,6 +488,14 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
"iv_context_preview": iv_context[:500] if iv_context else "",
"calendar": calendar[:8] if calendar else [],
"quotes_summary": {cls: [{"symbol": q.get("symbol"), "price": q.get("price"), "change_pct": q.get("change_pct")} for q in qs[:3]] for cls, qs in quotes.items()},
"portfolio_open_positions": {
"count": len(_open_trades_snap),
"concentration": _portfolio_conc,
"trades": [
{k: v for k, v in t.items() if k != "id"}
for t in _open_trades_snap
],
},
}
save_cycle_context_snapshot(run_id, _context_snapshot)
logger.info(f"[Cycle {run_id[:16]}] Context snapshot saved")
@@ -488,6 +511,8 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
tech_indicators_block=_tech_block,
fred_block=_fred_block,
price_discovery_block=_price_discovery_block,
portfolio_context_block=_portfolio_block,
run_id=run_id,
)
except Exception as e:
logger.warning(f"[Cycle] Suggestion step failed: {e}")
@@ -614,6 +639,8 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
tech_indicators_block=_tech_block,
fred_block=_fred_block,
price_discovery_block=_price_discovery_block,
portfolio_context_block=_portfolio_block,
run_id=run_id,
)
scored_with_id = [s for s in scored if s.get("pattern_id")]
scored_without_id = [s for s in scored if not s.get("pattern_id")]