feat: 매매일지 자동 생성 + 전략 자동 최적화 시스템 추가

- trade_analyzer.py: 거래 성과 분석(승률/손익비/전략별 패턴), 템플릿 기반 일지 자동 생성
- strategy_optimizer.py: 그리드 서치 기반 파라미터 최적화, Sharpe 10% 개선 시 자동 적용
- analysis.py: /performance /insights /generate-journals /optimize-strategy /optimization-history 엔드포인트
- journals.py: POST /generate/{trade_id}, GET /stats 엔드포인트 추가
- models.py: StrategyOptimizationLog 테이블 추가
- main.py: post_market_analyzer_scheduler (16:00 KST 자동 실행)
- frontend: Analysis 페이지 추가 (성과 요약/인사이트/일지 생성/최적화 이력)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 09:28:10 +09:00
parent f42e94fe6c
commit 2baf06a5fd
10 changed files with 1789 additions and 5 deletions

View File

@@ -372,6 +372,111 @@ async def day_trade_scheduler():
raise
async def post_market_analyzer_scheduler():
"""장 종료 후 16:00 KST 1회 실행 — 매매일지 자동 생성 + 전략 자동 최적화.
조건:
- 매일 16:00 KST 에 1회 실행 (이미 오늘 실행했으면 스킵)
- 최근 7일 내 SELL FILLED 거래 >= 5건이면 최적화 실행
- 마지막 최적화로부터 3일 이상 경과 시만 최적화 실행
"""
import pytz as _pytz
from datetime import datetime as _dt, date as _date, timedelta as _td
_KST = _pytz.timezone("Asia/Seoul")
_last_run: _date | None = None
await asyncio.sleep(60) # 부팅 안정화
logger.info("📝 Post-market analyzer scheduler started (daily 16:00 KST)")
try:
while True:
now_kst = _dt.now(_KST)
today = now_kst.date()
if _last_run == today:
tomorrow_16 = _KST.localize(_dt(today.year, today.month, today.day, 16, 0, 0)) + _td(days=1)
sleep_sec = (tomorrow_16 - now_kst).total_seconds()
await asyncio.sleep(max(sleep_sec, 60))
continue
if (now_kst.hour, now_kst.minute) < (16, 0):
target = _KST.localize(_dt(today.year, today.month, today.day, 16, 0, 0))
sleep_sec = (target - now_kst).total_seconds()
if sleep_sec > 0:
await asyncio.sleep(min(sleep_sec, 1800))
continue
if today.weekday() >= 5:
await asyncio.sleep(3600)
continue
logger.info("📝 [PostMarket] 매매일지 자동 생성 + 전략 최적화 시작")
_last_run = today
try:
from app.services.trade_analyzer import (
analyze_performance,
get_uncovered_trades,
generate_journal_for_trade,
create_journal_record,
)
from app.services.strategy_optimizer import (
run_optimization,
save_optimization_log,
get_last_optimization_date,
)
from app.models.models import TradeHistory
from sqlalchemy import func, and_
from datetime import datetime as _utcnow_dt
async with AsyncSessionLocal() as db:
uncovered = await get_uncovered_trades(db, days=7)
perf = await analyze_performance(db, days=30)
journal_count = 0
for trade in uncovered:
try:
content = generate_journal_for_trade(trade, perf)
await create_journal_record(db, trade, content)
journal_count += 1
except Exception as e:
logger.warning(f"일지 생성 실패 ({trade.stock_code}): {e}")
if journal_count > 0:
await db.commit()
logger.info(f"📝 [PostMarket] 일지 {journal_count}건 생성 완료")
since_7d = _utcnow_dt.utcnow() - _td(days=7)
trade_count_result = await db.execute(
select(func.count(TradeHistory.id)).where(
and_(
TradeHistory.order_type == "SELL",
TradeHistory.status == "FILLED",
TradeHistory.created_at >= since_7d,
)
)
)
recent_trade_count = trade_count_result.scalar() or 0
last_opt = await get_last_optimization_date(db)
days_since_opt = (today - last_opt).days if last_opt else 999
if recent_trade_count >= 5 and days_since_opt >= 3:
logger.info(f"🔧 [PostMarket] 전략 최적화 실행 (최근거래:{recent_trade_count}건, 마지막:{days_since_opt}일 전)")
result = await run_optimization(db, perf, trigger="auto", period_days=90)
await save_optimization_log(db, result, trigger="auto")
logger.info(f"✅ [PostMarket] 최적화 완료: applied={result.applied}, {result.apply_reason}")
else:
logger.info(f"⏭️ [PostMarket] 최적화 스킵 (거래:{recent_trade_count}건, 마지막최적화:{days_since_opt}일 전)")
except Exception as e:
logger.error(f"❌ [PostMarket] 분석 실패: {e}", exc_info=True)
await asyncio.sleep(3600)
except asyncio.CancelledError:
logger.info("⏹️ Post-market analyzer scheduler stopped")
raise
async def krx_data_collector_scheduler():
"""장 종료 후 KRX 일봉 데이터 자동 수집 스케줄러.
@@ -550,6 +655,7 @@ async def lifespan(app: FastAPI):
app.state.batch_analysis_task = asyncio.create_task(batch_analysis_scheduler())
app.state.day_trade_task = asyncio.create_task(day_trade_scheduler())
app.state.krx_collector_task = asyncio.create_task(krx_data_collector_scheduler())
app.state.post_market_task = asyncio.create_task(post_market_analyzer_scheduler())
# KIS 실시간 스트림 시작 (설정으로 제어 가능)
if settings.KIS_WS_ENABLED:
@@ -568,6 +674,7 @@ async def lifespan(app: FastAPI):
"batch_analysis_task",
"day_trade_task",
"krx_collector_task",
"post_market_task",
"db_worker_task",
"kis_stream_task",
):