fix: DB 타임스탬프 KST 통일 + KIS 토큰 캐시 영속화
시간대 혼재: 모델 기본값은 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>
This commit is contained in:
@@ -18,6 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db, AsyncSessionLocal
|
||||
from app.models.models import StrategyOptimizationLog, TradeJournal
|
||||
from app.core.timeutil import now_kst
|
||||
from app.services.trade_analyzer import (
|
||||
analyze_performance,
|
||||
build_insights,
|
||||
@@ -71,7 +72,7 @@ async def get_insights(
|
||||
"performance_summary": perf.get("summary", {}),
|
||||
"last_optimization_date": last_opt_date.isoformat() if last_opt_date else None,
|
||||
"period_days": days,
|
||||
"generated_at": datetime.utcnow().isoformat(),
|
||||
"generated_at": now_kst().isoformat(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"insights 생성 실패: {e}", exc_info=True)
|
||||
|
||||
@@ -42,6 +42,7 @@ from app.core.security import (
|
||||
verify_session_token,
|
||||
)
|
||||
from app.models.models import User
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -246,7 +247,7 @@ async def login(body: LoginRequest, request: Request, db: AsyncSession = Depends
|
||||
)
|
||||
|
||||
_failures.pop(ip, None)
|
||||
user.last_login_at = datetime.utcnow()
|
||||
user.last_login_at = now_kst()
|
||||
await db.commit()
|
||||
|
||||
token, expires_at = create_session_token(user.username)
|
||||
|
||||
@@ -27,6 +27,7 @@ from app.core.tenant_auth import CurrentUser, get_current_user
|
||||
from app.models.models import UserKisCredential
|
||||
from app.services.korea_investment_api import KoreaInvestmentAPI
|
||||
from app.services.tenant.registry import user_context_registry
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -172,7 +173,7 @@ async def put_credentials(
|
||||
if dup.scalar_one_or_none() is not None:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="이미 다른 사용자가 등록한 앱키입니다.")
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = now_kst()
|
||||
cred = await _get_own(db, user_id)
|
||||
is_new = cred is None
|
||||
if is_new:
|
||||
@@ -278,7 +279,7 @@ async def verify_credentials(
|
||||
"stock_value": _pick_float(output2, ["scts_evlu_amt", "evlu_amt_smtl_amt"]),
|
||||
}
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = now_kst()
|
||||
cred.last_verified_at = now
|
||||
cred.is_enabled = True
|
||||
cred.updated_at = now
|
||||
@@ -306,7 +307,7 @@ async def patch_auto_trade(
|
||||
)
|
||||
|
||||
cred.auto_trade_enabled = body.enabled
|
||||
cred.updated_at = datetime.utcnow()
|
||||
cred.updated_at = now_kst()
|
||||
await db.commit()
|
||||
# H3: 캐시된 UserContext.auto_trade_enabled 스냅샷이 갱신되도록 무효화(다음 호출에서 재조립).
|
||||
await user_context_registry.invalidate(user_id)
|
||||
|
||||
@@ -29,6 +29,7 @@ from app.services.utils import _is_domestic
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1428,7 +1429,7 @@ async def _create_journal_background(
|
||||
"trade_id": trade_id,
|
||||
"stock_code": stock_code,
|
||||
"stock_name": stock_name,
|
||||
"journal_date": datetime.utcnow(),
|
||||
"journal_date": now_kst(),
|
||||
"content": content,
|
||||
"strategy_name": "AUTO_TRADE",
|
||||
"mood": mood,
|
||||
@@ -1699,7 +1700,7 @@ async def update_active_trade_status(
|
||||
|
||||
# 상태 업데이트
|
||||
active_trade.status = new_status
|
||||
active_trade.updated_at = datetime.fromisoformat(request.updated_at) if request.updated_at else datetime.utcnow()
|
||||
active_trade.updated_at = datetime.fromisoformat(request.updated_at) if request.updated_at else now_kst()
|
||||
|
||||
# 부분 매도 시 잔여 비율 갱신 (CLAUDE.md 5.2: +2R 50% 분할 매도 후 잔량 추적 유지)
|
||||
if request.remaining_pct is not None:
|
||||
|
||||
@@ -28,6 +28,7 @@ def _today_kst() -> date:
|
||||
from app.core.config import settings
|
||||
from app.core.security import require_api_key
|
||||
from app.models.models import TradeHistory, TradingSession, Portfolio
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,7 +55,7 @@ async def get_risk_metrics(
|
||||
- 현재 계좌 상태
|
||||
"""
|
||||
try:
|
||||
since_date = datetime.utcnow() - timedelta(days=days)
|
||||
since_date = now_kst() - timedelta(days=days)
|
||||
|
||||
# 매도 완료된 거래만 조회 (실현 손익)
|
||||
result = await db.execute(
|
||||
@@ -149,7 +150,7 @@ async def get_sharpe_ratio(
|
||||
- < 1.0: 개선 필요
|
||||
"""
|
||||
try:
|
||||
since_date = datetime.utcnow() - timedelta(days=days)
|
||||
since_date = now_kst() - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
select(TradeHistory)
|
||||
@@ -443,7 +444,7 @@ async def get_win_rate(
|
||||
📊 승률 및 손익 통계
|
||||
"""
|
||||
try:
|
||||
since_date = datetime.utcnow() - timedelta(days=days)
|
||||
since_date = now_kst() - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
select(TradeHistory)
|
||||
|
||||
@@ -17,6 +17,7 @@ import re
|
||||
from app.core.database import get_db
|
||||
from app.models.models import TradeHistory
|
||||
from pydantic import BaseModel
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -62,7 +63,7 @@ async def get_trade_history(
|
||||
query = select(TradeHistory)
|
||||
|
||||
# 날짜 필터
|
||||
since_date = datetime.utcnow() - timedelta(days=days)
|
||||
since_date = now_kst() - timedelta(days=days)
|
||||
query = query.where(TradeHistory.created_at >= since_date)
|
||||
|
||||
# 종목 코드 필터
|
||||
@@ -158,7 +159,7 @@ async def get_trade_stats(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""거래 통계 조회"""
|
||||
since_date = datetime.utcnow() - timedelta(days=days)
|
||||
since_date = now_kst() - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
select(TradeHistory).where(TradeHistory.created_at >= since_date)
|
||||
|
||||
@@ -22,6 +22,7 @@ from app.core.tenant_auth import CurrentUser, get_current_user
|
||||
from app.models.models import TradeHistory
|
||||
from app.api.endpoints.portfolio_v2 import _resolve_target_user_id, _user_scope_filter
|
||||
from app.api.endpoints.trades import TradeHistoryResponse
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -44,7 +45,7 @@ async def get_trade_history_v2(
|
||||
|
||||
query = select(TradeHistory).where(_user_scope_filter(TradeHistory.user_id, target_user_id))
|
||||
|
||||
since_date = datetime.utcnow() - timedelta(days=days)
|
||||
since_date = now_kst() - timedelta(days=days)
|
||||
query = query.where(TradeHistory.created_at >= since_date)
|
||||
|
||||
if stock_code:
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.core.database import get_db
|
||||
from app.core.security import require_api_key
|
||||
from app.models.models import TradingRule
|
||||
from pydantic import BaseModel
|
||||
from app.core.timeutil import now_kst
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -118,7 +119,7 @@ async def update_trading_rule(
|
||||
for field, value in update_data.items():
|
||||
setattr(db_rule, field, value)
|
||||
|
||||
db_rule.updated_at = datetime.utcnow()
|
||||
db_rule.updated_at = now_kst()
|
||||
await db.commit()
|
||||
await db.refresh(db_rule)
|
||||
return db_rule
|
||||
@@ -148,7 +149,7 @@ async def toggle_trading_rule(rule_id: int, db: AsyncSession = Depends(get_db)):
|
||||
raise HTTPException(status_code=404, detail="Trading rule not found")
|
||||
|
||||
db_rule.is_active = not db_rule.is_active
|
||||
db_rule.updated_at = datetime.utcnow()
|
||||
db_rule.updated_at = now_kst()
|
||||
await db.commit()
|
||||
await db.refresh(db_rule)
|
||||
return db_rule
|
||||
|
||||
Reference in New Issue
Block a user