fix: 프로그램 검증 루프 7회차 — 리스크·주문·청산·인프라 결함 일괄 수정 + 거래 확대 결정 반영
하네스 규칙 검사 전역 위반 117→0, 회차별 검증(code-verifier opus 4도메인)으로 Critical/High 해소. 주문·리스크 - MDD 강제축소 반복 매도(종목별 완료 집합·재시도 상한·날짜 롤오버·잔고 오류 보류·epoch·장외 미주문) - KIS 거부 FAILED 기록, 결과 불명(전송 후) 재전송 금지·PENDING, 미전송 실패만 재시도(msg_cd 분류) - 국내 전량·손실 매도 시장가, 부분 익절 직전 현재가, 지정가만 미체결 취소(cancel_order_v2: TR 0013U·조직번호) + 0체결 롤백 - 누적 4%·현금·레버리지(신규분 포함)·사이클 내 매수 누적, 수동매수 공용 가드(buy_guards), 수동매도 DT 차감·락 - 가드 우회 엔드포인트 차단(/recommendations/auto-trade 위임, US /execute 매수 410·매도 가드) - 매수 슬롯=성공 매수, 평가 상한·회전, 업무거부/계좌 쿨다운, 프로세스 내 in-flight 주문 중복 차단 데이트레이딩 - 진입·청산 shield, 청산 진행 중 표식(소유 토큰), 2단계 청산 체크, 결과 불명 → 보유 감소 판정·비DT 차감 상한 - 전일 포지션 재시도(일시 실패 미산정), 분할매도 영속, ORB 구간 손절, /cycle 락, 사이클 내 현금 차감, 체결 수량 보정 스윙 - 손절 공백(판정 예외 시 하드 바닥), 단일 손절 소스·범위 검증, 매도 직전 KIS 잔고 상한(min(주문가능,보유)−DT) - 결과 불명 체결 대조·장부 반영, 대체가(나이·진입 이후·ATR 캐시), 분할 단계 기록, 국면 실패 fail-closed - 듀얼 플레이북·서킷브레이커 게이트, 평균회귀 우선·듀얼 독립, 시간손절 거래일 - 거래 확대(사용자 결정, 백테스트 미검증): RSI 46/35 정본 경로, 모멘텀 v2 ON, SIDEWAYS ADX AND, 트레일링 1차 익절 후, 20일 시간손절 ON 인프라·보안 - 계좌·손익 GET 인증, 세션 is_active·관리자 경로, 서명키 파생, realip, WS 토큰 마스킹 - KIS 토큰 즉시 실패(sleep 제거)·무효 시 캐시 삭제, 주문 5xx+JSON 본문 반환 - db_worker 종료 drain(멱등 재실행·커밋 불명 격리·예산·밴 커밋), stop_grace 40s, Redis 타임아웃·volatile-lru, /health DLQ·캐시 - KRX 휴장일, py3.11 비호환 f-string 수정 + Jenkins 이미지 내 compileall·헬스 실패 시 빌드 실패 하네스: rules_check 에 PEP 701 f-string 검사, 마커(allow-*), deploy 스모크 401 기준 CLAUDE.md: 실제 동작·플래그·백로그(§8.5-27~38) 갱신 backend 1451 passed / hooks 112 passed / tsc ok / py3.9 compile 0 errors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,43 @@ class Settings(BaseSettings):
|
||||
BOOTSTRAP_ADMIN_USERNAME: str = "admin" # 앱 최초 기동 시 users 테이블이 비어있으면 시드할 관리자 계정명
|
||||
BOOTSTRAP_ADMIN_PASSWORD: str = "" # 비어있으면 부트스트랩 관리자 시드 건너뜀
|
||||
|
||||
# ── 인증·보안 강화 (2026-10-02 검증 루프 2회차) ─────────────────────
|
||||
# 세션 토큰의 사용자 is_active/is_admin 재확인 캐시 TTL(초). revoke 후 최대 이 시간만 통과.
|
||||
SESSION_ACTIVE_CACHE_SECONDS: int = 30
|
||||
# SECRET_KEY/API_KEY 최소 길이 (CLAUDE.md §4: 64자+ 랜덤)
|
||||
MIN_SECRET_KEY_LENGTH: int = 64
|
||||
# True 이고 DEBUG=False 이면 약한/기본 키로 기동을 중단한다(fail-closed).
|
||||
# 기본 False: 운영 .env 상태 확인 전에는 error 로그 + /health "security":"warn" 만 낸다.
|
||||
SECURITY_FAIL_CLOSED: bool = False
|
||||
# X-Real-IP 를 신뢰할 직전 홉(프록시) 대역. 이 대역 밖에서 온 요청은 소켓 주소를 쓴다.
|
||||
TRUSTED_PROXY_CIDRS: str = "127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
||||
# holidays 패키지에 없는 KRX 임시 휴장일 (YYYY-MM-DD 콤마 구분)
|
||||
KRX_EXTRA_HOLIDAYS: str = ""
|
||||
# KIS 토큰 재발급 최소 간격(초) — KIS "1분당 1회" 제한 + 여유. 이 창 안에서는 대기하지 않고 즉시 실패한다.
|
||||
KIS_TOKEN_REISSUE_MIN_INTERVAL_SEC: int = 70
|
||||
# KIS 토큰 실제 유효기간(시간). 로컬 만료(23h)보다 길다 — 재발급 실패 시 이 한도 안에서만 기존 토큰을 연장한다.
|
||||
KIS_TOKEN_HARD_TTL_HOURS: int = 24
|
||||
# 재발급 실패 시 기존 토큰 연장 폭(분) — 짧게 잡아 실제 만료 전에 재발급을 다시 시도하게 한다.
|
||||
KIS_TOKEN_GRACE_EXTEND_MINUTES: int = 10
|
||||
# 종료 시 db_worker 를 멈추기 전에 백그라운드 태스크(_spawn_background)를 기다리는 상한(초).
|
||||
# uvicorn 정리 + 이 값 + db_worker drain(DRAIN_DEADLINE_SECONDS=20) 이 compose stop_grace_period(40s) 안에
|
||||
# 들어와야 한다 — 기동 시 main.py 가 합계를 CONTAINER_STOP_GRACE_SECONDS 와 비교해 넘으면 error 로그.
|
||||
SHUTDOWN_BACKGROUND_DRAIN_SECONDS: int = 8
|
||||
# uvicorn --timeout-graceful-shutdown. backend/Dockerfile CMD 가 같은 이름의 **환경변수**를 읽어
|
||||
# 적용한다(기본 5) — .env 로 바꾸면 실제 반영된다.
|
||||
UVICORN_GRACEFUL_SHUTDOWN_SECONDS: int = 5
|
||||
# docker-compose.yml backend stop_grace_period 와 같은 값(초). 컨테이너 설정을 앱이 읽을 수 없으므로
|
||||
# 여기 값을 기준으로 종료 예산 합계를 검증한다(테스트가 compose 와 대조).
|
||||
CONTAINER_STOP_GRACE_SECONDS: int = 40
|
||||
# 기동 시 DLQ replay 로 재큐잉된 밴이 커밋되기를 기다리는 상한(초) — 그 뒤 loss_tracker 재로드
|
||||
DLQ_REPLAY_SETTLE_SECONDS: int = 10
|
||||
# /health 결과 캐시(초) — 폴링 폭주 시 DB 커넥션 풀 경쟁 방지
|
||||
HEALTH_CACHE_SECONDS: float = 1.5
|
||||
# Redis 소켓 타임아웃(초) — 없으면 Redis 가 응답 없이 멈출 때 호출자(DT finalize·분산락)가 무기한 대기.
|
||||
# 코드베이스에 블로킹 명령(BLPOP/pubsub)이 없어 읽기 타임아웃이 정상 동작을 끊지 않는다(2026-10-02 확인).
|
||||
REDIS_SOCKET_TIMEOUT: float = 5.0
|
||||
REDIS_SOCKET_CONNECT_TIMEOUT: float = 3.0
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 데이터베이스 & 메시지 큐
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
@@ -179,6 +216,12 @@ class Settings(BaseSettings):
|
||||
# 몇 회 연속 갱신 실패부터 차단할지. 1~2회는 KIS 순간 오류로 흔하므로
|
||||
# 알림 임계값(swing/scheduler.MDD_UPDATE_FAILURE_ALERT)과 동일한 5회로 맞춘다.
|
||||
MDD_BLIND_FAILURE_THRESHOLD: int = 5
|
||||
# [C-1 2026-10-02] MDD 강제축소 종목별 재시도 상한 (KST 하루 단위).
|
||||
# 거래정지·주문가능 0 등으로 한 종목이 계속 실패해도 이미 축소된 종목은 재매도하지
|
||||
# 않는다(레벨별 완료 집합). 실패 종목만 재시도하고, 이 횟수에 도달하면 당일 포기 +
|
||||
# error 로그. 날짜가 바뀌면 시도 횟수만 초기화된다(완료 집합은 peak 갱신 시에만 초기화).
|
||||
# order_lock_busy(다른 경로가 같은 종목 주문 중)는 실패로 세지 않는다.
|
||||
MDD_REDUCTION_MAX_RETRIES: int = 5
|
||||
|
||||
# 가격 필터 (섹션 5.4)
|
||||
MIN_PRICE_KRW: int = 5000 # 국내 5,000원 이하 매수 금지
|
||||
@@ -230,6 +273,13 @@ class Settings(BaseSettings):
|
||||
# (3) 최대 연속 손실 횟수가 기존 경로 대비 악화되지 않음
|
||||
# — 3개를 모두 통과한 뒤에만 켠다. 하락장 진입을 늘리는 변경이므로 특히 위험.
|
||||
MEAN_REVERSION_BEAR_V2_ENABLED: bool = False # 기본 False = 기존 4중 AND 경로 그대로
|
||||
# [C1 2026-10-02] 추천 스캐너(recommendations.is_mean_reversion_candidate)의 RSI 임계
|
||||
# 출처 스위치. False(기본) = 현행: trading_rules(DB/JSON) conditions 의
|
||||
# rsi_bull_max/rsi_bear_max(없으면 40/30). True = CLAUDE.md §0 우선순위
|
||||
# (adaptive rsi_entry/bear_rsi_entry → strategy_cfg → settings 46/35).
|
||||
# 2026-10-02 사용자 결정(거래 확대 우선)으로 True — 진입 RSI 40/30 → 46/35.
|
||||
# ⚠️ 백테스트 미검증, 모의계좌 기준 결정. KIS_MODE=real 전환 전 재검토.
|
||||
MEAN_REVERSION_CANONICAL_PARAMS_ENABLED: bool = True
|
||||
# RSI 상한(필수 AND): 기존 임계값(35) + relax, 단 hard cap(40) 이하.
|
||||
# 근거: 40은 BULL 진입 임계(46)보다 낮게 유지해 "하락장은 더 보수적"이라는 원칙을 깨지 않는다.
|
||||
MEAN_REVERSION_BEAR_V2_RSI_RELAX: float = 5.0
|
||||
@@ -483,6 +533,18 @@ class Settings(BaseSettings):
|
||||
MAX_DAILY_TRADES: int = 5
|
||||
API_TRIGGERED_AUTO_TRADE: bool = False # GET /recommendations/rule-based?auto_trade=true 가 매매를 실행할지. 기본 OFF — 매매는 스윙 스케줄러 단일 경로
|
||||
MAX_DAILY_BUYS_PER_STOCK: int = 2 # 종목당 일일 최대 매수 횟수
|
||||
# [M-1 3회차 2026-10-02] 자동매매 1사이클 후보 순회. 매수 성공 건수가 한도
|
||||
# (MAX_DAILY_TRADES 잔여)에 도달할 때까지 후보를 계속 평가한다 — SKIP/HOLD 는 슬롯을
|
||||
# 쓰지 않는다. 아래는 KIS 호출량 보호용 사이클당 '신규 후보' 평가 상한(보유 종목의
|
||||
# 청산 판정은 상한과 무관하게 계속).
|
||||
AUTO_TRADE_MAX_EVALUATIONS_PER_CYCLE: int = 30
|
||||
# KIS 업무 거부(rt_cd "1": 잔고·호가·종목 상태 등) 매수 종목은 이 시간 동안 재주문하지
|
||||
# 않는다(인메모리, 재기동 시 초기화). 하루 차단이 아니라 같은 사이클 반복 주문 방지용.
|
||||
AUTO_TRADE_ORDER_REJECT_COOLDOWN_MIN: float = 15.0
|
||||
# [L-2 4회차 2026-10-02] 계좌 단위 거부 msg_cd — 종목이 아니라 계좌 문제라 다른 종목도
|
||||
# 같은 결과다. 받으면 그 사이클 신규 매수 중단 + 위 쿨다운 동안 전 종목 매수 보류(청산 무관).
|
||||
# 40910000 = 모의투자 계좌 교체/만료(메모리: kis-virtual-account-rotation).
|
||||
KIS_ACCOUNT_REJECT_MSG_CODES: list[str] = ["40910000"]
|
||||
ANALYSIS_STOCK_COUNT: int = 300 # 배치 분석 종목 수
|
||||
AUTO_TRADE_TOP_N_PER_RULE: int = 2 # 규칙당 자동매매 실행 최대 종목 수
|
||||
|
||||
@@ -538,6 +600,88 @@ class Settings(BaseSettings):
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
LOG_LEVEL: str = "INFO"
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 스윙 전략 플래그 (2026-10-02) — 사용자 결정("거래가 늘어나는 방향")으로 모멘텀·
|
||||
# 시간손절은 기본 ON. ⚠️ CLAUDE.md §8.1-5(백테스트 없이 실전 투입 금지) 미충족 상태.
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 모멘텀 돌파: 오늘(장중) 봉을 제외한 20일 고점/평균거래량으로 판정하는 _v2 사용.
|
||||
# False 면 기존 v1(오늘 봉 포함 → current_close > max(highs[-20:]) 이 구조적으로 불가,
|
||||
# 실전 발화 0)을 호출한다.
|
||||
MOMENTUM_BREAKOUT_ENABLED: bool = True
|
||||
# 스윙 시간손절(check_time_stop_exit) 배선. 기간·최소수익은 아래 두 키.
|
||||
SWING_TIME_STOP_ENABLED: bool = True
|
||||
SWING_TIME_STOP_DAYS: int = 20
|
||||
SWING_TIME_STOP_MIN_PROFIT_PCT: float = 3.0
|
||||
# [L-6 2026-10-02] 스윙 손절 파라미터 합리 범위(resolve_swing_stop_params_v2 의 소스 값 검증).
|
||||
# 범위 밖 값(strategy_cfg·적응형)은 warning 후 하위 소스(최종 config)로 폴백한다.
|
||||
# 손절률은 크기(abs, %) 기준, ATR 배수는 배수 그대로.
|
||||
SWING_STOP_LOSS_PCT_MIN: float = 1.0
|
||||
SWING_STOP_LOSS_PCT_MAX: float = 30.0
|
||||
SWING_ATR_STOP_MULT_MIN: float = 0.5
|
||||
SWING_ATR_STOP_MULT_MAX: float = 10.0
|
||||
# [M-2 2026-10-02 5회차] 결과 불명 스윙 매도 후 재매도 전 KIS 잔고 확인이 연속 이 횟수
|
||||
# 이상 실패하면 warning → error 로 승격(그동안 재매도는 보류).
|
||||
SWING_PENDING_SELL_CONFIRM_ERROR_AFTER: int = 3
|
||||
# [H-1/M-1 2026-10-02] 현재가 조회 실패 시 손절 판정용 대체 가격(직전 실시세·Portfolio 평가가)의
|
||||
# 최대 나이(초). 초과한 값은 쓰지 않는다 — 둘 다 초과면 손절 판정 skip + error(수동 확인).
|
||||
SWING_FALLBACK_PRICE_MAX_AGE_SEC: float = 900.0
|
||||
# [C-1 2026-10-02 6회차] 국내 스윙 매도 직전 KIS 잔고 확인이 종목별로 연속 이 횟수 이상 실패하면
|
||||
# warning → error 로 승격(그동안 매도는 보류 — DT 물량 매도 방지가 우선).
|
||||
SWING_PRESELL_HOLDING_CHECK_ERROR_AFTER: int = 3
|
||||
# [M-4 2026-10-02 6회차] 현재가·대체가 모두 없어 손절 판정을 연속 이 횟수 skip 한 종목은
|
||||
# 텔레그램 시스템 경고(종목별)를 보낸다.
|
||||
SWING_FALLBACK_SKIP_ALERT_AFTER: int = 5
|
||||
# [7회차 H-A] get_balance 첫 페이지 행 수(모의 20 / 실전 50). 종목 미발견 + 이 행 수 이상이면 페이지 초과로 보고 매도 보류.
|
||||
SWING_BALANCE_FULL_PAGE_ROWS: int = 20
|
||||
# ML 단독 SELL 트리거(신뢰도≥0.5). 운영 LSTM 재학습 전 난수(§8.5-20)이므로 OFF.
|
||||
ML_SELL_SIGNAL_ENABLED: bool = False
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# 데이트레이딩 하드코딩 이관 (2026-10-02) — 값은 기존 리터럴과 동일(동작 무변경)
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# ORB 갭 필터 상한(비율). 기본 판정은 |현재가−당일시가|/당일시가 (오버나이트 갭 아님).
|
||||
DAY_TRADE_ORB_MAX_GAP_PCT: float = 0.04
|
||||
# ORB 레인지 최소 폭(비율) — (고−저)/저 가 이 값 미만이면 노이즈 돌파로 보고 거부.
|
||||
DAY_TRADE_ORB_MIN_RANGE_PCT: float = 0.003
|
||||
# [기본 OFF] 갭 필터를 §7 문서대로 '전일 종가 대비 오버나이트 갭'으로 판정.
|
||||
# ON 이면 진입 경로가 워밍업(전일 포함) 1분봉을 ORB 판정에 넘기고, 전일 종가를 못 구하면
|
||||
# 기존 당일시가 판정으로 폴백(warning). 진입 감소 방향이라 사용자 결정으로 OFF 출고.
|
||||
DAY_TRADE_ORB_OVERNIGHT_GAP_ENABLED: bool = False
|
||||
# [기본 OFF] ORB 레인지를 09:00~09:29 전체로 확정. OFF(현행 실제 동작)는 첫 수집 성공
|
||||
# 시점(1분봉 ≥5개 → 대개 09:05 전후)의 레인지로 하루가 고정된다 — 돌파 신호가 더 많다.
|
||||
# ON: 09:30 이전엔 매 수집마다 재계산하고(Redis 저장분도 09:30 전엔 확정본으로 보지 않음),
|
||||
# 09:30 이후 수집분만 확정본으로 둔다.
|
||||
DAY_TRADE_ORB_FULL_RANGE: bool = False
|
||||
# 5분봉 RSI 과매수 청산 임계(기존 리터럴 70).
|
||||
DAY_TRADE_RSI_EXIT_OVERBOUGHT: float = 70.0
|
||||
# VWAP 반등 신호에서 ATR 이 없을 때 목표가 = VWAP × 이 값(기존 리터럴 1.03).
|
||||
DAY_TRADE_VWAP_TARGET_FALLBACK_MULT: float = 1.03
|
||||
# DT 청산 체크 처리량·상한 (exit_service 모듈 상수에서 이관, 값 동일).
|
||||
DAY_TRADE_EXIT_CHECK_CONCURRENCY: int = 4 # 포지션 동시 판정 수
|
||||
DAY_TRADE_EXIT_FAST_STOP_TIMEOUT_SEC: float = 15.0 # 손절 선판정 현재가·잔고 조회 상한
|
||||
DAY_TRADE_EXIT_CHECK_TIMEOUT_SEC: float = 45.0 # 포지션별 분봉 판정 상한
|
||||
DAY_TRADE_TOTAL_ASSETS_REFRESH_SEC: float = 300.0 # 청산 측 총자산 갱신 주기
|
||||
# 전일(stale) 포지션 매도 실패 시 재시도 (당일·프로세스 단위).
|
||||
# MAX_RETRIES 는 '업무 거부'만 센다(레이트리밋·전송 전 실패·전송 불명은 미산정).
|
||||
DAY_TRADE_STALE_SELL_MAX_RETRIES: int = 3
|
||||
DAY_TRADE_STALE_SELL_RETRY_BACKOFF_SEC: float = 60.0
|
||||
# 업무 거부가 MAX_RETRIES 에 도달한 뒤에도 당일 포기하지 않고 이 간격으로 계속 재시도.
|
||||
DAY_TRADE_STALE_SELL_EXHAUSTED_RETRY_SEC: float = 600.0
|
||||
# 청산 주문 1건 응답 대기 상한. 초과 = 결과 불명 → 당일 주문내역으로 접수 확인.
|
||||
# ⚠️ 이 상한에는 전송 전 대기(분산락·종목 세마포어·토큰)도 포함된다(전송 시점 표식은
|
||||
# trade_executor/korea_investment_api 쪽 훅이 필요 — 미구현).
|
||||
DAY_TRADE_EXIT_ORDER_TIMEOUT_SEC: float = 60.0
|
||||
# 결과 불명이고 접수 확인도 안 될 때 같은 종목 재매도 보류 시간(재시도 직전에 다시 확인).
|
||||
# 2026-10-02: 120 → 30 — 해제 직전 재확인 + 실보유 상한 도입에 맞춰 단축(손절 지연 축소).
|
||||
DAY_TRADE_EXIT_UNKNOWN_HOLD_SEC: float = 30.0
|
||||
# 결과 불명 주문을 당일 주문내역에서 찾을 때 주문 시각 허용 오차(초, 서버·KIS 시계 차).
|
||||
DAY_TRADE_EXIT_UNKNOWN_MATCH_WINDOW_SEC: float = 60.0
|
||||
# 체결 후 반영 꼬리 단계(Redis 저장·loss_tracker·포트폴리오 캐시) 단계별 상한(초).
|
||||
DAY_TRADE_EXIT_FINALIZE_STEP_TIMEOUT_SEC: float = 10.0
|
||||
# 결과 불명 매도 이후 확인용 조회(KIS 잔고·비DT 보유 수량)가 연속 이 횟수만큼 실패하면
|
||||
# 손절이 보류된 상태이므로 텔레그램으로 수동 청산 경보를 보낸다(종목·일 1회).
|
||||
DAY_TRADE_EXIT_UNKNOWN_LOOKUP_ALERT_COUNT: int = 3
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._apply_kis_mode()
|
||||
|
||||
Reference in New Issue
Block a user