Files
auto-stock/CLAUDE.md
shuim 1b9257c4d2 fix: 매매 파라미터 3중 소스 정합 + 리스크 가드 복원 + 죽은 코드 2,695줄 제거
## 배경
"전략이 혼란스럽고 현재와 맞지 않는다"를 조사한 결과, 전략 설계가 아니라
전략을 검증했다는 도구와 파라미터 소스가 어긋나 있었다.

## 파라미터 소스 단일화
- 우선순위가 adaptive > strategy_cfg(DB) > config.py 인데 문서는 맨 마지막인
  config.py 를 기술하고 있었다. 11곳이 불일치(BULL RSI 46 vs DB 30, SL -7% vs -6%).
- DEFAULT_PARAMS 를 config.py 에 정합 + alembic 0012 로 기존 DB 행 갱신
  (운영자가 UI 로 바꾼 값은 보존, 옛 기본값과 같은 행만 갱신, 테이블 가드·멱등)

## 백테스트를 실전 로직에 정합
- calculate_adx_simple 이 정확히 period 배(14.0×) 발산 → Aggressive 게이트
  `ADX>25` 가 전 레짐 100% 통과. 전환 빈도 5.6%→2.0%(-65%), 성과 상방 편향
- 백테스트 RSI 는 SMA, 실전은 Wilder → 진입 판정 10~11% 불일치
- us_backtest_engine RSI 1봉 지연 제거 (룩어헤드 없음을 절단 테스트로 증명)
- backend/backtest_results/ 25건은 폐기 대상 (§8.4)

## 리스크 가드 복원
- 4% 절대 상한이 try 블록 안에 있어 예외 시 적응형 6~7% 가 통과 → try 밖으로
- 청산을 매수보다 먼저 평가 (보유 종목이 추천에 들면 손절 대신 물타기가 나갔다)
- 리스크 가드 4개를 매수 전용화 → MDD -10%·보유한도 도달 시에도 손절 작동
- MDD 갱신 5회 연속 실패 시 신규 진입 차단 fail-safe (청산은 계속 허용)
- 스윙 청산이 DT 보유분까지 매도하던 문제, sell_pct 기준 혼재 정정

## 죽은 코드 제거 (2,695줄)
- short_term_strategy, kosdaq_scanner, app/strategy/ 패키지
- app/trading/ + korea_investment_api_adapter (real_market_api 정본은
  korea_investment_api.py:1482, adapter 는 import 0건)

## 지표·사이징 단일화 (수치 무변경 증명)
- indicators_core.py: 3,274건 비교 최대 오차 0.000e+00
- sizing_core.py: 42,105건 비교 불일치 0
- 미국 전략 상수 13개 config 편입 (hex 까지 동일, 3,168 케이스 전수 비교)

## 테스트 안전 (실 KIS 호출 차단)
- 유닛 테스트가 실제 KIS 에 매도 주문을 2회 전송하고 있었다. 리팩터링이
  close_stale_positions 의 대상을 _execute_exit → _execute_exit_stale 로
  바꿨는데 목이 전자에 걸려 빗나갔다. test_api_endpoints 도 캡처된 싱글톤
  참조 누수로 실제 잔고 조회를 타 141초를 소비했다.
- backend/tests/conftest.py 신설: 주문·토큰·시세·잔고를 호출 즉시 예외로 차단.
  예외는 BaseException 파생(프로덕션 except Exception 폴백에 삼켜지지 않게),
  클래스 메서드에 적용(캡처된 참조까지 덮음), DATABASE_URL `_test` 가드 강제
- 전체 유닛 스위트: 365초/6 failed → 11.8초/0 failed (소켓 차단 하 559 passed)

## 신규 (둘 다 플래그 OFF, 백테스트 미검증)
- range_trading_strategy.py — SIDEWAYS 담당 전략이 없던 빈 칸. 미배선
- check_bear_buy_signal_v2 — BEAR 4중 AND(발화율 2.3%) → 점수제(12.4%)

## 문서
- CLAUDE.md 를 실제 코드 기준으로 재작성. §0 파라미터 우선순위, §4.1 테스트 규약,
  §6.4 문서화되지 않았던 진입 차단 게이트 9종, §8.4 백테스트 폐기 근거,
  §8.5 미해결 19항목
