From 5aa9f11733cc7deb100d4ea6883f1c521c83b3d7 Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 15 Jun 2026 14:56:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=81=E4=BB=93=E7=9B=91=E6=8E=A7=E5=B9=B3?= =?UTF-8?q?=E4=BB=93=E8=87=AA=E5=8A=A8=E8=AE=B0=E5=85=A5=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=EF=BC=8C=E6=96=B0=E5=A2=9E=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E9=A1=B5=E4=B8=8E=E5=AE=9E=E7=9B=98=E8=B5=84?= =?UTF-8?q?=E9=87=91=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.py | 282 +++++++++++++++++++++++++++++++++++++++- contract_specs.py | 114 ++++++++++++++++ static/js/keys.js | 83 ++++++++++-- static/js/plans.js | 6 +- static/js/review.js | 2 + static/js/trades.js | 42 ++++++ templates/base.html | 39 +++++- templates/keys.html | 45 ++++++- templates/plans.html | 4 +- templates/records.html | 18 ++- templates/settings.html | 10 ++ templates/trades.html | 111 ++++++++++++++++ 12 files changed, 733 insertions(+), 23 deletions(-) create mode 100644 contract_specs.py create mode 100644 static/js/trades.js create mode 100644 templates/trades.html diff --git a/app.py b/app.py index 82b0258..b841f7a 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,7 @@ from flask import ( from werkzeug.security import check_password_hash, generate_password_hash from symbols import search_symbols, ths_to_codes +from contract_specs import calc_position_metrics from kline_chart import generate_review_kline_chart from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label @@ -62,6 +63,28 @@ def calc_holding_duration(open_time: str, close_time: str) -> str: return "" +def holding_to_minutes(open_time: str, close_time: str) -> int: + try: + o = datetime.fromisoformat(open_time.strip().replace(" ", "T")) + c = datetime.fromisoformat(close_time.strip().replace(" ", "T")) + secs = int((c - o).total_seconds()) + return max(0, secs // 60) + except Exception: + return 0 + + +def classify_close_result(direction: str, close: float, sl: float, tp: float) -> str: + """根据平仓价与止损/止盈距离判断结果。""" + if close is None: + return "手动平仓" + tol = max(abs(close) * 0.002, 1.0) + if abs(close - tp) <= tol: + return "止盈" + if abs(close - sl) <= tol: + return "止损" + return "手动平仓" + + def calc_rr_ratio(direction: str, entry: float, stop: float, target: float) -> Optional[float]: """盈亏比 = 盈利空间 / 风险空间。""" if entry is None or stop is None or target is None: @@ -213,6 +236,23 @@ def init_db(): kline_count INTEGER, kline_cutoff TEXT, behavior_tags TEXT, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') + c.execute('''CREATE TABLE IF NOT EXISTS position_monitors + (id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT, symbol_name TEXT, market_code TEXT, sina_code TEXT, + direction TEXT, lots REAL, entry_price REAL, + stop_loss REAL, take_profit REAL, open_time TEXT, + status TEXT DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') + c.execute('''CREATE TABLE IF NOT EXISTS trade_logs + (id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT, symbol_name TEXT, market_code TEXT, sina_code TEXT, + monitor_type TEXT, direction TEXT, + entry_price REAL, stop_loss REAL, take_profit REAL, close_price REAL, + lots REAL, margin REAL, holding_minutes INTEGER, + open_time TEXT, close_time TEXT, + pnl REAL, result TEXT, + verified INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') conn.commit() conn.close() @@ -551,7 +591,48 @@ def api_plan_prices(): return jsonify(out) -@app.route("/") +@app.route("/api/position_live") +@login_required +def api_position_live(): + capital = float(get_setting("live_capital", "0") or 0) + now_iso = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M") + conn = get_db() + rows = conn.execute( + "SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC" + ).fetchall() + conn.close() + out = [] + for r in rows: + sym = r["symbol"] + market = r["market_code"] or "" + sina = r["sina_code"] or "" + direction = r["direction"] + entry = float(r["entry_price"]) + sl = float(r["stop_loss"]) + tp = float(r["take_profit"]) + lots = float(r["lots"] or 1) + mark = fetch_price(sym, market, sina) + metrics = calc_position_metrics( + direction, entry, sl, tp, lots, mark, capital, sym, + ) + holding = calc_holding_duration(r["open_time"] or "", now_iso) + out.append({ + "id": r["id"], + "symbol": r["symbol_name"] or sym, + "symbol_code": sym, + "direction": "做多" if direction == "long" else "做空", + "lots": lots, + "entry_price": entry, + "stop_loss": sl, + "take_profit": tp, + "open_time": r["open_time"], + "mark_price": mark, + "holding_duration": holding, + **metrics, + }) + return jsonify(out) + + @login_required def index(): return redirect(url_for("plans")) @@ -647,8 +728,11 @@ def keys(): history = conn.execute( "SELECT * FROM key_monitors WHERE status='archived' ORDER BY archived_at DESC LIMIT 100" ).fetchall() + positions = conn.execute( + "SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC" + ).fetchall() conn.close() - return render_template("keys.html", keys=key_list, history=history) + return render_template("keys.html", keys=key_list, history=history, positions=positions) @app.route("/add_key", methods=["POST"]) @@ -679,6 +763,179 @@ def add_key(): return redirect(url_for("keys")) +@app.route("/add_position", methods=["POST"]) +@login_required +def add_position(): + d = request.form + symbol = d.get("symbol", "").strip() + symbol_name = d.get("symbol_name", "").strip() + market_code = d.get("market_code", "").strip() + sina_code = d.get("sina_code", "").strip() + if not symbol or not market_code: + flash("请从下拉列表选择品种") + return redirect(url_for("keys")) + entry = float(d["entry_price"]) + sl = float(d["stop_loss"]) + tp = float(d["take_profit"]) + direction = d.get("direction", "").strip() + if not direction: + direction = "long" if sl < entry else "short" + open_time = d.get("open_time", "").strip() + lots = float(d.get("lots") or 1) + conn = get_db() + conn.execute( + """INSERT INTO position_monitors + (symbol, symbol_name, market_code, sina_code, direction, + lots, entry_price, stop_loss, take_profit, open_time) + VALUES (?,?,?,?,?,?,?,?,?,?)""", + ( + symbol, symbol_name, market_code, sina_code, direction, + lots, entry, sl, tp, open_time, + ), + ) + conn.commit() + conn.close() + flash("持仓已添加") + return redirect(url_for("keys")) + + +@app.route("/del_position/") +@login_required +def del_position(pid): + return close_position(pid) + + +@app.route("/close_position/", methods=["POST"]) +@login_required +def close_position(pid): + conn = get_db() + row = conn.execute("SELECT * FROM position_monitors WHERE id=?", (pid,)).fetchone() + if not row: + conn.close() + flash("持仓不存在") + return redirect(url_for("keys")) + sym = row["symbol"] + market = row["market_code"] or "" + sina = row["sina_code"] or "" + direction = row["direction"] + entry = float(row["entry_price"]) + sl = float(row["stop_loss"]) + tp = float(row["take_profit"]) + lots = float(row["lots"] or 1) + open_time = row["open_time"] or "" + close_time = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M") + close_price = fetch_price(sym, market, sina) + if close_price is None: + conn.close() + flash("无法获取现价,平仓失败") + return redirect(url_for("keys")) + capital = float(get_setting("live_capital", "0") or 0) + metrics = calc_position_metrics(direction, entry, sl, tp, lots, close_price, capital, sym) + pnl = metrics.get("float_pnl") or 0.0 + result = classify_close_result(direction, close_price, sl, tp) + minutes = holding_to_minutes(open_time, close_time) + conn.execute( + """INSERT INTO trade_logs + (symbol, symbol_name, market_code, sina_code, monitor_type, direction, + entry_price, stop_loss, take_profit, close_price, lots, margin, + holding_minutes, open_time, close_time, pnl, result) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + ( + sym, row["symbol_name"], market, sina, "持仓监控", direction, + entry, sl, tp, close_price, lots, metrics["margin"], + minutes, open_time, close_time, pnl, result, + ), + ) + conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,)) + conn.commit() + conn.close() + flash(f"已平仓,盈亏 {pnl:.2f} 元,已记入交易记录") + return redirect(url_for("keys")) + + +@app.route("/trades") +@login_required +def trades(): + conn = get_db() + rows = conn.execute("SELECT * FROM trade_logs ORDER BY id DESC LIMIT 500").fetchall() + conn.close() + return render_template("trades.html", trades=rows) + + +@app.route("/update_trade/", methods=["POST"]) +@login_required +def update_trade(tid): + d = request.form + conn = get_db() + conn.execute( + """UPDATE trade_logs SET + symbol_name=?, monitor_type=?, direction=?, + entry_price=?, stop_loss=?, take_profit=?, close_price=?, + lots=?, margin=?, holding_minutes=?, open_time=?, close_time=?, + pnl=?, result=?, verified=1 + WHERE id=?""", + ( + d.get("symbol_name", "").strip(), + d.get("monitor_type", "").strip(), + d.get("direction", "").strip(), + float(d.get("entry_price") or 0), + float(d.get("stop_loss") or 0), + float(d.get("take_profit") or 0), + float(d.get("close_price") or 0), + float(d.get("lots") or 0), + float(d.get("margin") or 0), + int(d.get("holding_minutes") or 0), + d.get("open_time", "").strip(), + d.get("close_time", "").strip(), + float(d.get("pnl") or 0), + d.get("result", "").strip(), + tid, + ), + ) + conn.commit() + conn.close() + flash("交易记录已核对保存") + return redirect(url_for("trades")) + + +@app.route("/del_trade/") +@login_required +def del_trade(tid): + conn = get_db() + conn.execute("DELETE FROM trade_logs WHERE id=?", (tid,)) + conn.commit() + conn.close() + flash("已删除") + return redirect(url_for("trades")) + + +@app.route("/fill_review/") +@login_required +def fill_review_from_trade(tid): + conn = get_db() + row = conn.execute("SELECT * FROM trade_logs WHERE id=?", (tid,)).fetchone() + conn.close() + if not row: + flash("记录不存在") + return redirect(url_for("trades")) + q = { + "symbol": row["symbol"], + "symbol_name": row["symbol_name"] or row["symbol"], + "market_code": row["market_code"] or "", + "sina_code": row["sina_code"] or "", + "direction": row["direction"], + "entry_price": row["entry_price"], + "stop_loss": row["stop_loss"], + "take_profit": row["take_profit"], + "close_price": row["close_price"], + "lots": row["lots"], + "open_time": row["open_time"], + "close_time": row["close_time"], + "pnl": row["pnl"], + } + return redirect(url_for("records", **{k: v for k, v in q.items() if v is not None})) + + @app.route("/del_key/") @login_required def del_key(pid): @@ -719,6 +976,13 @@ def records(): ).fetchall() conn.close() + trade_prefill_keys = ( + "symbol", "symbol_name", "market_code", "sina_code", "direction", + "entry_price", "stop_loss", "take_profit", "close_price", + "lots", "open_time", "close_time", "pnl", + ) + prefill = {k: request.args.get(k) for k in trade_prefill_keys if request.args.get(k)} + return render_template( "records.html", reviews=review_list, @@ -726,6 +990,7 @@ def records(): preset=preset, start=start, end=end, + prefill=prefill, open_types=OPEN_TYPES, exit_triggers=EXIT_TRIGGERS, behavior_tags=BEHAVIOR_TAGS, @@ -941,6 +1206,17 @@ def settings(): webhook = request.form.get("wechat_webhook", "").strip() set_setting("wechat_webhook", webhook) flash("企业微信配置已保存") + elif action == "capital": + raw = request.form.get("live_capital", "").strip() + try: + val = float(raw) + if val < 0: + flash("实盘资金不能为负数") + else: + set_setting("live_capital", str(val)) + flash("实盘资金已保存") + except ValueError: + flash("请输入有效的实盘资金金额") elif action == "password": old_p = request.form.get("old_password", "") new_p = request.form.get("new_password", "") @@ -959,10 +1235,12 @@ def settings(): webhook = get_setting("wechat_webhook") username = get_setting("admin_username") + live_capital = get_setting("live_capital", "0") return render_template( "settings.html", webhook=webhook, username=username, + live_capital=live_capital, quote_label=get_quote_source_label(), ) diff --git a/contract_specs.py b/contract_specs.py new file mode 100644 index 0000000..85ead49 --- /dev/null +++ b/contract_specs.py @@ -0,0 +1,114 @@ +"""国内期货合约乘数与参考保证金比例(用于估算保证金与风险)。""" +import re +from typing import Optional + +DEFAULT_SPEC = {"mult": 10, "margin_rate": 0.10} + +# 参考交易所常见规格(乘数 + 保证金比例估算) +_SPEC_BY_THS: dict[str, dict] = { + "ag": {"mult": 15, "margin_rate": 0.14}, + "au": {"mult": 1000, "margin_rate": 0.10}, + "cu": {"mult": 5, "margin_rate": 0.10}, + "al": {"mult": 5, "margin_rate": 0.10}, + "zn": {"mult": 5, "margin_rate": 0.10}, + "pb": {"mult": 5, "margin_rate": 0.10}, + "ni": {"mult": 1, "margin_rate": 0.12}, + "sn": {"mult": 1, "margin_rate": 0.12}, + "rb": {"mult": 10, "margin_rate": 0.09}, + "hc": {"mult": 10, "margin_rate": 0.09}, + "ss": {"mult": 5, "margin_rate": 0.11}, + "sc": {"mult": 1000, "margin_rate": 0.11}, + "fu": {"mult": 10, "margin_rate": 0.11}, + "bu": {"mult": 10, "margin_rate": 0.11}, + "ru": {"mult": 10, "margin_rate": 0.11}, + "sp": {"mult": 10, "margin_rate": 0.10}, + "i": {"mult": 100, "margin_rate": 0.11}, + "j": {"mult": 100, "margin_rate": 0.12}, + "jm": {"mult": 60, "margin_rate": 0.12}, + "m": {"mult": 10, "margin_rate": 0.08}, + "y": {"mult": 10, "margin_rate": 0.08}, + "p": {"mult": 10, "margin_rate": 0.09}, + "c": {"mult": 10, "margin_rate": 0.08}, + "cs": {"mult": 10, "margin_rate": 0.08}, + "jd": {"mult": 10, "margin_rate": 0.09}, + "lh": {"mult": 16, "margin_rate": 0.12}, + "l": {"mult": 5, "margin_rate": 0.09}, + "pp": {"mult": 5, "margin_rate": 0.09}, + "v": {"mult": 5, "margin_rate": 0.09}, + "eg": {"mult": 10, "margin_rate": 0.09}, + "eb": {"mult": 5, "margin_rate": 0.10}, + "pg": {"mult": 20, "margin_rate": 0.10}, + "RM": {"mult": 10, "margin_rate": 0.08}, + "OI": {"mult": 10, "margin_rate": 0.08}, + "SR": {"mult": 10, "margin_rate": 0.08}, + "CF": {"mult": 5, "margin_rate": 0.08}, + "MA": {"mult": 10, "margin_rate": 0.09}, + "TA": {"mult": 5, "margin_rate": 0.09}, + "FG": {"mult": 20, "margin_rate": 0.10}, + "SA": {"mult": 20, "margin_rate": 0.10}, + "UR": {"mult": 20, "margin_rate": 0.10}, + "SF": {"mult": 5, "margin_rate": 0.10}, + "SM": {"mult": 5, "margin_rate": 0.10}, + "AP": {"mult": 10, "margin_rate": 0.10}, + "CJ": {"mult": 5, "margin_rate": 0.10}, + "PK": {"mult": 5, "margin_rate": 0.10}, + "IF": {"mult": 300, "margin_rate": 0.12}, + "IH": {"mult": 300, "margin_rate": 0.12}, + "IC": {"mult": 200, "margin_rate": 0.12}, + "IM": {"mult": 200, "margin_rate": 0.12}, +} + + +def get_contract_spec(ths_code: str) -> dict: + code = (ths_code or "").strip() + m = re.match(r"^([A-Za-z]+)", code) + if not m: + return dict(DEFAULT_SPEC) + letters = m.group(1) + spec = _SPEC_BY_THS.get(letters) or _SPEC_BY_THS.get(letters.upper()) or _SPEC_BY_THS.get(letters.lower()) + if spec: + return {"mult": spec["mult"], "margin_rate": spec["margin_rate"]} + return dict(DEFAULT_SPEC) + + +def calc_position_metrics( + direction: str, + entry: float, + stop_loss: float, + take_profit: float, + lots: float, + mark_price: Optional[float], + capital: float, + ths_code: str, +) -> dict: + spec = get_contract_spec(ths_code) + mult = spec["mult"] + margin_rate = spec["margin_rate"] + lots = lots or 1.0 + margin = entry * mult * lots * margin_rate + + if direction == "long": + risk_amt = max(0.0, (entry - stop_loss) * mult * lots) + reward = max(0.0, (take_profit - entry) * mult * lots) + float_pnl = (mark_price - entry) * mult * lots if mark_price is not None else None + else: + risk_amt = max(0.0, (stop_loss - entry) * mult * lots) + reward = max(0.0, (entry - take_profit) * mult * lots) + float_pnl = (entry - mark_price) * mult * lots if mark_price is not None else None + + risk_pct = (risk_amt / capital * 100) if capital > 0 else 0.0 + pos_pct = (margin / capital * 100) if capital > 0 else 0.0 + rr = (reward / risk_amt) if risk_amt > 0 else None + float_pct = (float_pnl / margin * 100) if margin > 0 and float_pnl is not None else None + + return { + "mult": mult, + "margin_rate": margin_rate, + "margin": round(margin, 2), + "risk_amount": round(risk_amt, 2), + "risk_pct": round(risk_pct, 2), + "position_pct": round(pos_pct, 2), + "float_pnl": round(float_pnl, 2) if float_pnl is not None else None, + "float_pct": round(float_pct, 2) if float_pct is not None else None, + "rr_ratio": round(rr, 2) if rr is not None else None, + } diff --git a/static/js/keys.js b/static/js/keys.js index 3ff4f3f..a1c67c7 100644 --- a/static/js/keys.js +++ b/static/js/keys.js @@ -1,12 +1,18 @@ (function () { - var timer = null; + var keyTimer = null; + var posTimer = null; function fmtDist(v) { if (v === null || v === undefined) return '--'; - return v.toFixed(2); + return Number(v).toFixed(2); } - function pollPrices() { + function fmtNum(v, digits) { + if (v === null || v === undefined) return '--'; + return Number(v).toFixed(digits === undefined ? 2 : digits); + } + + function pollKeyPrices() { var list = document.getElementById('key-monitor-list'); if (!list || !list.querySelector('.key-item')) return; @@ -19,9 +25,7 @@ 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 (priceEl) priceEl.textContent = row.price != null ? row.price : '--'; if (upEl) upEl.textContent = fmtDist(row.dist_upper); if (downEl) downEl.textContent = fmtDist(row.dist_lower); }); @@ -29,10 +33,71 @@ .catch(function () { /* ignore */ }); } + function buildPosCard(row) { + var pnlClass = ''; + if (row.float_pnl > 0) pnlClass = 'pnl-pos'; + if (row.float_pnl < 0) pnlClass = 'pnl-neg'; + var pnlText = '--'; + if (row.float_pnl != null) { + var sign = row.float_pnl >= 0 ? '+' : ''; + pnlText = sign + fmtNum(row.float_pnl) + '元'; + if (row.float_pct != null) { + pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)'; + } + } + var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--'; + var openT = (row.open_time || '').replace('T', ' ').slice(0, 16); + + return ( + '
' + + '
' + + '
' + row.symbol + ' ' + row.direction + '
' + + '
' + + '
' + + '
' + + '
来源 手动输入 · 风险 ' + + fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元
' + + '
' + + '
' + fmtNum(row.entry_price) + '
' + + '
' + fmtNum(row.stop_loss) + '
' + + '
' + fmtNum(row.take_profit) + '
' + + '
' + rr + '
' + + '
' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '
' + + '
' + pnlText + '
' + + '
' + + '
' + ); + } + + function pollPositions() { + var list = document.getElementById('position-live-list'); + if (!list) return; + + fetch('/api/position_live') + .then(function (r) { return r.json(); }) + .then(function (rows) { + if (!rows.length) { + list.innerHTML = '
暂无持仓,左侧录入后显示
'; + return; + } + list.innerHTML = rows.map(buildPosCard).join(''); + }) + .catch(function () { /* ignore */ }); + } + function startPolling() { - if (timer) clearInterval(timer); - pollPrices(); - timer = setInterval(pollPrices, 1000); + if (keyTimer) clearInterval(keyTimer); + if (posTimer) clearInterval(posTimer); + pollKeyPrices(); + pollPositions(); + keyTimer = setInterval(pollKeyPrices, 1000); + posTimer = setInterval(pollPositions, 1000); } document.addEventListener('DOMContentLoaded', startPolling); diff --git a/static/js/plans.js b/static/js/plans.js index 4021afc..7aac03f 100644 --- a/static/js/plans.js +++ b/static/js/plans.js @@ -25,10 +25,10 @@ } if (row.in_zone && distEl) { distEl.innerHTML = '在区间内'; - } else if (distEl && upEl && downEl) { + } else if (distEl) { distEl.innerHTML = - '距上 ' + fmtDist(row.dist_upper) + '' + - ' · 距下 ' + fmtDist(row.dist_lower) + ''; + '距上' + fmtDist(row.dist_upper) + ' ' + + '距下' + fmtDist(row.dist_lower) + ''; } }); }) diff --git a/static/js/review.js b/static/js/review.js index c9900e8..d687fce 100644 --- a/static/js/review.js +++ b/static/js/review.js @@ -162,4 +162,6 @@ bindModal(); recalc(); }); + + window.recalc = recalc; })(); diff --git a/static/js/trades.js b/static/js/trades.js new file mode 100644 index 0000000..07b2df4 --- /dev/null +++ b/static/js/trades.js @@ -0,0 +1,42 @@ +(function () { + var switchEl = document.getElementById('trade-edit-switch'); + if (!switchEl) return; + + function setEditMode(on) { + document.querySelectorAll('.cell-edit-hide').forEach(function (el) { + el.style.display = on ? 'none' : ''; + }); + document.querySelectorAll('.cell-edit-show').forEach(function (el) { + if (el.type === 'hidden') return; + el.style.display = on ? '' : 'none'; + }); + document.querySelectorAll('.trade-save-btn').forEach(function (btn) { + btn.disabled = !on; + }); + } + + switchEl.addEventListener('change', function () { + setEditMode(switchEl.checked); + }); + + document.querySelectorAll('.trade-save-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + var row = btn.closest('tr[data-trade-id]'); + if (!row) return; + var id = row.getAttribute('data-trade-id'); + var form = document.createElement('form'); + form.method = 'POST'; + form.action = '/update_trade/' + id; + row.querySelectorAll('.cell-edit-show').forEach(function (el) { + if (!el.name) return; + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = el.name; + input.value = el.value; + form.appendChild(input); + }); + document.body.appendChild(form); + form.submit(); + }); + }); +})(); diff --git a/templates/base.html b/templates/base.html index c6acb5f..b665ab4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -331,11 +331,41 @@ .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} - .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{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex:1;min-width:160px} + .key-live .live-price-line{font-size:.85rem;font-weight:600;color:var(--accent);white-space:nowrap} + .key-live .live-dist{font-size:.72rem;color:var(--text-muted);white-space:nowrap} .key-live .live-dist span{color:var(--text-primary)} .list-item.key-item{gap:.65rem} + .pos-card{background:var(--card-inner);border:1px solid var(--card-border);border-radius:12px;padding:1rem;margin-bottom:.75rem} + .pos-card-head{display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem;margin-bottom:.65rem} + .pos-card-head .title{font-size:1rem;font-weight:600;color:var(--text-title)} + .pos-card-meta{font-size:.75rem;color:var(--text-muted);margin-bottom:.65rem} + .pos-card-meta strong{color:var(--text-primary)} + .pos-metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem .65rem;margin-bottom:.65rem} + .pos-metrics .cell label{display:block;font-size:.68rem;color:var(--text-muted);margin-bottom:.15rem} + .pos-metrics .cell div{font-size:.88rem;color:var(--text-primary)} + .pos-metrics .cell.pnl-pos div{color:var(--profit)} + .pos-metrics .cell.pnl-neg div{color:var(--loss)} + .pos-footer{font-size:.72rem;color:var(--text-muted);display:flex;flex-wrap:wrap;gap:.35rem 1rem;padding-top:.65rem;border-top:1px solid var(--table-border)} + .pos-footer span{color:var(--text-primary)} + .pos-del{font-size:.75rem;padding:.35rem .65rem} + .trade-toolbar{display:flex;align-items:center;gap:1rem;margin-bottom:1rem;flex-wrap:wrap} + .trade-toolbar label{display:flex;align-items:center;gap:.4rem;font-size:.85rem;cursor:pointer;color:var(--text-muted)} + .trade-table-wrap{overflow-x:auto} + .trade-table{font-size:.8rem} + .trade-table th{font-size:.75rem;padding:.55rem .45rem} + .trade-table td{padding:.45rem .4rem;vertical-align:middle} + .trade-table input,.trade-table select{ + padding:.35rem .45rem;font-size:.78rem;border-radius:6px;width:100%;min-width:0; + } + .trade-table .cell-readonly{color:var(--text-primary)} + .trade-actions{display:flex;gap:.35rem;flex-wrap:wrap} + .trade-actions a,.trade-actions button{font-size:.72rem;padding:.3rem .55rem;border-radius:6px;text-decoration:none;border:none;cursor:pointer} + .btn-fill{background:var(--dir-bg);color:var(--accent)} + .btn-verify{background:var(--nav-active);color:#fff} + .btn-verify:disabled{opacity:.45;cursor:not-allowed} + .badge.result-manual{background:var(--dir-bg);color:var(--accent)} + .badge.result-external{background:var(--expired-bg);color:var(--expired-text)} .calc-readonly{background:var(--calc-bg);color:var(--accent)} @media(max-width:1100px){ .split-grid{grid-template-columns:1fr} @@ -366,7 +396,8 @@ diff --git a/templates/keys.html b/templates/keys.html index 0c8a6e5..2812fef 100644 --- a/templates/keys.html +++ b/templates/keys.html @@ -43,8 +43,8 @@ {{ '多' if k.direction == 'long' else '空' }}
- -- - 距上 -- · 距下 -- + 现价:-- + 距上-- 距下--
上{{ k.upper }} 下{{ k.lower }}
@@ -79,6 +79,47 @@ + +
+
+

持仓录入

+
+
+
+
+ + + + + +
+
+
+
开仓时间
+ +
+
+ + + +
+
+ +
+
+

方向根据止损与成交价自动判断;风险比例依赖系统设置中的实盘资金。

+
+
+ +
+

实时持仓

+
+ {% if not positions %} +
暂无持仓,左侧录入后显示
+ {% endif %} +
+
+
{% endblock %} {% block extra_js %} diff --git a/templates/plans.html b/templates/plans.html index aefc034..4b4e170 100644 --- a/templates/plans.html +++ b/templates/plans.html @@ -44,8 +44,8 @@ {% else %}已激活{% endif %}
- -- - 距上 -- · 距下 -- + 现价:-- + 距上-- 距下--
区间{{ p.zone_lower }}~{{ p.zone_upper }} diff --git a/templates/records.html b/templates/records.html index 934823a..12c801a 100644 --- a/templates/records.html +++ b/templates/records.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %} +{% block title %}复盘 - 国内期货监控系统{% endblock %} {% block content %}
@@ -178,4 +178,20 @@ {% endblock %} {% block extra_js %} +{% if prefill %} + +{% endif %} {% endblock %} diff --git a/templates/settings.html b/templates/settings.html index 1151826..7aee3e4 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -2,6 +2,16 @@ {% block title %}系统设置 - 国内期货监控系统{% endblock %} {% block content %} +
+

