fix: timezone handling and prediction logic
- api/models.py: Fix timezone conversion in stats queries by calculating UTC ranges in Python instead of using SQL CONVERT_TZ. Ensure JSON serializable types.
- api/prediction_service.py:
- Update ensure_predictions_for_month to check previous day (ET) to cover CST month start.
- Update _generate_prediction_for_hour to skip saving unclosed candles (current hour).
- trader/auto_trade_polymarket.py: Update to use hourly_model_predictions (V2) table for storing live trading predictions.
This commit is contained in:
1 parent
2993014fa8
commit
99fa79cde4
4 files changed
+232
-111
No files matched your search
+142
-57
@@ -4,7 +4,7 @@
|
||||
import pymysql
|
||||
import os
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import contextmanager
|
||||
from zoneinfo import ZoneInfo
|
||||
from dotenv import load_dotenv
|
||||
@@ -340,7 +340,29 @@ class PredictionModel:
|
||||
def get_monthly_stats_v2(month: str) -> Dict:
|
||||
"""
|
||||
获取月度统计 (V2 - 从宽表)
|
||||
修改:不使用 SQL 进行时区转换和格式化,而是查询 UTC 时间范围,然后在 Python 中处理
|
||||
"""
|
||||
# 计算 UTC 时间范围
|
||||
try:
|
||||
year, mon = map(int, month.split('-'))
|
||||
start_date_cst = datetime(year, mon, 1, tzinfo=TZ_CST)
|
||||
# 下个月第一天
|
||||
if mon == 12:
|
||||
end_date_cst = datetime(year + 1, 1, 1, tzinfo=TZ_CST)
|
||||
else:
|
||||
end_date_cst = datetime(year, mon + 1, 1, tzinfo=TZ_CST)
|
||||
|
||||
# 转换为 UTC
|
||||
start_dt_utc = start_date_cst.astimezone(TZ_UTC)
|
||||
end_dt_utc = end_date_cst.astimezone(TZ_UTC)
|
||||
|
||||
start_str = start_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
|
||||
end_str = end_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
except Exception as e:
|
||||
print(f"日期解析错误: {e}")
|
||||
return {}
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
@@ -352,25 +374,26 @@ class PredictionModel:
|
||||
col_pred = f"{model}_prediction"
|
||||
col_correct = f"{model}_correct"
|
||||
|
||||
# 查询 UTC 时间范围内的所有数据
|
||||
sql = f"""
|
||||
SELECT
|
||||
COUNT({col_pred}) as total,
|
||||
SUM(CASE WHEN {col_correct} = 1 THEN 1 ELSE 0 END) as correct,
|
||||
SUM(CASE WHEN {col_correct} = 0 THEN 1 ELSE 0 END) as incorrect
|
||||
FROM hourly_model_predictions
|
||||
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
|
||||
WHERE timestamp >= %s AND timestamp < %s
|
||||
AND actual_result IS NOT NULL
|
||||
"""
|
||||
cursor.execute(sql, (month,))
|
||||
cursor.execute(sql, (start_str, end_str))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
total = row['total']
|
||||
correct = row['correct'] or 0
|
||||
incorrect = row['incorrect'] or 0
|
||||
total = int(row['total'])
|
||||
correct = int(row['correct'] or 0)
|
||||
incorrect = int(row['incorrect'] or 0)
|
||||
accuracy = (correct / total * 100) if total > 0 else 0
|
||||
|
||||
# 格式化键名以匹配前端 (例如 dlinear -> DLinear)
|
||||
# 格式化键名以匹配前端
|
||||
model_key = {
|
||||
'dlinear': 'DLinear',
|
||||
'nbeats': 'N-BEATS',
|
||||
@@ -391,55 +414,97 @@ class PredictionModel:
|
||||
def get_daily_stats_v2(month: str, page: int = 1, limit: int = 10) -> Dict:
|
||||
"""
|
||||
获取每日统计 (V2 - 从宽表)
|
||||
修改:不使用 SQL 进行时区转换,查询 UTC 范围,然后在 Python 中转换
|
||||
"""
|
||||
# 计算 UTC 时间范围
|
||||
try:
|
||||
year, mon = map(int, month.split('-'))
|
||||
start_date_cst = datetime(year, mon, 1, tzinfo=TZ_CST)
|
||||
if mon == 12:
|
||||
end_date_cst = datetime(year + 1, 1, 1, tzinfo=TZ_CST)
|
||||
else:
|
||||
end_date_cst = datetime(year, mon + 1, 1, tzinfo=TZ_CST)
|
||||
|
||||
start_dt_utc = start_date_cst.astimezone(TZ_UTC)
|
||||
end_dt_utc = end_date_cst.astimezone(TZ_UTC)
|
||||
|
||||
start_str = start_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
|
||||
end_str = end_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except Exception:
|
||||
return {"data": [], "page": page, "limit": limit, "total": 0, "total_pages": 0}
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 分页查询日期
|
||||
offset = (page - 1) * limit
|
||||
date_sql = """
|
||||
SELECT DISTINCT DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as date_cst
|
||||
# 获取范围内的所有原始数据
|
||||
# 我们需要 timestamp, actual_result, 和各模型的 prediction/correct
|
||||
# 为了按 CST 日期分组,我们需要取出每一行,然后在 Python 中转换时间
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
timestamp,
|
||||
actual_result,
|
||||
dlinear_prediction, dlinear_correct,
|
||||
nbeats_prediction, nbeats_correct,
|
||||
ttm_prediction, ttm_correct,
|
||||
ensemble_prediction, ensemble_correct
|
||||
FROM hourly_model_predictions
|
||||
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
|
||||
ORDER BY date_cst DESC
|
||||
LIMIT %s OFFSET %s
|
||||
WHERE timestamp >= %s AND timestamp < %s
|
||||
AND actual_result IS NOT NULL
|
||||
ORDER BY timestamp DESC
|
||||
"""
|
||||
cursor.execute(date_sql, (month, limit, offset))
|
||||
dates = [row['date_cst'] for row in cursor.fetchall()]
|
||||
cursor.execute(sql, (start_str, end_str))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# 总天数
|
||||
count_sql = """
|
||||
SELECT COUNT(DISTINCT DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00'))) as cnt
|
||||
FROM hourly_model_predictions
|
||||
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
|
||||
"""
|
||||
cursor.execute(count_sql, (month,))
|
||||
total_count = cursor.fetchone()['cnt']
|
||||
# 在 Python 中处理分组
|
||||
daily_stats = {} # { "YYYY-MM-DD": { "date": "...", "dlinear": {...}, ... } }
|
||||
|
||||
daily_data = []
|
||||
models = ['dlinear', 'nbeats', 'ttm', 'ensemble']
|
||||
|
||||
for date_obj in dates:
|
||||
date_str = str(date_obj)
|
||||
day_stats = {"date": date_str}
|
||||
for row in rows:
|
||||
# 转换 timestamp 到 CST
|
||||
ts_utc = row['timestamp']
|
||||
# 确保是带时区的 datetime
|
||||
if isinstance(ts_utc, str):
|
||||
ts_utc = datetime.fromisoformat(str(ts_utc).replace('Z', '+00:00'))
|
||||
if ts_utc.tzinfo is None:
|
||||
ts_utc = ts_utc.replace(tzinfo=TZ_UTC)
|
||||
|
||||
ts_cst = ts_utc.astimezone(TZ_CST)
|
||||
date_str = ts_cst.strftime('%Y-%m-%d')
|
||||
|
||||
if date_str not in daily_stats:
|
||||
daily_stats[date_str] = {
|
||||
"date": date_str,
|
||||
"raw_data": {m: {"total": 0, "correct": 0} for m in ['dlinear', 'nbeats', 'ttm', 'ensemble']}
|
||||
}
|
||||
|
||||
# 累加统计
|
||||
models = ['dlinear', 'nbeats', 'ttm', 'ensemble']
|
||||
for model in models:
|
||||
col_pred = f"{model}_prediction"
|
||||
col_correct = f"{model}_correct"
|
||||
pred = row[f"{model}_prediction"]
|
||||
correct_flag = row[f"{model}_correct"]
|
||||
|
||||
sql = f"""
|
||||
SELECT
|
||||
COUNT({col_pred}) as total,
|
||||
SUM(CASE WHEN {col_correct} = 1 THEN 1 ELSE 0 END) as correct
|
||||
FROM hourly_model_predictions
|
||||
WHERE DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) = %s
|
||||
AND actual_result IS NOT NULL
|
||||
"""
|
||||
cursor.execute(sql, (date_str,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
total = row['total']
|
||||
correct = row['correct'] or 0
|
||||
if pred is not None:
|
||||
daily_stats[date_str]['raw_data'][model]['total'] += 1
|
||||
if correct_flag == 1:
|
||||
daily_stats[date_str]['raw_data'][model]['correct'] += 1
|
||||
|
||||
# 整理为最终格式
|
||||
sorted_dates = sorted(daily_stats.keys(), reverse=True)
|
||||
total_count = len(sorted_dates)
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * limit
|
||||
paginated_dates = sorted_dates[offset:offset + limit]
|
||||
|
||||
result_data = []
|
||||
for date_key in paginated_dates:
|
||||
day_item = daily_stats[date_key]
|
||||
formatted_item = {"date": day_item["date"]}
|
||||
|
||||
for model in ['dlinear', 'nbeats', 'ttm', 'ensemble']:
|
||||
raw = day_item['raw_data'][model]
|
||||
total = int(raw['total'])
|
||||
correct = int(raw['correct'])
|
||||
accuracy = (correct / total * 100) if total > 0 else 0
|
||||
|
||||
model_key = {
|
||||
@@ -449,16 +514,15 @@ class PredictionModel:
|
||||
'ensemble': 'Ensemble'
|
||||
}.get(model, model)
|
||||
|
||||
day_stats[model_key] = {
|
||||
formatted_item[model_key] = {
|
||||
"total": total,
|
||||
"correct": correct,
|
||||
"accuracy": round(accuracy, 2)
|
||||
}
|
||||
|
||||
daily_data.append(day_stats)
|
||||
result_data.append(formatted_item)
|
||||
|
||||
return {
|
||||
"data": daily_data,
|
||||
"data": result_data,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total_count,
|
||||
@@ -469,25 +533,46 @@ class PredictionModel:
|
||||
def get_hourly_details_v2(date: str) -> List[Dict]:
|
||||
"""
|
||||
获取指定日期的每小时详情 (V2)
|
||||
修改:根据传入的 CST 日期计算 UTC 范围,查询该范围内的数据
|
||||
"""
|
||||
try:
|
||||
# date 是 'YYYY-MM-DD' (CST)
|
||||
date_cst_start = datetime.strptime(date, '%Y-%m-%d').replace(tzinfo=TZ_CST)
|
||||
date_cst_end = date_cst_start + timedelta(days=1)
|
||||
|
||||
start_dt_utc = date_cst_start.astimezone(TZ_UTC)
|
||||
end_dt_utc = date_cst_end.astimezone(TZ_UTC)
|
||||
|
||||
start_str = start_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
|
||||
end_str = end_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except Exception as e:
|
||||
print(f"日期处理错误: {e}")
|
||||
return []
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
*,
|
||||
HOUR(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as hour_cst,
|
||||
CONVERT_TZ(timestamp, '+00:00', '+08:00') as timestamp_cst_dt
|
||||
SELECT *
|
||||
FROM hourly_model_predictions
|
||||
WHERE DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) = %s
|
||||
ORDER BY hour_cst
|
||||
""", (date,))
|
||||
WHERE timestamp >= %s AND timestamp < %s
|
||||
ORDER BY timestamp ASC
|
||||
""", (start_str, end_str))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
result = []
|
||||
|
||||
for row in rows:
|
||||
# 计算 CST 小时用于显示排序 (虽然 SQL 已经按 timestamp 排序了,但为了确保前端显示 hour 字段正确)
|
||||
ts_utc = row['timestamp']
|
||||
if isinstance(ts_utc, str):
|
||||
ts_utc = datetime.fromisoformat(str(ts_utc).replace('Z', '+00:00'))
|
||||
if ts_utc.tzinfo is None:
|
||||
ts_utc = ts_utc.replace(tzinfo=TZ_UTC)
|
||||
ts_cst = ts_utc.astimezone(TZ_CST)
|
||||
hour_cst = ts_cst.hour
|
||||
|
||||
hour_data = {
|
||||
"hour": row['hour_cst'],
|
||||
"hour": hour_cst,
|
||||
"timestamp_et": row['timestamp_et'],
|
||||
"timestamp_cst": convert_to_cst(row.get('timestamp', '')),
|
||||
"open_price": row['open_price'],
|
||||
|
||||
Reference in new issue
Block a user