# 자동 주식 매매 시스템 — 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) ### 배포 순서 (엄수) 1. `docker compose build` 2. **`alembic upgrade head` 를 앱 기동보다 먼저 실행** (`docker compose run --rm backend alembic upgrade head`) - 스키마 변경 시 필수: `create_all` 은 기존 테이블에 컬럼을 추가하지 않는다. - **전략 파라미터 마이그레이션(0012 계열)도 마찬가지다.** 앱이 먼저 뜨면 `strategy_cfg` 캐시(30초)가 옛 값으로 채워져 최대 30초간 낡은 사이징·손절로 매매가 돈다. 장중 배포라면 특히 위험하다. 3. `docker compose up -d --build` — **`--build` 필수.** `up -d` 만 하면 옛 이미지로 컨테이너가 재생성된다. 4. **섹터 캐시 생성(최초 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_schedule - `models/models.py` — Portfolio, TradeHistory, ActiveTrade, StockAnalysis, StockBan, StrategyConfig 등 - `realtime/` — websocket_client, event_handler - `services/` 전략 (활성): `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, scheduler - `services/` 순수 계산 모듈 (settings·DB·네트워크 참조 없음 — 이 원칙을 깨지 말 것): - `indicators_core.py` — RSI/BB/ATR/ADX 단일 출처. `rsi_wilder`/`rsi_sma`/`atr_sma`/`bollinger_sma`/`adx_wilder`/`true_ranges` - `sizing_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. 아키텍처 원칙 (필수) 1. **실시간 계층 DB I/O 절대 금지** — API 콜백·웹소켓·실시간 스케줄러에서 `AsyncSessionLocal()`/`commit()` 호출 금지. 2. **모든 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` 도 이 마커를 인식한다. 3. 지원 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_trade` payload 가 세 가지 선택 키를 받는다. 우선순위는 `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` 가 모두 이것을 쓴다 — **한 곳만 바꾸면 더 나쁜 불일치가 된다.** 4. **동시성**: trade_executor `_order_semaphores`(종목별 동시 주문 방지) / Redis 분산 락(워커 간 동시 매도 방지) / loss_tracker·mdd_manager `asyncio.Lock`. 동시 접근 경로에서는 `is_banned_safe()`, `record_trade_result_safe()` 사용. 5. **`Portfolio` 행은 `stock_code` UNIQUE** 라 같은 종목의 스윙+데이트레이딩 수량이 **합산**되어 있다. 한 전략이 청산할 때는 반드시 다른 전략 보유분을 차감할 것 (`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_KEY` 64자+ 랜덤. - 프롬프트는 토큰 최소화, 컴팩트 포맷. - **리스크 가드·한도 클램프를 `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/integration` - `backend/tests/conftest.py` 가 `DATABASE_URL` 의 DB 이름이 `_test` 로 끝나지 않으면 **세션을 중단**한다. - 같은 conftest 가 주문·토큰·시세·잔고 계열을 **호출 즉시 예외**로 치환한다. 차단 예외는 `BaseException` 파생(`KISCallBlocked`) — 프로덕션의 `except Exception:` 폴백에 삼켜지면 차단이 무력화되기 때문이다. - 차단은 **클래스 메서드**(`KoreaInvestmentAPI.`)에 적용한다. 인스턴스만 패치하면 `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. 신규 진입 수치는 이전과 동일. - 최대 **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%). - ⚠️ `industry` API 는 **`page`/`pageSize` 를 주지 않으면 20개 업종만 반환하고 나머지가 "기타"로 뭉쳐 보인다.** 수집 코드를 고칠 때 주의. - **배포 후 `collect_sector_map.py` 를 한 번 돌려야 캐시가 생긴다**(§1-4). 없으면 44종목 표로만 폴백. - **미매핑 종목의 섹터캡**: `SECTOR_CAP_UNMAPPED_ENABLED`(기본 **False**). 미매핑을 "UNKNOWN" 단일 버킷으로 묶어 3종목 한도를 적용하는 스위치다. 켜기 전 조건 — 운영 환경 커버리지 ≥95% & 캐시 7일 이내, 미매핑 잔여가 ETF/ETN 만(일반 주식이 남아 있으면 UNKNOWN 3종목 상한에 걸려 거래가 죽는다), 최대 버킷 <20%, 그리고 켠 상태 백테스트에서 체결 건수가 유의미하게 줄지 않고 `MAX_HOLDINGS_COUNT=20` 도달이 가능함을 확인. - 연속 손실: 동일 종목 **3연패 → `CONSECUTIVE_LOSS_BAN_DAYS`(42캘린더일 ≈ 30거래일) 진입 금지** (`loss_tracker`). 값은 config 연동이며 하드코딩이 아니다. `record_trade_result()` 후 반드시 `save_to_db()`(db_worker `upsert_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%) 이내의 소폭 이익 청산도 손실로 집계된다. ### 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()` - 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%~+2% + RSI 40~55 + RSI 반등 + 거래량 ≥1.2×20MA | 평균회귀와 동일 | **BULL 전용** | ### 6.1 시장 국면 (일봉) - **BULL** = 코스피200 > MA200 & MA200 기울기 > 0 - **BEAR** = 코스피200 < MA200 & 기울기 < 0 - **SIDEWAYS** → **모든 진입 무시** (market_analyzer: position_multiplier=0, max_trades=0) - 판정은 `ADX < 18` **AND** 기울기 ≈ 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배)로 올라가고 구 발화를 모두 포함하는 상위집합이다. ### 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% 가격 밴드"는 코드에 존재하지 않는다. - **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 구간에서 거짓). - `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 체크포인트 1. **파라미터 값을 주장하기 전에 §0의 세 소스를 모두 확인** (adaptive / strategy_cfg(DB) / config.py) 2. DB 쓰기 = db_worker 경유 (예외: §3.2의 3종) 3. 모든 매매에 §5 리스크 규칙 적용 4. SIDEWAYS 국면 진입 제안 금지 5. 백테스트·Walk-forward 없이 실전 투입 금지 — **단 §8.5의 백테스트 신뢰성 주의사항을 먼저 읽을 것** 6. **SL 선택 = 타이트한 쪽. 단 비교 기준(가격 vs 음수 퍼센트)에 따라 연산자가 다르고, 장중 최소 손절폭 보장은 의도적 예외** (§5.2, §7) 7. `record_trade_result()` 후 `save_to_db()` 호출 확인 8. 쓰기/삭제 API에 `require_api_key` 필수 9. JSON 출력 시 `_dumps()` / 새 기능은 새 파일 / 기존 시그니처 수정 금지 10. **리스크 가드·한도 클램프를 `try` 안에 두지 말 것.** 예외 로그에 `logger.debug` 금지 11. **청산은 항상 매수보다 우선.** 리스크 가드(MDD/밴/보유수/쿨다운)는 신규 매수만 차단하고 청산 판정은 통과시킬 것 12. 한 전략이 청산할 때 다른 전략 보유분 차감 확인 (§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(0~100)에 합산형 평활을 적용한 버그. 측정: `up 1361 vs 97 / flat 165 vs 12 / vol 206 vs 15` — 전 패턴에서 배율 14.0. 결과적으로 §8.3 Aggressive 게이트 `ADX > 25` 가 **모든 레짐에서 100% 통과**했다. 수정 후 43~87%. **Aggressive 전환 빈도 5.6% → 2.0% (−65%).** Aggressive 는 포지션 7%·SL −9% 이므로, 기존 백테스트는 해당 구간에서 실전보다 큰 사이즈·느슨한 손절로 돌았다 = **성과 상방 편향**. **② 백테스트 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_trades` 10~13건 — 파라미터당 최소 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 를 만족시키려면): 1. 국내 2023-03~2026-09 전 구간, BULL/BEAR/SIDEWAYS 각 6개월 이상 포함 2. Walk-forward 5구간 이상(Train:Test = 4:1, 앵커드 1회 + 롤링 1회). 판정선: OOS Sharpe / IS Sharpe ≥ 0.5, 구간별 Sharpe 변동계수 ≤ 0.6 3. 파라미터당 최소 100거래 → 유효 자유 파라미터 8~10개면 총 800~1,000 거래. 미달이면 유니버스 확대보다 **파라미터 감축이 정답** 4. **모드별 거래 수 분리 보고.** Aggressive 표본이 5.6%→2.0% 로 줄었으므로 100 미달 가능성이 높다 → 미달이면 "Aggressive 는 통계적으로 검증되지 않음"으로 명시하고 실전 활성화 보류 5. 거래비용 재확인(수수료 0.015%×2 + 매도세 0.18% + 슬리피지 0.1~0.3%). RSI 변경으로 진입 타점이 달라져 회전율이 바뀌므로 비용 민감도 재측정 6. 몬테카를로(거래 순서 셔플 10,000회) — MDD 95th pctile, §5.3 트리거 빈도 변화 7. 미국은 수정 전/후 A-B 로 1봉 정렬 효과를 분리 측정한 뒤 walk-forward ### 8.4.1 운영 실측 (2026-09-28) 운영 DB 를 직접 조회한 결과. 배포 전/후를 구분해 기록한다. - **거래 이력**: `daytrading` 257건(BUY 127/SELL 130, 2026-04-06~09-23) vs `swing` 53건(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_trade` 120 + `swing` 9)를 수동 정리한 백업이 배포 디렉터리에 남아 있다(`active_trades_backup_20260923_1036.sql`). 전략명 없이 close 해 엉뚱한 행이 닫히던 문제의 증거이며, 이번 세션에서 수정했다. ### 8.5 미해결 항목 (알려진 문제, 임의로 "고쳤다"고 판단하지 말 것) > 2026-09-27 작업으로 이전 목록 11건 + 추가 발견 결함 다수가 해소되었다. 아래는 **그 과정에서 새로 드러난 것과 의도적으로 남긴 것**이다. > 전체 유닛 스위트는 `559 passed / 0 failed`(11.8초, 소켓 차단 하) 상태다. **백테스트 (최우선)** 1. **기존 백테스트 산출물 전부 폐기 상태 — 재실행 필요.** §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 터널로 접근한다. 2. `RANGE_TRADE_ENABLED` / `MEAN_REVERSION_BEAR_V2_ENABLED` 는 **백테스트 통과 전 True 금지**. 각 파일 docstring 에 통과 조건이 기재되어 있다. (DB 가 살아 있으므로 실제로 검증할 수 있다.) BEAR v2 는 "반전캔들 없이 %B+거래량으로 통과"가 발화의 53% 이므로 그 버킷만 분리 집계해 PF < 1.0 이면 `_W_REVERSAL` 을 필수화하거나 `_MIN_SCORE` 를 0.75 로 올려야 한다. 3. 박스권 전략이 **스케줄러에 배선되어 있지 않다.** 배선 위치는 `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` 18~50 이라 ML BUY 가 구조적으로 0건이었다. 그러나 그 분포는 **역방향 시계열에서 나온 값**이므로 지금 임계값을 조정하면 잘못된 분포에 맞추는 것이 된다. 순서는 ① 재학습 → ② 분포 재측정 → ③ 임계값 판단이고, 값 변경은 §8.1-5(백테스트)를 거친다. 22. ⚠️ **`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~120봉 구간의 학습-추론 정합성은 재학습 후 A-B 로 확인할 것. 26. 종가 0/결측이 중간에 섞인 차트는 피처가 유한해 **여전히 점수가 산출된다**(NaN 문제가 아니라 데이터 품질 문제). 연속 0봉 비율 등 품질 검증은 별도 작업. **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:00~10:01 스윙 보유 10종목 전량 손절 순손익 **-236,765원**(trade_history id 450~459, 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. 32. 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(소켓 차단).*