fix: 매매일지 표시·중복·스케줄러 중단 + 전략 파라미터 자동적용 차단
매매일지는 정상 생성되고 있었다(205건, 커버리지 100%, 16:00 자동 생성도 동작).
"작성이 안 된다"의 실체는 아래 표시 계층 문제였다.
## 표시
- Dashboard journals 쿼리가 staleTime:Infinity + refetchInterval 없음이라
페이지 로드 순간의 목록에서 영구 고정됐다 → 30s/60s 폴링
- WS journal_created 는 205건 중 13건(6%)만 발화하고(16:00 스케줄러와
/analysis/generate-journals 는 브로드캐스트 없음) 그마저 커밋 전에 나간다
→ 이벤트 의존 제거, 수신 시 즉시+3초 재invalidate 로 레이스 보강
- 수동 버튼이 목록을 갱신하지 않고 안내가 "매매 로그"(없는 위치)를 가리켰다
→ invalidateQueries + 실제 위치·비동기 지연 명시
## 시각 +9h
- journals 가 KST naive 값에 UTC 라벨을 붙여 화면이 +9h 밀렸다
(실측 KST 15:48 → 화면 09-30 00:48, 즉 당일 16:00 일지가 "다음날 01:00")
- _utc_iso → _kst_iso, +09:00 명시. 세 응답 지점 통일(기존엔 목록/상세가 달랐다)
- /trades 는 raw naive + 프론트 보정으로 짝이 맞아 있어 건드리지 않았다
(회귀 테스트가 trades.py 에 +09:00 유입을 차단)
- JournalPanel 에 조건부 +09:00 정규화 추가(매매내역 경로와 동일 패턴)
## now_kst 섀도잉 4곳
- from timeutil import now_kst 해놓고 함수 내 now_kst = _dt.now(_KST) 로 대입해
같은 함수의 now_kst() 가 TypeError. post_market 에서 실장애였고 나머지 3곳은
무증상 잠재 → 전부 _now 로 리네임 (AST 전수, backend+mcp-server 잔존 0)
- TypeError 를 은폐하던 logger.debug → warning(exc_info=True), except: pass 1곳도
## trade_id=0 중복
- db_worker 에 _handle_insert_trade_journal 신설 — order_id 로 TradeHistory.id
해소, 해소 실패 시 None(0 아님). 정상경로·종료 drain 공용
- 커버 판정 trade_id > 0 (id 는 PK라 0 은 애초에 아무 행도 커버하지 않음)
- order_id 추출이 raw KIS 응답에 없는 top-level "odno" 였다 → output.ODNO 우선
## 전략 파라미터 자동 적용 차단 (STRATEGY_AUTO_APPLY_ENABLED=False)
섀도잉 수정이 post_market 의 자동 최적화를 부활시킨다 — run_optimization 이
strategy_cfg.upsert() 로 실매매 파라미터(RSI 진입·손절·포지션 크기)를 덮어쓴다.
게이트가 "최근 7일 SELL>=5건"이라 5거래 표본 그리드 서치가 alembic 0012 로
정합시킨 값을 조용히 바꿀 수 있다 — §8.1-5·§8.4·§8.5-1 과 정면 충돌.
- 계산·분석·로그는 유지하고 upsert 만 차단. 가드 2단, 둘 다 try 밖
- 차단 시 키별 현재값→제안값 + IS/OOS Sharpe 를 warning + apply_reason 에 기록
- 켜기 전 §8.4 백테스트 요건 충족 필요(config 주석에 명시)
## 부수 발견
strategy_config_service.upsert 가 __import__("datetime").now_kst() 로 항상
AttributeError → 전략 파라미터 저장이 UI·일괄·optimizer 전 경로에서 500이었다.
역설적으로 이것이 자동적용의 2차 방어선이었다 — 고쳤으므로 플래그가 필수다.
⚠️ 배포 후 UI 파라미터 저장이 처음으로 실제 동작한다(§0 정합 주의).
테스트 677 → 715 passed / 0 failed. 전략 임계값 정적 변경 0건.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,7 +164,14 @@ const JournalPanel = React.memo(({ journals, tradeHistory, onResetPending, reset
|
||||
/* ── 매매일지 탭 ── */
|
||||
journals && journals.length > 0 ? (
|
||||
journals.map((journal: Journal) => {
|
||||
const journalDate = new Date(journal.journal_date || journal.created_at || '')
|
||||
// DB가 KST로 저장하므로 타임존 없는 ISO 문자열에 +09:00 보정
|
||||
// (매매내역 경로 :102-103과 동일 규약 — 오프셋이 이미 있으면 그대로 둠)
|
||||
const rawJournalDt = journal.journal_date || journal.created_at || ''
|
||||
const journalDate = new Date(
|
||||
rawJournalDt.includes('+') || rawJournalDt.includes('Z')
|
||||
? rawJournalDt
|
||||
: rawJournalDt + '+09:00'
|
||||
)
|
||||
const dateStr = journalDate.toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit', timeZone: 'Asia/Seoul' })
|
||||
const timeStr = journalDate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Asia/Seoul' })
|
||||
const isGoodMood = journal.mood === '좋음' || journal.mood === 'good' || journal.mood === 'positive'
|
||||
|
||||
@@ -154,10 +154,19 @@ export default function Analysis() {
|
||||
})
|
||||
|
||||
// 일지 생성 뮤테이션
|
||||
// F-J2 (2026-09-30): onSuccess가 alert만 하고 ['journals'] 쿼리를 무효화하지 않아
|
||||
// 생성 후에도 목록이 갱신되지 않았다. 또한 일지 목록은 이 페이지가 아니라
|
||||
// 대시보드 우측 패널의 "매매일지" 탭(components/JournalPanel.tsx)에서만 보인다 —
|
||||
// 기존 문구의 "매매 로그"는 실재하지 않는 위치를 가리켰다.
|
||||
// 생성은 db_worker 큐를 경유하는 비동기 처리라 요청 직후엔 아직 안 보일 수 있어
|
||||
// "생성 요청됨"과 "생성 완료"를 구분해 안내한다.
|
||||
const generateJournals = useMutation({
|
||||
mutationFn: () => api.post('/analysis/generate-journals?days=7'),
|
||||
onSuccess: () => {
|
||||
alert('일지 생성을 시작했습니다. 잠시 후 매매 로그에서 확인하세요.')
|
||||
alert('일지 생성을 요청했습니다. 비동기로 처리되어 즉시 반영되지 않을 수 있습니다 — 잠시 후 대시보드 우측 "매매일지" 탭에서 확인하세요.')
|
||||
queryClient.invalidateQueries({ queryKey: ['journals'] })
|
||||
// db_worker 큐 처리 지연을 감안해 지연 재조회도 함께 트리거
|
||||
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['journals'] }), 5_000)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -438,13 +438,29 @@ const Dashboard = () => {
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
|
||||
// 매매일지 - 이벤트 기반 (WebSocket journal_created 또는 수동 갱신)
|
||||
// 매매일지 — 폴링 기반 (WebSocket journal_created는 보조 트리거일 뿐, 아래 참고)
|
||||
//
|
||||
// F-J1 (2026-09-30): 이전엔 staleTime: Infinity + refetchOnWindowFocus: false +
|
||||
// refetchInterval 없음이라, WebSocket 'journal_created' 없이는 페이지 로드 순간
|
||||
// 목록에서 영원히 멈췄다. 그런데 그 브로드캐스트 자체가 신뢰할 수 없다 —
|
||||
// 실측 205건 중 13건(6%)만 발화(16:00 스케줄러/‘/analysis/generate-journals’
|
||||
// 경로는 브로드캐스트 자체가 없음, backend/app/api/endpoints/portfolio.py 확인—
|
||||
// 읽기만 함, 백엔드는 journal-fix-be 담당).
|
||||
// 그래서 이벤트에 의존하지 않는 폴링을 기본으로 하고, 이벤트는 보조 트리거로만 쓴다.
|
||||
//
|
||||
// 60초 간격 근거: 일지는 포트폴리오(30초, usePortfolioData.ts)·매매내역(30초,
|
||||
// 위 tradeHistory)만큼 실시간성이 필요 없다(매매 직후 화면에 바로 안 보여도
|
||||
// 치명적이지 않음, 사후 기록 성격) — 그 두 배인 60초로 두어 불필요한 API 부하를
|
||||
// 늘리지 않는다. 탭이 백그라운드일 때 폴링을 줄이려 장이 닫혀 있으면(포트폴리오
|
||||
// 폴링과 동일 기준 재사용하지 않고, 일지는 장외에도 16:00 배치 등으로 계속
|
||||
// 생성되므로) 장 상태와 무관하게 항상 60초로 유지한다.
|
||||
const { data: journals } = useQuery({
|
||||
queryKey: ['journals'],
|
||||
queryFn: async () =>
|
||||
(await api.get('/journals', { params: { limit: 20 } })).data,
|
||||
staleTime: Infinity,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 30000,
|
||||
refetchInterval: 60000,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -492,7 +508,14 @@ const Dashboard = () => {
|
||||
}
|
||||
|
||||
if (msg.type === 'journal_created') {
|
||||
// 브로드캐스트가 db_worker INSERT 커밋 전에 발화하는 레이스가 있다
|
||||
// (backend/app/api/endpoints/portfolio.py 확인 — 큐 적재 직후 즉시
|
||||
// 브로드캐스트, 커밋은 비동기 워커가 나중에 수행). 즉시 1회 + 지연
|
||||
// 재조회 1회로 레이스에 대비 — 최종적으로는 60초 폴링이 항상 따라잡는다.
|
||||
queryClient.invalidateQueries({ queryKey: ['journals'] })
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['journals'] })
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}, [wsMessages, invalidatePortfolio, queryClient])
|
||||
|
||||
Reference in New Issue
Block a user