- 8회차 HIGH 수정 16건 레드팀 재검증: 12 확인·3 부분·1 미수정 → 전부 보강 - trade_executor/fill_sweep: 늦게 확정된 0체결 BUY → CANCELLED + ActiveTrade close, 부분체결 → 수량 축소(멱등, 재기동 후 TH 재구성) - korea_investment_api: get_balance_all_v2(tr_cont 연속조회, _partial), get_todays_orders_v2(실패 None), 브레이커 청산용 조회 면제(OPEN 중 미집계) - swing/exit_service·mdd_manager: 매도 직전 상한·MDD 축소가 전체 잔고 페이지 사용(§8.5-37), 불명 대기 중 부분익절 skip·쿨다운·pre_hldg 차감, 대기 TTL 15:30+재매수 폐기(대조 1회 선행)(§8.5-38 해소) - swing/scheduler: 청산 TIMED_OUT/SKIPPED/in-flight 시 MDD 축소·진입 보류(예외는 진입만), 3사이클 연속 시 error+텔레그램 - daytrading: 결과 불명 매도 재판정 v3(주문조회 v2 None→보류, 등록 이후 일치 행→accepted, 원장≤0→보류), 재기동 복원 min(주문가능,보유), 잔고 조회 후 일일손실한도 재확인, 단독 실행 Redis 접속 제거 - main: 스트림 무수신 경보 v4(희망 구독 기준), 첫 하트비트 없음 stale(HB_FIRST_BEAT_GRACE_SEC) - 운영: Jenkinsfile health_watch.py 디렉터리 잔존 복구, prod_smoke 사이드카 확인, config 키 12개 선언(동작 무변경) - 진입가 v3(max(AT,PA))는 KIS 이동평균 평단 전제 오류로 레드팀 반증 → 폐기 - CLAUDE.md §3.2 ③-d, §8.5-37/38 갱신, §8.5-39~48 신규(Crisis 손절 소급 §8.5-39 는 사용자 결정 대기) - 유닛 스위트 2781 passed / 0 failed (소켓 차단), 네트워크 시도 0건 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
73 KiB
자동 주식 매매 시스템 — Claude 가이드
모든 코드 수정·분석은 이 문서의 규칙을 엄격히 준수할 것. 이 문서는 코드가 실제로 하는 일을 기술한다. 이상(理想)이 아니라 현재 동작이 기준이다. 코드와 이 문서가 어긋나면 코드가 진실이고, 문서를 고치는 것이 1차 대응이다.
0. 파라미터 우선순위 — 가장 먼저 알아야 할 것
전략 파라미터의 진실 소스는 세 곳이고, 다음 순서로 앞선 것이 이깁니다.
adaptive_strategy(_MODE_PARAMS) > strategy_cfg(DB: strategy_configs) > settings(config.py)
adaptive_strategy는 2분 주기로 모드를 갱신하고, RSI 진입·포지션 크기·손절을 덮어씁니다. 모드가fallback_used=True로 떨어질 때만 아래 단계가 쓰입니다.strategy_cfg.get(KEY)는 DB 테이블strategy_configs를 읽습니다. 이 테이블은strategy_config_service.DEFAULT_PARAMS로 시드되지만,auto_init()이on_conflict_do_nothing이라 기존 행은 갱신되지 않습니다.reset_to_default()도 DB의default_value컬럼을 쓰므로 코드만 고쳐서는 반영되지 않습니다.config.py(settings)는 맨 마지막 폴백입니다. §8.2 표가 문서화하는 값이 바로 이것입니다.
따라서 "파라미터가 N이다"라고 주장하기 전에 세 소스를 모두 확인할 것. 2026-09-27 감사에서 이 세 소스가 11곳에서 어긋나 있었고(예: BULL 진입 RSI가 문서 46 / DB 30, 손절이 문서 -7% / DB -6%), 이것이 "문서가 현실과 맞지 않는다"의 근본 원인이었습니다. 2026-09-27 정합 완료: DEFAULT_PARAMS 11건을 config.py 에 맞췄고(AST 전수 대조 후 실질 불일치 0건, 잔여 2건은 부호 규약 차이), alembic 0012_refresh_strategy_config_defaults 가 기존 DB 행을 갱신합니다 — 운영자가 UI 로 직접 바꾼 값은 보존하고 옛 기본값과 같은 행만 갱신합니다.
그래도 우선순위 체계 자체는 그대로이므로, 값을 바꿀 때는 세 곳을 함께 보십시오.
1. 스택 & 환경
- 백엔드: Python 3.11 / FastAPI / SQLAlchemy 2.0(Async) / PostgreSQL 15 / Redis 7 / RabbitMQ 3
- 프론트: React 18 / TypeScript / Vite / Recharts / Lightweight Charts
- 인프라·ML: Docker Compose, Nginx, Alembic / PyTorch, LightGBM, scikit-learn
- 연동: 한국투자증권(KIS) REST·WebSocket, MCP 서버(stdio) / AI 계층:
AI_PROVIDER=claude|gemini - 주요 .env:
KIS_MODE(virtual|real),TOKEN_SAVE_MODE,AUTO_TRADE_ENABLED,DAY_TRADE_ENABLED,US_TRADING_ENABLED(기본 False),MULTI_TENANT_ENABLED(기본 False)
배포 순서 (엄수)
docker compose buildalembic upgrade head를 앱 기동보다 먼저 실행 (docker compose run --rm backend alembic upgrade head)- 스키마 변경 시 필수:
create_all은 기존 테이블에 컬럼을 추가하지 않는다. - 전략 파라미터 마이그레이션(0012 계열)도 마찬가지다. 앱이 먼저 뜨면
strategy_cfg캐시(30초)가 옛 값으로 채워져 최대 30초간 낡은 사이징·손절로 매매가 돈다. 장중 배포라면 특히 위험하다.
- 스키마 변경 시 필수:
docker compose up -d --build—--build필수.up -d만 하면 옛 이미지로 컨테이너가 재생성된다.- 섹터 캐시 생성(최초 1회):
docker compose exec backend python collect_sector_map.py- 없으면
sector_mapper가 44종목 큐레이션 표로만 폴백해 §5.1 섹터 필터가 사실상 무력화된다. - 캐시는
~/.cache/auto-stock/sector_map.json에 생성되고docker-compose.yml의./token_cache:/root/.cache/auto-stock마운트로 유지된다(추가 마운트 불필요). - 상장·폐지 반영을 위해 주기적 재실행 권장(평일 08:30 cron).
- 없으면
2. 구조 (backend/app/)
api/endpoints/— portfolio, market_data, trades, day_trading, backtest 등core/— config(설정), database, security(require_api_key), json_helpers(_dumps), market_schedulemodels/models.py— Portfolio, TradeHistory, ActiveTrade, StockAnalysis, StockBan, StrategyConfig 등realtime/— websocket_client, event_handlerservices/전략 (활성):mean_reversion_strategy(§6, 스윙 3전략 로직 전부 여기),day_trading_strategy(§7),adaptive_strategy(파라미터 오버레이),us_stock_strategy(US_TRADING_ENABLED=False로 정지)services/daytrading/·services/swing/— entry_service, exit_service, position_manager, schedulerservices/순수 계산 모듈 (settings·DB·네트워크 참조 없음 — 이 원칙을 깨지 말 것):indicators_core.py— RSI/BB/ATR/ADX 단일 출처.rsi_wilder/rsi_sma/atr_sma/bollinger_sma/adx_wilder/true_rangessizing_core.py— 포지션 사이징 원시 연산.budget_by_ratios/clamp_by_ratios/apply_confidence_scale/amount_to_quantity/half_kelly_fraction/effective_min_buy_amount/remaining_position_room. 한도 숫자는 이 모듈에 없다(호출부의 settings 소유)trade_cost_core.py— 왕복 비용(수수료×2 + 매도세)·순손익.trade_helpers.calculate_trade_costs와 동일 정의(테스트로 고정)
services/sector_mapper.py— 섹터 조회 단일화. 조회 경로는 파일 캐시만 읽고 네트워크를 쓰지 않는다(§3.1). 수집은backend/collect_sector_map.py배치 전용services/핵심: trade_executor(매매엔진), db_worker(비동기 DB워커), mdd_manager·loss_tracker·correlation_filter(리스크), korea_investment_api·kis_realtime_stream·naver_finance(데이터), feature_engineering·ml_ensemble·technical_indicators(분석), analysis_db_service(DB 기반 스캐너), market_analyzer(국면 게이트), market_playbook(이벤트 차단), ai_strategy_service(AI 필터 계층)mcp-server/— stdio_server, auto_trade_claude, claude_client, gemini_client, trade_monitor, risk_validator, mcp_tools- 배치(
backend/) — batch_daily_analysis, batch_intraday_analysis, train_ensemble, collect_krx_data
2026-09-27 삭제됨 (import 0건 확인 후 제거, 총 2,695줄):
services/short_term_strategy.py,services/kosdaq_scanner.py,app/strategy/패키지 전체 — 기능은 각각day_trading_strategy,analysis_db_service.run_db_scanner(),services/쪽에 이미 있다.app/trading/(kis_client·order_manager·market_data) +services/korea_investment_api_adapter.py— 유일한 진입점인 adapter 가 import 0건이었다.real_market_api의 정본은services/korea_investment_api.py:1482이고adaptive_strategy·market_analyzer가 거기서 import 한다. adapter 가 정의한 동명 객체는 아무도 쓰지 않았다.
되살리지 말 것. 되살리려면 먼저 "누가 import 하는가"를 grep 으로 확인하라.
config.py 의 SHORT_TERM_* 키들은 삭제된 전략의 잔재로 현재 아무 효과가 없습니다(백테스트 폴백에서만 참조).
3. 아키텍처 원칙 (필수)
- 실시간 계층 DB I/O 절대 금지 — API 콜백·웹소켓·실시간 스케줄러에서
AsyncSessionLocal()/commit()호출 금지. - 모든 DB 쓰기는
db_worker.py의db_task_queue경유 (실패 시 지수 백오프 1→2→4초, 최대 3회 재시도).- 허용 예외 ①:
sync_portfolio()— 단일 트랜잭션 필요. - 허용 예외 ②: auth/credentials/bootstrap 시드(회원가입·로그인,
user_kis_credentials등록/삭제, 기동 시 관리자·자격증명 시드) — 실시간 계층이 아님. - 허용 예외 ③ (읽기 전용):
daytrading/exit_service.close_stale_positions,daytrading/entry_service.populate_daytrading_watchlist,daytrading/scheduler기동 시restore_from_db— SELECT 전용이며commit()없음. 각 지점에 주석으로 명시되어 있다. 여기에 쓰기를 추가하지 말 것. - 허용 예외 ③-c (읽기 전용, 2026-10-02 추가):
daytrading/exit_service._fetch_non_dt_active_qty_impl— DT 매도 주문 결과 불명(전송 후 응답 유실) 시에만 비-DT active 수량을 1회 SELECT(not_dt_strategy_clause)해 재매도 상한에서 스윙 보유분을 뺀다. 그 외 인증 계층의 세션 사용자 상태 SELECT(core/security.py, WS 핸드셰이크 포함, 30초 캐시)와 기동 시 1일 지난 FAILED TradeHistory 정리(main.py)도 마커와 함께 허용된다. - 허용 예외 ③-d (읽기 전용, 2026-10-12 추가):
daytrading/exit_service._fetch_dt_ledger_net_qty_impl— DT 매도 결과 불명의 재매도 상한을 잡을 때만 TradeHistory(daytrading, 최근DAY_TRADE_ORPHAN_LOOKBACK_DAYS) 를 1회 SELECT 해 DT 순체결을 구한다(# DB-READ-EXCEPTION:마커). 조회 실패·순체결 ≤0 이면 재매도 보류(fail-closed). - 허용 예외 ③-b (읽기 전용, 2026-10-02 추가):
swing/scheduler.py의 사이클별 세션 4곳(청산 판정exit_session/ 추천 DB 폴백rec_session/ 진입 후보entry_session/ 듀얼 스캔dual_session). 전부 SELECT 전용(commit·add없음)이며 쓰기는db_task_queue경유다. 각 지점에# DB-READ-EXCEPTION:마커가 있다. 하네스rules_check도 이 마커를 인식한다.
- 허용 예외 ①:
- 지원 task: insert_trade_history, update_trade_history(+
_by_order_id), upsert_stock_analysis, bulk_upsert_stock_analysis, upsert_stock_ban, upsert_portfolio, upsert_active_trade, close_active_trade, insert_trade_journal, log_mcp_deny- 전략명 필터 (2026-09-27 추가):
close_active_trade/upsert_active_tradepayload 가 세 가지 선택 키를 받는다. 우선순위는strategy_name(동등) >strategy_names(IN) >strategy_name_not(NOT IN,strategy_name IS NULL행은 제외되지 않음). 키를 넘기지 않으면 동작은 이전과 동일하다. db_worker.not_dt_strategy_clause(column)가 "DT 아닌 행"의 정본 표현이다(DT_STRATEGY_NAMES = ("day_trade","daytrading")).trade_executor·swing/position_manager·swing/exit_service·mdd_manager가 모두 이것을 쓴다 — 한 곳만 바꾸면 더 나쁜 불일치가 된다.
- 전략명 필터 (2026-09-27 추가):
- 동시성: trade_executor
_order_semaphores(종목별 동시 주문 방지) / Redis 분산 락(워커 간 동시 매도 방지) / loss_tracker·mdd_managerasyncio.Lock. 동시 접근 경로에서는is_banned_safe(),record_trade_result_safe()사용. Portfolio행은stock_codeUNIQUE 라 같은 종목의 스윙+데이트레이딩 수량이 합산되어 있다. 한 전략이 청산할 때는 반드시 다른 전략 보유분을 차감할 것 (trade_executor._execute_sell,swing/exit_service양쪽에 구현됨). 차감을 빼먹으면 과다매도·주문거부가 난다.
4. 코딩 규칙
- 기존 함수/클래스 시그니처 변경 금지 → 새 전략은
_v2/_v3, 동기→비동기 확장은_safe, 내부 구현 분기는_impl접미사의 새 메서드. - JSON은
json.dumps금지,core.json_helpers._dumps()사용. - 쓰기/삭제 엔드포인트에
Depends(require_api_key)필수 (portfolio·trades·day_trading 등은app/api/__init__.py에서 라우터 단위로 적용됨). 외부 API 호출verify=True유지(KIS 포함).SECRET_KEY64자+ 랜덤. - 프롬프트는 토큰 최소화, 컴팩트 포맷.
- 리스크 가드·한도 클램프를
try블록 안에 두지 말 것. 예외가 나면 가드가 조용히 통과된다 (2026-09-27에 4% 상한이 정확히 이 이유로 무력화되어 있었다). 예외 로그는logger.debug금지 — 최소warning. - 로그·description 문자열에 임계값을 하드코딩하지 말 것. 실효값을 f-string 으로 넣어라. 2026-09-27 에
mean_reversion_strategy의 BULL 진입 description 이RSI<40을 박아둔 채 실제로는 46 을 쓰고 있었다 — 로그·TradeJournal·ML 학습 데이터에 거짓 임계값이 남는다.
4.1 테스트 규약 (안전 직결)
2026-09-27 에 유닛 테스트가 실제 KIS 에 매도 주문을 2회 전송하고 있었다. 리팩터링이 close_stale_positions() 의 호출 대상을 _execute_exit → _execute_exit_stale 로 바꿨는데 테스트 목이 전자에 걸려 있어 빗나갔고, 실주문 경로가 그대로 실행됐다. 별도로 test_api_endpoints.py 도 캡처된 싱글톤 참조 누수로 실제 잔고 조회를 타 141초를 소비했다.
- 실행:
cd backend && DATABASE_URL="...autostock_test" ../.venv/bin/python -m pytest tests/ --ignore=tests/integrationbackend/tests/conftest.py가DATABASE_URL의 DB 이름이_test로 끝나지 않으면 세션을 중단한다.- 같은 conftest 가 주문·토큰·시세·잔고 계열을 호출 즉시 예외로 치환한다. 차단 예외는
BaseException파생(KISCallBlocked) — 프로덕션의except Exception:폴백에 삼켜지면 차단이 무력화되기 때문이다. - 차단은 클래스 메서드(
KoreaInvestmentAPI.<method>)에 적용한다. 인스턴스만 패치하면from ... import korea_investment_api로 참조를 캡처한 모듈에 새어나간다. - 옵트아웃은 세션 단위
--allow-network하나뿐이다. per-test 예외 마커를 만들지 말 것.
- 목킹 지점은 공통 종단에 걸어라. 위임 래퍼(
_execute_exit/_execute_exit_stale)가 아니라_execute_exit_impl처럼 양쪽이 수렴하는 곳에 걸어야 리팩터링에 빗나가지 않는다. - 차단·목킹은 예외 발생형으로. 그럴듯한 값을 반환하는 스텁은 목킹 누락을 숨겨 거짓 통과를 만든다.
- 테스트가 수십 초 걸리면 실호출 신호다. (2026-10-02 이후
_handle_rate_limit은 재발급 간격(KIS_TOKEN_REISSUE_MIN_INTERVAL_SEC) 안이면 sleep 하지 않고KISTokenUnavailableError로 즉시 실패한다 — 과거의 70초 sleep 은 사라졌다. 그래도 수십 초면 다른 실호출·타임아웃을 의심할 것.) - 실호출 없음의 유일한 증거는 소켓 차단 하 통과다.
socket.getaddrinfo/create_connection/socket.connect/connect_ex를 예외로 치환한 pytest 플러그인으로 확인하라(스크래치패드에 두고 프로젝트에 남기지 말 것). sys.modules를 import 시점에 패치하는 방식은 순서 의존을 만든다. 전체 스위트에서는 앞선 파일이 이미 대상 모듈을 로드해 목이 샌다. 모듈 전역 참조는 테스트 시점 autouse 픽스처로 덮어라(test_mean_reversion.py가 이 이유로 단독 74 통과 / 전체 1 실패였다).- 테스트 기대값에 config 값을 하드코딩하지 말 것.
settings.*를 참조하라. 2026-09-27 에MAX_POSITION_SIZE(0.06→0.04)·SWING_MAX_CAPITAL_RATIO(0.50→0.70) 변경이 반영되지 않은 테스트 2건이 이 이유로 깨져 있었다.
5. 리스크 규칙
5.1 사이징·필터
- 종목당 4% (
MAX_POSITION_SIZE) — 절대 상한(hard ceiling). 적응형 모드가 6~7%를 지시해도 소비 지점에서min(_adp.position_size, MAX_POSITION_SIZE)로 클램프되고, 패턴메모리 조정 후에도try밖에서 재클램프된다.- 누적 기준 (2026-10-01): 상한은 주문 1건이 아니라 기존 보유분 포함 누적이다(
trade_executor._apply_position_cap_room_v2→sizing_core.remaining_position_room). 기존 보유 =max(Portfolio 행 수량, DT 인메모리 수량)× 현재가. 남은 한도 < 실효 최소주문금액이면 skip, 보유가 있는데 총자산 조회 실패면 추가매수 skip. 신규 진입 수치는 이전과 동일.
- 누적 기준 (2026-10-01): 상한은 주문 1건이 아니라 기존 보유분 포함 누적이다(
- 최대 20종목(
MAX_HOLDINGS_COUNT), 레버리지/인버스 ETF 최대 20%(LEVERAGE_ETF_MAX_RATIO), 전 전략 합산 현금 사용 상한 95%(MAX_TOTAL_CAPITAL_USAGE). - 최소 주문금액 15만원(
MIN_BUY_AMOUNT_KRW) — 단 실효값은min(150,000, 총자산 × 적응형포지션상한 × 0.9)다. 소액계좌에서 주문이 전부 탈락하는 것을 막기 위한 의도적 완화(2026-09-23 하루 141건 차단 사례). - 가격 필터: 국내 >5,000원, 미국 >$4 (
MIN_PRICE_KRW,MIN_PRICE_USD). ETF 예외는trade_executor경로에만 있고market_analyzer·portfolio_v2·manual_buy경로에는 없어 경로별로 동작이 다르다. - 상관관계: 섹터당 최대 3종목, 기존 포트폴리오와 상관계수 >0.7 진입 거부 (
correlation_filter).- 섹터 매핑 (2026-09-27 확충):
sector_mapper가 네이버 업종 API(m.stock.naver.com/api/stocks/industry)로 수집한 캐시를 쓴다. 실측 커버리지 97.27%(747/768) — 미매핑 21종목은 전부 ETF/ETN 이라 실질 주식 커버리지 100%. 조회 순서: 메모리 → 큐레이션 정적 표(44종목, 의도적 그룹핑이라 외부보다 우선) → 파일 캐시 →None.- 업종 79개 → 정본 섹터 20개. 네이버
no=25 "기타"(1,542종목: ETF/ETN/우선주/스팩)는 수집 단계에서 제외해 거대 버킷을 만들지 않는다(최대 버킷 9.6%). - ⚠️
industryAPI 는page/pageSize를 주지 않으면 20개 업종만 반환하고 나머지가 "기타"로 뭉쳐 보인다. 수집 코드를 고칠 때 주의. - 배포 후
collect_sector_map.py를 한 번 돌려야 캐시가 생긴다(§1-4). 없으면 44종목 표로만 폴백.
- 업종 79개 → 정본 섹터 20개. 네이버
- 미매핑 종목의 섹터캡:
SECTOR_CAP_UNMAPPED_ENABLED(기본 False). 미매핑을 "UNKNOWN" 단일 버킷으로 묶어 3종목 한도를 적용하는 스위치다. 켜기 전 조건 — 운영 환경 커버리지 ≥95% & 캐시 7일 이내, 미매핑 잔여가 ETF/ETN 만(일반 주식이 남아 있으면 UNKNOWN 3종목 상한에 걸려 거래가 죽는다), 최대 버킷 <20%, 그리고 켠 상태 백테스트에서 체결 건수가 유의미하게 줄지 않고MAX_HOLDINGS_COUNT=20도달이 가능함을 확인.
- 섹터 매핑 (2026-09-27 확충):
- 연속 손실: 동일 종목 3연패 →
CONSECUTIVE_LOSS_BAN_DAYS(42캘린더일 ≈ 30거래일) 진입 금지 (loss_tracker). 값은 config 연동이며 하드코딩이 아니다.record_trade_result()후 반드시save_to_db()(db_workerupsert_stock_ban경유). 재시작 시load_from_db()복원.- 의도적 예외 (2026-10-01): DT 시간손절(
day_trading_strategy.TIME_STOP_REASON_PREFIX)은 loss_tracker 에 기록하지 않는다(중립 — 연패를 늘리지도 끊지도 않음). 이 밴이 스윙 진입까지 막아 -0.3% 시간손절이 42일 금지를 유발했기 때문. 하드/폴백 손절·일일한도·EOD·트레일링은 기존대로 기록. DT 당일 3연패 쿨다운(loss_streak_today)은 시간손절도 계속 센다. - 손익 판정은 비용 차감 순손익 기준이라 왕복 비용(≈0.21%) 이내의 소폭 이익 청산도 손실로 집계된다.
- 의도적 예외 (2026-10-01): DT 시간손절(
5.2 SL/TP (일봉/스윙)
- 기본 SL: -7%(안정) / -10%(변동성, ATR/종가 > 3%). ATR SL:
진입가 − 2.5×ATR(ATR_STOP_MULTIPLIER). - SL은 기본/ATR 중 타이트한 쪽. 비교 기준에 따라 연산자가 다르다 — 혼동 금지:
- 가격 기준 비교: 더 높은 손절가가 타이트 →
max(atr_stop, pct_stop)이 올바르다. - 음수 퍼센트 기준 비교: 더 큰(0에 가까운) 값이 타이트 →
max()가 올바르다. stop_loss_pct = -abs(...)부호 가드를 항상 유지한다(양수가 들어가면 전 종목 매도 사고).
- 가격 기준 비교: 더 높은 손절가가 타이트 →
- TP: +2R 도달 시 50% 분할 매도, 잔량 트레일링 스탑
고점 − 1.5×ATR. - 분할매도
sell_pct는 "진입 시 원본 수량 대비" 비율이다.remaining_pct게이트(1.0→0.70→0.40)가 원본 누적 전제로 설계되어 있으므로 잔량 기준으로 바꾸면 같은 단계가 재트리거된다.
5.3 MDD (mdd_manager — swing/scheduler.py 에서 매 사이클 자동 호출, 중복 실행 방지 내장)
- -10%: 신규 진입 중단 + 포지션 30% 축소 + 데이트레이딩 중단 (REDUCE_AND_PAUSE)
- -20%: 전 포지션 50% 강제 축소, 레버리지/인버스 ETF 우선 청산 (FORCE_REDUCE)
- 잔고가 peak 갱신 시 모든 MDD 플래그 리셋.
- MDD 갱신은
swing/scheduler.py한 곳이 유일한 실효 경로다(portfolio.py경로는 deprecated). 갱신이 실패하면 peak/drawdown이 정지해 -10%/-20% 게이트가 영구 미발동한다. - fail-safe (2026-09-27 적용): 갱신이
MDD_BLIND_FAILURE_THRESHOLD(5)회 연속 실패하면can_open_new_position()이False를 반환해 모든 신규 매수가 차단된다(MDD_BLIND_BLOCK_ENTRY기본 True). 스윙 진입·수동매수 API·미국장·데이트레이딩 진입·trade_executor전 경로에 일괄 적용되고, 청산 경로는 이 메서드를 호출하지 않아 영향이 없다. 빈 계좌(tot_evlu_amt<=0)는 정상 응답으로 분류되어 차단 사유가 아니다. 갱신 1회 성공이면 재시작 없이 자동 해제.- ⚠️ KIS 장애가 5분 연속되면 당일 매수 0건이 될 수 있다. 토큰 충돌·계좌 교체(40910000)·네트워크 단절이 해당한다.
[MDD] 🚨 신규 진입 차단(fail-safe)error 로그로 즉시 식별되고, 급하면MDD_BLIND_BLOCK_ENTRY=false로 끌 수 있으나 그 상태의 진입은 -20% 구간에서도 무제한 추가 진입을 허용한다는 뜻이다. - 상태 조회:
mdd_manager.get_update_health()/swing.scheduler.get_mdd_update_health()
- ⚠️ KIS 장애가 5분 연속되면 당일 매수 0건이 될 수 있다. 토큰 충돌·계좌 교체(40910000)·네트워크 단절이 해당한다.
- MDD 축소가
ActiveTrade를 닫을 때는 스윙 전략명을 명시한다(_sync_active_trade_after_reduction). 전략명 없이 닫으면 db_worker가 "가장 최근 active 행"을 닫아 DT 행을 오폐기한다.
6. 스윙 전략 (mean_reversion_strategy, swing/) — 스윙 자본 최대 70%(SWING_MAX_CAPITAL_RATIO)
세 전략이 한 파일에 있고 모두 swing/entry_service 에서 호출됩니다.
| 전략 | 진입 | 청산 | 국면 조건 |
|---|---|---|---|
| 평균회귀 | RSI 과매도 + BB하단(%B) | RSI 55 부분 → RSI 70 전량 | BULL·BEAR |
| 모멘텀 돌파 | 20일 신고가 + 거래량 2× + RSI 50~70 | +10% 30% → +20% 30% → 트레일링 | BULL 전용 |
| 눌림목 매수 | MA20>MA60 + MA20 −3% |
평균회귀와 동일 | BULL 전용 |
6.1 시장 국면 (일봉)
- BULL = 코스피200 > MA200 & MA200 기울기 > 0
- BEAR = 코스피200 < MA200 & 기울기 < 0
- SIDEWAYS → 모든 진입 무시 (market_analyzer: position_multiplier=0, max_trades=0)
- 판정은
ADX < 18AND 기울기 ≈ 0 동시 충족 (히스테리시스: 진입 ADX<18 / 해제 ADX>22). 기울기 비교 기간은 20일. 2026-10-02 H2 로 코드가 이 서술대로 바뀌었다 — 그 전에는 기울기≈0 단독 분기가 ADX 를 무시했다. ADX 를 계산할 수 없을 때(highs/lows 없음)만 기울기 단독. HMM 앙상블이 SIDEWAYS 로 덮어쓰는 경로(market_analyzer)가 별도로 있다. 국면 조회 예외는 fail-closed(진입 0).
- 판정은
- 국면 커버리지 불균형: 신호원 3개 중 2개가 BULL 전용이고, BEAR는 평균회귀 분기 하나, SIDEWAYS는 담당 전략이 없다(진입 차단이 곧 전략). 코스피가 BULL이 아닌 구간에서는 사실상 거래가 나오지 않는다.
- 2026-09-27: 두 빈 칸을 채우는 코드를 넣었으나 둘 다 플래그 OFF 로 출고했다. DB(NAS) 다운으로 백테스트를 돌릴 수 없고, 게다가 기존 백테스트 자체가 실전과 다른 지표로 돌아간 것이 밝혀져(§8.4) 참고할 근거가 없다.
- 박스권:
services/range_trading_strategy.py+swing/entry_service.scan_range_trading_signals.RANGE_TRADE_ENABLED(기본 False). 스케줄러 배선도 아직 하지 않았다 — 호출부가 없어 현재 실행 경로에 존재하지 않는다. - BEAR 완화:
mean_reversion_strategy.check_bear_buy_signal_v2(점수제).MEAN_REVERSION_BEAR_V2_ENABLED(기본 False). OFF 면 기존 4중 AND 경로가 그대로 동작한다.
- 박스권:
- 측정된 문제 크기: 같은 합성 샘플에서 BEAR 발화율 2.3% vs BULL 13.2% — 실질 게이트 8개가 전부 AND 였다. v2 는 12.4%(5.4배)로 올라가고 구 발화를 모두 포함하는 상위집합이다.
- 2026-09-27: 두 빈 칸을 채우는 코드를 넣었으나 둘 다 플래그 OFF 로 출고했다. DB(NAS) 다운으로 백테스트를 돌릴 수 없고, 게다가 기존 백테스트 자체가 실전과 다른 지표로 돌아간 것이 밝혀져(§8.4) 참고할 근거가 없다.
6.2 평균회귀 진입 조건 (실제 코드)
⚠️ 실전 진입의 1차 게이트는
api/endpoints/recommendations.is_mean_reversion_candidate이고 아래check_buy_signal은 백테스트 전용이다(2026-10-02 검증). 실전 게이트는 RSI ≤ 임계(46/35 —MEAN_REVERSION_CANONICAL_PARAMS_ENABLED=True로 §0 우선순위 adaptive → strategy_cfg → settings, False 면 trading_rules 의 40/30), SMA RSI(market_analyzer인라인), ADX>30 차단, 거래량 하한·RSI 반등·적응형 %B 스케일 없음. 백테스트(<, Wilder, 필터 있음)와 실전이 다르다 — §8.4 계열 문제로 미해결.
- 공통 선행 필터 (BULL·BEAR 모두):
current_volume < avg_volume_20d→ 진입 거부 (거래량 하한, 무조건 선적용)- own_trend 게이트: 종목 MA20 < MA60 × 0.97 → 진입 거부
- RSI 반등 필수:
rsi <= rsi_prev면 진입 거부 (RSI 하락 중 진입 금지)
- BULL: RSI < 46 (
MEAN_REVERSION_RSI_THRESHOLD) + %B 임계값 방식으로 BB 하단 근접 판정- %B 임계: RSI<25 → 0.35 / RSI<30 → 0.30 / 그 외 → 0.20, 여기에 적응형
bb_pct_b/0.30스케일(상한 1.3) 적용 - ※ 과거 문서의 "BB하단 −3%~+1.5% 가격 밴드"는 코드에 존재하지 않는다.
- %B 임계: RSI<25 → 0.35 / RSI<30 → 0.30 / 그 외 → 0.20, 여기에 적응형
- BEAR: RSI < 35 (
MEAN_REVERSION_BEAR_RSI_THRESHOLD) + %B ≤ 0.20 + 거래량 > 1.5×20MA + 반전캔들 - 선제 진입(60분봉 RSI<30 + MA5 상향돌파)은 구현되어 있지 않다. 헬퍼
check_h1_ma5_breakout은 호출부가 없다. 기능이 필요하면 새로 배선할 것.
6.3 평균회귀 청산
- 50% 부분매도: RSI ≥ 55 or 가격 > BB중앙 or 수익 > 2×ATR — 단 최소수익 0.5% 가드가 있다
- 전량: RSI ≥ 70 or 가격 > BB상단(99%) or 트레일링(고점 − 1.5×ATR) — 트레일링은 1차 분할 익절 이후(remaining_pct ≤ 0.50)에만 적용(2026-10-02, 사용자 결정). 그 전에는 손절선까지 반납 가능.
- 시간손절: 20 KRX 거래일(
count_krx_trading_days_between) 보유 + 3% 미달 시 전량 (check_time_stop_exit→swing/exit_service._time_stop_signal_v2,SWING_TIME_STOP_ENABLED=True, 모멘텀 제외, 부분매도 신호보다 우선). 동기화 유입 행은 마지막 전량 청산 이후 TradeHistory BUY 일자를 진입일로. - 손절 파라미터 단일 소스
resolve_swing_stop_params_v2(적응형 → strategy_cfg → config, 범위 검증SWING_STOP_LOSS_PCT_MIN/MAX·SWING_ATR_STOP_MULT_MIN/MAX). 폴백 손절은 지표 판정이 불가할 때만(판정 예외 포함) 실행되고, 그 경우에도-abs(STOP_LOSS_PERCENT)하드 바닥이 try 밖에서 보장된다. (단 ATR 캐시가 없는 폴백 경로는-abs(STOP_LOSS_HIGH_VOL)(-10%, 느슨한 쪽)을 쓴다 — 2026-10-12 레드팀 확인) 현재가 조회 실패 시 대체가(마지막 실시간가 → Portfolio.current_price)로 손절만 판정.
6.4 문서화되지 않았던 진입 차단 게이트 (전부 실재하며 거래 빈도에 큰 영향)
- ML 앙상블 HOLD 게이트 (
trade_executor): ML이 HOLD이고 신뢰도 ≥60% 면 rule 매수를 차단 - 지수 서킷브레이커 (
swing/entry_service): KODEX200 종가 < MA20 이면 신규 진입 전면 중단 — 기본값 OFF(SWING_INDEX_BREAKER_ENABLED=False, 2026-09-17 사용자 결정; 켜면 평균회귀·듀얼 공용 적용) - 마켓 플레이북 (
market_playbook): FOMC·옵션만기 등 이벤트 당일 진입 차단 — 평균회귀·듀얼(모멘텀·눌림목) 모두 적용. position_multiplier 는 사이징에 배선돼 있지 않다. - 듀얼 전략 실행 순서 (2026-10-02): 평균회귀 진입 다음에 듀얼 스캔이 평균회귀 후보 유무·MCP 거부와 무관하게 실행된다(MDD/AUTO_TRADE/Crisis/플레이북/서킷브레이커/SIDEWAYS 게이트는 적용, MCP 평가는 적용하지 않음 — 사용자 결정). 일일 한도(
MAX_DAILY_TRADES)는 평균회귀가 먼저 쓴다. 모멘텀 돌파는 v2(직전 20봉, 오늘 봉 제외)로MOMENTUM_BREAKOUT_ENABLED=True— v1 은 오늘 고가를 포함해 구조적으로 발화 불가였다. - 거시/국면 position_multiplier (
market_analyzer): 장외 0.5, CRASH 0.0~0.1, BEAR ratio - 적응형 Crisis 모드: 진입 전면 차단 (§8.4)
- 추격매수 방지·추세 필터·이격도 과열·일일 매매한도·재매수 쿨다운 (
trade_executor)- 일일 매매한도(
MAX_DAILY_TRADES)는 스윙 소유 매수만 집계한다(strategy_type IS NULL OR = 'swing',_swing_owned_clause_v2). 집계 조회가 실패하면 fail-closed — 그 사이클 신규 진입 0건 +logger.error. - 재매수 쿨다운은 DT 매도 포함 전체 매도 기준, 부분매도 반복 차단(
_sold_today)만 비-DT 매도 기준. - 추천 경로(
execute_auto_trade)는 스윙 소유 행만 매수·매도한다(_sell_ownership_block_reason_v2): external/manual 은 수동 판단, daytrading 은 DT 엔진 소유, us_swing 등은 타 전략 — 매도도 추가매수도 skip(대칭). MDD 축소·swing/exit_service·DT 청산은 별도 주문 경로라 무관. 고아 daytrading 행(DT 인메모리 0)은 warning 으로 수동 확인을 요청한다.
- 일일 매매한도(
- AI 전략 계층 (
ai_strategy_service): 08:30 장전 분석 / 08:50 DT 후보 선정 / 뉴스 감시 / 15:40 장후 리뷰. 적합성 필터링과 뉴스 악재 배율로 실제 매매에 영향을 준다. - 듀얼 전략 후보 소스 의존:
StockAnalysis.analysis_date >= today에만 의존하므로 일일 배치가 실패하면 국면이 BULL이어도 후보 0건으로 조용히 아무 일도 일어나지 않는다.
2026-09-27 해소된 치명적 결함 2건 (이전에는 듀얼 전략이 사실상 매수를 못 했다):
scan_dual_strategy_signals가"signal"(단수 문자열)만 넣고"signals"(복수 리스트)를 넣지 않아,trade_executor의should_buy = any(k in "" for k in BUY_KEYWORDS)가 항상 False 였다.rule_id도 없어 rule 강제 매수 경로도 타지 않았다 → ML 앙상블이 독립적으로 BUY 를 낼 때만 진입했다. §8.3 의 "3종 병행" 서술과 어긋났다.- 비-rule 요건은
has_strong_signal OR (matched_count >= 2 AND score_val >= 4)인데 듀얼의score는 0.8/0.6 이라 STRONG 신호가 필수다. 모멘텀 돌파를 표현하는 값이 없어BuySignal.신고가돌파("20일 신고가 돌파")를 신설했다. - 신호 문자열은 실제 진입 근거와 일치해야 한다. 통과시키려고 맞지 않는 키워드를 끼우면 로그·저널·ML 학습 데이터가 오염된다(예: 모멘텀에
RSI 안정 상승은 정의가45~60이라 60~70 구간에서 거짓).
- 비-rule 요건은
execute_auto_trade_background가trade_results를 채우고도return문이 없어 항상None을 반환했다 → 호출부의isinstance(result, list)가 영구 False → "진입 n건" 로그가 나오지 않았다. 매매는 정상 수행되고 결과만 유실됐다. 이제 종료 경로 6개 전부 list 를 반환한다.- 반환 리스트에는
action="SELL"과success=False도 섞인다. "진입 n건"은action=="BUY" and success로 세야 한다(호출부 3곳 정정됨).
- 반환 리스트에는
거래가 나오지 않을 때는 전략 로직보다 이 목록을 먼저 확인할 것.
7. 데이트레이딩 (5분봉, day_trading_strategy, daytrading/) — 자본 최대 30%(DAY_TRADE_MAX_RATIO_DT, 스윙:DT = 70:30)
MDD -10% 시 비활성. CRASH/HALTED 국면 차단.
| 전략 | 진입 | SL |
|---|---|---|
| ORB | 시초 레인지 돌파(+0.2% 여유) + 거래량 ≥1.5×평균분당 + 직전 5분봉 양봉 + 갭 4%↑ 거부 + 레인지폭 0.3% 미만 거부 | max(ORB저점, 현재가×(1−3%)) → ATR 하한 → 최소 손절폭 -2% 클램프 |
| VWAP | VWAP 지지 반등(−1%~+0.5%) + RSI(14) ≤ 45 + 거래량 ≥1.3× + 양봉 | ATR 있으면 현재가 − 1.5×ATR, 없으면 −3.5% → 최소 손절폭 -2% 보장 |
| RSI_BB | RSI ≤ 35 + 종가 ≤ BB하단×1.01 + 거래량 ≥1.3×(직전 봉 대비) + 양봉 | VWAP과 동일 |
- 최소 손절폭 보장(
DAY_TRADE_MIN_STOP_LOSS_PCT= 2%)은 §5.2의 "타이트한 쪽" 규칙의 의도적 예외다. 장중 전략에서 손절이 너무 타이트하면 노이즈에 털리므로,min(raw_stop, min_stop)으로 더 느슨한 쪽을 고른다. 이min()을 "버그"로 판단해 뒤집지 말 것. - 사이징: 종목당 6%(
DAY_TRADE_MAX_CAPITAL_RATIO), 동시 5종목(DAY_TRADE_MAX_POSITIONS) = 5×6% = 30%. confidence 조정예산 × min(1.2, confidence/0.7)후 세 한도(종목당 6% / DT 잔여 30% / 가용현금)로 재클램프된다. - 청산: 1.5R(
DAY_TRADE_TARGET_MULTIPLIER) 도달 시 50% 분할 → 잔량 트레일링(ATR×1.5, 발동 최소수익 1%) / 진입 후 30분(DAY_TRADE_TIME_STOP_MINUTES) 경과 이후 언제든 수익률이 -0.3% 미만(DAY_TRADE_TIME_STOP_MIN_GAIN)이면 시간손절 — "30분 동안"이 아니라 30분 시점부터 EOD 까지 상한 없이 매 사이클 판정한다(day_trading_strategy.check_day_trade_exit). -0.3% 이상이면 보유 유지. 경과시간이 음수/390분 초과면 entry_time 기준 불일치로 보고 판정하지 않는다(_elapsed_minutes) / 5분 RSI ≥70 & 수수료 이상 수익 시 / 15:20 EOD 전량 청산 / 전일 미청산 포지션은 장중 발견 즉시 전량 청산(close_stale_positions) - 전일 포지션 청산 손익은 당일 실현손익에 넣지 않는다(
_finalize_exit_impl(count_daily_pnl=False)). 넣으면 당일 매매 0건인데 -3% 손실한도가 발동해 정상 포지션까지 전량 청산된다. 전일 손익은get_stale_realized_pnl()+ warning 로그 + TradeHistory로 추적한다. stale 청산 종목은closed_today에 추가해 당일 재진입을 막는다(주문 실패 시에도). - 트레일링 고점(
highest_price)은 진입 이후에 시작한 1분봉 고가 + 현재가로만 갱신한다(daytrading/exit_service._post_entry_candle_high, 2026-10-01). 진입이 속한 봉은 진입 전 체결이 섞여 제외하고, 라벨 ≥ 진입+60초 봉만 인정하며 날짜까지 비교한다. entry_time 이 없거나 정규장 밖(UTC 저장 추정)이면 현재가만 쓴다. 이전의max(candles_5m[-3:].high)는 진입 전 고가로 +1% 트리거를 오발동시켰다. - 분봉 조회 실패 시 폴백 손절이 있다(
_check_fallback_stop_loss). 현재가만으로 손절선 이탈을 판정해 전량 청산한다. 폴백 손절가는min()으로 고른 가장 타이트한 하드캡(-2%) 기준이며, 현재가조차 못 구하면logger.error로 수동 확인을 요청한다. - 제한: 15:00 이후 신규 마감(
DAY_TRADE_ENTRY_CUTOFF_*) / 일일 -3% 손실 시 당일 중단(DAY_TRADE_DAILY_LOSS_LIMIT) / 당일 3연패 쿨다운(position_manager) / 전량 매도 시 loss_tracker 기록 +save_to_db()(시간손절 제외 — §5.1) - 실현손익은 비용 차감 순손익(2026-10-01,
exit_service._finalize_exit_impl):daily_pnl·연패·loss_tracker·TradeHistory(net_profit_loss,profit_loss_rate,commission) 모두 순손익 기준이다. 따라서 일일 -3% 한도가 비용만큼 일찍 발동한다(의도). 미실현 손익은 여전히 비용 전. DT 포지션에 시장 구분이 없어 거래세는DAY_TRADE_DEFAULT_TAX_MARKET(KOSPI) 세율을 쓴다. - 체결가 보정(
daytrading/fill_reconciler,DAY_TRADE_FILL_CHECK_*): 주문 후 백그라운드로get_todays_orders를 ODNO 매칭해 실제 체결가로 매수 진입가(인메모리·ActiveTrade)와 매도 손익·daily_pnl을 보정한다(손절가·목표가는 절대가 유지). 보정 결과 일일한도를 넘으면 다음 사이클에 전량 청산된다. 실패·시간초과면 신호가 유지. 문제 시DAY_TRADE_FILL_CHECK_ENABLED=false. - 감시종목: 09:00에 당일 분석이 없으면 최근 분석일 결과로 선채움(ORB 수집 보장), 자동 목록은 30분마다 갱신(수동 설정 목록은 유지)
- 1분봉 데이터 주의:
get_minute_chart_full_day는 과거 데이터 포함 요청(FID_PW_DATA_INCU_YN="Y")이라 전일 캔들이 섞인다. dedupe·정렬 키에stck_bsop_date를 포함하고 당일 외 캔들을 제외해야 ORB 거래량(candles_1min[-1])·VWAP이 오염되지 않는다.
8. Claude 지침
8.1 체크포인트
- 파라미터 값을 주장하기 전에 §0의 세 소스를 모두 확인 (adaptive / strategy_cfg(DB) / config.py)
- DB 쓰기 = db_worker 경유 (예외: §3.2의 3종)
- 모든 매매에 §5 리스크 규칙 적용
- SIDEWAYS 국면 진입 제안 금지
- 백테스트·Walk-forward 없이 실전 투입 금지 — 단 §8.5의 백테스트 신뢰성 주의사항을 먼저 읽을 것
- SL 선택 = 타이트한 쪽. 단 비교 기준(가격 vs 음수 퍼센트)에 따라 연산자가 다르고, 장중 최소 손절폭 보장은 의도적 예외 (§5.2, §7)
record_trade_result()후save_to_db()호출 확인- 쓰기/삭제 API에
require_api_key필수 - JSON 출력 시
_dumps()/ 새 기능은 새 파일 / 기존 시그니처 수정 금지 - 리스크 가드·한도 클램프를
try안에 두지 말 것. 예외 로그에logger.debug금지 - 청산은 항상 매수보다 우선. 리스크 가드(MDD/밴/보유수/쿨다운)는 신규 매수만 차단하고 청산 판정은 통과시킬 것
- 한 전략이 청산할 때 다른 전략 보유분 차감 확인 (§3.5)
8.2 config.py 주요값
| 설정 | 값 | 비고 |
|---|---|---|
MAX_POSITION_SIZE |
0.04 | 절대 상한. 적응형보다 우선 |
MIN_BUY_AMOUNT_KRW |
150,000 | 실효값은 소액계좌에서 하향 (§5.1) |
MAX_HOLDINGS_COUNT |
20 | |
MAX_TOTAL_CAPITAL_USAGE |
0.95 | 현금 5% 유보 |
LEVERAGE_ETF_MAX_RATIO |
0.20 | |
ATR_STOP_MULTIPLIER |
2.5 | 스윙 ATR SL 배수 |
STOP_LOSS_PERCENT |
7.0 | config는 양수, DB는 -7.0 (부호 규약 차이) |
STOP_LOSS_HIGH_VOL |
10.0 | config는 양수, DB는 -10.0 |
MEAN_REVERSION_RSI_THRESHOLD |
46.0 | |
MEAN_REVERSION_BEAR_RSI_THRESHOLD |
35.0 | |
MEAN_REVERSION_RSI_EXIT_THRESHOLD |
55.0 | 1차 익절 |
MEAN_REVERSION_RSI_FULL_EXIT |
70.0 | 전량 청산 |
DAY_TRADE_MAX_POSITIONS |
5 | 5×6% = 30% |
DAY_TRADE_MAX_CAPITAL_RATIO |
0.06 | 종목당 |
DAY_TRADE_ORB_STOP_LOSS_PCT |
0.03 | |
DAY_TRADE_REVERSION_STOP_LOSS_PCT |
0.035 | VWAP/RSI_BB |
DAY_TRADE_MIN_STOP_LOSS_PCT |
0.02 | 최소 손절폭 (느슨한 쪽 선택) |
DAY_TRADE_TARGET_MULTIPLIER |
1.5 | 1.5R |
DAY_TRADE_TRAILING_ATR_MULT |
1.5 | |
DAY_TRADE_VWAP_RSI_THRESHOLD |
45.0 | |
DAY_TRADE_RSI_BB_RSI_THRESHOLD |
35.0 | |
DAY_TRADE_TIME_STOP_MIN_GAIN |
-0.003 | -0.3% 미만일 때 청산 |
DAY_TRADE_ENTRY_CUTOFF |
15:00 | |
DAY_TRADE_MAX_RATIO_DT / SWING_MAX_CAPITAL_RATIO |
0.30 / 0.70 | 합 100% |
CONSECUTIVE_LOSS_BAN_COUNT / _DAYS |
3 / 42 | 캘린더일 |
SECTOR_CAP_UNMAPPED_ENABLED |
False | 켜기 전 §5.1 확인 |
MDD_BLIND_BLOCK_ENTRY / _FAILURE_THRESHOLD |
True / 5 | MDD 갱신 실패 시 신규 진입 차단 (§5.3) |
RANGE_TRADE_ENABLED |
False | 박스권 전략. 백테스트 미검증 — True 금지 (§6.1) |
MEAN_REVERSION_BEAR_V2_ENABLED |
False | BEAR 점수제. 백테스트 미검증 — True 금지 (§6.1) |
MEAN_REVERSION_CANONICAL_PARAMS_ENABLED |
True | 실전 진입 RSI 40/30 → 46/35(§0 우선순위). ⚠️ 2026-10-02 사용자 결정(거래 확대), 백테스트 미검증, real 전환 전 재검토 |
MOMENTUM_BREAKOUT_ENABLED |
True | 모멘텀 돌파 v2. ⚠️ 동일(사용자 결정, 미검증) |
SWING_TIME_STOP_ENABLED / _DAYS / _MIN_PROFIT_PCT |
True / 20 / 3.0 | KRX 거래일. ⚠️ 동일. 켠 직후 첫 사이클에 조건 충족 포지션 일괄 청산 |
ML_SELL_SIGNAL_ENABLED |
False | ML 단독 SELL 트리거 차단(재학습 전 LSTM 난수, §8.5-20) |
DAY_TRADE_ORB_FULL_RANGE / DAY_TRADE_ORB_OVERNIGHT_GAP_ENABLED |
False / False | ORB 는 실제로 첫 5분 레인지로 확정되고 갭은 당일 시가 대비(현행 유지 — 거래 확대 결정) |
MDD_REDUCTION_MAX_RETRIES |
5 | MDD 축소 종목별 일일 재시도 상한, 날짜 변경 시 재시도 |
AUTO_TRADE_MAX_EVALUATIONS_PER_CYCLE / AUTO_TRADE_ORDER_REJECT_COOLDOWN_MIN |
30 / 15 | 신규 후보 평가 상한(회전), 업무거부 종목·계좌(KIS_ACCOUNT_REJECT_MSG_CODES) 쿨다운 |
8.3 적응형 모드 (adaptive_strategy.py, 2분 주기 자동 전환)
| 모드 | RSI 진입 | 포지션 | SL | 트리거 | 해제 |
|---|---|---|---|---|---|
| Aggressive | 50 | 7% → 4%로 클램프 | -9% | ATR백분위<30 AND ADX>25 AND 5일수익>+2% | ATR<40 & ADX>20 & 5일>0 |
| Normal | 46 | 6% → 4%로 클램프 | -7% | 기본 | — |
| Defensive | 38 | 4% | -6% | ATR백분위>70 OR 5일수익<-3% | ATR<60 & 5일>-1% |
| Crisis | 25 | 1.5% | -3% | ATR백분위>90 OR 5일수익<-5% OR MDD < -10% | ATR<80 & 5일>-3% & MDD>-8% |
_MODE_PARAMS숫자는 문서·백테스트 기준값이므로 바꾸지 말 것. 상한 제약은 소비 지점에서 클램프한다.- Crisis 는 파라미터 조정이 아니라 on/off 스위치다:
max_daily_trades=0으로 스윙·DT 신규 진입을 전면 차단한다. MDD -10%가 곧 Crisis를 유발하므로 §5.3의 REDUCE_AND_PAUSE와 이중으로 걸린다. - 모드 해제에는 히스테리시스 밴드가 있어 진입/해제 임계가 다르다(위 표).
8.4 백테스트 신뢰성 — ❌ 기존 결과물 전부 폐기
2026-09-27 조사 결론: 파라미터를 검증했다는 백테스트가 실전 로직을 돌린 적이 없다. 세 가지가 겹쳤다.
① run_backtest.calculate_adx_simple 이 정확히 period 배(14.0×)로 발산했다.
DX(0100)에 합산형 평활을 적용한 버그. 측정: 87%.
Aggressive 전환 빈도 5.6% → 2.0% (−65%). Aggressive 는 포지션 7%·SL −9% 이므로, 기존 백테스트는 해당 구간에서 실전보다 큰 사이즈·느슨한 손절로 돌았다 = 성과 상방 편향.up 1361 vs 97 / flat 165 vs 12 / vol 206 vs 15 — 전 패턴에서 배율 14.0.
결과적으로 §8.3 Aggressive 게이트 ADX > 25 가 모든 레짐에서 100% 통과했다. 수정 후 43
② 백테스트 RSI 는 SMA, 실전은 Wilder 였다.
측정(n≈70,000): mean|Δ| 4.7~6.5, p95 ≈ 15, max ≈ 35. 진입 게이트 RSI<46 판정이 10~11% 의 봉에서 불일치, 전량청산 RSI≥70 은 6~10% 불일치.
③ 미국 백테스트 RSI 가 1봉 지연이었다(같은 엔진의 BB 는 당일 포함, 실전도 당일 포함). 진입 게이트 판정 8.4% 불일치.
세 건 모두 indicators_core 로 정합화했다(룩어헤드 없음을 절단 테스트로 증명 — 각 i 의 값이 closes[:i+1] 만으로 결정됨).
backend/backtest_results/ 의 25개 JSON 은 인용 금지. 근거:
- 전부 2026-09-06 생성 = ADX 발산 수정(2026-09-16
a097381) 이전 코드의 산출물. 당시 SIDEWAYS 게이트 ADX 도 발산 상태여서 §6.1 의 "SIDEWAYS 전면 진입 무시"가 사실상 적용되지 않았다. total_trades1013건 — 파라미터당 최소 100거래 기준 대비 2자리수 부족. Sharpe −2.2−2.6 자체가 통계적 무의미.params.adaptive_modes.normal.rsi_entry가 파일별로 20/43/70 으로 흔들림 = optimizer 그리드 스윕 산출물. IS/OOS 분리·walk-forward 기록 없음.- 저장 기간은 2024-09 이후뿐이고 파라미터도 현행 config(4%/7.0/46)와 불일치(0.05/−6.0/43) → §8.2 표 주석의 "백테스트 검증 2023-03~2026-03 완료"를 뒷받침하는 산출물이 디렉터리에 없다. 현재 운영 파라미터는 검증 이력이 없다.
재실행 최소 요건 (§8.1-5 를 만족시키려면):
- 국내 2023-03~2026-09 전 구간, BULL/BEAR/SIDEWAYS 각 6개월 이상 포함
- Walk-forward 5구간 이상(Train:Test = 4:1, 앵커드 1회 + 롤링 1회). 판정선: OOS Sharpe / IS Sharpe ≥ 0.5, 구간별 Sharpe 변동계수 ≤ 0.6
- 파라미터당 최소 100거래 → 유효 자유 파라미터 8
10개면 총 8001,000 거래. 미달이면 유니버스 확대보다 파라미터 감축이 정답 - 모드별 거래 수 분리 보고. Aggressive 표본이 5.6%→2.0% 로 줄었으므로 100 미달 가능성이 높다 → 미달이면 "Aggressive 는 통계적으로 검증되지 않음"으로 명시하고 실전 활성화 보류
- 거래비용 재확인(수수료 0.015%×2 + 매도세 0.18% + 슬리피지 0.1~0.3%). RSI 변경으로 진입 타점이 달라져 회전율이 바뀌므로 비용 민감도 재측정
- 몬테카를로(거래 순서 셔플 10,000회) — MDD 95th pctile, §5.3 트리거 빈도 변화
- 미국은 수정 전/후 A-B 로 1봉 정렬 효과를 분리 측정한 뒤 walk-forward
8.4.1 운영 실측 (2026-09-28)
운영 DB 를 직접 조회한 결과. 배포 전/후를 구분해 기록한다.
- 거래 이력:
daytrading257건(BUY 127/SELL 130, 2026-04-06~09-23) vsswing53건(BUY 15/SELL 38). DT 가 거래 건수로는 주력이었다. DT 포지션이 항상 0인 것은 15:20 EOD 전량 청산이 설계대로 동작한 결과이며 결함이 아니다. - 2026-09-24~25 거래·분석 0건은 추석 연휴다. (정정 2026-10-02:
market_schedule은 원래holidays.KR()로 법정 공휴일을 판정하고 있었고, 이번에is_krx_holiday로 KRX 전용 휴장(5/1, 연말 휴장,KRX_EXTRA_HOLIDAYS)을 추가했다. ⚠️ holidays 라이브러리가 2026-07-17(제헌절)을 휴장으로 판정한다 — KRX 공지와 대조 필요.) strategy_configs가 옛 값으로 운영되고 있었다 (alembic 0012 배포 전): BULL 진입 RSI 30(config 46), 손절 -6%(-7%), DT 3종목·종목당 10%. RSI<46 후보는 95종목인데 RSI<30 은 32종목 — 후보가 3분의 1 이었다. 2026-09-28 배포(alembic 0011→0012)로 해소됨.- 룰 점수는 0~15 스케일이다(
market_analyzer.py:608이combined_score/100*15로 축소 저장). 실측 분포는 최소 -1 / 최대 6 / 평균 1.4 (199종목). 따라서score >= 60형태의 비교는 구조적으로 충족 불가다. - 배치는 cron 이 아니라 앱 내부 스케줄러가 돌린다(
main.py:269→schedulers.py:74, 10분 간격). NAS 에 crontab 은 등록되어 있지 않다. - 9/23 에
active_trades유령 행 129개(day_trade120 +swing9)를 수동 정리한 백업이 배포 디렉터리에 남아 있다(active_trades_backup_20260923_1036.sql). 전략명 없이 close 해 엉뚱한 행이 닫히던 문제의 증거이며, 이번 세션에서 수정했다.
8.5 미해결 항목 (알려진 문제, 임의로 "고쳤다"고 판단하지 말 것)
2026-09-27 작업으로 이전 목록 11건 + 추가 발견 결함 다수가 해소되었다. 아래는 그 과정에서 새로 드러난 것과 의도적으로 남긴 것이다. 전체 유닛 스위트는
559 passed / 0 failed(11.8초, 소켓 차단 하) 상태다.
백테스트 (최우선)
- 기존 백테스트 산출물 전부 폐기 상태 — 재실행 필요. §8.4 의 최소 요건 7개를 갖춘 새 산출물이 나오기 전까지 어떤 파라미터도 "검증됐다"고 말할 수 없다.
✅ 2026-09-28 정정: 재실행이 가능하다. 이전 판이 "NAS 다운으로 백테스트 불가"라고 적은 것은 오류였다 — NAS(
nas.shium.net, DNS 정상)는 가동 중이고autostock-postgres도 떠 있다. DB 포트가127.0.0.1:15433로 바인딩돼 외부에서 직접 붙을 수 없을 뿐이며, NAS 안에서(docker exec autostock-postgres psql) 또는 SSH 터널로 접근한다. RANGE_TRADE_ENABLED/MEAN_REVERSION_BEAR_V2_ENABLED는 백테스트 통과 전 True 금지. 각 파일 docstring 에 통과 조건이 기재되어 있다. (DB 가 살아 있으므로 실제로 검증할 수 있다.) BEAR v2 는 "반전캔들 없이 %B+거래량으로 통과"가 발화의 53% 이므로 그 버킷만 분리 집계해 PF < 1.0 이면_W_REVERSAL을 필수화하거나_MIN_SCORE를 0.75 로 올려야 한다.- 박스권 전략이 스케줄러에 배선되어 있지 않다. 배선 위치는
swing/scheduler.py의 MDD/AUTO_TRADE_ENABLED게이트 직후(기존 step 7 은 추천 데이터 존재를 전제하므로 SIDEWAYS 에서 도달하지 못한다).swing/exit_service.py에RANGE_TRADE전용 청산 분기도 없어 현재는 평균회귀 청산으로 처리된다 — 박스 상단 익절·박스 이탈 즉시 손절이 적용되지 않는다.
미국 전략 (US_TRADING_ENABLED=False 라 당장 위험은 없으나 켜기 전 필수)
4. us_backtest_engine.py 는 import 자체가 불가하다. check_us_sell_signal 을 import 하는데 us_stock_strategy.py 에 그 함수가 없다(check_us_buy_signal 만 존재). 즉 미국 백테스트는 실행된 적이 없다.
5. us_backtest_engine.US_COMMISSION_RATE = 0.001(0.1%) vs config.US_COMMISSION_RATE = 0.0(0%) — 백테스트와 실전의 수수료가 다르다. §8.4 의 "백테스트가 실전과 다르다" 계열이 미국 쪽에도 있다.
6. US_STOP_LOSS_PCT 는 음수 규약(-7.0)이다. 국내 STOP_LOSS_PERCENT(7.0 양수)와 다르며, us_backtest_engine.py:581 이 pnl_pct <= US_STOP_LOSS_PCT 로 음수 그대로 비교한다. 부호를 바꾸면 백테스트 손절이 "수익률 ≤ +7%" 로 뒤집힌다.
7. us_market_scheduler.US_DEFAULT_FX_RATE(1350.0)는 주문 수량 결정 입력이다(표시값이 아니다). buy_budget_krw / exchange_rate → quantity. 잔고 조회 실패 또는 "USD 자산 0"(신규 계좌) 경로에서 발동하며, 실환율 1450 에 1350 을 쓰면 약 7% 과다 주문이 된다.
8. us_market_scheduler.py 에 남은 하드코딩: 시장 시간(dtime(9,30)/(16,0)/…), 현금 하한 500,000, 최소 주문 $100, 지정가 마진 1.005/0.995, 총자산 폴백 10,000,000. us_stock_strategy.check_us_exit_signal 의 인라인 리터럴(rsi >= 65, bb_upper * 0.99, sell_pct=0.50, remaining_pct > 0.25). round(buy_price * 0.93, 2) 는 1+(-7.0/100)=0.9299999999999999 와 부동소수 비트가 달라 의도적으로 리터럴을 유지했다.
지표 정확성 (전부 SL/신호를 움직이므로 백테스트 동반 필수)
9. atr_sma 는 TR 의 단순평균이며 표준 Wilder ATR 이 아니다. 바꾸면 스윙 SL(진입가−2.5×ATR)·트레일링(고점−1.5×ATR)·DT SL 이 전부 이동한다. 비교 지표는 CAGR 이 아니라 손절 체결 건수·평균 손절폭·whipsaw 비율.
10. determine_adaptive_mode() 의 ATR 백분위가 closes[:i](당일 제외) 기준으로 1봉 지연. 백분위라 영향이 작을 것으로 보이나 확인되지 않았다.
11. run_backtest.calculate_adx_simple 은 본체가 adx_wilder 위임으로 통일됐으나 폴백 계약이 달라 함수 2개로 남아 있다(전자는 float+20.0 폴백, 후자는 Optional[float]).
12. us_backtest_engine._compute_sma 미통합(중복 아님, 유일 구현). _compute_bollinger 는 봉별 슬라이싱으로 O(n²) — 값은 정확하고 성능만 손실.
설정·구조
13. config.py 의 SHORT_TERM_* 키들은 삭제된 전략의 잔재로 아무 효과가 없다(백테스트 폴백에서만 참조).
14. DT 호출부(api/endpoints/day_trading.py, daytrading/entry_service.py)는 예산→주수 변환에 float // 를 쓰고, 스윙/집행은 sizing_core.amount_to_quantity(Decimal)를 쓴다. 이관 시 극단적 경우 1주 차이가 날 수 있어 동일성 재검증이 필요하다. sizing_core.apply_confidence_scale 도 아직 그쪽에 도입되지 않았다.
15. scan_range_trading_signals 가 결과에 action=="BUY" 만 확인하고 success 를 확인하지 않아 주문 실패건이 섞일 수 있다.
16. portfolio.py 의 ActiveTrade ORM UPDATE 경로들은 같은 트랜잭션에서 SELECT 한 행의 수정이라 남겨 두었다. 직접 INSERT 는 코드베이스 전체 0건이다.
17. backend/test_krx.py(tests/ 밖, 백엔드 루트 스크립트)는 인자 없이 pytest 를 실행하면 수집돼 컬렉션 에러를 낸다. pytest tests/ --ignore=tests/integration 로 돌리면 무관. pytest.ini 에 testpaths = tests 로 정리 가능.
18. naver_finance 등 KIS 외 외부 HTTP 경로는 conftest 가 전역 차단하지 않는다. 현재 소켓 차단 하 전체 통과로 실호출은 없음을 확인했으나, 추가 방어가 필요하면 별도 작업.
멀티테넌트
19. MT 2단계 잔여 — 스케줄러 사용자별 순회, legacy 라우터 격리, user_id 를 실제로 payload 에 싣는 호출부 전환, alembic 0014(NOT NULL + uq_portfolio_user_stock). db_worker 의 MultipleResultsFound 전제조건 1건은 해소됐다. MULTI_TENANT_ENABLED=True 는 계속 금지.
체결 확인 (2026-10-01 — get_order_execution 이 존재하지 않는 경로 /trading/inquire-ccnl 을 호출해 스윙 체결 확인이 한 번도 성공하지 못했던 결함 수정)
27. 체결 조회는 /trading/inquire-daily-ccld(TR TTTC0081R/VTTC0081R 우선 → rt_cd≠0 이면 구 TR *8001R 폴백, 인스턴스에 기억) + EXCG_ID_DVSN_CD=KRX. 배포 후 로그 [daily-ccld] TR legacy 로 전환 / 대체 TR 재시도 빈도를 확인할 것 — "결과 없음"을 rt_cd≠0 으로 주면 매 조회가 2배가 된다.
28. 부분체결 잔량 자동 재주문은 하지 않는다. _reorder_unfilled 는 MDD/밴/누적 4%/현금/주문 락/TradeHistory 를 전부 우회하고, 잔량 소멸은 사실상 HTS 취소·KIS 거부·만료일 때만 일어난다. 잔량 소멸 시 warning("수동 확인 필요")만 남긴다. 되살리지 말 것.
29. 후속: (a) 체결 0주로 취소·거부된 주문이 COMPLETED·전량으로 남는다 → CANCELLED 강등 필요, (b) 스윙 부분체결 매도의 net_profit_loss/commission 이 주문 전량 기준이라 성과 과대, (c) 폴링 상수(max_polls=5, poll_interval=3, ORDER_EXEC_MAX_PAGES=5) config 화, (d) 모의 서버 초당 2건 제한 대비 주문당 최대 5~10회 조회.
30. TradeHistory.order_id 갱신(db_worker._handle_update_trade_history_by_order_id)은 당일(KST) + 최신 1행만 대상이다. KIS 주문번호가 일별 순번이라 과거 행을 덮어쓰던 위험을 막는다. 0건이면 재시도 없이 warning.
ML 앙상블 (2026-09-28 — 코드는 고쳤으나 모델 재학습 전까지 신뢰 금지)
20. 운영 LSTM 체크포인트가 무의미하다. seq_mean=[0,0,0,0,0], seq_std=[1e-8]×5 — X_seq 가 전부 0 인 상태로 학습된 흔적이다(구 prepare_training_data 가 시퀀스에 NaN 을 넣어 X_seq.mean 이 NaN 이 된 결과). std≈0 이면 정규화가 x/1e-8 로 폭주해 가중치 35% 를 가진 LSTM 출력이 사실상 난수다. 시계열 방향 수정 + 시퀀스 NaN 수정이 들어갔으므로 재학습이 필요하다. 재학습 후 새 metrics.json 의 lstm_seq_std_min 과 lstm_per_class 로 판정할 것 — 정확도만 보면 "전부 HOLD" 모델을 통과시킨다.
21. ML_ENSEMBLE_BUY_THRESHOLD(60) 재보정 보류 중. 관측 ensemble_score 1850 이라 ML BUY 가 구조적으로 0건이었다. 그러나 그 분포는 역방향 시계열에서 나온 값이므로 지금 임계값을 조정하면 잘못된 분포에 맞추는 것이 된다. 순서는 ① 재학습 → ② 분포 재측정 → ③ 임계값 판단이고, 값 변경은 §8.1-5(백테스트)를 거친다.
22. ⚠️ 120봉 구간의 학습-추론 정합성은 재학습 후 A-B 로 확인할 것.
26. 종가 0/결측이 중간에 섞인 차트는 피처가 유한해 여전히 점수가 산출된다(NaN 문제가 아니라 데이터 품질 문제). 연속 0봉 비율 등 품질 검증은 별도 작업.trade_executor 의 ML HOLD 게이트가 다시 작동한다. 지난 닷새간 ml_signal=NULL 이라 무력화돼 있었다(§6.4). 이번 수정으로 HOLD & 신뢰도≥60% 조건이 되살아나므로 배포 후 매수 건수가 줄 수 있다. 20번(LSTM 무의미)이 해소되기 전에는 이 게이트가 난수로 매수를 막을 수 있다 — 매수 0건이 재발하면 ml_signal 분포를 먼저 확인할 것.
23. load_models() 는 all-or-nothing. lgbm 로드 실패 한 번으로 scaler/rf/lstm 까지 버리고 is_trained=False 가 된다(로컬에서 libomp 부재로 실제 발생). logger.error 는 남는다. 부분 로드 허용은 정책 결정이라 손대지 않았다.
24. KIS 폴백 경로의 실제 봉수가 미확인이다. get_daily_chart(limit=240) 은 페이지네이션(tr_cont F/M)에 의존하므로 연속조회가 실패하면 80봉 미달로 ML 이 계속 스킵된다. 운영 로그 ✅ Fetched N chart data points 의 N 을 확인할 것. 네이버 경로는 154봉으로 충족(실측).
25. 하드코딩 3건이 남아 있다 — market_analyzer._NAVER_ML_CHART_DAYS=200, _NAVER_LEGACY_DAYS=80, _ML_SKIP_LOG_EVERY=50. config 화가 필요하면 ML_CHART_DAYS/ML_SKIP_LOG_EVERY 로 편입. RECOMMENDED_BARS_FOR_PREDICT=120 은 현재 강제하지 않는다(최소 80만 강제) — RSI(Wilder)·EMA 가 재귀라 80
2026-10-02 프로그램 검증 루프 (5회차) 후 잔여
27. 체결 조회는 /trading/inquire-daily-ccld(TTTC0081R 우선→구 TR 폴백). 부분체결 잔량 자동 재주문은 하지 않는다(가드 전부 우회 + 취소/거부 시 발동) — 되살리지 말 것. 미체결 국내 SELL 은 폴링 종료 시 취소한다. 0체결 취소된 SELL 은 주문 시점에 기록한 ActiveTrade close·loss_tracker 를 롤백하지 않는다(Portfolio 는 KIS sync 가 보정).
28. 국내 전량 청산·손실 매도는 시장가, 부분 익절 지정가는 직전 현재가로 재산정. 주문 결과 분류: safe_place_order/place_order 의 msg_cd(ORDER_LOCK_BUSY, KIS_PRESEND_*=미전송, KIS_TRANSPORT_UNKNOWN=전송 후 불명). DT 는 불명이면 당일 주문내역 확인 → 없으면 KIS 매도가능 수량 상한으로만 재주문.
29. TradeHistory.order_id 갱신은 당일+최신 1행. DLQ: 커밋 불명 INSERT 는 중복 확인 후에만 재큐잉, 불가 시 dlq/manual/(기동 시 error + 텔레그램, /health 에 건수). 종료 예산: uvicorn 5 + 백그라운드 8 + drain 20 ≤ stop_grace_period 40s(compose 재생성 필요).
30. 인증: 계좌·손익·리스크 GET 은 require_api_key(무인증은 /auth/login|register, /market/indices, /, /health 뿐 — 회귀 가드 테스트). 세션 토큰은 is_active 확인(30초 캐시), 서명키는 SECRET_KEY 파생. 위험 쓰기(ADMIN_ONLY_ROUTES)는 관리자 전용. 미결정: 일반 사용자에게 DT /cycle·auto-trade·수동매매·rules·ML 학습을 관리자 전용으로 할지(M-C). SECURITY_FAIL_CLOSED(기본 False). nginx realip 은 NAS 프록시가 XFF 를 덧붙인다는 전제(미확인).
31. DT 진입 경로에서 KOSPI/KOSDAQ 구분 불가 → 거래세는 DAY_TRADE_DEFAULT_TAX_MARKET. 실전 모드 동일 appkey 이중 인스턴스(real_market_api) 토큰 상호 무효화 구조 문제 잔존(무효 응답 시 자동 재발급으로 완화).
33. 주문 결과 불명의 경로 간 오귀속(6회차 H-2, 미해결): 스윙 executor·swing exit·DT·MDD 가 각자 "결과 불명" 상태를 갖고 잔고 증감으로 체결을 판정한다. 같은 종목에 두 경로가 동시에 불명이면 다른 경로의 체결을 자기 체결로 오인할 수 있다(DT 포지션 고아화, MDD 이중 차감). 해결 방향: 종목별 공용 불명 레지스트리(수량·경로·시각) + 판정을 "자기 주문 수량과 일치하는 감소 + 주문내역 ODNO 일치"로 강화, 타 경로 불명이 걸려 있으면 판정 보류. 스윙 시장가 매도는 매 주문 직전 KIS 잔고 상한(min(ord_psbl, hldg) − DT)으로 DT 물량 매도를 구조적으로 막는다.
34. 결과 불명 BUY 는 PENDING 으로 남아 체결 확인 태스크가 없다 → 실제 미접수여도 당일 재매수 차단·일일 슬롯 1개 소진(거래 확대 관점 손실). N초 후 _find_recent_order+잔고로 FAILED 정정 필요.
35. 지정가 익절 미체결 취소: 체결조회 5회 실패 시 취소 미시도(warning), 취소 수량을 폴링 시점 잔량으로 지정(부분체결 경합 시 거부) → 전량 취소(quantity=None)로 바꾸고 텔레그램 경보 필요. 그동안 묶인 수량이 스윙·MDD 시장가 손절을 거부시킬 수 있다. MDD 'wait'(주문가능<보유) 분기에 상한 없음.
37. get_balance 는 첫 페이지만 조회한다(연속조회 없음 — 모의 20행/실전 50행). ✅ 2026-10-12 부분 해소: get_balance_all_v2(tr_cont 연속조회, KIS_BALANCE_MAX_PAGES=5, 불완전하면 _partial=True) 를 신설해 스윙 매도 직전 상한(_fetch_kis_holding_v3)·MDD 축소 가 쓴다 — _partial 이면 기존처럼 보류, 완전 응답+미발견은 미보유로 확정. sync_portfolio 와 DT _fetch_kis_holdings_v3_impl, get_todays_orders(_v2) 는 여전히 첫 페이지만 본다. (이하 원문) 스윙 매도 직전 잔고 상한은 "미발견 + 페이지 가득"을 조회 실패로 보고 매도를 보류(error)한다(SWING_BALANCE_FULL_PAGE_ROWS). 보유 종목이 많아지면 손절이 보류되므로 연속조회(get_balance_all_v2) 구현이 필요하다. sync_portfolio 도 첫 페이지만 보고 "응답에 없는" 행을 삭제한다(기존 결함).
38. ✅ 2026-10-12 해소(9회차): H-B — 불명 대기 중에는 부분익절(sell_pct<1) 신호를 건너뛰고(손절·전량은 KIS 상한 하에 계속), 불명 등록 시 _recent_exits 쿨다운 기록, 정상 접수 시 pre_hldg 차감. H-C — 대기 항목은 등록일 15:30 KST 경과 또는 같은 종목 재매수 관측 시 _reconcile 1회 후 폐기(warning+텔레그램). 원문: 스윙 결과 불명 매도 잔여(7회차): (H-B) 불명 주문이 미결(주문가능<보유)인 동안에도 같은 분할 단계가 재매도되고 정상 매도의 잔고 감소를 불명 체결로 이중 기록할 수 있다 — 대기 중 종목은 매도 skip, accepted 시 pre_hldg 갱신 필요. (H-C) 보유 목록에서 빠진 종목의 불명 대기는 대조되지 않고 남아, 재매수한 새 포지션에 가짜 체결을 기록할 수 있다 — 주기 시작 시 잔고 스냅샷 1회로 전체 대기를 대조 + TTL + 매수 확인 시 폐기. 둘 다 결과 불명 주문이 있어야만 발생.
36. KIS 주문 엔드포인트가 EGW00201·토큰 만료를 HTTP 500 으로 주는지 실측 필요 — 5xx+JSON 본문은 dict 로 반환해 msg_cd 로 분류(2026-10-02), 본문 없는 5xx 는 결과 불명.
2026-10-12 하드닝 9회차 후 잔여 (발견 5·수정 5·검증 2 에이전트, Critical 0 / High 4 → 코드로 닫은 것 2 + 사용자 결정 1 + 구조 보류 1)
39. ⚠️ [사용자 결정 필요] 적응형 Crisis 모드가 기존 보유분 손절선을 소급 조정한다. mean_reversion_strategy.resolve_swing_stop_params_v2 는 청산 판정마다 현재 모드의 stop_loss_pct/atr_stop_multiplier 를 읽는다(진입 시점 값 고정 없음). 2026-10-12(월) 09:0010:01 스윙 보유 10종목 전량 손절 순손익 -236,765원(trade_history id 450459, exit_reason 전부 -3.0%/-5.0%기준·ATR 1.5배 = Crisis 값). 퍼센트 기준 단독 발동 5건(450·455·456·457·458, -95,242원)은 Normal(-7/-10%) 이면 미발동이고, 나머지 5건도 ATR 2.5배로는 미발동(타 세션 반사실 계산, 메모리 adaptive-crisis-retroactive-stops). 선택지: (a) 손절 파라미터를 진입 시점 모드로 ActiveTrade 에 고정, (b) Crisis 는 신규 진입 차단(max_daily_trades=0)만 하고 손절은 Normal 유지, (c) 현행 유지. 백테스트 없이 바꾸지 말 것(§8.1-5) — 단 §8.3 _MODE_PARAMS 숫자는 그대로 두고 소비 방식만 바꾸는 것이 전제.
40. [사용자 결정] DT check_day_trade_exit 가 시간손절을 트레일링보다 먼저 판정한다(day_trading_strategy.py:816-823 vs 841-857). 고점 후 -0.3% 아래로 떨어진 청산이 "시간 손절"로 라벨돼 loss_tracker 제외(§5.1 예외 확대)·TradeJournal·ML 라벨 오염. 순서 변경은 청산 의미 변경이라 백테스트 대상.
41. 스윙 진입 게이트가 Redis 추천 캐시 히트 여부에 따라 다르다(recommendations.py:920-925 vs 683-689·736): 캐시 히트면 DB 규칙 id 1(RSI<30+거래량 1.5배)이 일반 매처를 거쳐 rule_id 강제매수 후보가 되고 평균회귀 게이트(own_trend·ADX·%B)를 거치지 않는다. 캐시 미스면 DB 빌더가 MEAN_REVERSION 외 규칙을 불일치 처리해 대체 목록에서만 게이트를 적용. 두 빌더의 후보 판정을 한 함수로 통일해야 한다(메모리 swing-entry-path-mismatch).
42. get_todays_orders_v2(실패 시 None) 는 DT 결과 불명 재시도 경로만 쓴다. trade_executor.py:352 등 나머지 결과 불명 판정은 여전히 get_todays_orders(실패 시 [] → "미접수" 편향) 를 쓴다. DT 부분체결 미종결 상태에서 불명 매도가 주문 없이 accepted 로 오판되는 경로(인메모리 10 vs 실보유 4)도 남아 있다. 당일 주문 조회는 첫 페이지(모의 15건)만 본다.
43. DT 결과 불명 재매도의 잔여 위험(_resolve_unknown_exit_v3_impl 판정 순서: 주문조회 None→보류 / 등록시각−skew 윈도우 이후·수량 q 일치·거부/취소0체결 아닌 SELL 행→accepted / 행 없음+비온전→보류 / 원장 실패·≤0→보류 / []+원장>0→min(dt_sellable, ledger, q) 재매도): ① 당일 주문내역이 첫 페이지(모의 15건)를 넘겨 비면 수동 보유분이 q 까지 팔릴 수 있다. ② DT 주문 직전 60초 안의 같은 수량 스윙 매도가 accepted 로 오인되면 DT 포지션만 닫히고 주식은 미관리로 남는다(과소매도, 고아 감지가 후속). ③ 종일 보류는 EOD 청산까지 막아 오버나이트가 되고 익일 stale 청산은 수동분을 구분하지 못한다. ④ 스윙 불명 대기의 만료 대조는 사실상 익일 첫 사이클에 돌아, 재매수와 겹치면 체결된 불명 매도를 '미체결'로 조용히 정리할 수 있다(warning). 결과 불명 DT SELL 은 TradeHistory 행을 남기지 않아 원장이 BUY 수량 그대로다(PENDING SELL 행 기록이 구조적 해법).
44. config.py 에 미선언 키 약 40개(MDD_*, SWING_* 알림·타임아웃, DAY_TRADE_* 일부)가 getattr(settings, KEY, default) 로만 읽힌다 — .env 로 바꿀 수 없다. 선언하면 기존 테스트 ~40건이 패치하는 모듈 기본값(_DEFAULT_*)보다 settings 가 우선해 목킹이 무력화되므로 테스트를 settings 패치로 이관한 뒤 선언할 것. MDD_UPDATE_STALE_SEC 는 실효 기본값 900(mdd_manager.py:28) 과 다른 파일의 300 리터럴이 공존한다. 2026-10-12 에 12개 키만 선언(소비처 기본값과 동일, 동작 무변경).
45. KIS 서킷브레이커 면제가 엔드포인트 단위(잔고·현재가·당일체결)라 스캐너의 현재가 호출도 OPEN 을 우회한다. OPEN/HALF_OPEN 중 면제 호출은 성공·실패를 집계하지 않아 브레이커는 비면제 프로브로만 닫힌다. 호출부 플래그(청산 경로만 면제) 방식이 후보.
46. 테스트 커버리지: 기존 14개 파일이 get_balance_all_v2 를 get_balance 목으로 별칭(_pages 없음)해 완전/불완전 응답 분기는 test_swing_exit_r9_20261012.py 만 검증한다. test_mdd_state_backup 은 단독 실행 시 Redis 실접속(전체 스위트에서는 앞선 파일의 목으로 통과 — §4.1 순서 의존). _swing_redis_guard autouse 가 test_swing*·round5 에 들어갔다.
47. 백로그(8회차 건너뜀 157건 재판정: 83그룹 중 해소 18 / 열림 25 / 백테스트·결정 필요 34 / 판정 불가 6): Redis volatile-lru+appendonly 없음(daytrading:daily:* 축출 가능), 스윙 SELL executed_price=None 소비처(AI 장후 리뷰·일지 가격 0), fill_sweep 이 전일 이전 미확인 체결 행을 보정하지 않음(created_at >= 당일), StockAnalysis 에 price_date/adj_factor 부재, ActiveTrade.stop_loss 퍼센트·가격 혼재, trade_executor.py:3203 DT 수량 조회 예외를 로그 없이 0 처리, adaptive 스케줄러가 _spawn_scheduler_v2 감독 밖.
48. 운영: health-watch 사이드카 마운트 scripts/health_watch.py 가 구버전 배포에서 디렉터리로 생성돼 있으면 Jenkins 가 제거를 시도하고 실패 시 경고만 남긴다(NAS 수동 삭제 필요). prod_smoke.sh 의 사이드카 확인은 PROD_SSH_HOST 미설정 시 SKIP. 스트림 무수신 경보는 희망 구독(대시보드) 기준 v4, 스케줄러 첫 하트비트 없음은 가동 HB_FIRST_BEAT_GRACE_SEC(600초) 후 stale.
- 2026-10-02 거래 확대 변경(§8.2 표 ⚠️ 항목, SIDEWAYS H2, 폴백 손절 축소, 트레일링 범위, 듀얼 독립)은 모의계좌 사용자 결정이며 백테스트로 검증되지 않았다.
KIS_MODE=real전환 전 §8.4 요건으로 재검토할 것.
9. 하네스 (.claude/) — 규칙을 훅·스크립트가 강제한다
설정은 .claude/settings.json(팀 공유, 커밋 대상). 개인 설정 .claude/settings.local.json 은 git 추적 제외 대상이다. 훅은 Python3 표준 라이브러리만 쓰고, 입력 파싱 실패 시 조용히 통과한다.
| 훅 | 시점 | 강제 내용 |
|---|---|---|
bash_guard.py |
PreToolUse(Bash) | deny: 로컬 백엔드/docker compose up 기동(NAS 와 KIS 앱키 공유 → 토큰 무효화), 운영 DB URL 대상 로컬 alembic, _test 아닌 DB 로 pytest, --allow-network, main 강제 푸시, 레포루트·backend·.git rm -rf, backend 스테이징 시 유닛 스위트 실패한 git commit(120초). ask: main 갱신 푸시·deploy.sh 실행(KST 시각, 장중이면 경고). |
rules_check.py |
PostToolUse(Edit/Write) | backend/**/*.py 의 py_compile, 그리고 추가된 라인만 대상으로 json.dumps(§4), except 직후 logger.debug(§4·§8.1-10), except Exception: pass, 실시간 계층의 AsyncSessionLocal(/.commit((§3.1; SELECT 전용 예외는 # DB-READ-EXCEPTION 마커 또는 §3.2 ③ 함수). 위반은 exit 2 로 Claude 에게 되돌린다. 라우터 require_api_key 누락은 경고만. |
session_context.py |
SessionStart | 브랜치·미커밋·origin/main 대비·KST 장 상태·alembic head·금지/배포/테스트 3줄 요약 주입. |
stop_check.py |
Stop | frontend/src 변경 시 npx tsc --noEmit(90초). 실패 시 exit 2. |
auto-orchestrate.py |
UserPromptSubmit | 작업성 프롬프트에 오케스트레이션 주입. 서브에이전트 보고·시스템 알림·질문형 프롬프트는 제외, #직접 으로 우회. |
- 배포:
/deploy스킬 →.claude/scripts/deploy.sh(worktree 테스트 → push dev → 임시 worktree 에서--no-ff병합·push main → 재기동 감시 → 스모크).--watch-only,--dry-run,--no-test(경고). 스모크 단독:.claude/scripts/prod_smoke.sh. - 우회: 훅이 막은 것을 에이전트가 환경변수·명령 변형으로 피하지 말 것. 정당한 예외는 사용자가 직접 훅을 끄거나 명령을 실행한다(
ask는 사용자 승인 프롬프트). 규칙 자체가 틀렸으면 훅과 이 절을 함께 고친다. - 훅 테스트:
.venv/bin/python -m pytest .claude/hooks/tests -q(backend 테스트와 분리, DB 불필요). - 한계: 커밋 게이트는 현재 워킹트리를 테스트하므로 스테이징 내용과 워킹트리가 다르면(
git add -p등) 실제 커밋 내용과 어긋날 수 있다. 공휴일은 하네스 장 상태 판정(session_context·bash_guard)에 반영되지 않는다(앱의market_schedule.is_krx_holiday와 별개).
마지막 업데이트: 2026-09-27 — 죽은 코드 2,695줄 제거 / 지표·사이징 단일화(수치 무변경 증명) / 파라미터 3중 소스 정합 + alembic 0012 / 섹터 매핑 44→747종목 / 백테스트를 실전 로직에 정합(ADX 14배 발산·RSI SMA↔Wilder·1봉 지연) / 미국 상수 13개 config 편입 / 테스트 실 KIS 호출 차단(conftest 신설) / 유닛 스위트 559 passed·0 failed
2026-09-28 — 운영 파라미터 alembic 0012 배포 완료(RSI 30→46, 손절 -6%→-7%) / ML 앙상블 전면 미작동 수정(시계열 역방향·시퀀스 워밍업 NaN·except Exception: pass) + 회귀 테스트 20건 / 유닛 스위트 579 passed·0 failed. ML 은 재학습 전까지 신뢰 금지(§8.5-20).
2026-10-02 — 하네스 구성 + 프로그램 검증 루프 5회차(규칙 위반 117→0, Critical/High 1회차 22건 → 5회차 해소) / 거래 확대 사용자 결정 반영(미검증, §8.5-32) / 유닛 스위트 1341 passed·0 failed.
2026-10-12 — 하드닝 9회차(발견 5·수정 5·검증 2 에이전트): 8회차 HIGH 수정 16건 중 12 확인·3 부분·1 미수정 → 전부 보강 / 늦은 0체결·부분체결 매수 원복, 잔고 연속조회(get_balance_all_v2), 브레이커 청산 조회 면제, 결과 불명 매도 재매도·대기 TTL, 청산 타임아웃 시 MDD·진입 보류, DT 수동 보유분 재매도 차단, 스트림·하트비트 경보 보강 / Crisis 손절 소급(§8.5-39)은 사용자 결정 대기 / 유닛 스위트 2781 passed·0 failed(소켓 차단).