- 루트 md 6개 → 2개(docs/ 이동), 중복 배치 문서 병합, 스테일 worktree 제거

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 08:58:18 +09:00

333 lines
42 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`. **한도 숫자는 이 모듈에 없다**(호출부의 settings 소유)
- `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()` 없음. 각 지점에 주석으로 명시되어 있다. **여기에 쓰기를 추가하지 말 것.**
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` 처럼 양쪽이 수렴하는 곳에 걸어야 리팩터링에 빗나가지 않는다.
- **차단·목킹은 예외 발생형으로.** 그럴듯한 값을 반환하는 스텁은 목킹 누락을 숨겨 거짓 통과를 만든다.
- **테스트가 수십 초 걸리면 실호출 신호다.** `korea_investment_api._handle_rate_limit()` 의 "토큰 발급 1분 제한" `asyncio.sleep(≈70초)` 에 걸린 것이다. 소켓을 막아도 예외가 폴백에 삼켜진 뒤 잠들기 때문에 느린 것은 그대로다.
- **실호출 없음의 유일한 증거는 소켓 차단 하 통과다.** `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` 밖에서 재클램프된다.
- 최대 **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()` 복원.
### 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일.
- **국면 커버리지 불균형**: 신호원 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 평균회귀 진입 조건 (실제 코드)
- **공통 선행 필터** (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)
- 시간손절: 20일 보유 + 3% 미달 시 전량 (`check_time_stop_exit`)
### 6.4 문서화되지 않았던 진입 차단 게이트 (전부 실재하며 거래 빈도에 큰 영향)
- **ML 앙상블 HOLD 게이트** (`trade_executor`): ML이 HOLD이고 신뢰도 ≥60% 면 rule 매수를 차단
- **지수 서킷브레이커** (`swing/entry_service`): KODEX200 종가 < MA20 이면 신규 진입 **전면 중단**
- **마켓 플레이북** (`market_playbook`): FOMC·옵션만기 등 이벤트 당일 진입 차단
- **거시/국면 position_multiplier** (`market_analyzer`): 장외 0.5, CRASH 0.0~0.1, BEAR ratio
- **적응형 Crisis 모드**: 진입 전면 차단 (§8.4)
- **추격매수 방지·추세 필터·이격도 과열·일일 매매한도·재매수 쿨다운** (`trade_executor`)
- **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분(6봉)간 **-0.3% 미만**이면 시간손절(수익 중이면 보유 유지) / 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` 에 추가해 당일 재진입을 막는다(주문 실패 시에도).
- **분봉 조회 실패 시 폴백 손절**이 있다(`_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()`
- 감시종목: 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) |
### 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.5 미해결 항목 (알려진 문제, 임의로 "고쳤다"고 판단하지 말 것)
> 2026-09-27 작업으로 이전 목록 11건 + 추가 발견 결함 다수가 해소되었다. 아래는 **그 과정에서 새로 드러난 것과 의도적으로 남긴 것**이다.
> 전체 유닛 스위트는 `559 passed / 0 failed`(11.8초, 소켓 차단 하) 상태다.
**백테스트 (최우선)**
1. **기존 백테스트 산출물 전부 폐기 상태 — 재실행 필요.** §8.4 의 최소 요건 7개를 갖춘 새 산출물이 나오기 전까지 **어떤 파라미터도 "검증됐다"고 말할 수 없다.** NAS(DB) 복구가 선행 조건.
2. `RANGE_TRADE_ENABLED` / `MEAN_REVERSION_BEAR_V2_ENABLED` 는 **백테스트 통과 전 True 금지**. 각 파일 docstring 에 통과 조건이 기재되어 있다. 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 **0013**(NOT NULL + `uq_portfolio_user_stock`). `db_worker` 의 `MultipleResultsFound` 전제조건 1건은 해소됐다. **`MULTI_TENANT_ENABLED=True` 는 계속 금지.**
*마지막 업데이트: 2026-09-27 — 죽은 코드 2,695줄 제거 / 지표·사이징 단일화(수치 무변경 증명) / 파라미터 3중 소스 정합 + alembic 0012 / 섹터 매핑 44→747종목 / 백테스트를 실전 로직에 정합(ADX 14배 발산·RSI SMA↔Wilder·1봉 지연) / 미국 상수 13개 config 편입 / 테스트 실 KIS 호출 차단(conftest 신설) / 유닛 스위트 559 passed·0 failed*