fix: 하드닝 루프 8회차 — 체결 보정·고아 감지·청산 경보·실시간/차트·리스크 가드 일괄 수정
- 스윙 체결가 확인 전 executed_price NULL + fill_sweep, 진입가 resolve(ActiveTrade↔Portfolio), 0체결/부분체결 매수 장부 원복 - DT 고아 감지(근거 한정)·0체결 매도 복원·stale 점검 확대, 시간손절 -0.3% 원복 - 폴백 손절 ATR 캐시, 거래량부족-차단 신호 매수 차단, 현금 5% 유보 재적용, MDD peak/reset 수정 - KIS 서킷브레이커 청산 우회·경보, db_worker 재큐잉 순서, heartbeat·health-watch 사이드카 - 실시간 WS 재구독·41건 우선순위·다건 프레임 분할, 차트 샘플 제거·당일봉 합성, WS 서브프로토콜 인증 - 프론트: 미관리 그룹·확인 모달·차트 실시간 반영·예수금 표시 정정 - 전문가 에이전트 6종 추가(.claude/agents) - 유닛 스위트 2667 passed (소켓 차단 하), tsc OK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y83Za98XsNw9zuA4FTUytN
This commit is contained in:
@@ -39,6 +39,18 @@ async def check_opening_range_volatility() -> bool:
|
||||
return is_opening_range_period()
|
||||
|
||||
|
||||
def _bars_oldest_first(rows, date_key: str) -> list:
|
||||
"""KIS 일봉 output2 를 과거→최근 순으로 정렬한다.
|
||||
|
||||
KIS 는 최신 봉이 앞(newest-first)이고 30행을 준다. 날짜 키가 모두 있으면
|
||||
날짜 오름차순으로, 없으면 KIS 기본 순서(최신 우선)로 보고 뒤집는다.
|
||||
"""
|
||||
rows = [r for r in (rows or []) if isinstance(r, dict)]
|
||||
if rows and all(r.get(date_key) for r in rows):
|
||||
return sorted(rows, key=lambda r: str(r.get(date_key)))
|
||||
return list(reversed(rows))
|
||||
|
||||
|
||||
async def check_surge_prevention(
|
||||
stock_code: str,
|
||||
is_domestic: bool,
|
||||
@@ -69,13 +81,25 @@ async def check_surge_prevention(
|
||||
)
|
||||
chart_data = chart_resp.get("output2", [])
|
||||
|
||||
if len(chart_data) >= 4:
|
||||
chart_data = _bars_oldest_first(
|
||||
chart_data, "stck_bsop_date" if is_domestic else "xymd"
|
||||
)
|
||||
# 오늘 봉(장중 미완성)이 포함되면 [-4:] 첫 봉 = 3거래일 전,
|
||||
# 아직 없으면 [-3:] 첫 봉 = 3거래일 전(오늘 기준).
|
||||
_win = 4
|
||||
if is_domestic and chart_data:
|
||||
_today = now_kst().strftime("%Y%m%d")
|
||||
if str(chart_data[-1].get("stck_bsop_date")) != _today:
|
||||
_win = 3
|
||||
chart_data = chart_data[-_win:]
|
||||
|
||||
if len(chart_data) >= _win:
|
||||
closes = (
|
||||
[float(item.get("stck_clpr", 0)) for item in chart_data[-4:]]
|
||||
[float(item.get("stck_clpr", 0)) for item in chart_data]
|
||||
if is_domestic
|
||||
else [float(item.get("clos", 0)) for item in chart_data[-4:]]
|
||||
else [float(item.get("clos", 0)) for item in chart_data]
|
||||
)
|
||||
if len(closes) >= 4:
|
||||
if len(closes) >= _win:
|
||||
price_3days_ago = closes[0]
|
||||
recent_gain = (
|
||||
((current_price - price_3days_ago) / price_3days_ago) * 100
|
||||
@@ -83,14 +107,14 @@ async def check_surge_prevention(
|
||||
)
|
||||
try:
|
||||
highs = (
|
||||
[float(item.get("stck_hgpr", 0)) for item in chart_data[-4:]]
|
||||
[float(item.get("stck_hgpr", 0)) for item in chart_data]
|
||||
if is_domestic
|
||||
else [float(item.get("high", 0)) for item in chart_data[-4:]]
|
||||
else [float(item.get("high", 0)) for item in chart_data]
|
||||
)
|
||||
lows = (
|
||||
[float(item.get("stck_lwpr", 0)) for item in chart_data[-4:]]
|
||||
[float(item.get("stck_lwpr", 0)) for item in chart_data]
|
||||
if is_domestic
|
||||
else [float(item.get("low", 0)) for item in chart_data[-4:]]
|
||||
else [float(item.get("low", 0)) for item in chart_data]
|
||||
)
|
||||
daily_ranges = [
|
||||
abs(highs[i] - lows[i]) for i in range(len(closes))
|
||||
@@ -156,16 +180,29 @@ async def check_trend_filter(
|
||||
if ma20 is None or ma60 is None:
|
||||
if is_domestic:
|
||||
chart_resp = await korea_investment_api.get_daily_chart(stock_code, "D")
|
||||
closes_local = [float(it.get("stck_clpr", 0)) for it in (chart_resp.get("output") or [])]
|
||||
_rows = _bars_oldest_first(
|
||||
chart_resp.get("output2") or chart_resp.get("output") or [], "stck_bsop_date"
|
||||
)
|
||||
closes_local = [float(it.get("stck_clpr", 0)) for it in _rows]
|
||||
else:
|
||||
chart_resp = await korea_investment_api.get_overseas_daily_chart(stock_code, exchange_code, "D")
|
||||
closes_local = [float(it.get("clos", 0)) for it in (chart_resp.get("output2") or [])]
|
||||
_rows = _bars_oldest_first(chart_resp.get("output2") or [], "xymd")
|
||||
closes_local = [float(it.get("clos", 0)) for it in _rows]
|
||||
|
||||
if len(closes_local) >= max(TREND_MA_SHORT, TREND_MA_LONG):
|
||||
ma20 = _ma(closes_local, TREND_MA_SHORT)
|
||||
ma60 = _ma(closes_local, TREND_MA_LONG)
|
||||
ema20 = _ema(closes_local, TREND_MA_SHORT)
|
||||
ema60 = _ema(closes_local, TREND_MA_LONG)
|
||||
elif not (ma20 and ma60):
|
||||
# 데이터 부족(KIS 폴백 30봉)은 하락추세 증거가 아니다. 산출 불가 시 통과시키되
|
||||
# 로그를 남긴다(전 비-평균회귀 매수 전면 차단 방지).
|
||||
warn_throttled(
|
||||
logger, f"trade_helpers:trend_nodata:{stock_code}",
|
||||
f"[TREND_FILTER] {stock_code} MA{TREND_MA_LONG} 산출 불가"
|
||||
f"(봉 {len(closes_local)}<{max(TREND_MA_SHORT, TREND_MA_LONG)}) -> 필터 통과(데이터 부족)",
|
||||
)
|
||||
return {"skip": False}
|
||||
|
||||
ma_ok = bool(
|
||||
ma20 and ma60
|
||||
|
||||
Reference in New Issue
Block a user