实盘资金

+
+ + + +
+

用于持仓监控的风险比例、仓位占比计算,保存在数据库中。

+
+

行情说明

diff --git a/templates/trades.html b/templates/trades.html new file mode 100644 index 0000000..102f537 --- /dev/null +++ b/templates/trades.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} +{% block title %}交易记录 - 国内期货监控系统{% endblock %} +{% block content %} +

+

交易记录

+
+
+ +
+
+ + + + + + + + + + + + {% for t in trades %} + + + + + + + + + + + + + + + + + {% else %} + + {% endfor %} + +
品种类型方向成交止损(开仓)止盈基数杠杆持仓分钟开仓时间平仓时间盈亏(元)结果操作
{{ t.symbol_name or t.symbol }} + {{ t.monitor_type }} + + + + {{ '做多' if t.direction == 'long' else '做空' }} + + + + {{ t.entry_price }} + + + {{ t.stop_loss }} + + + {{ t.take_profit }} + + + {{ t.lots }}手 / {{ t.margin or '-' }} + + + + {{ t.holding_minutes or 0 }} + + + {{ (t.open_time or '')[:16].replace('T',' ') }} + + + {{ (t.close_time or '')[:16].replace('T',' ') }} + + + + {{ t.pnl if t.pnl is not none else '-' }} + + + + + + + {% if t.result == '止盈' %}{{ t.result }} + {% elif t.result == '止损' %}{{ t.result }} + {% elif t.result == '手动平仓' %}{{ t.result }} + {% else %}{{ t.result }}{% endif %} + {% if t.verified %}已核对{% endif %} + + + +
+ 填入复盘 + + 删除 +
+
暂无交易记录
+
+
+
+{% endblock %} +{% block extra_js %} + +{% endblock %}