update
This commit is contained in:
parent
65276c5beb
commit
aa800c74d0
@ -102,12 +102,14 @@ class CoinSelectionEngine:
|
|||||||
# 6. 保存选币结果到数据库
|
# 6. 保存选币结果到数据库
|
||||||
self.logger.info(f"保存{len(all_signals)}个选币结果到数据库...")
|
self.logger.info(f"保存{len(all_signals)}个选币结果到数据库...")
|
||||||
saved_count = 0
|
saved_count = 0
|
||||||
|
saved_long = 0
|
||||||
|
saved_short = 0
|
||||||
|
|
||||||
for signal in all_signals:
|
for signal in all_signals:
|
||||||
try:
|
try:
|
||||||
selection_id = self.db.insert_coin_selection(
|
selection_id = self.db.insert_coin_selection(
|
||||||
symbol=signal.symbol,
|
symbol=signal.symbol,
|
||||||
score=signal.score,
|
qualified_factors=signal.qualified_factors,
|
||||||
reason=signal.reason,
|
reason=signal.reason,
|
||||||
entry_price=signal.entry_price,
|
entry_price=signal.entry_price,
|
||||||
stop_loss=signal.stop_loss,
|
stop_loss=signal.stop_loss,
|
||||||
@ -125,24 +127,30 @@ class CoinSelectionEngine:
|
|||||||
self.logger.info(f"保存{signal.symbol}({signal.strategy_type}-{signal_type_cn})选币结果,ID: {selection_id}")
|
self.logger.info(f"保存{signal.symbol}({signal.strategy_type}-{signal_type_cn})选币结果,ID: {selection_id}")
|
||||||
saved_count += 1
|
saved_count += 1
|
||||||
|
|
||||||
|
# 统计实际保存的多空数量
|
||||||
|
if signal.signal_type == "LONG":
|
||||||
|
saved_long += 1
|
||||||
|
else:
|
||||||
|
saved_short += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"保存{signal.symbol}选币结果失败: {e}")
|
self.logger.error(f"保存{signal.symbol}选币结果失败: {e}")
|
||||||
|
|
||||||
# 检查并标记过期的选币
|
# 检查并标记过期的选币
|
||||||
self.db.check_and_expire_selections()
|
self.db.check_and_expire_selections()
|
||||||
|
|
||||||
self.logger.info(f"选币完成!成功保存{saved_count}个信号(多头: {len(long_signals)}个, 空头: {len(short_signals)}个)")
|
self.logger.info(f"选币完成!成功保存{saved_count}个信号(多头: {saved_long}个, 空头: {saved_short}个)")
|
||||||
|
|
||||||
# # 发送钉钉通知
|
# 发送钉钉通知
|
||||||
# try:
|
try:
|
||||||
# self.logger.info("发送钉钉通知...")
|
self.logger.info("发送钉钉通知...")
|
||||||
# notification_sent = self.dingtalk_notifier.send_coin_selection_notification(all_signals)
|
notification_sent = self.dingtalk_notifier.send_coin_selection_notification(all_signals)
|
||||||
# if notification_sent:
|
if notification_sent:
|
||||||
# self.logger.info("✅ 钉钉通知发送成功")
|
self.logger.info("✅ 钉钉通知发送成功")
|
||||||
# else:
|
else:
|
||||||
# self.logger.info("📱 钉钉通知发送失败或未配置")
|
self.logger.info("📱 钉钉通知发送失败或未配置")
|
||||||
# except Exception as e:
|
except Exception as e:
|
||||||
# self.logger.error(f"发送钉钉通知时出错: {e}")
|
self.logger.error(f"发送钉钉通知时出错: {e}")
|
||||||
|
|
||||||
return all_signals
|
return all_signals
|
||||||
|
|
||||||
@ -159,18 +167,18 @@ class CoinSelectionEngine:
|
|||||||
key = f"{strategy}-{signal_type}"
|
key = f"{strategy}-{signal_type}"
|
||||||
|
|
||||||
if key not in strategy_stats:
|
if key not in strategy_stats:
|
||||||
strategy_stats[key] = {'count': 0, 'avg_score': 0, 'scores': []}
|
strategy_stats[key] = {'count': 0, 'avg_factors': 0, 'factors': []}
|
||||||
|
|
||||||
strategy_stats[key]['count'] += 1
|
strategy_stats[key]['count'] += 1
|
||||||
strategy_stats[key]['scores'].append(signal.score)
|
strategy_stats[key]['factors'].append(signal.qualified_factors)
|
||||||
|
|
||||||
# 计算平均分数
|
# 计算平均符合因子数
|
||||||
for key, stats in strategy_stats.items():
|
for key, stats in strategy_stats.items():
|
||||||
stats['avg_score'] = sum(stats['scores']) / len(stats['scores'])
|
stats['avg_factors'] = sum(stats['factors']) / len(stats['factors'])
|
||||||
|
|
||||||
self.logger.info("策略分布统计:")
|
self.logger.info("策略分布统计:")
|
||||||
for key, stats in sorted(strategy_stats.items(), key=lambda x: x[1]['count'], reverse=True):
|
for key, stats in sorted(strategy_stats.items(), key=lambda x: x[1]['count'], reverse=True):
|
||||||
self.logger.info(f" {key}: {stats['count']}个信号, 平均分数: {stats['avg_score']:.1f}")
|
self.logger.info(f" {key}: {stats['count']}个信号, 平均符合因子: {stats['avg_factors']:.1f}/4")
|
||||||
|
|
||||||
def run_strategy_specific_analysis(self, symbols: List[str], strategy_name: str) -> List[CoinSignal]:
|
def run_strategy_specific_analysis(self, symbols: List[str], strategy_name: str) -> List[CoinSignal]:
|
||||||
"""针对特定策略运行专门的分析"""
|
"""针对特定策略运行专门的分析"""
|
||||||
@ -217,7 +225,7 @@ class CoinSelectionEngine:
|
|||||||
conn = self.db.get_connection()
|
conn = self.db.get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT id, symbol, score, reason, entry_price, stop_loss, take_profit,
|
SELECT id, symbol, qualified_factors, reason, entry_price, stop_loss, take_profit,
|
||||||
timeframe, selection_time, status, actual_entry_price, exit_price,
|
timeframe, selection_time, status, actual_entry_price, exit_price,
|
||||||
exit_time, pnl_percentage, notes, strategy_type, holding_period,
|
exit_time, pnl_percentage, notes, strategy_type, holding_period,
|
||||||
risk_reward_ratio, expiry_time, is_expired, action_suggestion,
|
risk_reward_ratio, expiry_time, is_expired, action_suggestion,
|
||||||
@ -233,7 +241,7 @@ class CoinSelectionEngine:
|
|||||||
selection = {
|
selection = {
|
||||||
'id': detailed_row[0],
|
'id': detailed_row[0],
|
||||||
'symbol': detailed_row[1],
|
'symbol': detailed_row[1],
|
||||||
'score': detailed_row[2],
|
'qualified_factors': detailed_row[2],
|
||||||
'reason': detailed_row[3],
|
'reason': detailed_row[3],
|
||||||
'entry_price': detailed_row[4],
|
'entry_price': detailed_row[4],
|
||||||
'stop_loss': detailed_row[5],
|
'stop_loss': detailed_row[5],
|
||||||
@ -324,7 +332,7 @@ class CoinSelectionEngine:
|
|||||||
|
|
||||||
for i, signal in enumerate(signals, 1):
|
for i, signal in enumerate(signals, 1):
|
||||||
summary += f"{i}. {signal.symbol} [{signal.strategy_type}] - {signal.action_suggestion}\n"
|
summary += f"{i}. {signal.symbol} [{signal.strategy_type}] - {signal.action_suggestion}\n"
|
||||||
summary += f" 评分: {signal.score:.1f}分 ({signal.confidence}信心)\n"
|
summary += f" 符合因子: {signal.qualified_factors}/4 ({signal.confidence}信心)\n"
|
||||||
summary += f" 理由: {signal.reason}\n"
|
summary += f" 理由: {signal.reason}\n"
|
||||||
summary += f" 入场: ${signal.entry_price:.4f}\n"
|
summary += f" 入场: ${signal.entry_price:.4f}\n"
|
||||||
summary += f" 止损: ${signal.stop_loss:.4f} ({((signal.stop_loss - signal.entry_price) / signal.entry_price * 100):.2f}%)\n"
|
summary += f" 止损: ${signal.stop_loss:.4f} ({((signal.stop_loss - signal.entry_price) / signal.entry_price * 100):.2f}%)\n"
|
||||||
|
|||||||
73
database.py
73
database.py
@ -93,7 +93,59 @@ class DatabaseManager:
|
|||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# 检查并添加新列(如果表已存在)
|
# 修改score字段为可空(如果表已存在)
|
||||||
|
try:
|
||||||
|
# SQLite不能直接修改列约束,所以我们需要检查并处理
|
||||||
|
cursor.execute("PRAGMA table_info(coin_selections)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
|
||||||
|
# 检查是否需要重建表结构
|
||||||
|
score_column = next((col for col in columns if col[1] == 'score'), None)
|
||||||
|
if score_column and score_column[3] == 1: # NOT NULL constraint exists
|
||||||
|
# 备份现有数据
|
||||||
|
cursor.execute("ALTER TABLE coin_selections RENAME TO coin_selections_backup")
|
||||||
|
|
||||||
|
# 重新创建表(score字段改为可空)
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE coin_selections (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
score REAL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
entry_price REAL NOT NULL,
|
||||||
|
stop_loss REAL NOT NULL,
|
||||||
|
take_profit REAL NOT NULL,
|
||||||
|
timeframe TEXT NOT NULL,
|
||||||
|
selection_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
actual_entry_price REAL,
|
||||||
|
exit_price REAL,
|
||||||
|
exit_time TIMESTAMP,
|
||||||
|
pnl_percentage REAL,
|
||||||
|
notes TEXT,
|
||||||
|
strategy_type TEXT NOT NULL DEFAULT '中线',
|
||||||
|
holding_period INTEGER NOT NULL DEFAULT 7,
|
||||||
|
risk_reward_ratio REAL NOT NULL DEFAULT 2.0,
|
||||||
|
expiry_time TIMESTAMP,
|
||||||
|
is_expired BOOLEAN DEFAULT FALSE,
|
||||||
|
action_suggestion TEXT DEFAULT '等待回调买入',
|
||||||
|
signal_type TEXT DEFAULT 'LONG',
|
||||||
|
direction TEXT DEFAULT 'BUY',
|
||||||
|
qualified_factors INTEGER NOT NULL DEFAULT 3
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 迁移数据
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO coin_selections
|
||||||
|
SELECT * FROM coin_selections_backup
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 删除备份表
|
||||||
|
cursor.execute("DROP TABLE coin_selections_backup")
|
||||||
|
except Exception as e:
|
||||||
|
# 如果出错,可能是表不存在或已经正确,继续执行
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE coin_selections ADD COLUMN strategy_type TEXT NOT NULL DEFAULT '中线'")
|
cursor.execute("ALTER TABLE coin_selections ADD COLUMN strategy_type TEXT NOT NULL DEFAULT '中线'")
|
||||||
except:
|
except:
|
||||||
@ -135,6 +187,12 @@ class DatabaseManager:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 添加新的符合因子字段
|
||||||
|
try:
|
||||||
|
cursor.execute("ALTER TABLE coin_selections ADD COLUMN qualified_factors INTEGER NOT NULL DEFAULT 3")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# 技术指标表
|
# 技术指标表
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS technical_indicators (
|
CREATE TABLE IF NOT EXISTS technical_indicators (
|
||||||
@ -175,7 +233,7 @@ class DatabaseManager:
|
|||||||
"""获取数据库连接"""
|
"""获取数据库连接"""
|
||||||
return sqlite3.connect(self.db_path)
|
return sqlite3.connect(self.db_path)
|
||||||
|
|
||||||
def insert_coin_selection(self, symbol, score, reason, entry_price, stop_loss, take_profit,
|
def insert_coin_selection(self, symbol, qualified_factors, reason, entry_price, stop_loss, take_profit,
|
||||||
timeframe, strategy_type, holding_period, risk_reward_ratio, expiry_hours, action_suggestion,
|
timeframe, strategy_type, holding_period, risk_reward_ratio, expiry_hours, action_suggestion,
|
||||||
signal_type="LONG", direction="BUY"):
|
signal_type="LONG", direction="BUY"):
|
||||||
"""插入选币结果 - 支持多空方向"""
|
"""插入选币结果 - 支持多空方向"""
|
||||||
@ -185,13 +243,16 @@ class DatabaseManager:
|
|||||||
# 计算过期时间
|
# 计算过期时间
|
||||||
expiry_time = datetime.now(timezone.utc) + timedelta(hours=expiry_hours)
|
expiry_time = datetime.now(timezone.utc) + timedelta(hours=expiry_hours)
|
||||||
|
|
||||||
|
# 将qualified_factors转换为分数(兼容旧系统)
|
||||||
|
score = qualified_factors * 25.0 if qualified_factors else 75.0 # 3/4 = 75分,4/4 = 100分
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO coin_selections
|
INSERT INTO coin_selections
|
||||||
(symbol, score, reason, entry_price, stop_loss, take_profit, timeframe,
|
(symbol, score, qualified_factors, reason, entry_price, stop_loss, take_profit, timeframe,
|
||||||
strategy_type, holding_period, risk_reward_ratio, expiry_time, action_suggestion,
|
strategy_type, holding_period, risk_reward_ratio, expiry_time, action_suggestion,
|
||||||
signal_type, direction)
|
signal_type, direction)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (symbol, score, reason, entry_price, stop_loss, take_profit, timeframe,
|
''', (symbol, score, qualified_factors, reason, entry_price, stop_loss, take_profit, timeframe,
|
||||||
strategy_type, holding_period, risk_reward_ratio, expiry_time, action_suggestion,
|
strategy_type, holding_period, risk_reward_ratio, expiry_time, action_suggestion,
|
||||||
signal_type, direction))
|
signal_type, direction))
|
||||||
|
|
||||||
@ -215,7 +276,7 @@ class DatabaseManager:
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT * FROM coin_selections
|
SELECT * FROM coin_selections
|
||||||
WHERE status = 'active' AND is_expired = FALSE
|
WHERE status = 'active' AND is_expired = FALSE
|
||||||
ORDER BY selection_time DESC, score DESC
|
ORDER BY selection_time DESC, qualified_factors DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
''', (limit, offset))
|
''', (limit, offset))
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
597
templates/dashboard_table.html
Normal file
597
templates/dashboard_table.html
Normal file
@ -0,0 +1,597 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AI选币系统 - 表格视图</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 1rem 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3b82f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #3b82f6;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-group {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeframe-subgroup {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-header {
|
||||||
|
background: #64748b;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-header.timeframe-15m {
|
||||||
|
background: #ef4444;
|
||||||
|
border-left-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-header.timeframe-1h {
|
||||||
|
background: #f59e0b;
|
||||||
|
border-left-color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-header.timeframe-4h {
|
||||||
|
background: #10b981;
|
||||||
|
border-left-color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-header.timeframe-1d {
|
||||||
|
background: #8b5cf6;
|
||||||
|
border-left-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeframe-subgroup:first-child .subgroup-header {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeframe-subgroup:last-child .coins-table {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coins-table {
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coins-table table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coins-table th,
|
||||||
|
.coins-table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coins-table th {
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coins-table tbody tr:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin-symbol-cell {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factors-cell {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factors-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factors-6 {
|
||||||
|
background: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factors-5 {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factors-4 {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factors-3 {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeframe-cell {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeframe-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-type-cell {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-long {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #15803d;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-short {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #b91c1c;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-cell {
|
||||||
|
text-align: right;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||||
|
min-width: 90px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-entry {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-stop {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-profit {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-cell {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
background: #64748b; /* 默认背景色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用JavaScript动态设置颜色,这里先设置默认样式 */
|
||||||
|
|
||||||
|
.action-cell {
|
||||||
|
max-width: 200px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-wait {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-ready {
|
||||||
|
color: #059669;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-cell {
|
||||||
|
max-width: 280px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-cell {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #64748b;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data i {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coins-table {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coins-table th,
|
||||||
|
.coins-table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-cell {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-cell {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="nav-content">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
AI选币系统
|
||||||
|
</div>
|
||||||
|
<button class="refresh-btn" onclick="refreshData()" id="refresh-btn">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
刷新数据
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-container">
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon"><i class="fas fa-coins"></i></div>
|
||||||
|
<div class="stat-value">{{ total_signals }}</div>
|
||||||
|
<div class="stat-label">总信号数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon"><i class="fas fa-arrow-up" style="color: #10b981;"></i></div>
|
||||||
|
<div class="stat-value">{{ long_signals }}</div>
|
||||||
|
<div class="stat-label">多头信号</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon"><i class="fas fa-arrow-down" style="color: #ef4444;"></i></div>
|
||||||
|
<div class="stat-value">{{ short_signals }}</div>
|
||||||
|
<div class="stat-label">空头信号</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon"><i class="fas fa-clock"></i></div>
|
||||||
|
<div class="stat-value">{{ update_time }}</div>
|
||||||
|
<div class="stat-label">更新时间</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 选币结果表格 -->
|
||||||
|
<div id="selections-container">
|
||||||
|
{% if grouped_selections %}
|
||||||
|
{% for group_time, timeframe_groups in grouped_selections.items() %}
|
||||||
|
<div class="time-group">
|
||||||
|
<div class="group-header">
|
||||||
|
<i class="fas fa-calendar-alt"></i>
|
||||||
|
{{ group_time }}
|
||||||
|
{% set total_coins = timeframe_groups.values() | map('length') | sum %}
|
||||||
|
({{ total_coins }}个币种)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间级别分组 -->
|
||||||
|
{% for timeframe, selections in timeframe_groups.items() %}
|
||||||
|
<div class="timeframe-subgroup">
|
||||||
|
<div class="subgroup-header timeframe-{{ timeframe }}">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
{{ timeframe }} 时间级别 ({{ selections|length }}个信号)
|
||||||
|
</div>
|
||||||
|
<div class="coins-table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>币种</th>
|
||||||
|
<th>符合因子</th>
|
||||||
|
<th>信号类型</th>
|
||||||
|
<th>策略</th>
|
||||||
|
<th>入场价</th>
|
||||||
|
<th>止损价</th>
|
||||||
|
<th>止盈价</th>
|
||||||
|
<th>操作建议</th>
|
||||||
|
<th>选择理由</th>
|
||||||
|
<th>时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for selection in selections %}
|
||||||
|
<tr>
|
||||||
|
<td class="coin-symbol-cell">
|
||||||
|
{{ selection.symbol.replace('USDT', '') }}
|
||||||
|
</td>
|
||||||
|
<td class="factors-cell">
|
||||||
|
<span class="factors-badge factors-{{ selection.qualified_factors if selection.qualified_factors is not none else selection.score|round|int }}">
|
||||||
|
{{ selection.qualified_factors if selection.qualified_factors is not none else selection.score|round|int }}/6
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="signal-type-cell">
|
||||||
|
<span class="signal-{{ 'short' if selection.signal_type == 'SHORT' else 'long' }}">
|
||||||
|
{{ '做空' if selection.signal_type == 'SHORT' else '做多' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="strategy-cell">
|
||||||
|
<span class="strategy-badge">
|
||||||
|
{{ selection.strategy_type }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="price-cell price-entry">
|
||||||
|
${{ "%.4f"|format(selection.entry_price) }}
|
||||||
|
</td>
|
||||||
|
<td class="price-cell price-stop">
|
||||||
|
${{ "%.4f"|format(selection.stop_loss) }}
|
||||||
|
</td>
|
||||||
|
<td class="price-cell price-profit">
|
||||||
|
${{ "%.4f"|format(selection.take_profit) }}
|
||||||
|
</td>
|
||||||
|
<td class="action-cell">
|
||||||
|
<span class="{{ 'action-ready' if '分批' in selection.action_suggestion else 'action-wait' }}">
|
||||||
|
{{ selection.action_suggestion }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="reason-cell">
|
||||||
|
{{ selection.reason }}
|
||||||
|
</td>
|
||||||
|
<td class="time-cell">
|
||||||
|
{{ selection.selection_time.split(' ')[1][:5] if selection.selection_time else '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- 加载更多按钮 -->
|
||||||
|
{% if total_count >= current_limit %}
|
||||||
|
<div class="load-more-container">
|
||||||
|
<button class="load-more-btn" onclick="loadMoreData()">
|
||||||
|
<i class="fas fa-arrow-down"></i>
|
||||||
|
加载更多
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="no-data">
|
||||||
|
<div><i class="fas fa-chart-line"></i></div>
|
||||||
|
<h3>暂无选币数据</h3>
|
||||||
|
<p>请稍后刷新或运行选币程序</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 设置策略标签颜色
|
||||||
|
function setStrategyColors() {
|
||||||
|
document.querySelectorAll('.strategy-badge').forEach(badge => {
|
||||||
|
const text = badge.textContent.trim();
|
||||||
|
if (text.includes('极品信号')) {
|
||||||
|
badge.style.background = '#7c3aed'; // 紫色
|
||||||
|
} else if (text.includes('优质信号')) {
|
||||||
|
badge.style.background = '#10b981'; // 绿色
|
||||||
|
} else if (text.includes('标准信号')) {
|
||||||
|
badge.style.background = '#3b82f6'; // 蓝色
|
||||||
|
} else if (text.includes('基础信号')) {
|
||||||
|
badge.style.background = '#64748b'; // 灰色
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后设置颜色
|
||||||
|
document.addEventListener('DOMContentLoaded', setStrategyColors);
|
||||||
|
|
||||||
|
async function refreshData() {
|
||||||
|
const btn = document.getElementById('refresh-btn');
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 刷新中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/refresh');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('刷新失败: ' + result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('刷新失败: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreData() {
|
||||||
|
// 实现加载更多逻辑
|
||||||
|
console.log('加载更多数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动刷新
|
||||||
|
setInterval(() => {
|
||||||
|
const now = new Date();
|
||||||
|
if (now.getMinutes() % 30 === 0 && now.getSeconds() < 10) {
|
||||||
|
refreshData();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
51
web_app.py
51
web_app.py
@ -60,7 +60,7 @@ async def dashboard(request: Request, limit: int = 20, offset: int = 0):
|
|||||||
lambda: engine.get_latest_selections(limit + 5, offset)
|
lambda: engine.get_latest_selections(limit + 5, offset)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 按年月日时分分组选币结果,转换时间为东八区显示
|
# 按年月日时分分组选币结果,再按时间级别二级分组
|
||||||
grouped_selections = {}
|
grouped_selections = {}
|
||||||
latest_update_time = None
|
latest_update_time = None
|
||||||
|
|
||||||
@ -77,12 +77,17 @@ async def dashboard(request: Request, limit: int = 20, offset: int = 0):
|
|||||||
time_key = beijing_time[:16] # "YYYY-MM-DD HH:MM"
|
time_key = beijing_time[:16] # "YYYY-MM-DD HH:MM"
|
||||||
|
|
||||||
if time_key not in grouped_selections:
|
if time_key not in grouped_selections:
|
||||||
grouped_selections[time_key] = []
|
grouped_selections[time_key] = {}
|
||||||
|
|
||||||
|
# 按时间级别进行二级分组
|
||||||
|
timeframe = selection.get('timeframe', '未知')
|
||||||
|
if timeframe not in grouped_selections[time_key]:
|
||||||
|
grouped_selections[time_key][timeframe] = []
|
||||||
|
|
||||||
# 更新选币记录的显示时间,但不修改原始时间
|
# 更新选币记录的显示时间,但不修改原始时间
|
||||||
selection_copy = selection.copy()
|
selection_copy = selection.copy()
|
||||||
selection_copy['selection_time'] = beijing_time
|
selection_copy['selection_time'] = beijing_time
|
||||||
grouped_selections[time_key].append(selection_copy)
|
grouped_selections[time_key][timeframe].append(selection_copy)
|
||||||
|
|
||||||
# 按时间降序排序(最新的在前面)
|
# 按时间降序排序(最新的在前面)
|
||||||
sorted_grouped_selections = dict(sorted(
|
sorted_grouped_selections = dict(sorted(
|
||||||
@ -91,13 +96,24 @@ async def dashboard(request: Request, limit: int = 20, offset: int = 0):
|
|||||||
reverse=True
|
reverse=True
|
||||||
))
|
))
|
||||||
|
|
||||||
return templates.TemplateResponse("dashboard.html", {
|
# 对每个时间组内的时间级别进行排序(15m, 1h, 4h, 1d)
|
||||||
|
timeframe_order = {'15m': 1, '1h': 2, '4h': 3, '1d': 4}
|
||||||
|
for time_key in sorted_grouped_selections:
|
||||||
|
sorted_timeframes = dict(sorted(
|
||||||
|
sorted_grouped_selections[time_key].items(),
|
||||||
|
key=lambda x: timeframe_order.get(x[0], 99)
|
||||||
|
))
|
||||||
|
sorted_grouped_selections[time_key] = sorted_timeframes
|
||||||
|
|
||||||
|
return templates.TemplateResponse("dashboard_table.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"grouped_selections": sorted_grouped_selections,
|
"grouped_selections": sorted_grouped_selections,
|
||||||
"last_update": latest_update_time + " CST" if latest_update_time else "暂无数据",
|
"update_time": latest_update_time.split(' ')[1][:5] if latest_update_time else "--:--",
|
||||||
|
"total_signals": len(selections),
|
||||||
|
"long_signals": len([s for s in selections if s.get('signal_type') == 'LONG']),
|
||||||
|
"short_signals": len([s for s in selections if s.get('signal_type') == 'SHORT']),
|
||||||
"total_count": len(selections),
|
"total_count": len(selections),
|
||||||
"current_limit": limit,
|
"current_limit": limit
|
||||||
"current_offset": offset
|
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@ -126,6 +142,27 @@ async def get_selections(limit: int = 20, offset: int = 0):
|
|||||||
"message": str(e)
|
"message": str(e)
|
||||||
}, status_code=500)
|
}, status_code=500)
|
||||||
|
|
||||||
|
@app.get("/api/refresh")
|
||||||
|
async def refresh_data():
|
||||||
|
"""刷新数据API - 仅刷新显示数据,不执行选币"""
|
||||||
|
try:
|
||||||
|
# 清除缓存
|
||||||
|
cache_data.clear()
|
||||||
|
|
||||||
|
# 获取现有数据进行显示
|
||||||
|
selections = engine.get_latest_selections(50, 0)
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"message": f"数据刷新完成,当前有{len(selections)}个选币记录",
|
||||||
|
"count": len(selections)
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse({
|
||||||
|
"success": False,
|
||||||
|
"message": str(e)
|
||||||
|
}, status_code=500)
|
||||||
|
|
||||||
@app.post("/api/run_selection")
|
@app.post("/api/run_selection")
|
||||||
async def run_selection():
|
async def run_selection():
|
||||||
"""执行选币API"""
|
"""执行选币API"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user