update
This commit is contained in:
parent
65276c5beb
commit
aa800c74d0
@ -102,12 +102,14 @@ class CoinSelectionEngine:
|
||||
# 6. 保存选币结果到数据库
|
||||
self.logger.info(f"保存{len(all_signals)}个选币结果到数据库...")
|
||||
saved_count = 0
|
||||
saved_long = 0
|
||||
saved_short = 0
|
||||
|
||||
for signal in all_signals:
|
||||
try:
|
||||
selection_id = self.db.insert_coin_selection(
|
||||
symbol=signal.symbol,
|
||||
score=signal.score,
|
||||
qualified_factors=signal.qualified_factors,
|
||||
reason=signal.reason,
|
||||
entry_price=signal.entry_price,
|
||||
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}")
|
||||
saved_count += 1
|
||||
|
||||
# 统计实际保存的多空数量
|
||||
if signal.signal_type == "LONG":
|
||||
saved_long += 1
|
||||
else:
|
||||
saved_short += 1
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存{signal.symbol}选币结果失败: {e}")
|
||||
|
||||
# 检查并标记过期的选币
|
||||
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:
|
||||
# self.logger.info("发送钉钉通知...")
|
||||
# notification_sent = self.dingtalk_notifier.send_coin_selection_notification(all_signals)
|
||||
# if notification_sent:
|
||||
# self.logger.info("✅ 钉钉通知发送成功")
|
||||
# else:
|
||||
# self.logger.info("📱 钉钉通知发送失败或未配置")
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"发送钉钉通知时出错: {e}")
|
||||
# 发送钉钉通知
|
||||
try:
|
||||
self.logger.info("发送钉钉通知...")
|
||||
notification_sent = self.dingtalk_notifier.send_coin_selection_notification(all_signals)
|
||||
if notification_sent:
|
||||
self.logger.info("✅ 钉钉通知发送成功")
|
||||
else:
|
||||
self.logger.info("📱 钉钉通知发送失败或未配置")
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送钉钉通知时出错: {e}")
|
||||
|
||||
return all_signals
|
||||
|
||||
@ -159,18 +167,18 @@ class CoinSelectionEngine:
|
||||
key = f"{strategy}-{signal_type}"
|
||||
|
||||
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]['scores'].append(signal.score)
|
||||
strategy_stats[key]['factors'].append(signal.qualified_factors)
|
||||
|
||||
# 计算平均分数
|
||||
# 计算平均符合因子数
|
||||
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("策略分布统计:")
|
||||
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]:
|
||||
"""针对特定策略运行专门的分析"""
|
||||
@ -217,7 +225,7 @@ class CoinSelectionEngine:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
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,
|
||||
exit_time, pnl_percentage, notes, strategy_type, holding_period,
|
||||
risk_reward_ratio, expiry_time, is_expired, action_suggestion,
|
||||
@ -233,7 +241,7 @@ class CoinSelectionEngine:
|
||||
selection = {
|
||||
'id': detailed_row[0],
|
||||
'symbol': detailed_row[1],
|
||||
'score': detailed_row[2],
|
||||
'qualified_factors': detailed_row[2],
|
||||
'reason': detailed_row[3],
|
||||
'entry_price': detailed_row[4],
|
||||
'stop_loss': detailed_row[5],
|
||||
@ -324,7 +332,7 @@ class CoinSelectionEngine:
|
||||
|
||||
for i, signal in enumerate(signals, 1):
|
||||
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.entry_price:.4f}\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:
|
||||
cursor.execute("ALTER TABLE coin_selections ADD COLUMN strategy_type TEXT NOT NULL DEFAULT '中线'")
|
||||
except:
|
||||
@ -135,6 +187,12 @@ class DatabaseManager:
|
||||
except:
|
||||
pass
|
||||
|
||||
# 添加新的符合因子字段
|
||||
try:
|
||||
cursor.execute("ALTER TABLE coin_selections ADD COLUMN qualified_factors INTEGER NOT NULL DEFAULT 3")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 技术指标表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS technical_indicators (
|
||||
@ -175,7 +233,7 @@ class DatabaseManager:
|
||||
"""获取数据库连接"""
|
||||
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,
|
||||
signal_type="LONG", direction="BUY"):
|
||||
"""插入选币结果 - 支持多空方向"""
|
||||
@ -185,13 +243,16 @@ class DatabaseManager:
|
||||
# 计算过期时间
|
||||
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('''
|
||||
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,
|
||||
signal_type, direction)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (symbol, score, reason, entry_price, stop_loss, take_profit, timeframe,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (symbol, score, qualified_factors, reason, entry_price, stop_loss, take_profit, timeframe,
|
||||
strategy_type, holding_period, risk_reward_ratio, expiry_time, action_suggestion,
|
||||
signal_type, direction))
|
||||
|
||||
@ -215,7 +276,7 @@ class DatabaseManager:
|
||||
cursor.execute('''
|
||||
SELECT * FROM coin_selections
|
||||
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))
|
||||
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
# 按年月日时分分组选币结果,转换时间为东八区显示
|
||||
# 按年月日时分分组选币结果,再按时间级别二级分组
|
||||
grouped_selections = {}
|
||||
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"
|
||||
|
||||
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_time'] = beijing_time
|
||||
grouped_selections[time_key].append(selection_copy)
|
||||
grouped_selections[time_key][timeframe].append(selection_copy)
|
||||
|
||||
# 按时间降序排序(最新的在前面)
|
||||
sorted_grouped_selections = dict(sorted(
|
||||
@ -91,13 +96,24 @@ async def dashboard(request: Request, limit: int = 20, offset: int = 0):
|
||||
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,
|
||||
"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),
|
||||
"current_limit": limit,
|
||||
"current_offset": offset
|
||||
"current_limit": limit
|
||||
})
|
||||
except Exception as 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)
|
||||
}, 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")
|
||||
async def run_selection():
|
||||
"""执行选币API"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user