关键位与今日计划列表实时现价及距区间距离(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", "")
|
q = request.args.get("q", "")
|
||||||
return jsonify(search_symbols(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("/")
|
@app.route("/")
|
||||||
@login_required
|
@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{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 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}
|
.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)}
|
.calc-readonly{background:var(--calc-bg);color:var(--accent)}
|
||||||
@media(max-width:1100px){
|
@media(max-width:1100px){
|
||||||
.split-grid{grid-template-columns:1fr}
|
.split-grid{grid-template-columns:1fr}
|
||||||
|
|||||||
+10
-3
@@ -8,7 +8,7 @@
|
|||||||
<form action="{{ url_for('add_key') }}" method="post" class="form-compact">
|
<form action="{{ url_for('add_key') }}" method="post" class="form-compact">
|
||||||
<div class="form-line line-3">
|
<div class="form-line line-3">
|
||||||
<div class="symbol-wrap">
|
<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" required>
|
||||||
<input type="hidden" name="symbol_name">
|
<input type="hidden" name="symbol_name">
|
||||||
<input type="hidden" name="market_code" required>
|
<input type="hidden" name="market_code" required>
|
||||||
@@ -35,13 +35,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<h3 class="section-label">监控列表</h3>
|
<h3 class="section-label">监控列表</h3>
|
||||||
<div class="list card-scroll">
|
<div class="list card-scroll" id="key-monitor-list">
|
||||||
{% for k in keys %}
|
{% 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>
|
<div>
|
||||||
<strong>{{ k.symbol_name or k.symbol }}</strong> {{ k.monitor_type }}
|
<strong>{{ k.symbol_name or k.symbol }}</strong> {{ k.monitor_type }}
|
||||||
<span class="badge dir">{{ '多' if k.direction == 'long' else '空' }}</span>
|
<span class="badge dir">{{ '多' if k.direction == 'long' else '空' }}</span>
|
||||||
</div>
|
</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>
|
<div>上{{ k.upper }} 下{{ k.lower }}</div>
|
||||||
<a href="{{ url_for('del_key', pid=k.id) }}" class="btn-del" onclick="return confirm('移入历史?')">删</a>
|
<a href="{{ url_for('del_key', pid=k.id) }}" class="btn-del" onclick="return confirm('移入历史?')">删</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,3 +80,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="{{ url_for('static', filename='js/keys.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -34,15 +34,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<h3 class="section-label">进行中</h3>
|
<h3 class="section-label">进行中</h3>
|
||||||
<div class="list card-scroll">
|
<div class="list card-scroll" id="plan-monitor-list">
|
||||||
{% for p in plans %}
|
{% 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>
|
<div>
|
||||||
<strong>{{ p.symbol_name or p.symbol }}</strong>
|
<strong>{{ p.symbol_name or p.symbol }}</strong>
|
||||||
<span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span>
|
<span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span>
|
||||||
{% if p.status == 'planned' %}<span class="badge planned">待触发</span>
|
{% if p.status == 'planned' %}<span class="badge planned">待触发</span>
|
||||||
{% else %}<span class="badge active">已激活</span>{% endif %}
|
{% else %}<span class="badge active">已激活</span>{% endif %}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
区间{{ p.zone_lower }}~{{ p.zone_upper }}
|
区间{{ p.zone_lower }}~{{ p.zone_upper }}
|
||||||
{% if p.decision_reason %} · {{ p.decision_reason }}{% endif %}
|
{% if p.decision_reason %} · {{ p.decision_reason }}{% endif %}
|
||||||
@@ -93,3 +97,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="{{ url_for('static', filename='js/plans.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user