Compare commits
4 Commits
9fd0116dec
...
223001d8a2
| Author | SHA1 | Date | |
|---|---|---|---|
| 223001d8a2 | |||
| 3a32429a5f | |||
| 18575d1c55 | |||
| d62304b2c4 |
@@ -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,14 +114,10 @@ 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;
|
|
||||||
# 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)
|
total_missing = len(missing)
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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',
|
||||||
|
'<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 = [];
|
var expanded = [];
|
||||||
document.querySelectorAll('.product-body').forEach(function(b, i) {
|
document.querySelectorAll('.product-body').forEach(function(b, i) {
|
||||||
if (b.style.display === 'block') expanded.push(i);
|
if (b.style.display === 'block') expanded.push(i);
|
||||||
});
|
});
|
||||||
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {}
|
try { sessionStorage.setItem('ft-expanded', JSON.stringify(expanded)); } catch(e) {}
|
||||||
setTimeout(function() { location.reload(); }, 1200);
|
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 %}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
if (!isNext) {
|
||||||
var row = cell.parentElement.parentElement;
|
var row = cell.parentElement.parentElement;
|
||||||
if (activeRow) activeRow.classList.remove('active');
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
row.classList.add('active');
|
row.classList.add('active');
|
||||||
activeRow = row;
|
activeRow = row;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
|
image: ft-app
|
||||||
|
container_name: app
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Reference in New Issue
Block a user