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:
61
backend/tests/test_timezone_consistency.py
Normal file
61
backend/tests/test_timezone_consistency.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""DB 타임스탬프 KST 통일 검증.
|
||||
|
||||
2026-09-23: 모델 기본값은 datetime.utcnow(UTC), 일부 경로는 _get_kst_now(KST)로 저장해
|
||||
같은 테이블 안에서 9시간이 섞였다(화면 매매 시각이 02:14 처럼 표시). 조회 구간도
|
||||
utcnow 기준이라 비교가 9시간 어긋났다(추가매수 쿨다운 상시 발동).
|
||||
"""
|
||||
import inspect
|
||||
import pathlib
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
|
||||
from app.core.timeutil import KST, now_kst, today_kst
|
||||
from app.models.models import ActiveTrade, Portfolio, TradeHistory
|
||||
|
||||
|
||||
def test_now_kst_is_naive_korean_time():
|
||||
v = now_kst()
|
||||
assert v.tzinfo is None
|
||||
real = datetime.now(pytz.timezone("Asia/Seoul")).replace(tzinfo=None)
|
||||
assert abs((real - v).total_seconds()) < 5
|
||||
assert today_kst() == real.date()
|
||||
|
||||
|
||||
def test_now_kst_differs_from_utc_by_nine_hours():
|
||||
delta = now_kst() - datetime.utcnow()
|
||||
assert 8.9 * 3600 < delta.total_seconds() < 9.1 * 3600
|
||||
|
||||
|
||||
def test_model_defaults_use_kst():
|
||||
for model, cols in [
|
||||
(TradeHistory, ["created_at"]),
|
||||
(ActiveTrade, ["created_at", "updated_at"]),
|
||||
(Portfolio, ["updated_at"]),
|
||||
]:
|
||||
for col in cols:
|
||||
default = model.__table__.columns[col].default
|
||||
assert default is not None, f"{model.__name__}.{col} 기본값 없음"
|
||||
assert getattr(default.arg, "__name__", "") == "now_kst", (
|
||||
f"{model.__name__}.{col} 이 KST 기준이 아님"
|
||||
)
|
||||
|
||||
|
||||
def test_no_utcnow_left_in_app_code():
|
||||
"""app/ 전체에서 utcnow() 호출이 남아 있으면 다시 섞인다."""
|
||||
root = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
offenders = []
|
||||
for f in root.rglob("*.py"):
|
||||
for i, line in enumerate(f.read_text().splitlines(), 1):
|
||||
code = line.split("#", 1)[0]
|
||||
if "utcnow()" in code:
|
||||
offenders.append(f"{f.relative_to(root)}:{i}")
|
||||
assert not offenders, f"utcnow() 잔존: {offenders}"
|
||||
|
||||
|
||||
def test_cooldown_compares_same_basis():
|
||||
from app.services import trade_executor as te
|
||||
src = inspect.getsource(te)
|
||||
i = src.index("recent_buys[0].created_at")
|
||||
window = src[max(0, i - 300): i + 100]
|
||||
assert "now_kst()" in window, "쿨다운 비교가 KST 기준이 아님"
|
||||
Reference in New Issue
Block a user