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.""" """Data collector — fetch OHLCV from akshare and upsert into daily_bars."""
import socket
import threading import threading
import time import time
from datetime import date from datetime import date
socket.setdefaulttimeout(15)
from app.database import SessionLocal from app.database import SessionLocal
from app.models import DailyBar, Contract from app.models import DailyBar, Contract
from app.engine.lock_strategy import compute_amp_5d 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 r[0] for r in
db.query(DailyBar.date) db.query(DailyBar.date)
.filter(DailyBar.contract == code) .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; missing = [d for d in bar_dates if d not in existing_dates]
# 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]
total_missing = len(missing) total_missing = len(missing)
with _lock: with _lock:
@@ -347,10 +346,10 @@ def _sync_positions_for_contract(db, contract_code: str) -> int:
r[0] for r in r[0] for r in
db.query(DailyBar.date) db.query(DailyBar.date)
.filter(DailyBar.contract == code) .filter(DailyBar.contract == code)
.order_by(DailyBar.date) .order_by(DailyBar.date.desc())
.limit(20)
.all() .all()
] ]
inserted = 0 inserted = 0
for d in bar_dates: for d in bar_dates:
if d in existing_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) .filter(DailyBar.contract == c.code)
.count() .count()
) )
pos_days = len({
r[0] for r in
db.query(PositionRanking.date)
.filter(PositionRanking.contract_code == c.code)
.distinct().all()
})
contract_list.append({ contract_list.append({
"id": c.id, "code": c.code, "name": c.name, "id": c.id, "code": c.code, "name": c.name,
"is_active": c.is_active, "bar_count": bar_count, "is_active": c.is_active, "bar_count": bar_count,
"pos_days": pos_days,
}) })
product_data.append({ product_data.append({
+35 -15
View File
@@ -59,11 +59,12 @@
</div> </div>
<div class="product-body" style="display:none;"> <div class="product-body" style="display:none;">
<table style="font-size:0.84rem;"> <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 %} {% for c in p.contracts %}
<tr> <tr>
<td style="text-align:left;padding:6px 14px;"><strong>{{ c.code }}</strong></td> <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.bar_count }}</td>
<td style="text-align:center;padding:6px 14px;">{{ c.pos_days }}</td>
<td style="text-align:center;padding:6px 14px;"> <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 %} {% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}
</td> </td>
@@ -244,16 +245,24 @@ function pollProgress() {
clearInterval(syncTimer); clearInterval(syncTimer);
syncTimer = null; syncTimer = null;
document.getElementById('sync-progress-bar').style.width = '100%'; document.getElementById('sync-progress-bar').style.width = '100%';
var resultText = data.result > 0 ? ' · +' + data.result : ''; if (data.result > 0) {
document.getElementById('sync-progress-label').textContent = data.label + ' ✓' + resultText; 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 = ''; document.getElementById('sync-progress-pct').textContent = '';
// Remember which product headers are expanded before reload document.getElementById('sync-progress').insertAdjacentHTML('beforeend',
var expanded = []; '<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>');
document.querySelectorAll('.product-body').forEach(function(b, i) { if (data.result > 0) {
if (b.style.display === 'block') expanded.push(i); var expanded = [];
}); document.querySelectorAll('.product-body').forEach(function(b, i) {
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {} if (b.style.display === 'block') expanded.push(i);
setTimeout(function() { location.reload(); }, 1200); });
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {}
setTimeout(function() { location.reload(); }, 1200);
}
}, delay); }, delay);
} }
}); });
@@ -266,11 +275,9 @@ function startSync(url) {
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
if (!data.ok) { if (!data.ok) {
document.getElementById('sync-progress-label').textContent = '已有同步任务进行中,请稍后再试'; // Another sync is already running — just show its progress
document.getElementById('sync-progress-pct').textContent = ''; syncTimer = setInterval(pollProgress, 500);
document.getElementById('sync-progress-bar').style.width = '0%'; pollProgress();
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>');
return; return;
} }
syncTimer = setInterval(pollProgress, 500); syncTimer = setInterval(pollProgress, 500);
@@ -318,5 +325,18 @@ function toggleProduct(header) {
} catch(e) {} } 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> </script>
{% endblock %} {% endblock %}
+15 -16
View File
@@ -13,21 +13,18 @@
<span style="font-size:0.78rem;color:var(--sub);">共 {{ total_rows }} 条</span> <span style="font-size:0.78rem;color:var(--sub);">共 {{ total_rows }} 条</span>
</div> </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"> <div class="table-wrap">
<table id="bars-table"> <table id="bars-table">
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>波幅</th><th>5日均振幅</th></tr> <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 %} {% for row in rows %}
<tr data-index="{{ row.global_idx }}" data-diff="{{ row.diff }}" data-date="{{ row.date }}" data-weekday="{{ row.weekday }}"> <tr data-index="{{ row.global_idx }}" data-diff="{{ row.diff }}" data-date="{{ row.date }}" data-weekday="{{ row.weekday }}">
<td>{{ row.date }}</td> <td>{{ row.date }}</td>
@@ -150,10 +147,12 @@ function showDrawer(cell, product, calcRows, isNext) {
document.getElementById('overlay').classList.add('show'); document.getElementById('overlay').classList.add('show');
document.getElementById('drawer').classList.add('show'); document.getElementById('drawer').classList.add('show');
var row = cell.parentElement.parentElement; if (!isNext) {
if (activeRow) activeRow.classList.remove('active'); var row = cell.parentElement.parentElement;
row.classList.add('active'); if (activeRow) activeRow.classList.remove('active');
activeRow = row; row.classList.add('active');
activeRow = row;
}
} }
</script> </script>
{% endblock %} {% endblock %}
BIN
View File
Binary file not shown.
+2
View File
@@ -1,6 +1,8 @@
services: services:
app: app:
build: . build: .
image: ft-app
container_name: app
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes: