fix: address code review round 2 — 9 issues (2 critical, 3 important, 4 minor)
Critical: - #2: Add _entry_lock in RiskManager to serialize concurrent entry (balance race) - #3: Add startTime to get_recent_income + record _entry_time_ms (SYNC PnL fix) Important: - #1: Add threading.Lock + _run_api() helper for thread-safe Client access - #4: Convert reset_daily to async with lock - #8: Add 24h TTL to exchange_info_cache Minor: - #7: Remove duplicate Indicators creation in _open_position (use ATR directly) - #11: Add input validation for LEVERAGE, MARGIN ratios, ML_THRESHOLD - #12: Replace hardcoded corr[0]/corr[1] with dict-based dynamic access - #14: Add fillna(0.0) to LightGBM path for NaN consistency with ONNX Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,3 +48,28 @@ def test_config_max_same_direction_default():
|
||||
"""동일 방향 최대 수 기본값 2."""
|
||||
cfg = Config()
|
||||
assert cfg.max_same_direction == 2
|
||||
|
||||
|
||||
def test_config_rejects_zero_leverage():
|
||||
"""LEVERAGE=0은 ValueError."""
|
||||
os.environ["LEVERAGE"] = "0"
|
||||
with pytest.raises(ValueError, match="LEVERAGE"):
|
||||
Config()
|
||||
os.environ["LEVERAGE"] = "10" # 복원
|
||||
|
||||
|
||||
def test_config_rejects_invalid_margin_ratio():
|
||||
"""MARGIN_MAX_RATIO가 0이면 ValueError."""
|
||||
os.environ["MARGIN_MAX_RATIO"] = "0"
|
||||
with pytest.raises(ValueError, match="MARGIN_MAX_RATIO"):
|
||||
Config()
|
||||
os.environ["MARGIN_MAX_RATIO"] = "0.50" # 복원
|
||||
|
||||
|
||||
def test_config_rejects_min_gt_max_margin():
|
||||
"""MARGIN_MIN > MAX이면 ValueError."""
|
||||
os.environ["MARGIN_MIN_RATIO"] = "0.80"
|
||||
os.environ["MARGIN_MAX_RATIO"] = "0.50"
|
||||
with pytest.raises(ValueError, match="MARGIN_MIN_RATIO"):
|
||||
Config()
|
||||
os.environ["MARGIN_MIN_RATIO"] = "0.20" # 복원
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import threading
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from src.exchange import BinanceFuturesClient
|
||||
@@ -25,6 +26,7 @@ def client():
|
||||
c.symbol = config.symbol
|
||||
c._qty_precision = 1
|
||||
c._price_precision = 4
|
||||
c._api_lock = threading.Lock()
|
||||
return c
|
||||
|
||||
|
||||
@@ -43,6 +45,7 @@ def exchange():
|
||||
c.client = MagicMock()
|
||||
c._qty_precision = 1
|
||||
c._price_precision = 4
|
||||
c._api_lock = threading.Lock()
|
||||
return c
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
import os
|
||||
from src.risk_manager import RiskManager
|
||||
@@ -137,3 +138,31 @@ async def test_max_positions_global_limit(shared_risk):
|
||||
await shared_risk.register_position("XRPUSDT", "LONG")
|
||||
await shared_risk.register_position("TRXUSDT", "SHORT")
|
||||
assert await shared_risk.can_open_new_position("DOGEUSDT", "LONG") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_daily_with_lock(shared_risk):
|
||||
"""reset_daily가 lock 하에서 PnL을 초기화한다."""
|
||||
await shared_risk.close_position("DUMMY", 5.0) # dummy 기록
|
||||
shared_risk.open_positions.clear() # clean up
|
||||
assert shared_risk.daily_pnl == 5.0
|
||||
await shared_risk.reset_daily()
|
||||
assert shared_risk.daily_pnl == 0.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entry_lock_serializes_access(shared_risk):
|
||||
"""_entry_lock이 동시 접근을 직렬화하는지 확인."""
|
||||
order = []
|
||||
|
||||
async def simulated_entry(name: str):
|
||||
async with shared_risk._entry_lock:
|
||||
order.append(f"{name}_start")
|
||||
await asyncio.sleep(0.05)
|
||||
order.append(f"{name}_end")
|
||||
|
||||
await asyncio.gather(simulated_entry("A"), simulated_entry("B"))
|
||||
# 직렬화 확인: A_start, A_end, B_start, B_end 또는 B_start, B_end, A_start, A_end
|
||||
assert order[0].endswith("_start")
|
||||
assert order[1].endswith("_end")
|
||||
assert order[0][0] == order[1][0] # 같은 이름으로 시작/끝
|
||||
|
||||
Reference in New Issue
Block a user