Files
auto-stock/CLAUDE.md
shuim 9fab4018d4 fix: 프로그램 검증 루프 7회차 — 리스크·주문·청산·인프라 결함 일괄 수정 + 거래 확대 결정 반영
하네스 규칙 검사 전역 위반 117→0, 회차별 검증(code-verifier opus 4도메인)으로 Critical/High 해소.

주문·리스크
- MDD 강제축소 반복 매도(종목별 완료 집합·재시도 상한·날짜 롤오버·잔고 오류 보류·epoch·장외 미주문)
- KIS 거부 FAILED 기록, 결과 불명(전송 후) 재전송 금지·PENDING, 미전송 실패만 재시도(msg_cd 분류)
- 국내 전량·손실 매도 시장가, 부분 익절 직전 현재가, 지정가만 미체결 취소(cancel_order_v2: TR 0013U·조직번호) + 0체결 롤백
- 누적 4%·현금·레버리지(신규분 포함)·사이클 내 매수 누적, 수동매수 공용 가드(buy_guards), 수동매도 DT 차감·락
- 가드 우회 엔드포인트 차단(/recommendations/auto-trade 위임, US /execute 매수 410·매도 가드)
- 매수 슬롯=성공 매수, 평가 상한·회전, 업무거부/계좌 쿨다운, 프로세스 내 in-flight 주문 중복 차단
데이트레이딩
- 진입·청산 shield, 청산 진행 중 표식(소유 토큰), 2단계 청산 체크, 결과 불명 → 보유 감소 판정·비DT 차감 상한
- 전일 포지션 재시도(일시 실패 미산정), 분할매도 영속, ORB 구간 손절, /cycle 락, 사이클 내 현금 차감, 체결 수량 보정
스윙
- 손절 공백(판정 예외 시 하드 바닥), 단일 손절 소스·범위 검증, 매도 직전 KIS 잔고 상한(min(주문가능,보유)−DT)
- 결과 불명 체결 대조·장부 반영, 대체가(나이·진입 이후·ATR 캐시), 분할 단계 기록, 국면 실패 fail-closed
- 듀얼 플레이북·서킷브레이커 게이트, 평균회귀 우선·듀얼 독립, 시간손절 거래일
- 거래 확대(사용자 결정, 백테스트 미검증): RSI 46/35 정본 경로, 모멘텀 v2 ON, SIDEWAYS ADX AND, 트레일링 1차 익절 후, 20일 시간손절 ON
인프라·보안
- 계좌·손익 GET 인증, 세션 is_active·관리자 경로, 서명키 파생, realip, WS 토큰 마스킹
- KIS 토큰 즉시 실패(sleep 제거)·무효 시 캐시 삭제, 주문 5xx+JSON 본문 반환
- db_worker 종료 drain(멱등 재실행·커밋 불명 격리·예산·밴 커밋), stop_grace 40s, Redis 타임아웃·volatile-lru, /health DLQ·캐시
- KRX 휴장일, py3.11 비호환 f-string 수정 + Jenkins 이미지 내 compileall·헬스 실패 시 빌드 실패
하네스: rules_check 에 PEP 701 f-string 검사, 마커(allow-*), deploy 스모크 401 기준
CLAUDE.md: 실제 동작·플래그·백로그(§8.5-27~38) 갱신

backend 1451 passed / hooks 112 passed / tsc ok / py3.9 compile 0 errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:01:31 +09:00

418 lines
65 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 자동 주식 매매 시스템 — 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`)도 마커와 함께 허용된다.
- 허용 예외 ③-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.<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. 신규 진입 수치는 이전과 동일.
- 최대 **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 밖에서 보장된다. 현재가 조회 실패 시 대체가(마지막 실시간가 → Portfolio.current_price)로 손절만 판정.
### 6.4 문서화되지 않았던 진입 차단 게이트 (전부 실재하며 거래 빈도에 큰 영향)
- **ML 앙상블 HOLD 게이트** (`trade_executor`): ML이 HOLD이고 신뢰도 ≥60% 면 rule 매수를 차단
- **지수 서킷브레이커** (`swing/entry_service`): KODEX200 종가 < MA20 이면 신규 진입 **전면 중단**
- **마켓 플레이북** (`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행).** 스윙 매도 직전 잔고 상한은 "미발견 + 페이지 가득"을 조회 실패로 보고 매도를 보류(error)한다(`SWING_BALANCE_FULL_PAGE_ROWS`). 보유 종목이 많아지면 손절이 보류되므로 연속조회(`get_balance_all_v2`) 구현이 필요하다. `sync_portfolio` 도 첫 페이지만 보고 "응답에 없는" 행을 삭제한다(기존 결함).
38. 스윙 결과 불명 매도 잔여(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 는 결과 불명.
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.*