关键位与今日计划列表实时现价及距区间距离(1s轮询)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 14:30:01 +08:00
parent 58020b6e9c
commit 38ff40111a
6 changed files with 177 additions and 7 deletions
+69 -1
View File
@@ -481,7 +481,75 @@ def api_symbol_search():
q = request.args.get("q", "")
return jsonify(search_symbols(q))
# —————————————— 页面路由 ——————————————
@app.route("/api/key_prices")
@login_required
def api_key_prices():
"""关键位监控列表:批量现价与距上/下沿距离。"""
conn = get_db()
rows = conn.execute(
"SELECT id, symbol, market_code, sina_code, upper, lower "
"FROM key_monitors WHERE status='active' OR status IS NULL"
).fetchall()
conn.close()
out = []
for r in rows:
sym = r["symbol"]
market = r["market_code"] or ""
sina = r["sina_code"] or ""
upper = float(r["upper"])
lower = float(r["lower"])
price = fetch_price(sym, market, sina)
dist_upper = None
dist_lower = None
if price is not None:
dist_upper = round(upper - price, 2)
dist_lower = round(price - lower, 2)
out.append({
"id": r["id"],
"price": price,
"dist_upper": dist_upper,
"dist_lower": dist_lower,
})
return jsonify(out)
@app.route("/api/plan_prices")
@login_required
def api_plan_prices():
"""今日计划:批量现价与距决策区间上/下沿距离。"""
today = today_str()
conn = get_db()
rows = conn.execute(
"SELECT id, symbol, market_code, sina_code, zone_upper, zone_lower "
"FROM order_plans WHERE plan_date=? AND status IN ('planned', 'active')",
(today,),
).fetchall()
conn.close()
out = []
for r in rows:
sym = r["symbol"]
market = r["market_code"] or ""
sina = r["sina_code"] or ""
upper = float(r["zone_upper"])
lower = float(r["zone_lower"])
price = fetch_price(sym, market, sina)
dist_upper = None
dist_lower = None
in_zone = False
if price is not None:
dist_upper = round(upper - price, 2)
dist_lower = round(price - lower, 2)
in_zone = lower <= price <= upper
out.append({
"id": r["id"],
"price": price,
"dist_upper": dist_upper,
"dist_lower": dist_lower,
"in_zone": in_zone,
})
return jsonify(out)
@app.route("/")
@login_required
+39
View File
@@ -0,0 +1,39 @@
(function () {
var timer = null;
function fmtDist(v) {
if (v === null || v === undefined) return '--';
return v.toFixed(2);
}
function pollPrices() {
var list = document.getElementById('key-monitor-list');
if (!list || !list.querySelector('.key-item')) return;
fetch('/api/key_prices')
.then(function (r) { return r.json(); })
.then(function (rows) {
rows.forEach(function (row) {
var el = list.querySelector('.key-item[data-key-id="' + row.id + '"]');
if (!el) return;
var priceEl = el.querySelector('.live-price');
var upEl = el.querySelector('.dist-up');
var downEl = el.querySelector('.dist-down');
if (priceEl) {
priceEl.textContent = row.price != null ? row.price : '--';
}
if (upEl) upEl.textContent = fmtDist(row.dist_upper);
if (downEl) downEl.textContent = fmtDist(row.dist_lower);
});
})
.catch(function () { /* ignore */ });
}
function startPolling() {
if (timer) clearInterval(timer);
pollPrices();
timer = setInterval(pollPrices, 1000);
}
document.addEventListener('DOMContentLoaded', startPolling);
})();
+45
View File
@@ -0,0 +1,45 @@
(function () {
var timer = null;
function fmtDist(v) {
if (v === null || v === undefined) return '--';
return v.toFixed(2);
}
function pollPrices() {
var list = document.getElementById('plan-monitor-list');
if (!list || !list.querySelector('.plan-item')) return;
fetch('/api/plan_prices')
.then(function (r) { return r.json(); })
.then(function (rows) {
rows.forEach(function (row) {
var el = list.querySelector('.plan-item[data-plan-id="' + row.id + '"]');
if (!el) return;
var priceEl = el.querySelector('.live-price');
var distEl = el.querySelector('.live-dist');
var upEl = el.querySelector('.dist-up');
var downEl = el.querySelector('.dist-down');
if (priceEl) {
priceEl.textContent = row.price != null ? row.price : '--';
}
if (row.in_zone && distEl) {
distEl.innerHTML = '<span class="text-profit" style="font-weight:600">在区间内</span>';
} else if (distEl && upEl && downEl) {
distEl.innerHTML =
'距上 <span class="dist-up">' + fmtDist(row.dist_upper) + '</span>' +
' · 距下 <span class="dist-down">' + fmtDist(row.dist_lower) + '</span>';
}
});
})
.catch(function () { /* ignore */ });
}
function startPolling() {
if (timer) clearInterval(timer);
pollPrices();
timer = setInterval(pollPrices, 1000);
}
document.addEventListener('DOMContentLoaded', startPolling);
})();
+5 -1
View File
@@ -331,7 +331,11 @@
.review-detail-image{flex-shrink:0;padding-top:.75rem;border-top:1px solid var(--table-border)}
.review-detail-image img{width:100%;border-radius:10px;border:1px solid var(--card-border)}
.review-detail-image .no-img{color:var(--text-muted);font-size:.85rem;padding:2rem;text-align:center;background:var(--card-inner);border-radius:10px}
.modal-close{float:right;color:var(--text-muted);cursor:pointer;font-size:1.2rem}
.key-live{display:flex;flex-direction:column;align-items:center;gap:.15rem;min-width:100px;font-size:.8rem}
.key-live .live-price{font-size:1rem;font-weight:600;color:var(--accent)}
.key-live .live-dist{color:var(--text-muted);font-size:.72rem;white-space:nowrap}
.key-live .live-dist span{color:var(--text-primary)}
.list-item.key-item{gap:.65rem}
.calc-readonly{background:var(--calc-bg);color:var(--accent)}
@media(max-width:1100px){
.split-grid{grid-template-columns:1fr}
+10 -3
View File
@@ -8,7 +8,7 @@
<form action="{{ url_for('add_key') }}" method="post" class="form-compact">
<div class="form-line line-3">
<div class="symbol-wrap">
<input type="text" class="symbol-input" placeholder="中文名或同花顺代码" autocomplete="off" required>
<input type="text" class="symbol-input" placeholder="主力合约" autocomplete="off" required>
<input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required>
@@ -35,13 +35,17 @@
</div>
</form>
<h3 class="section-label">监控列表</h3>
<div class="list card-scroll">
<div class="list card-scroll" id="key-monitor-list">
{% for k in keys %}
<div class="list-item" style="padding:.75rem;font-size:.85rem">
<div class="list-item key-item" data-key-id="{{ k.id }}" style="padding:.75rem;font-size:.85rem">
<div>
<strong>{{ k.symbol_name or k.symbol }}</strong> {{ k.monitor_type }}
<span class="badge dir">{{ '多' if k.direction == 'long' else '空' }}</span>
</div>
<div class="key-live">
<span class="live-price">--</span>
<span class="live-dist">距上 <span class="dist-up">--</span> · 距下 <span class="dist-down">--</span></span>
</div>
<div>上{{ k.upper }} 下{{ k.lower }}</div>
<a href="{{ url_for('del_key', pid=k.id) }}" class="btn-del" onclick="return confirm('移入历史?')"></a>
</div>
@@ -76,3 +80,6 @@
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/keys.js') }}"></script>
{% endblock %}
+9 -2
View File
@@ -34,15 +34,19 @@
</div>
</form>
<h3 class="section-label">进行中</h3>
<div class="list card-scroll">
<div class="list card-scroll" id="plan-monitor-list">
{% for p in plans %}
<div class="list-item" style="padding:.75rem;font-size:.85rem">
<div class="list-item key-item plan-item" data-plan-id="{{ p.id }}" style="padding:.75rem;font-size:.85rem">
<div>
<strong>{{ p.symbol_name or p.symbol }}</strong>
<span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span>
{% if p.status == 'planned' %}<span class="badge planned">待触发</span>
{% else %}<span class="badge active">已激活</span>{% endif %}
</div>
<div class="key-live">
<span class="live-price">--</span>
<span class="live-dist">距上 <span class="dist-up">--</span> · 距下 <span class="dist-down">--</span></span>
</div>
<div>
区间{{ p.zone_lower }}~{{ p.zone_upper }}
{% if p.decision_reason %} · {{ p.decision_reason }}{% endif %}
@@ -93,3 +97,6 @@
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/plans.js') }}"></script>
{% endblock %}