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

42 KiB
Raw Blame History

자동 주식 매매 시스템 — 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 4055 + 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(0100)에 합산형 평활을 적용한 버그. 측정: up 1361 vs 97 / flat 165 vs 12 / vol 206 vs 15 — 전 패턴에서 배율 14.0. 결과적으로 §8.3 Aggressive 게이트 ADX > 25 가 모든 레짐에서 100% 통과했다. 수정 후 4387%. 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 1013건 — 파라미터당 최소 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거래 → 유효 자유 파라미터 810개면 총 8001,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