关键位与今日计划列表实时现价及距区间距离(1s轮询)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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
@@ -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
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user