feat(ai_trade): emit signal에 cycle_id + expires_at 부착 (F5 part 3)
- generate_signals 진입에서 state.signal_cycle_id += 1 (emit 여부 무관 증가) - _build_buy_signal/_build_sell_signal에 cycle_id + expires_at 필드 추가 - expires_at = as_of + settings.signal_ttl_seconds (default 300s) - 매수/매도 양쪽 로그에 cycle=N 추가 기존 test_poll_loop_calls_generate_signals_after_cycle의 settings MagicMock에 signal_ttl_seconds=300 명시 (timedelta가 MagicMock 받으면 TypeError). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,7 +20,12 @@ MOMENTUM_SCORES = {
|
||||
|
||||
|
||||
def generate_signals(state, dedup, settings) -> None:
|
||||
"""Phase 4 entry — state-mutating. Evaluation order: sell first (priority), then buy. A ticker receiving a sell signal in this cycle is excluded from buy evaluation to avoid silent overwrite."""
|
||||
"""Phase 4 entry — state-mutating. F5: cycle_id += 1 (호출마다, emit 여부 무관).
|
||||
|
||||
Evaluation order: sell first (priority), then buy. A ticker receiving a sell
|
||||
signal in this cycle is excluded from buy evaluation to avoid silent overwrite.
|
||||
"""
|
||||
state.signal_cycle_id += 1
|
||||
_evaluate_sell_signals(state, dedup, settings)
|
||||
_evaluate_buy_signals(state, dedup, settings)
|
||||
|
||||
@@ -45,9 +50,10 @@ def _evaluate_buy_signals(state, dedup, settings) -> None:
|
||||
if dedup.is_recent(ticker, "buy", within_hours=24):
|
||||
logger.debug("buy %s skipped: dedup 24h", ticker)
|
||||
continue
|
||||
state.signals[ticker] = _build_buy_signal(state, ticker, name, rank, confidence)
|
||||
state.signals[ticker] = _build_buy_signal(state, ticker, name, rank, confidence, settings)
|
||||
dedup.record(ticker, "buy", confidence=confidence)
|
||||
logger.info("signal emit %s buy conf=%.3f rank=%s", ticker, confidence, rank)
|
||||
logger.info("signal emit %s buy conf=%.3f rank=%s cycle=%d",
|
||||
ticker, confidence, rank, state.signal_cycle_id)
|
||||
|
||||
|
||||
def _buy_candidates(state) -> list[tuple[str, str, int | None]]:
|
||||
@@ -96,8 +102,11 @@ def _compute_buy_confidence(state, ticker: str, rank: int | None) -> float:
|
||||
return chronos_conf * 0.5 + minute_score * 0.3 + screener_norm * 0.2
|
||||
|
||||
|
||||
def _build_buy_signal(state, ticker: str, name: str, rank: int | None, confidence: float) -> dict:
|
||||
def _build_buy_signal(state, ticker: str, name: str, rank: int | None, confidence: float, settings) -> dict:
|
||||
ap = state.asking_price[ticker]
|
||||
as_of_dt = datetime.now(KST)
|
||||
ttl = getattr(settings, "signal_ttl_seconds", 300)
|
||||
expires_at = (as_of_dt + timedelta(seconds=ttl)).isoformat()
|
||||
return {
|
||||
"ticker": ticker,
|
||||
"name": name,
|
||||
@@ -107,7 +116,9 @@ def _build_buy_signal(state, ticker: str, name: str, rank: int | None, confidenc
|
||||
"avg_price": None,
|
||||
"pnl_pct": None,
|
||||
"context": _build_context(state, ticker, rank),
|
||||
"as_of": datetime.now(KST).isoformat(),
|
||||
"as_of": as_of_dt.isoformat(),
|
||||
"cycle_id": state.signal_cycle_id,
|
||||
"expires_at": expires_at,
|
||||
}
|
||||
|
||||
|
||||
@@ -132,23 +143,24 @@ def _evaluate_sell_signals(state, dedup, settings) -> None:
|
||||
continue
|
||||
state.signals[ticker] = sell
|
||||
dedup.record(ticker, "sell", confidence=sell["confidence_webai"])
|
||||
logger.info("signal emit %s sell conf=%.3f reason=%s",
|
||||
logger.info("signal emit %s sell conf=%.3f reason=%s cycle=%d",
|
||||
ticker, sell["confidence_webai"],
|
||||
sell.get("context", {}).get("sell_reason"))
|
||||
sell.get("context", {}).get("sell_reason"),
|
||||
state.signal_cycle_id)
|
||||
|
||||
|
||||
def _try_stop_loss(state, holding: dict, settings) -> dict | None:
|
||||
pnl = holding.get("pnl_pct")
|
||||
if pnl is None or pnl >= settings.stop_loss_pct:
|
||||
return None
|
||||
return _build_sell_signal(state, holding, confidence=1.0, reason="stop_loss")
|
||||
return _build_sell_signal(state, holding, confidence=1.0, reason="stop_loss", settings=settings)
|
||||
|
||||
|
||||
def _try_take_profit(state, holding: dict, settings) -> dict | None:
|
||||
pnl = holding.get("pnl_pct")
|
||||
if pnl is None or pnl <= settings.take_profit_pct:
|
||||
return None
|
||||
return _build_sell_signal(state, holding, confidence=0.6, reason="take_profit")
|
||||
return _build_sell_signal(state, holding, confidence=0.6, reason="take_profit", settings=settings)
|
||||
|
||||
|
||||
def _try_anomaly(state, holding: dict, settings) -> dict | None:
|
||||
@@ -168,11 +180,14 @@ def _try_anomaly(state, holding: dict, settings) -> dict | None:
|
||||
confidence = pred["conf"] * 0.5 + minute_score * 0.3 + 1.0 * 0.2
|
||||
if confidence <= settings.confidence_threshold:
|
||||
return None
|
||||
return _build_sell_signal(state, holding, confidence=confidence, reason="anomaly")
|
||||
return _build_sell_signal(state, holding, confidence=confidence, reason="anomaly", settings=settings)
|
||||
|
||||
|
||||
def _build_sell_signal(state, holding: dict, confidence: float, reason: str) -> dict:
|
||||
def _build_sell_signal(state, holding: dict, confidence: float, reason: str, settings=None) -> dict:
|
||||
ticker = holding["ticker"]
|
||||
as_of_dt = datetime.now(KST)
|
||||
ttl = getattr(settings, "signal_ttl_seconds", 300) if settings else 300
|
||||
expires_at = (as_of_dt + timedelta(seconds=ttl)).isoformat()
|
||||
return {
|
||||
"ticker": ticker,
|
||||
"name": holding.get("name", ticker),
|
||||
@@ -182,7 +197,9 @@ def _build_sell_signal(state, holding: dict, confidence: float, reason: str) ->
|
||||
"avg_price": holding.get("avg_price"),
|
||||
"pnl_pct": holding.get("pnl_pct"),
|
||||
"context": _build_context(state, ticker, rank=None, sell_reason=reason),
|
||||
"as_of": datetime.now(KST).isoformat(),
|
||||
"as_of": as_of_dt.isoformat(),
|
||||
"cycle_id": state.signal_cycle_id,
|
||||
"expires_at": expires_at,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user