운영 DB 가 옛 값으로 돌고 있던 문제를 해소한다:
MEAN_REVERSION_RSI_THRESHOLD 30.0 → 46.0 (RSI<46 후보 95종목 vs RSI<30 32종목)
STOP_LOSS_PERCENT -6.0 → -7.0
DAY_TRADE_MAX_POSITIONS 3 → 5
DAY_TRADE_PER_STOCK_RATIO 0.10 → 0.06
ANALYSIS_STOCK_COUNT 150 → 300
CONSECUTIVE_LOSS_BAN_DAYS 30 → 42
alembic 0012 가 운영자 커스텀 값은 보존하고 옛 기본값과 같은 행만 갱신한다.
KIS_MODE=virtual (모의투자). alembic 현재 리비전 0011 → 0012 적용.
## 배경
"전략이 혼란스럽고 현재와 맞지 않는다"를 조사한 결과, 전략 설계가 아니라
전략을 검증했다는 도구와 파라미터 소스가 어긋나 있었다.
## 파라미터 소스 단일화
- 우선순위가 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>
docker-compose.yml 에 ./token_cache 볼륨을 추가했는데 배포 디렉터리에 없어
'Bind mount failed: /volume1/docker/autostock/token_cache does not exist' 로
DB Migration 단계부터 실패, Deploy 가 스킵돼 2026-09-23 11:30 이후 변경분이
서버에 반영되지 않았다(컨테이너는 옛 이미지로 계속 구동).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
스윙이 이미 47%라 신규 매수가 swing_capital_cap 에 막혀(오늘 60건) 표본이 쌓이지 않았다.
- SWING_MAX_CAPITAL_RATIO 0.50 → 0.70
- DAY_TRADE_MAX_RATIO_DT 0.50 → 0.30, DAY_TRADE_MAX_POSITIONS 8 → 5 (5×6%=30%)
- 두 상한의 합이 100%를 넘지 않는지 테스트로 고정
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md §5.2 기준으로 계산해 매수 추천 금액 아래에 함께 표시.
· 손절 = ATR(2.5×)과 -7% 중 타이트한 쪽(가격 높은 쪽), 적응형 모드면 그 값 사용
· 목표 = 진입가 + 2R (R = 진입가 − 손절가) — +2R 50% 분할 매도 지점
ATR 은 stock_analysis.atr_14 우선, 없으면 DB 일봉으로 계산(KIS API 미사용). 테스트 4건 추가.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BUY 신호일 때 실제 자동매매와 동일한 기준으로 매수 금액·수량을 계산해 패널 상단에 표시.
· 종목당 상한 = 총자산 × MAX_POSITION_SIZE(적응형 모드면 그 값)
· 신뢰도 보정 = 예산 × (confidence/0.7), 0.5~1.2배
· 현금 여력(최소 현금보유 제외)·스윙 합산 상한(SWING_MAX_CAPITAL_RATIO) 잔여로 클램프
BUY 가 아니거나 예산이 1주 미만이면 금액 대신 사유를 표시. 테스트 6건.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
재배포로 DB 복원된 데이트레이딩 포지션의 entry_time 이 UTC(구 기본값)라 KST now 와의
차이가 +540분이 되어, 진입 25분 만에 '시간 손절: 565분 경과'로 청산됐다
(2026-09-23 산일전기 -0.5%, 미스토홀딩스 -0.4% — 익절 조건까지 가보지도 못함).
_elapsed_minutes(): 경과가 음수이거나 정규장 길이(390분)를 넘으면 기준 불일치로 보고 무시.
시간손절·트레일링 최소보유 판정 모두 이 헬퍼를 사용.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
시간대 혼재: 모델 기본값은 datetime.utcnow(UTC), 수동 매매 등 일부 경로는 _get_kst_now(KST)로
저장해 같은 테이블에 9시간 차가 섞였다(화면 매매시각 02:14 등). 조회 구간(utcnow - days)도
KST 저장분과 비교돼 어긋났고, 추가매수 쿨다운은 (UTC now - KST created_at)이 음수라 상시
발동했다. app.core.timeutil.now_kst() 도입 후 모델 기본값 27곳과 app/ 전역 utcnow() 호출을
모두 KST 기준으로 통일(잔존 0건, 테스트로 고정).
KIS 토큰 캐시(~/.cache/auto-stock)가 컨테이너에만 있어 배포마다 사라지고 토큰을 새로
발급받다 KIS가 EGW00103(유효하지 않은 AppKey)로 거부 → 502·잔고 0 유발. 볼륨 마운트로
재배포 후에도 재사용.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 예수금: dnca_tot_amt(D+2 기준)를 먼저 읽어 당일 매수가 많으면 -913,965원처럼 음수가
표시됐다. 주문가능현금(_ord_psbl_cash) → prvs_rcdl_excc_amt → nxdy_excc_amt 순으로
첫 양수를 사용한다(데이트레이딩 엔진과 동일 기준).
- 프론트: /auth/me 가 한 번 실패하면(배포 중 502) useTenantMode.mode 가 undefined 로 남아
포트폴리오·매매 쿼리가 전부 enabled:false 로 멈췄다(오늘 26분간 API 호출 0건 확인).
auth-me 를 실패 시 10초 간격 재시도로 바꾸고, queryClient 기본값에 retry 3회/백오프,
refetchOnReconnect, networkMode:'always' 를 적용해 백엔드 복구 시 자동으로 되살아나게 함.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- portfolio_cache_bridge: 잔량 산출을 Portfolio(DB) 기준으로. 캐시는 sync 실패 시 최대
5분 낡아 실제와 다른 잔량이 기록될 수 있었음(감사 #14). 조회 실패 시 캐시 폴백.
- ORB 캐시 Redis 영속화(save/load_orb_cache) — 재배포 시 인메모리가 날아가 당일 ORB
전략이 통째로 불가능했음. 수집 후 저장, 스케줄러 기동·수집 시 복원.
- us_market_scheduler: 부분청산이 Portfolio.quantity 를 갱신하지 않던 문제 → reflect_sell 연동.
- 듀얼 전략(MOMENTUM_BREAKOUT/PULLBACK_BUY)이 ActiveTrade.strategy_name 에 저장되지 않아
CLAUDE.md §8.4 모멘텀 전용 청산(+10%/+20% 분할)이 한 번도 실행된 적 없었음 → 태깅 전달.
매도 시에는 실제 행의 strategy_name 으로 close (swing 으로 닫으면 유령 행 잔존).
- 수동매도: current_price NULL 시 TypeError 로 주문 후 상태 갱신이 통째로 누락되던 엣지 수정.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- korea_investment_api: 주문 계열 엔드포인트는 내부 재시도 금지(_NON_IDEMPOTENT_ENDPOINTS).
상위 _place_order_with_retry 와 중첩돼 타임아웃 시 동일 주문 최대 12회 전송 가능했음.
get_todays_orders() 추가.
- trade_executor: 예외 후 재시도 전에 당일 주문내역으로 접수 여부 확인(_find_recent_order)
→ 접수됐으면 재주문하지 않고 기존 주문 반환.
- daytrading/exit_service: 주문 체결 이후 상태 반영을 _finalize_exit 로 분리하고
asyncio.shield 로 보호 — 스케줄러 단계 타임아웃이 이 구간을 취소하면 브로커는 팔았는데
인메모리는 그대로라 다음 사이클이 재매도.
- swing/position_manager: active 행이 2개면 MultipleResultsFound 로 해당 종목 청산이
통째로 스킵됐음(손절 미작동) → 최신 행 사용.
- swing/exit_service: 큐 지연·DLQ 시 같은 분할매도가 반복되지 않도록 180초 인메모리 쿨다운.
- portfolio_v2: 부분매도 ActiveTrade 잔량 갱신 + 전략명 매핑(daytrading→day_trade,
us_swing→us_mean_reversion) — 매칭 실패로 active 행이 영구 잔존했음.
- portfolio.sell_stock: safe_place_order 경유(세마포어·분산락) + DT 인메모리 잔량 반영.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- mdd_manager: 강제 축소가 TradeHistory/remaining_pct 만 갱신하고 실제 수량은 어디도
줄이지 않아 다음 청산이 축소 전 수량으로 매도했음 → _reflect_reduction()으로
Portfolio·DT 인메모리 반영, ActiveTrade.quantity 도 잔량으로 갱신 (두 경로 모두)
- swing/exit_service: close/upsert 에 strategy_name 미전달 시 db_worker 가 최신 active
행(=DT)을 닫거나 트레일링 고점을 덮어씀 → 항상 전략명 명시
- trade_executor._execute_sell: Portfolio 행(스윙+DT 합계)을 그대로 매도 대상으로 삼아
DT 보유분까지 팔았음 → 엔진 보유 수량을 제외한 자기 전략 보유분만 대상, upsert 에 전략명
- portfolio.sync: DT 태깅이 해외 루프에만 적용돼 국내 DT 매수분이 swing 으로 저장되면
스윙 엔진이 손절 → 국내에도 적용. _dt_qty 정의를 output1 블록 밖으로 올려 NameError 방지
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
감사 CRITICAL 4건:
- /daytrading/{watchlist,execute,close-all,cycle} 에 require_api_key 누락 → 인증 없이
전량 청산 가능했음 (CLAUDE.md §4 위반)
- 레거시 day_trading._positions 가 엔진 day_trading_state 와 별도로 기동 시 복원돼
같은 포지션이 두 엔진에 존재 → /close-all 시 이중 매도. 복원 제거 + close-all/cycle 을
엔진에 위임, /execute 실매수는 410(엔진 미관리 포지션 방지)
- reflect_buy 가 기존 행을 daytrading 으로 재태깅 → 같은 종목 스윙 보유분이
get_swing_holdings 에서 빠져 손절·익절 정지. 신규 행일 때만 태깅
- close_stale_positions 가 인메모리만 순회 → 자정 reset 후 대상이 사라져 무력.
ActiveTrade(day_trade/active/전일) 조회 기반으로 변경하고 인메모리가 비어도 실행
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
다른 인스턴스/증권사 앱에서 매수돼 동기화로 들어온 포지션은 ActiveTrade 행이 없어
손절·분할익절·트레일링 관리를 전혀 받지 못했다(2026-09-22 카카오뱅크가 -13%까지
방치되다 청산). 동기화 시 보유 중인데 active 행이 없으면 평균단가를 진입가로 등록하고
손절가를 STOP_LOSS_PERCENT 기준으로 설정한다. 데이트레이딩 태깅 종목은 제외
(DT 엔진이 당일 청산 대상으로 복원해버리므로).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
수동/분할 매도 누락으로 ActiveTrade 잔량이 실제 보유량과 어긋나면 다음 청산이
보유량보다 많이 매도한다. 동기화 시 종목당 active 행이 1개뿐일 때 실제 보유량으로
교정하고, 보유가 없으면 closed 처리(스윙+DT 혼재 종목은 자동 교정 제외).
운영 교정용으로 PATCH /portfolio/active-trades/{code}/status 에 quantity 필드 추가.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DT에서 찾은 '분할매도 후 ActiveTrade.quantity 미갱신' 패턴이 3곳 더 있었음:
- us_market_scheduler: total_qty = trade.quantity 를 매도량 산정에 직접 사용 →
분할매도 후 다음 사이클이 원래 수량 기준으로 팔아 과다매도(US_TRADING_ENABLED=true 상태)
- swing/exit_service, trade_executor._execute_sell: remaining_pct만 갱신
또한 수동 매도(/portfolio/sell)가 ActiveTrade를 전혀 정리하지 않아 청산된 종목이 active로
잔존(삼아알미늄·LG 등) → 재매수 시 옛 remaining_pct가 살아나 1차 익절이 건너뛰어짐.
전량 매도 시 close, 분할 시 잔량 갱신으로 수정.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1) DT 분할매도가 ActiveTrade.quantity를 갱신하지 않아 재기동 복원 시 원래 수량이 되살아남
→ 잔량 4주인데 7주 매도되어 같은 종목 스윙 보유분 3주까지 청산됨(2026-09-23 LG씨엔에스).
2) reflect_sell이 '전략 포지션 잔량'을 Portfolio 행 잔량으로 사용 → DT 전량청산이 스윙
보유분까지 0으로 만듦. 행 잔량은 항상 (행 수량 - 매도 수량)으로 계산.
3) LSTM 체크포인트에 numpy 배열이 들어가 weights_only=True 로드가 매번 실패 → 앙상블에서
LSTM이 조용히 빠진 채 예측 중이었음. 저장을 list로 바꾸고, 기존 파일은 MODEL_DIR 내부일
때만 완화 재시도.
4) 수동 매도(행 삭제)와 sync 동시 실행 시 StaleDataError로 sync 전체 실패 → 전량 매도는
행 삭제 대신 수량 0, sync는 충돌 시 실패 기록 없이 다음 주기 재시도.
추가로 비강제 sync는 락 점유 중이면 30초 대기 없이 캐시 반환.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
로그 분석(2026-09-22): 15:03:46 진입 스캔 이후 데이트레이딩 스케줄러가 16:52 재배포까지
1시간 49분간 아무 로그도 남기지 않음 = KIS API 무응답(500/Server disconnected 연속)으로
await가 반환되지 않아 루프 정지 → 15:20 EOD 강제청산 미실행 → 포지션 익일 보유.
- _guard(): 각 단계를 asyncio.wait_for로 감싸 초과 시 취소 후 다음 사이클 진행
- 단계별 상한: MDD 30s / watchlist 120s / ORB·EOD청산 300s / 청산·진입 180s
- MDD 판단 불가 시 보수적으로 신규 진입 차단
- 테스트 3건
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
모든 스케줄러가 장 마감 중 고정 1800초를 자서 09:00 개장을 최대 30분 건너뛰었고,
배치 분석은 개장 후에야 시작돼 추천 종목이 09:00에 비어 있었음.
- market_schedule.is_pre_market_window() 추가 (08:30~09:00, 거래일만)
- batch_analysis_scheduler: 개장 전 준비 구간에도 분석 실행 → stock_analysis·Redis 추천
캐시를 미리 채워 09:00에 추천 즉시 표시
- batch/auto_trade/swing 스케줄러: 마감 중 sleep = min(1800, 개장까지 남은 시간)
- 테스트 1건 추가 (총 5건)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
데이트레이딩 스케줄러는 장 마감 시 무조건 1800초를 자서, 08:31~09:00 사이에 잠들면
09:01~09:30에 깨어나 ORB 수집 구간(09:00~09:30)을 통째로 놓쳤음(2026-09-23 감시종목 0 확인).
- market_schedule.seconds_until_market_open() 추가 (주말·공휴일 건너뜀)
- 스케줄러: 마감 중 sleep = min(1800, 개장까지 남은 시간), 개장 3분 전 watchlist 선구성
- 회귀 테스트 4건
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- portfolio_cache_bridge 신설: 매매 엔진 체결 시 Portfolio(db_worker 큐) + 인메모리 캐시 즉시 갱신.
DT 매수는 strategy_type='daytrading'으로 태깅되어 보유 종목 패널의 '데이트레이딩' 그룹에 표시되고
스윙 청산 로직(strategy_type='swing' 대상)에서 제외됨. (기존엔 KIS 동기화로 'swing'으로 유입돼
DT 포지션이 안 보이고 스윙 엔진이 같이 관리하는 문제)
- DT 매도·스윙 자동 매도도 캐시에서 즉시 제거/차감 + 최근매도 기록(동기화 되살림 방지).
- 동기화 시 DT 엔진 보유 종목은 'daytrading'으로 태깅.
- 프론트: 수동 매도 성공 시 낙관적 갱신으로 서버 재조회 전 즉시 목록에서 제거.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 프론트: 토스트 알림 시스템(lib/toast + ToastHost) 추가, 매도/매수 성공·실패를 화면에 표시
(기존엔 console.log만, 실패는 블로킹 alert). 성공 토스트에 손익 표시.
- 매도 직후 예수금/평가액 요약 즉시 갱신(apply_sell), 최근 5분 내 수동 매도 종목은
KIS 잔고 반영 지연으로 동기화가 되살리지 않도록 스킵(recent_sells).
- 예수금이 D+2 결제 기준(dnca_tot_amt)이라 당일 매도대금이 화면·데이트레이딩 가용현금에
반영되지 않던 문제: 매수가능조회(inquire-psbl-order) ord_psbl_cash 를 우선 사용.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- ORB: 09:00에 당일 분석이 없으면 최근 분석일 결과로 watchlist 선채움 → ORB 수집 가능.
자동 목록은 30분마다 갱신(당일 분석 완료 시 교체), 수동 설정 목록은 덮어쓰지 않음.
- 자본 배분: DAY_TRADE_MAX_RATIO_DT 0.25→0.50, 종목당 4%→6%, 동시 3→8종목.
SWING_MAX_CAPITAL_RATIO=0.50 신설 — 스윙 합산 투자액 초과 시 스윙 신규 매수 차단(trade_executor).
- 시간대: 점심(11:30~13:00) VWAP/RSI_BB 제한 제거, 하드코딩 14:30 마감 → 설정값 15:00.
- 회귀 테스트 3건, CLAUDE.md §6/§7.3/§8.2 갱신.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>