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:
@@ -22,7 +22,14 @@ def get_client() -> Optional[OpenAI]:
|
||||
return _client
|
||||
|
||||
|
||||
def _chat(system: str, user: str, model: str = "gpt-4o-mini", json_mode: bool = True, max_tokens: int = 1500) -> Optional[Dict]:
|
||||
def _chat(
|
||||
system: str,
|
||||
user: str,
|
||||
model: str = "gpt-4o-mini",
|
||||
json_mode: bool = True,
|
||||
max_tokens: int = 1500,
|
||||
log_meta: Optional[Dict] = None,
|
||||
) -> Optional[Dict]:
|
||||
import time as _time
|
||||
client = get_client()
|
||||
if not client:
|
||||
@@ -35,18 +42,22 @@ def _chat(system: str, user: str, model: str = "gpt-4o-mini", json_mode: bool =
|
||||
}
|
||||
if json_mode:
|
||||
kwargs["response_format"] = {"type": "json_object"}
|
||||
|
||||
last_exc = None
|
||||
for attempt in range(4): # up to 3 retries
|
||||
result: Optional[Dict] = None
|
||||
usage = None
|
||||
t0 = _time.time()
|
||||
|
||||
for attempt in range(4):
|
||||
try:
|
||||
resp = client.chat.completions.create(**kwargs)
|
||||
content = resp.choices[0].message.content
|
||||
if json_mode:
|
||||
return json.loads(content)
|
||||
return {"text": content}
|
||||
usage = resp.usage
|
||||
result = json.loads(content) if json_mode else {"text": content}
|
||||
break # success
|
||||
except Exception as e:
|
||||
last_exc = e
|
||||
err_str = str(e)
|
||||
# 429 rate-limit: respect the retry-after hint, then back off
|
||||
if "429" in err_str or "rate_limit" in err_str:
|
||||
import re as _re
|
||||
m = _re.search(r"try again in ([\d.]+)s", err_str)
|
||||
@@ -54,7 +65,32 @@ def _chat(system: str, user: str, model: str = "gpt-4o-mini", json_mode: bool =
|
||||
_time.sleep(min(wait, 60.0))
|
||||
continue
|
||||
raise # non-429 errors propagate immediately
|
||||
raise last_exc
|
||||
|
||||
if result is None:
|
||||
raise last_exc
|
||||
|
||||
# Persist AI call log when requested (non-blocking)
|
||||
if log_meta and log_meta.get("run_id"):
|
||||
try:
|
||||
from services.database import save_ai_call_log
|
||||
duration_ms = int((_time.time() - t0) * 1000)
|
||||
save_ai_call_log(
|
||||
run_id=log_meta["run_id"],
|
||||
call_type=log_meta.get("call_type", "unknown"),
|
||||
system_prompt=system,
|
||||
user_prompt=user,
|
||||
response_json=json.dumps(result, ensure_ascii=False),
|
||||
model=model,
|
||||
tokens_prompt=usage.prompt_tokens if usage else 0,
|
||||
tokens_completion=usage.completion_tokens if usage else 0,
|
||||
duration_ms=duration_ms,
|
||||
pattern_id=log_meta.get("pattern_id"),
|
||||
pattern_name=log_meta.get("pattern_name"),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ── News / Article Analysis ───────────────────────────────────────────────────
|
||||
@@ -356,6 +392,8 @@ def score_patterns_with_context(
|
||||
tech_indicators_block: str = "",
|
||||
fred_block: str = "",
|
||||
price_discovery_block: str = "",
|
||||
portfolio_context_block: str = "",
|
||||
run_id: str = "",
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Score all patterns with rich context (news, prices, IV, risk clusters) using GPT-4o."""
|
||||
if not get_client():
|
||||
@@ -577,6 +615,7 @@ Instructions de notation:
|
||||
_tech_sc_section = f"\n{tech_indicators_block}\n" if tech_indicators_block else ""
|
||||
_fred_sc_section = f"\n{fred_block}\n" if fred_block else ""
|
||||
_pd_sc_section = f"\n{price_discovery_block}\n" if price_discovery_block else ""
|
||||
_portfolio_sc_section = f"\n{portfolio_context_block}\n" if portfolio_context_block else ""
|
||||
|
||||
user = f"""CONTEXTE GLOBAL:
|
||||
- Score risque géopolitique: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})
|
||||
@@ -586,6 +625,7 @@ Instructions de notation:
|
||||
{_fred_sc_section}
|
||||
{_pd_sc_section}
|
||||
{_tech_sc_section}
|
||||
{_portfolio_sc_section}
|
||||
TEMPLATE DE NOTATION:
|
||||
{scoring_template}
|
||||
|
||||
@@ -765,7 +805,10 @@ TEMPLATE DE NOTATION:
|
||||
+ _return_schema
|
||||
)
|
||||
try:
|
||||
res = _chat(SYSTEM_SCORER, batch_user, model="gpt-4o", json_mode=True, max_tokens=12000)
|
||||
res = _chat(
|
||||
SYSTEM_SCORER, batch_user, model="gpt-4o", json_mode=True, max_tokens=12000,
|
||||
log_meta={"run_id": run_id, "call_type": "score_batch", "pattern_name": f"batch:{','.join(ids[:3])}"} if run_id else None,
|
||||
)
|
||||
except Exception as e:
|
||||
_scorer_log.error(f"[Scorer] GPT-4o call failed for batch {ids}: {e}")
|
||||
res = None
|
||||
@@ -977,6 +1020,8 @@ def suggest_patterns_from_market_context(
|
||||
tech_indicators_block: str = "",
|
||||
fred_block: str = "",
|
||||
price_discovery_block: str = "",
|
||||
portfolio_context_block: str = "",
|
||||
run_id: str = "",
|
||||
) -> List[Dict]:
|
||||
"""Ask GPT-4o to propose new patterns based on current geo/market + macro regime context."""
|
||||
_cycle_meta = cycle_meta or {}
|
||||
@@ -1125,6 +1170,7 @@ Règles supplémentaires:
|
||||
tech_block_section = f"\n{tech_indicators_block}\n" if tech_indicators_block else ""
|
||||
fred_section = f"\n{fred_block}\n" if fred_block else ""
|
||||
pd_section = f"\n{price_discovery_block}\n" if price_discovery_block else ""
|
||||
portfolio_section = f"\n{portfolio_context_block}\n" if portfolio_context_block else ""
|
||||
|
||||
user = f"""Tu es un stratège géopolitique et financier senior, expert en options.
|
||||
{macro_block}{geo_block}{lessons_block}{reliability_block}{iv_block}
|
||||
@@ -1134,6 +1180,7 @@ Règles supplémentaires:
|
||||
{fred_section}
|
||||
{pd_section}
|
||||
{tech_block_section}
|
||||
{portfolio_section}
|
||||
## Calendrier économique à venir
|
||||
{cal_block}
|
||||
|
||||
@@ -1188,7 +1235,10 @@ Retourne UNIQUEMENT ce JSON:
|
||||
]
|
||||
}}"""
|
||||
|
||||
result = _chat(SYSTEM_SCORER, user, model="gpt-4o", json_mode=True, max_tokens=4000)
|
||||
result = _chat(
|
||||
SYSTEM_SCORER, user, model="gpt-4o", json_mode=True, max_tokens=4000,
|
||||
log_meta={"run_id": run_id, "call_type": "suggest"} if run_id else None,
|
||||
)
|
||||
if not result:
|
||||
return []
|
||||
return result.get("patterns", [])
|
||||
|
||||
Reference in New Issue
Block a user