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:
2026-09-23 11:24:14 +09:00
parent e2d4b607cb
commit ec2fa2b423
22 changed files with 177 additions and 69 deletions

View File

@@ -17,6 +17,7 @@ from typing import Optional, List
from sqlalchemy import Column, Integer, BigInteger, String, Numeric, Boolean, DateTime, Text, ARRAY, Date, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB
from app.core.database import Base
from app.core.timeutil import now_kst
class StockDailyPrice(Base):
@@ -57,8 +58,8 @@ class TradingSession(Base):
starting_balance = Column(Numeric(15, 2))
ending_balance = Column(Numeric(15, 2))
peak_balance = Column(Numeric(15, 2))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_at = Column(DateTime, default=now_kst)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class TradingRule(Base):
@@ -72,8 +73,8 @@ class TradingRule(Base):
actions = Column(JSONB, nullable=False)
is_active = Column(Boolean, default=True)
priority = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_at = Column(DateTime, default=now_kst)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class StrategyConfig(Base):
@@ -118,8 +119,8 @@ class StrategyConfig(Base):
is_active = Column(Boolean, default=True, nullable=False) # False면 기본값 사용
updated_by = Column(String(100), nullable=True) # 마지막 수정자
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_at = Column(DateTime, default=now_kst)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class TradeHistory(Base):
@@ -153,7 +154,7 @@ class TradeHistory(Base):
currency_code = Column(String(3), nullable=False, default="KRW")
exchange_code = Column(String(10), nullable=True)
fx_rate = Column(Numeric(10, 4), nullable=True) # 체결 시점 환율
created_at = Column(DateTime, default=datetime.utcnow, index=True)
created_at = Column(DateTime, default=now_kst, index=True)
executed_at = Column(DateTime)
@@ -175,7 +176,7 @@ class Portfolio(Base):
strategy_type = Column(String(20), nullable=False, default="swing", index=True)
currency_code = Column(String(3), nullable=False, default="KRW")
exchange_code = Column(String(10), nullable=True) # NASD, NYSE, AMEX, KRX
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class MarketData(Base):
@@ -189,7 +190,7 @@ class MarketData(Base):
high_price = Column(Numeric(15, 2))
low_price = Column(Numeric(15, 2))
open_price = Column(Numeric(15, 2))
timestamp = Column(DateTime, default=datetime.utcnow, index=True)
timestamp = Column(DateTime, default=now_kst, index=True)
class News(Base):
@@ -204,7 +205,7 @@ class News(Base):
sentiment = Column(String(20)) # positive / negative / neutral
sentiment_score = Column(Numeric(5, 3), nullable=True) # -1.000 ~ +1.000
published_at = Column(DateTime, index=True)
created_at = Column(DateTime, default=datetime.utcnow)
created_at = Column(DateTime, default=now_kst)
class ClaudeAnalysisLog(Base):
@@ -219,7 +220,7 @@ class ClaudeAnalysisLog(Base):
analysis_result = Column(JSONB)
decision = Column(String(50))
confidence = Column(Numeric(5, 4))
created_at = Column(DateTime, default=datetime.utcnow, index=True)
created_at = Column(DateTime, default=now_kst, index=True)
class MCPToolCall(Base):
@@ -232,7 +233,7 @@ class MCPToolCall(Base):
result = Column(JSONB)
status = Column(String(50))
execution_time_ms = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
created_at = Column(DateTime, default=now_kst, index=True)
class StockAnalysis(Base):
@@ -265,8 +266,8 @@ class StockAnalysis(Base):
ml_score = Column(Integer, nullable=True)
ml_confidence = Column(Numeric(5, 4), nullable=True)
ml_signal = Column(String(10), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_at = Column(DateTime, default=now_kst)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class ActiveTrade(Base):
@@ -293,8 +294,8 @@ class ActiveTrade(Base):
strategy_name = Column(String(100), nullable=True)
currency_code = Column(String(3), nullable=False, default="KRW")
exchange_code = Column(String(10), nullable=True) # NASD, NYSE, AMEX, KRX
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_at = Column(DateTime, default=now_kst)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class ExchangeRate(Base):
@@ -305,7 +306,7 @@ class ExchangeRate(Base):
currency_pair = Column(String(7), nullable=False, index=True) # USD/KRW
rate = Column(Numeric(12, 4), nullable=False)
source = Column(String(50), default="KIS")
recorded_at = Column(DateTime, default=datetime.utcnow, index=True)
recorded_at = Column(DateTime, default=now_kst, index=True)
class UsStockUniverse(Base):
@@ -322,7 +323,7 @@ class UsStockUniverse(Base):
sector = Column(String(100), nullable=True)
market_cap = Column(BigInteger, nullable=True)
is_active = Column(Boolean, default=True, index=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class TradeJournal(Base):
@@ -333,14 +334,14 @@ class TradeJournal(Base):
trade_id = Column(Integer)
stock_code = Column(String(20), nullable=False, index=True)
stock_name = Column(String(100))
journal_date = Column(DateTime, default=datetime.utcnow)
journal_date = Column(DateTime, default=now_kst)
content = Column(Text)
strategy_name = Column(String(100))
mood = Column(String(50))
tags = Column(JSONB)
screenshot_url = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_at = Column(DateTime, default=now_kst)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class StockBan(Base):
@@ -351,7 +352,7 @@ class StockBan(Base):
stock_code = Column(String(20), primary_key=True)
consecutive_losses = Column(Integer, default=0, nullable=False)
ban_until = Column(DateTime, nullable=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class StockBanV2(Base):
@@ -371,7 +372,7 @@ class StockBanV2(Base):
stock_code = Column(String(20), nullable=False, index=True)
consecutive_losses = Column(Integer, default=0, nullable=False)
ban_until = Column(DateTime, nullable=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)
class WatchlistStock(Base):
@@ -419,7 +420,7 @@ class StrategyOptimizationLog(Base):
applied = Column(Boolean, default=False, nullable=False)
apply_reason = Column(Text, nullable=True)
total_combinations = Column(Integer, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
created_at = Column(DateTime, default=now_kst, index=True)
class User(Base):
@@ -436,7 +437,7 @@ class User(Base):
password_hash = Column(String(128), nullable=False)
is_active = Column(Boolean, default=False, nullable=False)
is_admin = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
created_at = Column(DateTime, default=now_kst, nullable=False)
last_login_at = Column(DateTime, nullable=True)
@@ -462,5 +463,5 @@ class UserKisCredential(Base):
auto_trade_enabled = Column(Boolean, default=False, nullable=False)
last_verified_at = Column(DateTime, nullable=True)
key_version = Column(Integer, default=1, nullable=False) # 암호화 키 로테이션용
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_at = Column(DateTime, default=now_kst, nullable=False)
updated_at = Column(DateTime, default=now_kst, onupdate=now_kst)