시간대 혼재: 모델 기본값은 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>
62 lines
2.2 KiB
Python
62 lines
2.2 KiB
Python
"""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 기준이 아님"
|