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:
@@ -237,8 +237,62 @@ async def get_all_tradable_stocks(limit: int = 500) -> list[tuple[str, str]]:
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
CHART_COLLECT_CONCURRENCY = 5 # KIS API 동시 호출 수 (차트 수집용)
|
||||
# KIS 일봉은 수정주가(FID_ORG_ADJ_PRC=0)라 원주가 테이블 stock_daily_prices 를 오염시킨다
|
||||
# (change_rate 는 KRX 원주가 기준이라 한 행에 기준이 섞임). 원주가 조회 _v2 가 생기기 전까지 OFF.
|
||||
CHART_DB_UPSERT_ENABLED = False
|
||||
CHART_COLLECT_DAYS = 400 # 수집할 캘린더일 수 (약 240 거래일)
|
||||
|
||||
_MARKET_CONFIRM_HOUR_MIN = (15, 30) # 이후에는 당일 봉을 확정봉으로 간주
|
||||
|
||||
|
||||
def _bar_cutoff_date_v2(now_kst: datetime) -> date:
|
||||
"""저장 허용 마지막 거래일. 장 마감(15:30) 전이면 전일까지만(당일 미확정 봉 제외)."""
|
||||
today = now_kst.date()
|
||||
if (now_kst.hour, now_kst.minute) >= _MARKET_CONFIRM_HOUR_MIN:
|
||||
return today
|
||||
return today - timedelta(days=1)
|
||||
|
||||
|
||||
def _to_int_v2(v) -> int:
|
||||
return int(float(str(v if v is not None else "0").replace(",", "")))
|
||||
|
||||
|
||||
def _parse_kis_daily_record_v2(
|
||||
item: dict, stock_code: str, stock_name: str, cutoff_date: date,
|
||||
) -> Optional[dict]:
|
||||
"""KIS 일봉 1행 -> DB 레코드. 0/비정상 OHLC·미확정 당일 봉이면 None."""
|
||||
try:
|
||||
s = item.get("stck_bsop_date", "")
|
||||
if not s or len(s) != 8:
|
||||
return None
|
||||
td = date(int(s[:4]), int(s[4:6]), int(s[6:8]))
|
||||
if td > cutoff_date:
|
||||
return None
|
||||
o = _to_int_v2(item.get("stck_oprc"))
|
||||
h = _to_int_v2(item.get("stck_hgpr"))
|
||||
l = _to_int_v2(item.get("stck_lwpr"))
|
||||
c = _to_int_v2(item.get("stck_clpr"))
|
||||
if min(o, h, l, c) <= 0:
|
||||
return None
|
||||
if h < l or h < max(o, c) or l > min(o, c):
|
||||
return None
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"stock_name": stock_name,
|
||||
"trade_date": td,
|
||||
"open_price": o,
|
||||
"high_price": h,
|
||||
"low_price": l,
|
||||
"close_price": c,
|
||||
"volume": int(float(str(item.get("acml_vol", 0) or 0).replace(",", ""))),
|
||||
"change_rate": None,
|
||||
"market_cap": None,
|
||||
}
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
|
||||
async def _collect_chart_for_stock(
|
||||
stock_code: str,
|
||||
stock_name: str,
|
||||
@@ -253,6 +307,9 @@ async def _collect_chart_for_stock(
|
||||
from app.services.korea_investment_api import korea_investment_api
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
if not CHART_DB_UPSERT_ENABLED:
|
||||
return stock_code, 0
|
||||
|
||||
async with semaphore:
|
||||
try:
|
||||
raw_items = await korea_investment_api.get_daily_ohlcv_range(
|
||||
@@ -262,33 +319,13 @@ async def _collect_chart_for_stock(
|
||||
return stock_code, 0
|
||||
|
||||
records = []
|
||||
cutoff_date = _bar_cutoff_date_v2(datetime.now(KST))
|
||||
for item in raw_items:
|
||||
try:
|
||||
trade_date_str = item.get("stck_bsop_date", "")
|
||||
if not trade_date_str or len(trade_date_str) != 8:
|
||||
continue
|
||||
td = date(
|
||||
int(trade_date_str[:4]),
|
||||
int(trade_date_str[4:6]),
|
||||
int(trade_date_str[6:8]),
|
||||
)
|
||||
close_price = int(float(str(item.get("stck_clpr", "0")).replace(",", "")))
|
||||
if close_price == 0:
|
||||
continue
|
||||
records.append({
|
||||
"stock_code": stock_code,
|
||||
"stock_name": stock_name,
|
||||
"trade_date": td,
|
||||
"open_price": int(float(str(item.get("stck_oprc", "0")).replace(",", ""))),
|
||||
"high_price": int(float(str(item.get("stck_hgpr", "0")).replace(",", ""))),
|
||||
"low_price": int(float(str(item.get("stck_lwpr", "0")).replace(",", ""))),
|
||||
"close_price": close_price,
|
||||
"volume": int(item.get("acml_vol", 0)),
|
||||
"change_rate": None,
|
||||
"market_cap": None,
|
||||
})
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
rec = _parse_kis_daily_record_v2(
|
||||
item, stock_code, stock_name, cutoff_date,
|
||||
)
|
||||
if rec is not None:
|
||||
records.append(rec)
|
||||
|
||||
if not records:
|
||||
return stock_code, 0
|
||||
@@ -331,6 +368,10 @@ async def collect_chart_data_for_targets(
|
||||
|
||||
Returns: 총 저장된 행 수
|
||||
"""
|
||||
if not CHART_DB_UPSERT_ENABLED:
|
||||
logger.info("📊 차트 DB 수집 비활성(수정주가 오염 방지) — 건너뜀")
|
||||
return 0
|
||||
|
||||
from app.models.models import Portfolio
|
||||
|
||||
# 1. 보유종목 조회
|
||||
|
||||
Reference in New Issue
Block a user