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>
This commit is contained in:
2026-10-02 12:01:31 +09:00
parent 0f89a9d678
commit 9fab4018d4
99 changed files with 15712 additions and 1718 deletions

View File

@@ -66,6 +66,8 @@ adaptive_strategy(_MODE_PARAMS) > strategy_cfg(DB: strategy_configs) > setti
- 허용 예외 ①: `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` 가 모두 이것을 쓴다 — **한 곳만 바꾸면 더 나쁜 불일치가 된다.**
@@ -90,7 +92,7 @@ adaptive_strategy(_MODE_PARAMS) > strategy_cfg(DB: strategy_configs) > setti
- 옵트아웃은 세션 단위 `--allow-network` 하나뿐이다. per-test 예외 마커를 만들지 말 것.
- **목킹 지점은 공통 종단에 걸어라.** 위임 래퍼(`_execute_exit`/`_execute_exit_stale`)가 아니라 `_execute_exit_impl` 처럼 양쪽이 수렴하는 곳에 걸어야 리팩터링에 빗나가지 않는다.
- **차단·목킹은 예외 발생형으로.** 그럴듯한 값을 반환하는 스텁은 목킹 누락을 숨겨 거짓 통과를 만든다.
- **테스트가 수십 초 걸리면 실호출 신호다.** `korea_investment_api._handle_rate_limit()` 의 "토큰 발급 1분 제한" `asyncio.sleep(≈70초)` 에 걸린 것이다. 소켓을 막아도 예외가 폴백에 삼켜진 뒤 잠들기 때문에 느린 것은 그대로다.
- **테스트가 수십 초 걸리면 실호출 신호다.** (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건이 이 이유로 깨져 있었다.
@@ -145,7 +147,7 @@ adaptive_strategy(_MODE_PARAMS) > strategy_cfg(DB: strategy_configs) > setti
- **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일.
- 판정은 `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**). **스케줄러 배선도 아직 하지 않았다** — 호출부가 없어 현재 실행 경로에 존재하지 않는다.
@@ -153,6 +155,7 @@ adaptive_strategy(_MODE_PARAMS) > strategy_cfg(DB: strategy_configs) > setti
- 측정된 문제 크기: 같은 합성 샘플에서 **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 → 진입 거부
@@ -165,13 +168,15 @@ adaptive_strategy(_MODE_PARAMS) > strategy_cfg(DB: strategy_configs) > setti
### 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`)
- 전량: 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·옵션만기 등 이벤트 당일 진입 차단
- **마켓 플레이북** (`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`)
@@ -260,6 +265,13 @@ adaptive_strategy(_MODE_PARAMS) > strategy_cfg(DB: strategy_configs) > setti
| `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분 주기 자동 전환)
@@ -310,7 +322,7 @@ DX(0~100)에 합산형 평활을 적용한 버그. 측정: `up 1361 vs 97 / flat
운영 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` 에 휴장일 판정이 없어 로그만으로는 장애와 구분되지 않는다.
- **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 은 등록되어 있지 않다.
@@ -366,6 +378,21 @@ DX(0~100)에 합산형 평활을 적용한 버그. 측정: `up 1361 vs 97 / flat
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 표준 라이브러리만 쓰고, 입력 파싱 실패 시 조용히 통과한다.
@@ -381,8 +408,10 @@ DX(0~100)에 합산형 평활을 적용한 버그. 측정: `up 1361 vs 97 / flat
- **배포**: `/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` 등) 실제 커밋 내용과 어긋날 수 있다. 공휴일은 장 상태 판정에 반영되지 않는다(`market_schedule` 과 동일).
- 한계: 커밋 게이트는 **현재 워킹트리**를 테스트하므로 스테이징 내용과 워킹트리가 다르면(`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.*