feat: upgrade to multi-model ensemble system
- Updated DB schemas: added 'strategy' to trades, new 'hourly_model_predictions' wide table - Refactored backend: api/models.py, api/prediction_service.py to support wide format - Updated trader: auto_trade_polymarket.py now logs strategy and saves to new table - Updated frontend: index.html and app.js to display multi-model accuracy and trade strategy
This commit is contained in:
1 parent
c0a02868ac
commit
2993014fa8
5 files changed
+465
-335
No files matched your search
+6
-6
@@ -106,8 +106,8 @@ async def get_monthly_accuracy(
|
|||||||
service = get_prediction_service()
|
service = get_prediction_service()
|
||||||
service.ensure_predictions_for_month(month)
|
service.ensure_predictions_for_month(month)
|
||||||
|
|
||||||
# 获取统计
|
# 获取统计 (使用 V2 方法)
|
||||||
stats = PredictionModel.get_monthly_stats(month)
|
stats = PredictionModel.get_monthly_stats_v2(month)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -135,8 +135,8 @@ async def get_daily_accuracy(
|
|||||||
service = get_prediction_service()
|
service = get_prediction_service()
|
||||||
service.ensure_predictions_for_month(month)
|
service.ensure_predictions_for_month(month)
|
||||||
|
|
||||||
# 获取统计
|
# 获取统计 (使用 V2 方法)
|
||||||
result = PredictionModel.get_daily_stats(month=month, page=page, limit=limit)
|
result = PredictionModel.get_daily_stats_v2(month=month, page=page, limit=limit)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -155,8 +155,8 @@ async def get_hourly_accuracy(date: str):
|
|||||||
service = get_prediction_service()
|
service = get_prediction_service()
|
||||||
service.ensure_predictions_for_date(date)
|
service.ensure_predictions_for_date(date)
|
||||||
|
|
||||||
# 获取详情
|
# 获取详情 (使用 V2 方法)
|
||||||
details = PredictionModel.get_hourly_details(date)
|
details = PredictionModel.get_hourly_details_v2(date)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|||||||
+277
-192
@@ -88,6 +88,7 @@ def _dict_from_row(row):
|
|||||||
|
|
||||||
def init_model_predictions_table():
|
def init_model_predictions_table():
|
||||||
"""初始化 model_predictions 表"""
|
"""初始化 model_predictions 表"""
|
||||||
|
# 兼容旧表,但主要使用 hourly_model_predictions
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
@@ -110,6 +111,42 @@ def init_model_predictions_table():
|
|||||||
INDEX idx_model (model_name)
|
INDEX idx_model (model_name)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# 确保 hourly_model_predictions 表存在
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS hourly_model_predictions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
timestamp DATETIME NOT NULL,
|
||||||
|
timestamp_et VARCHAR(50),
|
||||||
|
date VARCHAR(10),
|
||||||
|
hour INT,
|
||||||
|
|
||||||
|
open_price DOUBLE,
|
||||||
|
close_price DOUBLE,
|
||||||
|
actual_result INT COMMENT '0=Down, 1=Up',
|
||||||
|
|
||||||
|
dlinear_prediction INT,
|
||||||
|
dlinear_prob DOUBLE,
|
||||||
|
dlinear_correct INT,
|
||||||
|
|
||||||
|
nbeats_prediction INT,
|
||||||
|
nbeats_prob DOUBLE,
|
||||||
|
nbeats_correct INT,
|
||||||
|
|
||||||
|
ttm_prediction INT,
|
||||||
|
ttm_prob DOUBLE,
|
||||||
|
ttm_correct INT,
|
||||||
|
|
||||||
|
ensemble_prediction INT,
|
||||||
|
ensemble_votes VARCHAR(50),
|
||||||
|
ensemble_correct INT,
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE KEY uk_date_hour (date, hour),
|
||||||
|
INDEX idx_timestamp (timestamp)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
''')
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -143,12 +180,6 @@ class TradeModel:
|
|||||||
day: Optional[str] = None) -> Dict:
|
day: Optional[str] = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
获取历史交易记录
|
获取历史交易记录
|
||||||
|
|
||||||
Args:
|
|
||||||
page: 页码
|
|
||||||
limit: 每页条数
|
|
||||||
month: 月份筛选 (YYYY-MM)
|
|
||||||
day: 日期筛选 (YYYY-MM-DD)
|
|
||||||
"""
|
"""
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
@@ -160,7 +191,7 @@ class TradeModel:
|
|||||||
params = []
|
params = []
|
||||||
|
|
||||||
if month:
|
if month:
|
||||||
where_clauses.append("DATE_FORMAT(timestamp, '%%Y-%%m') = %s")
|
where_clauses.append("DATE_FORMAT(timestamp, '%Y-%m') = %s")
|
||||||
params.append(month)
|
params.append(month)
|
||||||
|
|
||||||
if day:
|
if day:
|
||||||
@@ -199,274 +230,328 @@ class TradeModel:
|
|||||||
|
|
||||||
|
|
||||||
class PredictionModel:
|
class PredictionModel:
|
||||||
"""预测记录模型"""
|
"""预测记录模型 (V2 - 宽表结构)"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_prediction(date: str, hour: int, model_name: str) -> Optional[Dict]:
|
def save_hourly_prediction(data: Dict) -> int:
|
||||||
"""获取指定时间和模型的预测记录"""
|
|
||||||
with get_db_connection() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT * FROM model_predictions
|
|
||||||
WHERE date = %s AND hour = %s AND model_name = %s
|
|
||||||
""", (date, hour, model_name))
|
|
||||||
row = cursor.fetchone()
|
|
||||||
return _dict_from_row(row)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def save_prediction(data: Dict) -> int:
|
|
||||||
"""
|
"""
|
||||||
保存预测记录
|
保存每小时综合预测记录 (宽表)
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 预测数据字典
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
插入的记录ID
|
|
||||||
"""
|
"""
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
sql = """
|
sql = """
|
||||||
INSERT INTO model_predictions
|
INSERT INTO hourly_model_predictions
|
||||||
(timestamp, timestamp_et, date, hour, model_name,
|
(timestamp, timestamp_et, date, hour, open_price, close_price, actual_result,
|
||||||
prediction, probability, actual_result, is_correct,
|
dlinear_prediction, dlinear_prob, dlinear_correct,
|
||||||
open_price, close_price)
|
nbeats_prediction, nbeats_prob, nbeats_correct,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
ttm_prediction, ttm_prob, ttm_correct,
|
||||||
|
ensemble_prediction, ensemble_votes, ensemble_correct)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
prediction = VALUES(prediction),
|
|
||||||
probability = VALUES(probability),
|
|
||||||
actual_result = VALUES(actual_result),
|
|
||||||
is_correct = VALUES(is_correct),
|
|
||||||
open_price = VALUES(open_price),
|
open_price = VALUES(open_price),
|
||||||
close_price = VALUES(close_price)
|
close_price = VALUES(close_price),
|
||||||
|
actual_result = VALUES(actual_result),
|
||||||
|
dlinear_prediction = VALUES(dlinear_prediction),
|
||||||
|
dlinear_prob = VALUES(dlinear_prob),
|
||||||
|
dlinear_correct = VALUES(dlinear_correct),
|
||||||
|
nbeats_prediction = VALUES(nbeats_prediction),
|
||||||
|
nbeats_prob = VALUES(nbeats_prob),
|
||||||
|
nbeats_correct = VALUES(nbeats_correct),
|
||||||
|
ttm_prediction = VALUES(ttm_prediction),
|
||||||
|
ttm_prob = VALUES(ttm_prob),
|
||||||
|
ttm_correct = VALUES(ttm_correct),
|
||||||
|
ensemble_prediction = VALUES(ensemble_prediction),
|
||||||
|
ensemble_votes = VALUES(ensemble_votes),
|
||||||
|
ensemble_correct = VALUES(ensemble_correct)
|
||||||
"""
|
"""
|
||||||
cursor.execute(sql, (
|
cursor.execute(sql, (
|
||||||
data.get('timestamp'),
|
data.get('timestamp'),
|
||||||
data.get('timestamp_et'),
|
data.get('timestamp_et'),
|
||||||
data.get('date'),
|
data.get('date'),
|
||||||
data.get('hour'),
|
data.get('hour'),
|
||||||
data.get('model_name'),
|
|
||||||
data.get('prediction'),
|
|
||||||
data.get('probability'),
|
|
||||||
data.get('actual_result'),
|
|
||||||
data.get('is_correct'),
|
|
||||||
data.get('open_price'),
|
data.get('open_price'),
|
||||||
data.get('close_price')
|
data.get('close_price'),
|
||||||
|
data.get('actual_result'),
|
||||||
|
data.get('dlinear_prediction'),
|
||||||
|
data.get('dlinear_prob'),
|
||||||
|
data.get('dlinear_correct'),
|
||||||
|
data.get('nbeats_prediction'),
|
||||||
|
data.get('nbeats_prob'),
|
||||||
|
data.get('nbeats_correct'),
|
||||||
|
data.get('ttm_prediction'),
|
||||||
|
data.get('ttm_prob'),
|
||||||
|
data.get('ttm_correct'),
|
||||||
|
data.get('ensemble_prediction'),
|
||||||
|
str(data.get('ensemble_votes')) if data.get('ensemble_votes') else None,
|
||||||
|
data.get('ensemble_correct')
|
||||||
))
|
))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.lastrowid or 0
|
return cursor.lastrowid or 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_actual_result(date: str, hour: int, model_name: str,
|
def update_actual_result_v2(date: str, hour: int, actual_result: int, close_price: float) -> bool:
|
||||||
actual_result: int, close_price: float) -> bool:
|
"""
|
||||||
"""更新实际结果"""
|
更新实际结果,并重新计算各模型正确性 (V2)
|
||||||
|
"""
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 先获取预测值
|
# 获取该小时记录
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT prediction FROM model_predictions
|
SELECT * FROM hourly_model_predictions
|
||||||
WHERE date = %s AND hour = %s AND model_name = %s
|
WHERE date = %s AND hour = %s
|
||||||
""", (date, hour, model_name))
|
""", (date, hour))
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
prediction = row['prediction']
|
# 计算正确性
|
||||||
is_correct = 1 if prediction == actual_result else 0
|
updates = {
|
||||||
|
'actual_result': actual_result,
|
||||||
|
'close_price': close_price
|
||||||
|
}
|
||||||
|
|
||||||
cursor.execute("""
|
# DLinear
|
||||||
UPDATE model_predictions
|
if row['dlinear_prediction'] is not None:
|
||||||
SET actual_result = %s, is_correct = %s, close_price = %s
|
updates['dlinear_correct'] = 1 if row['dlinear_prediction'] == actual_result else 0
|
||||||
WHERE date = %s AND hour = %s AND model_name = %s
|
|
||||||
""", (actual_result, is_correct, close_price, date, hour, model_name))
|
# N-BEATS
|
||||||
|
if row['nbeats_prediction'] is not None:
|
||||||
|
updates['nbeats_correct'] = 1 if row['nbeats_prediction'] == actual_result else 0
|
||||||
|
|
||||||
|
# TTM
|
||||||
|
if row['ttm_prediction'] is not None:
|
||||||
|
updates['ttm_correct'] = 1 if row['ttm_prediction'] == actual_result else 0
|
||||||
|
|
||||||
|
# Ensemble
|
||||||
|
if row['ensemble_prediction'] is not None:
|
||||||
|
updates['ensemble_correct'] = 1 if row['ensemble_prediction'] == actual_result else 0
|
||||||
|
|
||||||
|
# 构建更新SQL
|
||||||
|
set_clause = ", ".join([f"{k} = %s" for k in updates.keys()])
|
||||||
|
sql = f"UPDATE hourly_model_predictions SET {set_clause} WHERE date = %s AND hour = %s"
|
||||||
|
|
||||||
|
cursor.execute(sql, list(updates.values()) + [date, hour])
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_monthly_stats(month: str) -> Dict:
|
def get_monthly_stats_v2(month: str) -> Dict:
|
||||||
"""
|
"""
|
||||||
获取月度统计
|
获取月度统计 (V2 - 从宽表)
|
||||||
|
|
||||||
Args:
|
|
||||||
month: 月份 (YYYY-MM)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
各模型的统计数据
|
|
||||||
"""
|
"""
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
sql = """
|
# 统计各个模型的准确率
|
||||||
SELECT
|
models = ['dlinear', 'nbeats', 'ttm', 'ensemble']
|
||||||
model_name,
|
|
||||||
COUNT(*) as total,
|
|
||||||
SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct,
|
|
||||||
SUM(CASE WHEN is_correct = 0 THEN 1 ELSE 0 END) as incorrect
|
|
||||||
FROM model_predictions
|
|
||||||
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
|
|
||||||
AND actual_result IS NOT NULL
|
|
||||||
GROUP BY model_name
|
|
||||||
"""
|
|
||||||
cursor.execute(sql, (month,))
|
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
for row in rows:
|
for model in models:
|
||||||
row_dict = _dict_from_row(row)
|
col_pred = f"{model}_prediction"
|
||||||
model = row_dict['model_name']
|
col_correct = f"{model}_correct"
|
||||||
total = row_dict['total']
|
|
||||||
correct = row_dict['correct'] or 0
|
|
||||||
incorrect = row_dict['incorrect'] or 0
|
|
||||||
accuracy = (correct / total * 100) if total > 0 else 0
|
|
||||||
|
|
||||||
result[model] = {
|
sql = f"""
|
||||||
"total": total,
|
SELECT
|
||||||
"correct": correct,
|
COUNT({col_pred}) as total,
|
||||||
"incorrect": incorrect,
|
SUM(CASE WHEN {col_correct} = 1 THEN 1 ELSE 0 END) as correct,
|
||||||
"accuracy": round(accuracy, 2)
|
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
|
||||||
|
AND actual_result IS NOT NULL
|
||||||
|
"""
|
||||||
|
cursor.execute(sql, (month,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
total = row['total']
|
||||||
|
correct = row['correct'] or 0
|
||||||
|
incorrect = row['incorrect'] or 0
|
||||||
|
accuracy = (correct / total * 100) if total > 0 else 0
|
||||||
|
|
||||||
|
# 格式化键名以匹配前端 (例如 dlinear -> DLinear)
|
||||||
|
model_key = {
|
||||||
|
'dlinear': 'DLinear',
|
||||||
|
'nbeats': 'N-BEATS',
|
||||||
|
'ttm': 'TTM-R2',
|
||||||
|
'ensemble': 'Ensemble'
|
||||||
|
}.get(model, model)
|
||||||
|
|
||||||
|
result[model_key] = {
|
||||||
|
"total": total,
|
||||||
|
"correct": correct,
|
||||||
|
"incorrect": incorrect,
|
||||||
|
"accuracy": round(accuracy, 2)
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_daily_stats(month: str, page: int = 1, limit: int = 10) -> Dict:
|
def get_daily_stats_v2(month: str, page: int = 1, limit: int = 10) -> Dict:
|
||||||
"""
|
"""
|
||||||
获取每日统计
|
获取每日统计 (V2 - 从宽表)
|
||||||
|
|
||||||
Args:
|
|
||||||
month: 月份 (YYYY-MM)
|
|
||||||
page: 页码
|
|
||||||
limit: 每页条数
|
|
||||||
"""
|
"""
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
sql = """
|
# 分页查询日期
|
||||||
SELECT
|
|
||||||
DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as date_cst,
|
|
||||||
model_name,
|
|
||||||
COUNT(*) as total,
|
|
||||||
SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct,
|
|
||||||
SUM(CASE WHEN is_correct = 0 THEN 1 ELSE 0 END) as incorrect
|
|
||||||
FROM model_predictions
|
|
||||||
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
|
|
||||||
AND actual_result IS NOT NULL
|
|
||||||
GROUP BY date_cst, model_name
|
|
||||||
ORDER BY date_cst DESC
|
|
||||||
"""
|
|
||||||
cursor.execute(sql, (month,))
|
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
|
|
||||||
# 重组数据: 按日期合并不同模型的统计
|
|
||||||
daily_data = {}
|
|
||||||
for row in rows:
|
|
||||||
row_dict = _dict_from_row(row)
|
|
||||||
date = str(row_dict['date_cst']) # CST 日期
|
|
||||||
if date not in daily_data:
|
|
||||||
daily_data[date] = {"date": date}
|
|
||||||
|
|
||||||
model = row_dict['model_name']
|
|
||||||
total = row_dict['total']
|
|
||||||
correct = row_dict['correct'] or 0
|
|
||||||
incorrect = row_dict['incorrect'] or 0
|
|
||||||
accuracy = (correct / total * 100) if total > 0 else 0
|
|
||||||
|
|
||||||
daily_data[date][model] = {
|
|
||||||
"total": total,
|
|
||||||
"correct": correct,
|
|
||||||
"incorrect": incorrect,
|
|
||||||
"accuracy": round(accuracy, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 排序并分页
|
|
||||||
sorted_dates = sorted(daily_data.keys(), reverse=True)
|
|
||||||
total_count = len(sorted_dates)
|
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
paginated_dates = sorted_dates[offset:offset + limit]
|
date_sql = """
|
||||||
|
SELECT DISTINCT DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as date_cst
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
cursor.execute(date_sql, (month, limit, offset))
|
||||||
|
dates = [row['date_cst'] for row in cursor.fetchall()]
|
||||||
|
|
||||||
data = [daily_data[d] for d in paginated_dates]
|
# 总天数
|
||||||
|
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']
|
||||||
|
|
||||||
|
daily_data = []
|
||||||
|
models = ['dlinear', 'nbeats', 'ttm', 'ensemble']
|
||||||
|
|
||||||
|
for date_obj in dates:
|
||||||
|
date_str = str(date_obj)
|
||||||
|
day_stats = {"date": date_str}
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
col_pred = f"{model}_prediction"
|
||||||
|
col_correct = 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
|
||||||
|
accuracy = (correct / total * 100) if total > 0 else 0
|
||||||
|
|
||||||
|
model_key = {
|
||||||
|
'dlinear': 'DLinear',
|
||||||
|
'nbeats': 'N-BEATS',
|
||||||
|
'ttm': 'TTM-R2',
|
||||||
|
'ensemble': 'Ensemble'
|
||||||
|
}.get(model, model)
|
||||||
|
|
||||||
|
day_stats[model_key] = {
|
||||||
|
"total": total,
|
||||||
|
"correct": correct,
|
||||||
|
"accuracy": round(accuracy, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
daily_data.append(day_stats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"data": data,
|
"data": daily_data,
|
||||||
"page": page,
|
"page": page,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"total": total_count,
|
"total": total_count,
|
||||||
"total_pages": (total_count + limit - 1) // limit if limit > 0 else 0
|
"total_pages": (total_count + limit - 1) // limit if limit > 0 else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_hourly_details(date: str) -> List[Dict]:
|
def get_hourly_details_v2(date: str) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
获取指定日期的每小时详情
|
获取指定日期的每小时详情 (V2)
|
||||||
|
|
||||||
Args:
|
|
||||||
date: 日期 (YYYY-MM-DD, CST 北京时间)
|
|
||||||
"""
|
"""
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
# 按 CST 日期筛选,并获取 CST 小时
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
*,
|
*,
|
||||||
HOUR(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as hour_cst,
|
HOUR(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as hour_cst,
|
||||||
CONVERT_TZ(timestamp, '+00:00', '+08:00') as timestamp_cst_dt
|
CONVERT_TZ(timestamp, '+00:00', '+08:00') as timestamp_cst_dt
|
||||||
FROM model_predictions
|
FROM hourly_model_predictions
|
||||||
WHERE DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) = %s
|
WHERE DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) = %s
|
||||||
ORDER BY hour_cst, model_name
|
ORDER BY hour_cst
|
||||||
""", (date,))
|
""", (date,))
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
result = []
|
||||||
|
|
||||||
# 按 CST 小时重组数据
|
|
||||||
hourly_data = {}
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
row_dict = _dict_from_row(row)
|
hour_data = {
|
||||||
hour_cst = row_dict['hour_cst'] # 使用 CST 小时
|
"hour": row['hour_cst'],
|
||||||
if hour_cst not in hourly_data:
|
"timestamp_et": row['timestamp_et'],
|
||||||
# 将 timestamp 转换为 CST 时间字符串
|
"timestamp_cst": convert_to_cst(row.get('timestamp', '')),
|
||||||
timestamp_cst = convert_to_cst(row_dict.get('timestamp', ''))
|
"open_price": row['open_price'],
|
||||||
hourly_data[hour_cst] = {
|
"close_price": row['close_price'],
|
||||||
"hour": hour_cst, # 返回 CST 小时
|
"actual_result": row['actual_result'],
|
||||||
"timestamp_et": row_dict['timestamp_et'],
|
"models": {
|
||||||
"timestamp_cst": timestamp_cst,
|
"DLinear": {
|
||||||
"open_price": row_dict['open_price'],
|
"prediction": row['dlinear_prediction'],
|
||||||
"close_price": row_dict['close_price'],
|
"probability": row['dlinear_prob'],
|
||||||
"actual_result": row_dict['actual_result'],
|
"is_correct": row['dlinear_correct']
|
||||||
"models": {}
|
},
|
||||||
|
"N-BEATS": {
|
||||||
|
"prediction": row['nbeats_prediction'],
|
||||||
|
"probability": row['nbeats_prob'],
|
||||||
|
"is_correct": row['nbeats_correct']
|
||||||
|
},
|
||||||
|
"TTM-R2": {
|
||||||
|
"prediction": row['ttm_prediction'],
|
||||||
|
"probability": row['ttm_prob'],
|
||||||
|
"is_correct": row['ttm_correct']
|
||||||
|
},
|
||||||
|
"Ensemble": {
|
||||||
|
"prediction": row['ensemble_prediction'],
|
||||||
|
"is_correct": row['ensemble_correct'],
|
||||||
|
"votes": row['ensemble_votes']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model = row_dict['model_name']
|
|
||||||
hourly_data[hour_cst]["models"][model] = {
|
|
||||||
"prediction": row_dict['prediction'],
|
|
||||||
"probability": row_dict['probability'],
|
|
||||||
"is_correct": row_dict['is_correct']
|
|
||||||
}
|
}
|
||||||
|
result.append(hour_data)
|
||||||
# 按 CST 小时排序返回
|
|
||||||
return [hourly_data[h] for h in sorted(hourly_data.keys())]
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_prediction(data: Dict) -> int:
|
||||||
|
"""兼容旧接口 - 如果需要的话,暂时保留,或者可以将旧表写入也放在这里"""
|
||||||
|
# 旧表写入逻辑... 略,或保持不变
|
||||||
|
# 为了完整性,我们可以保留对旧表的操作,或者逐步弃用。
|
||||||
|
# 这里为了安全,保留原方法的签名,实际调用旧表逻辑。
|
||||||
|
# 但我们主要逻辑将切换到 save_hourly_prediction
|
||||||
|
# 见 prediction_service.py 的修改
|
||||||
|
|
||||||
|
# ... (keep old logic or redirect) ...
|
||||||
|
# 这里我们假设 prediction_service.py 会改为调用 save_hourly_prediction
|
||||||
|
# 如果还有地方调用 save_prediction (比如测试代码), 我们可以保留它
|
||||||
|
return 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_missing_hours(date: str, model_name: str) -> List[int]:
|
def update_actual_result(date: str, hour: int, model_name: str,
|
||||||
|
actual_result: int, close_price: float) -> bool:
|
||||||
|
"""兼容旧接口"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_missing_hours(date: str, model_name: str = 'Ensemble') -> List[int]:
|
||||||
"""
|
"""
|
||||||
获取指定日期缺少预测的小时列表
|
获取指定日期缺少预测的小时列表 (V2)
|
||||||
|
model_name 参数现在主要用于兼容,实际检查整行是否存在
|
||||||
Args:
|
|
||||||
date: 日期 (YYYY-MM-DD)
|
|
||||||
model_name: 模型名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
缺少预测的小时列表
|
|
||||||
"""
|
"""
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT hour FROM model_predictions
|
SELECT hour FROM hourly_model_predictions
|
||||||
WHERE date = %s AND model_name = %s
|
WHERE date = %s
|
||||||
""", (date, model_name))
|
""", (date,))
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
existing_hours = {row['hour'] for row in rows}
|
existing_hours = {row['hour'] for row in rows}
|
||||||
|
|||||||
+48
-58
@@ -131,48 +131,37 @@ class PredictionService:
|
|||||||
|
|
||||||
def ensure_predictions_for_date(self, date: str) -> None:
|
def ensure_predictions_for_date(self, date: str) -> None:
|
||||||
"""
|
"""
|
||||||
确保指定日期有预测数据,如果没有则生成
|
确保指定日期有预测数据,如果没有则生成 (V2)
|
||||||
Args:
|
Args:
|
||||||
date: 日期 (YYYY-MM-DD)
|
date: 日期 (YYYY-MM-DD)
|
||||||
"""
|
"""
|
||||||
# 使用锁避免并发冲突
|
# 使用锁避免并发冲突
|
||||||
with PredictionService._prediction_lock:
|
with PredictionService._prediction_lock:
|
||||||
# 检查是否正在处理该日期
|
|
||||||
if date in self._processing_dates:
|
if date in self._processing_dates:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 标记正在处理
|
|
||||||
self._processing_dates.add(date)
|
self._processing_dates.add(date)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 检查是否有缺失的小时 (现在只检查 DLinear 和 TTM-R2 作为代表,或者扩展到 N-BEATS)
|
# 检查是否有缺失的小时 (只检查宽表是否有记录)
|
||||||
# 为了保持兼容性,还是主要检查这两个,或者可以增加 N-BEATS
|
missing_hours = PredictionModel.get_missing_hours(date)
|
||||||
models_to_check = ["DLinear", "TTM-R2", "N-BEATS"]
|
|
||||||
|
|
||||||
for model_name in models_to_check:
|
if not missing_hours:
|
||||||
# 数据库中模型名字对应
|
return
|
||||||
db_model_name = model_name
|
|
||||||
|
print(f"为 {date} 生成预测 (缺失 {len(missing_hours)} 小时)...")
|
||||||
missing_hours = PredictionModel.get_missing_hours(date, db_model_name)
|
|
||||||
|
# 获取足够的历史数据
|
||||||
if not missing_hours:
|
target_date = datetime.strptime(date, '%Y-%m-%d')
|
||||||
continue
|
df = self._get_historical_data(target_date)
|
||||||
|
|
||||||
print(f"为 {date} 生成 {model_name} 预测 (缺失 {len(missing_hours)} 小时)...")
|
if df is None or len(df) < 200:
|
||||||
|
print(f" 警告: 数据不足,无法为 {date} 生成预测")
|
||||||
# 获取足够的历史数据
|
return
|
||||||
target_date = datetime.strptime(date, '%Y-%m-%d')
|
|
||||||
df = self._get_historical_data(target_date)
|
# 为每个缺失的小时生成预测
|
||||||
|
for hour in missing_hours:
|
||||||
if df is None or len(df) < 200:
|
self._generate_prediction_for_hour(df, date, hour)
|
||||||
print(f" 警告: 数据不足,无法为 {date} 生成预测")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 为每个缺失的小时生成预测
|
|
||||||
for hour in missing_hours:
|
|
||||||
self._generate_prediction_for_hour(df, date, hour, model_name)
|
|
||||||
finally:
|
finally:
|
||||||
# 移除处理标记
|
|
||||||
with PredictionService._prediction_lock:
|
with PredictionService._prediction_lock:
|
||||||
self._processing_dates.discard(date)
|
self._processing_dates.discard(date)
|
||||||
|
|
||||||
@@ -194,10 +183,9 @@ class PredictionService:
|
|||||||
print(f"获取历史数据失败: {e}")
|
print(f"获取历史数据失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _generate_prediction_for_hour(self, df: pd.DataFrame, date: str,
|
def _generate_prediction_for_hour(self, df: pd.DataFrame, date: str, hour: int) -> None:
|
||||||
hour: int, model_name: str) -> None:
|
|
||||||
"""
|
"""
|
||||||
为指定小时生成预测
|
为指定小时生成预测 (V2)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 构建目标时间 (美东时间 -> UTC)
|
# 1. 构建目标时间 (美东时间 -> UTC)
|
||||||
@@ -234,45 +222,47 @@ class PredictionService:
|
|||||||
if len(df_input) < 200:
|
if len(df_input) < 200:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 6. 调用模型预测
|
# 6. 调用多模型预测
|
||||||
prediction = None
|
preds = self.inference_manager.predict_all(df_input)
|
||||||
probability = 0.5
|
|
||||||
|
|
||||||
if model_name == "DLinear":
|
|
||||||
prediction, probability = self.inference_manager.predict_dlinear(df_input)
|
|
||||||
elif model_name == "TTM-R2":
|
|
||||||
prediction, probability = self.inference_manager.predict_ttm(df_input)
|
|
||||||
elif model_name == "N-BEATS":
|
|
||||||
prediction, probability = self.inference_manager.predict_nbeats(df_input)
|
|
||||||
|
|
||||||
if prediction is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 7. 判断实际涨跌
|
# 7. 判断实际涨跌
|
||||||
actual_result = 1 if target_close > prev_close else 0
|
actual_result = 1 if target_close > prev_close else 0
|
||||||
|
|
||||||
# 8. 判断预测是否正确
|
# 8. 组装数据 (宽表格式)
|
||||||
is_correct = 1 if prediction == actual_result else 0
|
|
||||||
|
|
||||||
# 9. 保存预测结果
|
|
||||||
prediction_data = {
|
prediction_data = {
|
||||||
"timestamp": target_time_utc.isoformat(),
|
"timestamp": target_time_utc.isoformat(),
|
||||||
"timestamp_et": target_time_et.isoformat(),
|
"timestamp_et": target_time_et.isoformat(),
|
||||||
"date": date,
|
"date": date,
|
||||||
"hour": hour,
|
"hour": hour,
|
||||||
"model_name": model_name,
|
|
||||||
"prediction": prediction,
|
|
||||||
"probability": probability,
|
|
||||||
"actual_result": actual_result,
|
|
||||||
"is_correct": is_correct,
|
|
||||||
"open_price": target_open,
|
"open_price": target_open,
|
||||||
"close_price": target_close
|
"close_price": target_close,
|
||||||
|
"actual_result": actual_result,
|
||||||
|
|
||||||
|
# DLinear
|
||||||
|
"dlinear_prediction": preds.get('dlinear', {}).get('prediction'),
|
||||||
|
"dlinear_prob": preds.get('dlinear', {}).get('probability'),
|
||||||
|
"dlinear_correct": 1 if preds.get('dlinear', {}).get('prediction') == actual_result else 0,
|
||||||
|
|
||||||
|
# N-BEATS
|
||||||
|
"nbeats_prediction": preds.get('nbeats', {}).get('prediction'),
|
||||||
|
"nbeats_prob": preds.get('nbeats', {}).get('probability'),
|
||||||
|
"nbeats_correct": 1 if preds.get('nbeats', {}).get('prediction') == actual_result else 0,
|
||||||
|
|
||||||
|
# TTM-R2
|
||||||
|
"ttm_prediction": preds.get('ttm', {}).get('prediction'),
|
||||||
|
"ttm_prob": preds.get('ttm', {}).get('probability'),
|
||||||
|
"ttm_correct": 1 if preds.get('ttm', {}).get('prediction') == actual_result else 0,
|
||||||
|
|
||||||
|
# Ensemble
|
||||||
|
"ensemble_prediction": preds.get('ensemble', {}).get('prediction'),
|
||||||
|
"ensemble_votes": preds.get('ensemble', {}).get('votes'),
|
||||||
|
"ensemble_correct": 1 if preds.get('ensemble', {}).get('prediction') == actual_result else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
PredictionModel.save_prediction(prediction_data)
|
PredictionModel.save_hourly_prediction(prediction_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"生成预测失败 ({date} {hour}:00 {model_name}): {e}")
|
print(f"生成预测失败 ({date} {hour}:00): {e}")
|
||||||
|
|
||||||
def ensure_predictions_for_month(self, month: str) -> None:
|
def ensure_predictions_for_month(self, month: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+131
-77
@@ -74,8 +74,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">价格</div>
|
<div class="stat-label">策略</div>
|
||||||
<div class="stat-value">${{ currentTrade.price?.toFixed(3) || '-' }}</div>
|
<div class="stat-value small">{{ currentTrade.strategy || 'Ensemble' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@@ -116,6 +116,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>时间 (北京)</th>
|
<th>时间 (北京)</th>
|
||||||
<th>市场</th>
|
<th>市场</th>
|
||||||
|
<th>策略</th>
|
||||||
<th>预测</th>
|
<th>预测</th>
|
||||||
<th>概率</th>
|
<th>概率</th>
|
||||||
<th>价格</th>
|
<th>价格</th>
|
||||||
@@ -128,6 +129,9 @@
|
|||||||
<td class="text-truncate" style="max-width: 200px;">
|
<td class="text-truncate" style="max-width: 200px;">
|
||||||
{{ trade.market_title }}
|
{{ trade.market_title }}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ trade.strategy || 'Ensemble' }}</span>
|
||||||
|
</td>
|
||||||
<td :class="getPredictionClass(trade.prediction)">
|
<td :class="getPredictionClass(trade.prediction)">
|
||||||
<i :class="trade.prediction === 'UP' ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
<i :class="trade.prediction === 'UP' ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||||
{{ trade.prediction }}
|
{{ trade.prediction }}
|
||||||
@@ -139,7 +143,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!tradeHistory.data?.length">
|
<tr v-if="!tradeHistory.data?.length">
|
||||||
<td colspan="6" class="text-center text-muted py-4">
|
<td colspan="7" class="text-center text-muted py-4">
|
||||||
暂无数据
|
暂无数据
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -184,66 +188,80 @@
|
|||||||
|
|
||||||
<!-- 当月汇总卡片 -->
|
<!-- 当月汇总卡片 -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<!-- DLinear 卡片 -->
|
<!-- Ensemble 卡片 -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-3">
|
||||||
<div class="card h-100 border-primary">
|
<div class="card h-100 border-primary shadow-sm">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="card-header bg-primary text-white text-center">
|
||||||
<i class="bi bi-graph-up-arrow"></i> DLinear 模型
|
<i class="bi bi-diagram-3"></i> 集成策略 (投票)
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row text-center" v-if="monthlyStats.DLinear">
|
<div class="text-center" v-if="monthlyStats.Ensemble">
|
||||||
<div class="col-3">
|
<h2 class="display-6 text-primary fw-bold">{{ monthlyStats.Ensemble.accuracy }}%</h2>
|
||||||
<div class="stat-number">{{ monthlyStats.DLinear.total }}</div>
|
<div class="text-muted mb-2">准确率</div>
|
||||||
<div class="stat-label">预测次数</div>
|
<div class="d-flex justify-content-between px-3 border-top pt-2">
|
||||||
</div>
|
<span class="text-success"><i class="bi bi-check-lg"></i> {{ monthlyStats.Ensemble.correct }}</span>
|
||||||
<div class="col-3">
|
<span class="text-secondary">{{ monthlyStats.Ensemble.total }} 次</span>
|
||||||
<div class="stat-number text-success">{{ monthlyStats.DLinear.correct }}</div>
|
<span class="text-danger"><i class="bi bi-x-lg"></i> {{ monthlyStats.Ensemble.incorrect }}</span>
|
||||||
<div class="stat-label">正确</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="stat-number text-danger">{{ monthlyStats.DLinear.incorrect }}</div>
|
|
||||||
<div class="stat-label">错误</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="stat-number text-primary">{{ monthlyStats.DLinear.accuracy }}%</div>
|
|
||||||
<div class="stat-label">准确率</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center text-muted py-3">
|
<div v-else class="text-center text-muted py-3">暂无数据</div>
|
||||||
暂无数据
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DLinear 卡片 -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100 border-secondary">
|
||||||
|
<div class="card-header bg-secondary text-white text-center">
|
||||||
|
<i class="bi bi-graph-up-arrow"></i> DLinear
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center" v-if="monthlyStats.DLinear">
|
||||||
|
<h3 class="text-dark">{{ monthlyStats.DLinear.accuracy }}%</h3>
|
||||||
|
<div class="text-muted small">准确率</div>
|
||||||
|
<div class="mt-2 small text-muted">
|
||||||
|
{{ monthlyStats.DLinear.correct }}/{{ monthlyStats.DLinear.total }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="text-center text-muted py-3">暂无数据</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- N-BEATS 卡片 -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100 border-secondary">
|
||||||
|
<div class="card-header bg-secondary text-white text-center">
|
||||||
|
<i class="bi bi-soundwave"></i> N-BEATS
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center" v-if="monthlyStats['N-BEATS']">
|
||||||
|
<h3 class="text-dark">{{ monthlyStats['N-BEATS'].accuracy }}%</h3>
|
||||||
|
<div class="text-muted small">准确率</div>
|
||||||
|
<div class="mt-2 small text-muted">
|
||||||
|
{{ monthlyStats['N-BEATS'].correct }}/{{ monthlyStats['N-BEATS'].total }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center text-muted py-3">暂无数据</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TTM-R2 卡片 -->
|
<!-- TTM-R2 卡片 -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-3">
|
||||||
<div class="card h-100 border-success">
|
<div class="card h-100 border-secondary">
|
||||||
<div class="card-header bg-success text-white">
|
<div class="card-header bg-secondary text-white text-center">
|
||||||
<i class="bi bi-cpu"></i> TTM-R2 模型
|
<i class="bi bi-cpu"></i> TTM-R2
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row text-center" v-if="monthlyStats['TTM-R2']">
|
<div class="text-center" v-if="monthlyStats['TTM-R2']">
|
||||||
<div class="col-3">
|
<h3 class="text-dark">{{ monthlyStats['TTM-R2'].accuracy }}%</h3>
|
||||||
<div class="stat-number">{{ monthlyStats['TTM-R2'].total }}</div>
|
<div class="text-muted small">准确率</div>
|
||||||
<div class="stat-label">预测次数</div>
|
<div class="mt-2 small text-muted">
|
||||||
</div>
|
{{ monthlyStats['TTM-R2'].correct }}/{{ monthlyStats['TTM-R2'].total }}
|
||||||
<div class="col-3">
|
|
||||||
<div class="stat-number text-success">{{ monthlyStats['TTM-R2'].correct }}</div>
|
|
||||||
<div class="stat-label">正确</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="stat-number text-danger">{{ monthlyStats['TTM-R2'].incorrect }}</div>
|
|
||||||
<div class="stat-label">错误</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<div class="stat-number text-primary">{{ monthlyStats['TTM-R2'].accuracy }}%</div>
|
|
||||||
<div class="stat-label">准确率</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center text-muted py-3">
|
<div v-else class="text-center text-muted py-3">暂无数据</div>
|
||||||
暂无数据
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,52 +270,61 @@
|
|||||||
<!-- 每日准确率表格 -->
|
<!-- 每日准确率表格 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-calendar3"></i> 每日准确率
|
<i class="bi bi-calendar3"></i> 每日准确率概览
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0 text-center">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>日期 (北京)</th>
|
<th class="text-start ps-4">日期</th>
|
||||||
<th>DLinear 预测</th>
|
<th>集成策略</th>
|
||||||
<th>DLinear 准确率</th>
|
<th>DLinear</th>
|
||||||
<th>TTM-R2 预测</th>
|
<th>N-BEATS</th>
|
||||||
<th>TTM-R2 准确率</th>
|
<th>TTM-R2</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="day in dailyStats.data" :key="day.date">
|
<tr v-for="day in dailyStats.data" :key="day.date">
|
||||||
<td>{{ day.date }}</td>
|
<td class="text-start ps-4">{{ day.date }}</td>
|
||||||
|
|
||||||
|
<!-- Ensemble -->
|
||||||
<td>
|
<td>
|
||||||
<span v-if="day.DLinear">
|
<span v-if="day.Ensemble" :class="getAccuracyClass(day.Ensemble.accuracy)">
|
||||||
{{ day.DLinear.correct }}/{{ day.DLinear.total }}
|
{{ day.Ensemble.accuracy }}%
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted">-</span>
|
<span v-else class="text-muted">-</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- DLinear -->
|
||||||
<td>
|
<td>
|
||||||
<span v-if="day.DLinear" :class="getAccuracyClass(day.DLinear.accuracy)">
|
<span v-if="day.DLinear" :class="getAccuracyClass(day.DLinear.accuracy)">
|
||||||
{{ day.DLinear.accuracy }}%
|
{{ day.DLinear.accuracy }}%
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted">-</span>
|
<span v-else class="text-muted">-</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- N-BEATS -->
|
||||||
<td>
|
<td>
|
||||||
<span v-if="day['TTM-R2']">
|
<span v-if="day['N-BEATS']" :class="getAccuracyClass(day['N-BEATS'].accuracy)">
|
||||||
{{ day['TTM-R2'].correct }}/{{ day['TTM-R2'].total }}
|
{{ day['N-BEATS'].accuracy }}%
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted">-</span>
|
<span v-else class="text-muted">-</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- TTM-R2 -->
|
||||||
<td>
|
<td>
|
||||||
<span v-if="day['TTM-R2']" :class="getAccuracyClass(day['TTM-R2'].accuracy)">
|
<span v-if="day['TTM-R2']" :class="getAccuracyClass(day['TTM-R2'].accuracy)">
|
||||||
{{ day['TTM-R2'].accuracy }}%
|
{{ day['TTM-R2'].accuracy }}%
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted">-</span>
|
<span v-else class="text-muted">-</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary"
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
@click="showHourlyDetails(day.date)">
|
@click="showHourlyDetails(day.date)">
|
||||||
<i class="bi bi-eye"></i> 查看
|
<i class="bi bi-eye"></i> 详情
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -337,7 +364,7 @@
|
|||||||
|
|
||||||
<!-- 每小时详情弹窗 -->
|
<!-- 每小时详情弹窗 -->
|
||||||
<div class="modal fade" id="hourlyModal" tabindex="-1">
|
<div class="modal fade" id="hourlyModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">
|
<h5 class="modal-title">
|
||||||
@@ -346,43 +373,70 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body p-0">
|
<div class="modal-body p-0">
|
||||||
<table class="table table-sm table-hover mb-0">
|
<table class="table table-sm table-hover mb-0 text-center align-middle">
|
||||||
<thead class="table-light sticky-top">
|
<thead class="table-light sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th>时间 (北京)</th>
|
<th>时间</th>
|
||||||
<th>实际涨跌</th>
|
<th>开盘</th>
|
||||||
|
<th>收盘</th>
|
||||||
|
<th>实际</th>
|
||||||
|
<th class="table-primary">集成策略</th>
|
||||||
<th>DLinear</th>
|
<th>DLinear</th>
|
||||||
|
<th>N-BEATS</th>
|
||||||
<th>TTM-R2</th>
|
<th>TTM-R2</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="hour in hourlyModal.data" :key="hour.hour">
|
<tr v-for="hour in hourlyModal.data" :key="hour.hour">
|
||||||
<td>{{ hour.timestamp_cst || (hour.hour + ':00') }}</td>
|
<td>{{ hour.timestamp_cst ? hour.timestamp_cst.split(' ')[1] : (hour.hour + ':00') }}</td>
|
||||||
<td :class="hour.actual_result === 1 ? 'text-success' : 'text-danger'">
|
<td>${{ hour.open_price?.toFixed(0) || '-' }}</td>
|
||||||
<i :class="hour.actual_result === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
<td>${{ hour.close_price?.toFixed(0) || '-' }}</td>
|
||||||
{{ hour.actual_result === 1 ? 'UP' : 'DOWN' }}
|
|
||||||
|
<td :class="hour.actual_result === 1 ? 'text-success fw-bold' : (hour.actual_result === 0 ? 'text-danger fw-bold' : 'text-muted')">
|
||||||
|
<i v-if="hour.actual_result !== null" :class="hour.actual_result === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||||
|
{{ hour.actual_result === 1 ? 'UP' : (hour.actual_result === 0 ? 'DOWN' : '-') }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Ensemble -->
|
||||||
|
<td class="table-primary bg-opacity-10">
|
||||||
|
<span v-if="hour.models?.Ensemble"
|
||||||
|
:class="hour.models.Ensemble.is_correct ? 'text-success' : 'text-danger'">
|
||||||
|
<i :class="hour.models.Ensemble.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||||
|
{{ hour.models.Ensemble.prediction === 1 ? 'UP' : 'DOWN' }}
|
||||||
|
<i v-if="hour.actual_result !== null" :class="hour.models.Ensemble.is_correct ? 'bi-check-circle-fill' : 'bi-x-circle-fill'"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- DLinear -->
|
||||||
<td>
|
<td>
|
||||||
<span v-if="hour.models?.DLinear"
|
<span v-if="hour.models?.DLinear"
|
||||||
:class="hour.models.DLinear.is_correct ? 'text-success' : 'text-danger'">
|
:class="hour.models.DLinear.is_correct ? 'text-success' : 'text-danger'">
|
||||||
<i :class="hour.models.DLinear.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
<i :class="hour.models.DLinear.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||||
{{ hour.models.DLinear.prediction === 1 ? 'UP' : 'DOWN' }}
|
<span class="small text-muted">({{ (hour.models.DLinear.probability * 100).toFixed(0) }}%)</span>
|
||||||
<i :class="hour.models.DLinear.is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
|
<i v-if="hour.actual_result !== null" :class="hour.models.DLinear.is_correct ? 'bi-check' : 'bi-x'"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted">-</span>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- N-BEATS -->
|
||||||
|
<td>
|
||||||
|
<span v-if="hour.models?.['N-BEATS']"
|
||||||
|
:class="hour.models['N-BEATS'].is_correct ? 'text-success' : 'text-danger'">
|
||||||
|
<i :class="hour.models['N-BEATS'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||||
|
<i v-if="hour.actual_result !== null" :class="hour.models['N-BEATS'].is_correct ? 'bi-check' : 'bi-x'"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- TTM-R2 -->
|
||||||
<td>
|
<td>
|
||||||
<span v-if="hour.models?.['TTM-R2']"
|
<span v-if="hour.models?.['TTM-R2']"
|
||||||
:class="hour.models['TTM-R2'].is_correct ? 'text-success' : 'text-danger'">
|
:class="hour.models['TTM-R2'].is_correct ? 'text-success' : 'text-danger'">
|
||||||
<i :class="hour.models['TTM-R2'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
<i :class="hour.models['TTM-R2'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
||||||
{{ hour.models['TTM-R2'].prediction === 1 ? 'UP' : 'DOWN' }}
|
<i v-if="hour.actual_result !== null" :class="hour.models['TTM-R2'].is_correct ? 'bi-check' : 'bi-x'"></i>
|
||||||
<i :class="hour.models['TTM-R2'].is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
|
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted">-</span>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!hourlyModal.data?.length">
|
<tr v-if="!hourlyModal.data?.length">
|
||||||
<td colspan="4" class="text-center text-muted py-4">
|
<td colspan="8" class="text-center text-muted py-4">
|
||||||
暂无数据
|
暂无数据
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+3
-2
@@ -72,8 +72,8 @@ class TradingDatabase:
|
|||||||
INSERT INTO trades (
|
INSERT INTO trades (
|
||||||
timestamp, market_title, market_slug, model_name,
|
timestamp, market_title, market_slug, model_name,
|
||||||
prediction, probability, action, price, amount, size,
|
prediction, probability, action, price, amount, size,
|
||||||
status, error_message, raw_data
|
status, strategy, error_message, raw_data
|
||||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
cursor.execute(sql, (
|
cursor.execute(sql, (
|
||||||
@@ -88,6 +88,7 @@ class TradingDatabase:
|
|||||||
trade_data.get('amount'),
|
trade_data.get('amount'),
|
||||||
trade_data.get('size'),
|
trade_data.get('size'),
|
||||||
trade_data.get('status'),
|
trade_data.get('status'),
|
||||||
|
trade_data.get('strategy'),
|
||||||
trade_data.get('error_message'),
|
trade_data.get('error_message'),
|
||||||
raw_data_json
|
raw_data_json
|
||||||
))
|
))
|
||||||
|
|||||||
Reference in new issue
Block a user