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

@@ -238,6 +238,23 @@ def test_rules_warning_level_ok(proj):
assert rules(proj, "backend/app/services/new.py", src).returncode == 0
def test_rules_allow_markers(proj):
rel = "backend/app/services/new.py"
base = "import json, logging\nlogger = logging.getLogger()\n"
# json.dumps: 같은 줄 / 직전 줄 마커 허용, 다른 종류 마커·무마커는 위반
assert rules(proj, rel, base + "x = json.dumps({}) # harness: allow-json-dumps DLQ 파일 기록\n").returncode == 0
assert rules(proj, rel, base + "# harness: allow-json-dumps DLQ 파일 기록\nx = json.dumps({})\n").returncode == 0
assert rules(proj, rel, base + "# harness: allow-except-pass 사유\nx = json.dumps({})\n").returncode == 2
assert rules(proj, rel, base + "x = json.dumps({})\n").returncode == 2
# except-pass: except 직전 줄 / pass 줄 마커 허용
body = "def f():\n try:\n 1\n"
assert rules(proj, rel, base + body + " # harness: allow-except-pass 종료 정리\n except Exception:\n pass\n").returncode == 0
assert rules(proj, rel, base + body + " except Exception:\n pass # harness: allow-except-pass 종료 정리\n").returncode == 0
assert rules(proj, rel, base + body + " except Exception:\n pass\n").returncode == 2
# except-pass 마커는 logger.debug 위반을 면제하지 않는다
assert rules(proj, rel, base + body + " except Exception: # harness: allow-except-pass x\n logger.debug('x')\n").returncode == 2
def test_rules_realtime_db(proj):
rel = "backend/app/services/daytrading/exit_service.py"
bad = "async def f():\n async with AsyncSessionLocal() as s:\n await s.commit()\n"
@@ -316,3 +333,76 @@ def test_orchestrate(prompt, expected):
@pytest.mark.parametrize("sh", ["deploy.sh", "prod_smoke.sh"])
def test_scripts_syntax(sh):
assert subprocess.run(["bash", "-n", str(ROOT / ".claude/scripts" / sh)]).returncode == 0
# ── 운영 스모크: 인증 필수 엔드포인트는 401 이 정상, 키 없이 200 은 노출 회귀 (2026-10-02 H-1) ──
def _smoke_against(status_by_path):
"""로컬 스텁 HTTP 서버를 PROD_BASE_URL 로 두고 prod_smoke.sh 실행 (외부 네트워크 0)."""
import http.server
import threading
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
code = status_by_path.get(self.path, 404)
body = b'{"ok": 1}' if code == 200 else b'{"detail": "x"}'
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a):
pass
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H)
t = threading.Thread(target=srv.serve_forever, daemon=True)
t.start()
try:
env = {**os.environ, "PROD_BASE_URL": f"http://127.0.0.1:{srv.server_address[1]}"}
p = subprocess.run(["bash", str(ROOT / ".claude/scripts/prod_smoke.sh")],
capture_output=True, text=True, env=env, timeout=60)
finally:
srv.shutdown()
return p
_AUTH_PATHS = ("/api/v1/journals/stats", "/api/v1/analysis/performance", "/api/v1/market-intelligence/dashboard")
def test_prod_smoke_passes_when_auth_enforced():
p = _smoke_against({"/api/v1/market/indices": 200, **{a: 401 for a in _AUTH_PATHS}})
assert p.returncode == 0, p.stdout + p.stderr
def test_prod_smoke_fails_on_unauthenticated_leak():
p = _smoke_against({"/api/v1/market/indices": 200, **{a: 200 for a in _AUTH_PATHS}})
assert p.returncode == 1 and "LEAK" in p.stdout
def test_prod_smoke_fails_when_down():
p = _smoke_against({"/api/v1/market/indices": 502, **{a: 502 for a in _AUTH_PATHS}})
assert p.returncode == 1
def test_deploy_watch_treats_401_as_up():
src = (ROOT / ".claude/scripts/deploy.sh").read_text()
assert 'is_up() { [ "$1" = "200" ] || [ "$1" = "401" ]; }' in src
assert "analysis/performance" not in src.split("WATCH_URL=")[1].splitlines()[0]
def test_rules_py311_fstring_pep701_detector():
"""운영 이미지 python:3.11 에서 SyntaxError 인 PEP 701 f-string 을 잡는다(2026-10-02 mdd_manager 사고)."""
import sys as _sys
_sys.path.insert(0, str(HOOKS)) if "HOOKS" in globals() else None
import importlib
import pathlib as _pl
_sys.path.insert(0, str(_pl.Path(__file__).resolve().parents[1]))
rc = importlib.import_module("rules_check")
if _sys.version_info < (3, 12):
assert rc.check_py311_fstring("x.py", 'x=f"{"a"}"\n') == []
return
assert len(rc.check_py311_fstring("x.py", 'x=f"{"[MDD]"} a"\n')) == 1
assert len(rc.check_py311_fstring("x.py", 'u=1\nx=f"{f"[v2] {u}"} a"\n')) == 1
assert rc.check_py311_fstring("x.py", "x=f\"{'[MDD]'} a\"\n") == []
assert rc.check_py311_fstring("x.py", 'x=f"""{"a"} b"""\n') == []
assert rc.check_py311_fstring("x.py", "d={'k':1}\nx=f\"{d['k']}\"\n") == []