This commit is contained in:
aaron 2025-08-17 21:27:42 +08:00
parent 65276c5beb
commit aa800c74d0
5 changed files with 1954 additions and 2281 deletions

View File

@ -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"

View File

@ -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

View 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>

View File

@ -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"""