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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user