Compare commits

..

4 Commits

Author SHA1 Message Date
fish 223001d8a2 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-25 22:27:03 +08:00
fish 3a32429a5f 网络请求加超时防止卡死,持仓同步限制最近20天,次日预测移出表格
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-25 22:23:59 +08:00
fish 18575d1c55 已有同步任务时直接显示进度条,不再弹阻塞提示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-25 21:54:55 +08:00
fish d62304b2c4 数据同步页增加持仓天数展示,修复持仓同步无法拉取历史数据的问题
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-25 21:52:32 +08:00
6 changed files with 68 additions and 41 deletions
+9 -10
View File
@@ -1,7 +1,10 @@
"""Data collector — fetch OHLCV from akshare and upsert into daily_bars."""
import socket
import threading
import time
from datetime import date
socket.setdefaulttimeout(15)
from app.database import SessionLocal
from app.models import DailyBar, Contract
from app.engine.lock_strategy import compute_amp_5d
@@ -111,15 +114,11 @@ def sync_positions_bg(contract_code: str) -> bool:
r[0] for r in
db.query(DailyBar.date)
.filter(DailyBar.contract == code)
.order_by(DailyBar.date).all()
.order_by(DailyBar.date.desc())
.limit(20)
.all()
]
# Only sync dates within or after existing position date range;
# akshare may not have data for very old dates
if existing_dates:
min_pos = min(existing_dates)
missing = [d for d in bar_dates if d not in existing_dates and d >= min_pos]
else:
missing = [d for d in bar_dates if d not in existing_dates]
missing = [d for d in bar_dates if d not in existing_dates]
total_missing = len(missing)
with _lock:
@@ -347,10 +346,10 @@ def _sync_positions_for_contract(db, contract_code: str) -> int:
r[0] for r in
db.query(DailyBar.date)
.filter(DailyBar.contract == code)
.order_by(DailyBar.date)
.order_by(DailyBar.date.desc())
.limit(20)
.all()
]
inserted = 0
for d in bar_dates:
if d in existing_dates:
+7
View File
@@ -31,9 +31,16 @@ def admin_page(request: Request, db: Session = Depends(get_db)):
.filter(DailyBar.contract == c.code)
.count()
)
pos_days = len({
r[0] for r in
db.query(PositionRanking.date)
.filter(PositionRanking.contract_code == c.code)
.distinct().all()
})
contract_list.append({
"id": c.id, "code": c.code, "name": c.name,
"is_active": c.is_active, "bar_count": bar_count,
"pos_days": pos_days,
})
product_data.append({
+35 -15
View File
@@ -59,11 +59,12 @@
</div>
<div class="product-body" style="display:none;">
<table style="font-size:0.84rem;">
<tr><th style="text-align:left;padding:8px 14px;">合约</th><th style="text-align:center;padding:8px 14px;">数据条数</th><th style="text-align:center;padding:8px 14px;">状态</th><th style="text-align:center;padding:8px 14px;">操作</th></tr>
<tr><th style="text-align:left;padding:8px 14px;">合约</th><th style="text-align:center;padding:8px 14px;">行情</th><th style="text-align:center;padding:8px 14px;">持仓</th><th style="text-align:center;padding:8px 14px;">状态</th><th style="text-align:center;padding:8px 14px;">操作</th></tr>
{% for c in p.contracts %}
<tr>
<td style="text-align:left;padding:6px 14px;"><strong>{{ c.code }}</strong></td>
<td style="text-align:center;padding:6px 14px;">{{ c.bar_count }}</td>
<td style="text-align:center;padding:6px 14px;">{{ c.pos_days }}</td>
<td style="text-align:center;padding:6px 14px;">
{% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}
</td>
@@ -244,16 +245,24 @@ function pollProgress() {
clearInterval(syncTimer);
syncTimer = null;
document.getElementById('sync-progress-bar').style.width = '100%';
var resultText = data.result > 0 ? ' · +' + data.result : '';
document.getElementById('sync-progress-label').textContent = data.label + ' ✓' + resultText;
if (data.result > 0) {
document.getElementById('sync-progress-label').textContent = data.label + ' ✓ · +' + data.result;
} else if (data.total > 0) {
document.getElementById('sync-progress-label').textContent = data.label + ' ⚠ 无新数据,历史日期无可用持仓';
} else {
document.getElementById('sync-progress-label').textContent = data.label + ' ✓ 已是最新';
}
document.getElementById('sync-progress-pct').textContent = '';
// Remember which product headers are expanded before reload
var expanded = [];
document.querySelectorAll('.product-body').forEach(function(b, i) {
if (b.style.display === 'block') expanded.push(i);
});
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {}
setTimeout(function() { location.reload(); }, 1200);
document.getElementById('sync-progress').insertAdjacentHTML('beforeend',
'<button onclick="hideProgress()" style="display:block;margin-top:10px;padding:4px 14px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.8rem;">关闭</button>');
if (data.result > 0) {
var expanded = [];
document.querySelectorAll('.product-body').forEach(function(b, i) {
if (b.style.display === 'block') expanded.push(i);
});
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {}
setTimeout(function() { location.reload(); }, 1200);
}
}, delay);
}
});
@@ -266,11 +275,9 @@ function startSync(url) {
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
document.getElementById('sync-progress-label').textContent = '已有同步任务进行中,请稍后再试';
document.getElementById('sync-progress-pct').textContent = '';
document.getElementById('sync-progress-bar').style.width = '0%';
document.getElementById('sync-progress').insertAdjacentHTML('beforeend',
'<button onclick="hideProgress()" style="display:block;margin-top:10px;padding:4px 14px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.8rem;">关闭</button>');
// Another sync is already running — just show its progress
syncTimer = setInterval(pollProgress, 500);
pollProgress();
return;
}
syncTimer = setInterval(pollProgress, 500);
@@ -318,5 +325,18 @@ function toggleProduct(header) {
} catch(e) {}
})();
// Auto-show progress bar if a sync is already running
(function() {
fetch('/admin/sync-status')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.running) {
showProgress();
syncTimer = setInterval(pollProgress, 500);
pollProgress();
}
});
})();
</script>
{% endblock %}
+15 -16
View File
@@ -13,21 +13,18 @@
<span style="font-size:0.78rem;color:var(--sub);">共 {{ total_rows }} 条</span>
</div>
{% if page == 1 and next_date %}
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:8px;padding:12px 18px;margin-bottom:14px;display:flex;align-items:center;gap:16px;">
<span style="font-size:0.78rem;color:var(--sub);">📅 次日预测</span>
<span style="font-weight:600;">{{ next_date }} {{ next_weekday }}</span>
<span style="color:var(--sub);">预计振幅</span>
<span class="amp-cell" style="color:var(--accent);font-weight:700;font-size:1.1rem;cursor:pointer;" data-next="1">{{ next_amp }}</span>
</div>
{% endif %}
<div class="table-wrap">
<table id="bars-table">
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>波幅</th><th>5日均振幅</th></tr>
{% if page == 1 and next_date %}
<tr style="background:var(--accent-light);font-weight:500;">
<td>{{ next_date }}</td>
<td>{{ next_weekday }}</td>
<td class="na"></td>
<td class="na"></td>
<td class="na"></td>
<td class="na"></td>
<td class="na"></td>
<td><span class="amp-cell" style="color:var(--accent);font-weight:700;" data-next="1">{{ next_amp }}</span></td>
</tr>
{% endif %}
{% for row in rows %}
<tr data-index="{{ row.global_idx }}" data-diff="{{ row.diff }}" data-date="{{ row.date }}" data-weekday="{{ row.weekday }}">
<td>{{ row.date }}</td>
@@ -150,10 +147,12 @@ function showDrawer(cell, product, calcRows, isNext) {
document.getElementById('overlay').classList.add('show');
document.getElementById('drawer').classList.add('show');
var row = cell.parentElement.parentElement;
if (activeRow) activeRow.classList.remove('active');
row.classList.add('active');
activeRow = row;
if (!isNext) {
var row = cell.parentElement.parentElement;
if (activeRow) activeRow.classList.remove('active');
row.classList.add('active');
activeRow = row;
}
}
</script>
{% endblock %}
BIN
View File
Binary file not shown.
+2
View File
@@ -1,6 +1,8 @@
services:
app:
build: .
image: ft-app
container_name: app
ports:
- "8000:8000"
volumes: