# 자동 주식 매매 시스템 — 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.`)에 적용한다. 인스턴스만 패치하면 `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.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건은 추석 연휴**다. `market_schedule.py` 에 휴장일 판정이 없어 로그만으로는 장애와 구분되지 않는다. - **`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 **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*