From c2e77a73102ad58a377b0d92918934bb2700ddbe Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 25 May 2026 19:39:15 +0900 Subject: [PATCH] =?UTF-8?q?fix(ai=5Ftrade):=20Chronos=20confidence?= =?UTF-8?q?=EB=A5=BC=20absolute=20spread=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20(F4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 리뷰 F4: signal_generator의 hard gate(L79)는 absolute spread(0.6 threshold)를 쓰지만 chronos_predictor:106의 confidence는 relative spread (q90-q10)/max(|median|, 0.001). zero-shot median≈0 케이스에서 spread가 폭증하여 conf=0으로 눌리고 결국 모든 매수 신호가 confidence_threshold(0.7)를 못 넘김. 산식 통일: conf = max(0, min(1, 1 - spread/_SPREAD_THRESHOLD)). _SPREAD_THRESHOLD=0.6 은 signal_generator hard gate와 동일. - spread≈0 → conf≈1 (확신) - spread=0.3 → conf=0.5 (중간) - spread≥0.6 → conf=0 (거부) Co-Authored-By: Claude Opus 4.7 (1M context) --- ai_trade/chronos_predictor.py | 12 ++++-- ai_trade/tests/test_chronos_predictor.py | 51 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/ai_trade/chronos_predictor.py b/ai_trade/chronos_predictor.py index e9ea196..4f4c591 100644 --- a/ai_trade/chronos_predictor.py +++ b/ai_trade/chronos_predictor.py @@ -10,6 +10,10 @@ import numpy as np logger = logging.getLogger(__name__) KST = ZoneInfo("Asia/Seoul") +# F4: signal_generator hard gate와 동일한 absolute spread threshold. +# zero-shot median≈0에서 conf가 0으로 폭락하던 relative 산식 (spread/abs(median)) 대체. +_SPREAD_THRESHOLD = 0.6 + @dataclass class ChronosPrediction: @@ -103,8 +107,8 @@ class ChronosPredictor: median = float((q50_price - last_close) / last_close) q10 = float((q10_price - last_close) / last_close) q90 = float((q90_price - last_close) / last_close) - spread = (q90 - q10) / max(abs(median), 0.001) - conf = float(max(0.0, min(1.0, 1.0 - spread / 2.0))) + spread = q90 - q10 # F4: absolute spread + conf = float(max(0.0, min(1.0, 1.0 - spread / _SPREAD_THRESHOLD))) results[ticker] = ChronosPrediction( median=median, q10=q10, q90=q90, conf=conf, as_of=now_iso, ) @@ -124,8 +128,8 @@ class ChronosPredictor: median = float(np.quantile(returns, 0.5)) q10 = float(np.quantile(returns, 0.1)) q90 = float(np.quantile(returns, 0.9)) - spread = (q90 - q10) / max(abs(median), 0.001) - conf = float(max(0.0, min(1.0, 1.0 - spread / 2.0))) + spread = q90 - q10 # F4: absolute spread + conf = float(max(0.0, min(1.0, 1.0 - spread / _SPREAD_THRESHOLD))) results[ticker] = ChronosPrediction( median=median, q10=q10, q90=q90, conf=conf, as_of=now_iso, ) diff --git a/ai_trade/tests/test_chronos_predictor.py b/ai_trade/tests/test_chronos_predictor.py index 804044b..638060c 100644 --- a/ai_trade/tests/test_chronos_predictor.py +++ b/ai_trade/tests/test_chronos_predictor.py @@ -90,3 +90,54 @@ def test_return_computed_from_price_relative_to_last_close(mock_pipeline, mock_t daily = {"005930": _daily_ohlcv(list(range(41, 101)))} # last = 100 result = predictor.predict_batch(daily) assert abs(result["005930"].median - 0.10) < 0.001 + + +# ----- F4: absolute spread 기반 confidence ----- + +def test_confidence_high_when_spread_near_zero(mock_pipeline, mock_torch_cpu): + """F4 — median≈0 + spread≈0 일 때 conf≈1 (현 relative 산식의 회귀 케이스). + + 한국 주가 100000원, q10=q50=q90=100000 → median=0, spread=0. + Relative 산식 (spread/abs(median))은 0/0.001 보호선이라 spread=0이면 conf=1로 + 동작하지만, median≈0 + 미세 spread(예 1원) 케이스에서 폭증 → conf=0. + Absolute 산식은 그런 폭증 없음. + """ + quantiles = _mk_quantiles_tensor(100000.0, 100000.0, 100000.0) + mock_pipeline.predict_quantiles.return_value = (quantiles, None) + + from ai_trade.chronos_predictor import ChronosPredictor + predictor = ChronosPredictor(model_name="mock-model") + daily = {"005930": _daily_ohlcv([100000] * 60)} + result = predictor.predict_batch(daily) + assert result["005930"].conf > 0.95, ( + f"median≈0 + spread≈0인데 conf={result['005930'].conf} (F4 회귀)" + ) + + +def test_confidence_half_at_spread_03(mock_pipeline, mock_torch_cpu): + """F4 — spread 0.30일 때 conf ≈ 0.5 (1 - 0.3/0.6).""" + # q10=85000 → -0.15, q90=115000 → 0.15, q50=100000 → 0.0 + # spread = 0.30, conf = 1 - 0.30/0.60 = 0.50 + quantiles = _mk_quantiles_tensor(85000.0, 100000.0, 115000.0) + mock_pipeline.predict_quantiles.return_value = (quantiles, None) + + from ai_trade.chronos_predictor import ChronosPredictor + predictor = ChronosPredictor(model_name="mock-model") + daily = {"005930": _daily_ohlcv([100000] * 60)} + result = predictor.predict_batch(daily) + conf = result["005930"].conf + assert 0.45 < conf < 0.55, f"spread=0.30에서 conf={conf} (expected ≈0.5)" + + +def test_confidence_zero_at_threshold_spread(mock_pipeline, mock_torch_cpu): + """F4 — spread가 _SPREAD_THRESHOLD(0.6)이면 conf=0.""" + quantiles = _mk_quantiles_tensor(70000.0, 100000.0, 130000.0) + mock_pipeline.predict_quantiles.return_value = (quantiles, None) + + from ai_trade.chronos_predictor import ChronosPredictor + predictor = ChronosPredictor(model_name="mock-model") + daily = {"005930": _daily_ohlcv([100000] * 60)} + result = predictor.predict_batch(daily) + assert result["005930"].conf < 0.05, ( + f"spread=threshold에서 conf={result['005930'].conf} (expected ≈0)" + )