fix: resolve 6 warning issues from code review

5. Add daily PnL reset loop — UTC midnight auto-reset via
   _daily_reset_loop in main.py, prevents stale daily_pnl accumulation
6. Fix set_base_balance race condition — call once in main.py before
   spawning bots, instead of each bot calling independently
7. Remove realized_pnl != 0 from close detection — prevents entry
   orders with small rp values being misclassified as closes
8. Rename xrp_btc_rs/xrp_eth_rs → primary_btc_rs/primary_eth_rs —
   generic column names for multi-symbol support (dataset_builder,
   ml_features, and tests updated consistently)
9. Replace asyncio.get_event_loop() → get_running_loop() — fixes
   DeprecationWarning on Python 3.10+
10. Parallelize candle preload — asyncio.gather for all symbols
    instead of sequential REST calls, ~3x faster startup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
21in7
2026-03-16 22:44:40 +09:00
parent 8803c71bf9
commit 64f56806d2
10 changed files with 85 additions and 61 deletions

View File

@@ -432,9 +432,6 @@ class TradingBot:
logger.info(f"[{self.symbol}] 봇 시작, 레버리지 {self.config.leverage}x")
await self._recover_position()
await self._init_oi_history()
balance = await self.exchange.get_balance()
self.risk.set_base_balance(balance)
logger.info(f"[{self.symbol}] 기준 잔고 설정: {balance:.2f} USDT (동적 증거금 비율 기준점)")
user_stream = UserDataStream(
symbol=self.symbol,

View File

@@ -161,26 +161,31 @@ class MultiSymbolStream:
df.set_index("timestamp", inplace=True)
return df
async def _preload_one(self, client: AsyncClient, symbol: str, limit: int):
"""단일 심볼의 과거 캔들을 버퍼에 채운다."""
logger.info(f"{symbol.upper()} 과거 캔들 {limit}개 로드 중...")
klines = await client.futures_klines(
symbol=symbol.upper(),
interval=self.interval,
limit=limit,
)
for k in klines[:-1]:
self.buffers[symbol].append({
"timestamp": k[0],
"open": float(k[1]),
"high": float(k[2]),
"low": float(k[3]),
"close": float(k[4]),
"volume": float(k[5]),
"is_closed": True,
})
logger.info(f"{symbol.upper()} {len(self.buffers[symbol])}개 로드 완료")
async def _preload_history(self, client: AsyncClient, limit: int = _PRELOAD_LIMIT):
"""REST API로 모든 심볼의 과거 캔들을 버퍼에 미리 채운다."""
for symbol in self.symbols:
logger.info(f"{symbol.upper()} 과거 캔들 {limit}개 로드 중...")
klines = await client.futures_klines(
symbol=symbol.upper(),
interval=self.interval,
limit=limit,
)
for k in klines[:-1]:
self.buffers[symbol].append({
"timestamp": k[0],
"open": float(k[1]),
"high": float(k[2]),
"low": float(k[3]),
"close": float(k[4]),
"volume": float(k[5]),
"is_closed": True,
})
logger.info(f"{symbol.upper()} {len(self.buffers[symbol])}개 로드 완료")
"""REST API로 모든 심볼의 과거 캔들을 병렬로 버퍼에 미리 채운다."""
await asyncio.gather(*[
self._preload_one(client, symbol, limit) for symbol in self.symbols
])
async def start(self, api_key: str, api_secret: str):
client = await AsyncClient.create(

View File

@@ -266,15 +266,15 @@ def _calc_features_vectorized(
eth_r3 = _align(eth_ret_3, n).astype(np.float32)
eth_r5 = _align(eth_ret_5, n).astype(np.float32)
xrp_r1 = ret_1.astype(np.float32)
xrp_btc_rs_raw = np.divide(
xrp_r1, btc_r1,
out=np.zeros_like(xrp_r1),
primary_r1 = ret_1.astype(np.float32)
primary_btc_rs_raw = np.divide(
primary_r1, btc_r1,
out=np.zeros_like(primary_r1),
where=(btc_r1 != 0),
).astype(np.float32)
xrp_eth_rs_raw = np.divide(
xrp_r1, eth_r1,
out=np.zeros_like(xrp_r1),
primary_eth_rs_raw = np.divide(
primary_r1, eth_r1,
out=np.zeros_like(primary_r1),
where=(eth_r1 != 0),
).astype(np.float32)
@@ -285,8 +285,8 @@ def _calc_features_vectorized(
"eth_ret_1": _rolling_zscore(eth_r1),
"eth_ret_3": _rolling_zscore(eth_r3),
"eth_ret_5": _rolling_zscore(eth_r5),
"xrp_btc_rs": _rolling_zscore(xrp_btc_rs_raw),
"xrp_eth_rs": _rolling_zscore(xrp_eth_rs_raw),
"primary_btc_rs": _rolling_zscore(primary_btc_rs_raw),
"primary_eth_rs": _rolling_zscore(primary_eth_rs_raw),
}, index=d.index)
result = pd.concat([result, extra], axis=1)

View File

@@ -73,7 +73,7 @@ class BinanceFuturesClient:
return qty_rounded
async def set_leverage(self, leverage: int) -> dict:
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None,
lambda: self.client.futures_change_leverage(
@@ -82,7 +82,7 @@ class BinanceFuturesClient:
)
async def get_balance(self) -> float:
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
balances = await loop.run_in_executor(
None, self.client.futures_account_balance
)
@@ -100,7 +100,7 @@ class BinanceFuturesClient:
stop_price: float = None,
reduce_only: bool = False,
) -> dict:
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
params = dict(
symbol=self.symbol,
@@ -123,7 +123,7 @@ class BinanceFuturesClient:
raise
async def get_position(self) -> dict | None:
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
positions = await loop.run_in_executor(
None,
lambda: self.client.futures_position_information(
@@ -137,7 +137,7 @@ class BinanceFuturesClient:
async def cancel_all_orders(self):
"""오픈 주문을 모두 취소한다."""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None,
lambda: self.client.futures_cancel_all_open_orders(
@@ -147,7 +147,7 @@ class BinanceFuturesClient:
async def get_recent_income(self, limit: int = 5) -> list[dict]:
"""최근 REALIZED_PNL + COMMISSION 내역을 조회한다."""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
try:
rows = await loop.run_in_executor(
None,
@@ -168,7 +168,7 @@ class BinanceFuturesClient:
async def get_open_interest(self) -> float | None:
"""현재 미결제약정(OI)을 조회한다. 오류 시 None 반환."""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
try:
result = await loop.run_in_executor(
None,
@@ -181,7 +181,7 @@ class BinanceFuturesClient:
async def get_funding_rate(self) -> float | None:
"""현재 펀딩비를 조회한다. 오류 시 None 반환."""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
try:
result = await loop.run_in_executor(
None,
@@ -194,7 +194,7 @@ class BinanceFuturesClient:
async def get_oi_history(self, limit: int = 5) -> list[float]:
"""최근 OI 변화율 히스토리를 조회한다 (봇 초기화용). 실패 시 빈 리스트."""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
try:
result = await loop.run_in_executor(
None,
@@ -218,7 +218,7 @@ class BinanceFuturesClient:
async def create_listen_key(self) -> str:
"""POST /fapi/v1/listenKey — listenKey 신규 발급"""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(
None,
lambda: self.client.futures_stream_get_listen_key(),
@@ -227,7 +227,7 @@ class BinanceFuturesClient:
async def keepalive_listen_key(self, listen_key: str) -> None:
"""PUT /fapi/v1/listenKey — listenKey 만료 연장 (60분 → 리셋)"""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None,
lambda: self.client.futures_stream_keepalive(listenKey=listen_key),
@@ -235,7 +235,7 @@ class BinanceFuturesClient:
async def delete_listen_key(self, listen_key: str) -> None:
"""DELETE /fapi/v1/listenKey — listenKey 삭제 (정상 종료 시)"""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
try:
await loop.run_in_executor(
None,

View File

@@ -7,7 +7,7 @@ FEATURE_COLS = [
"ret_1", "ret_3", "ret_5", "signal_strength", "side",
"btc_ret_1", "btc_ret_3", "btc_ret_5",
"eth_ret_1", "eth_ret_3", "eth_ret_5",
"xrp_btc_rs", "xrp_eth_rs",
"primary_btc_rs", "primary_eth_rs",
# 시장 미시구조: OI 변화율(z-score), 펀딩비(z-score)
"oi_change", "funding_rate",
# OI 파생 피처
@@ -28,11 +28,11 @@ def _calc_ret(closes: pd.Series, n: int) -> float:
return (closes.iloc[-1] - prev) / prev if prev != 0 else 0.0
def _calc_rs(xrp_ret: float, other_ret: float) -> float:
"""상대강도 = xrp_ret / other_ret. 분모 0이면 0.0."""
def _calc_rs(primary_ret: float, other_ret: float) -> float:
"""상대강도 = primary_ret / other_ret. 분모 0이면 0.0."""
if other_ret == 0.0:
return 0.0
return xrp_ret / other_ret
return primary_ret / other_ret
def _rolling_zscore_last(arr: np.ndarray, window: int = _ZSCORE_WINDOW) -> float:
@@ -144,8 +144,8 @@ def build_features(
"eth_ret_1": float(eth_ret_1),
"eth_ret_3": float(eth_ret_3),
"eth_ret_5": float(eth_ret_5),
"xrp_btc_rs": float(_calc_rs(ret_1, btc_ret_1)),
"xrp_eth_rs": float(_calc_rs(ret_1, eth_ret_1)),
"primary_btc_rs": float(_calc_rs(ret_1, btc_ret_1)),
"primary_eth_rs": float(_calc_rs(ret_1, eth_ret_1)),
})
# 실시간에서 실제 값이 제공되면 사용, 없으면 0으로 채운다
@@ -293,8 +293,8 @@ def build_features_aligned(
"eth_ret_1": _rolling_zscore_last(eth_r1),
"eth_ret_3": _rolling_zscore_last(eth_r3),
"eth_ret_5": _rolling_zscore_last(eth_r5),
"xrp_btc_rs": _rolling_zscore_last(rs_btc),
"xrp_eth_rs": _rolling_zscore_last(rs_eth),
"primary_btc_rs": _rolling_zscore_last(rs_btc),
"primary_eth_rs": _rolling_zscore_last(rs_eth),
})
# OI/펀딩비 z-score (실시간 값이 제공되면 히스토리 끝에 추가하여 z-score)

View File

@@ -85,8 +85,8 @@ class UserDataStream:
is_reduce = order.get("R", False)
realized_pnl = float(order.get("rp", "0"))
# 청산 주문 판별: reduceOnly이거나, TP/SL 타입이거나, rp != 0
is_close = is_reduce or order_type in _CLOSE_ORDER_TYPES or realized_pnl != 0
# 청산 주문 판별: reduceOnly이거나 TP/SL 타입
is_close = is_reduce or order_type in _CLOSE_ORDER_TYPES
if not is_close:
return