持仓监控平仓自动记入交易记录,新增交易记录页与实盘资金设置
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from flask import (
|
|||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from symbols import search_symbols, ths_to_codes
|
from symbols import search_symbols, ths_to_codes
|
||||||
|
from contract_specs import calc_position_metrics
|
||||||
from kline_chart import generate_review_kline_chart
|
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
|
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 ""
|
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]:
|
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:
|
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,
|
kline_count INTEGER, kline_cutoff TEXT,
|
||||||
behavior_tags TEXT, notes TEXT,
|
behavior_tags TEXT, notes TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -551,7 +591,48 @@ def api_plan_prices():
|
|||||||
return jsonify(out)
|
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
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
return redirect(url_for("plans"))
|
return redirect(url_for("plans"))
|
||||||
@@ -647,8 +728,11 @@ def keys():
|
|||||||
history = conn.execute(
|
history = conn.execute(
|
||||||
"SELECT * FROM key_monitors WHERE status='archived' ORDER BY archived_at DESC LIMIT 100"
|
"SELECT * FROM key_monitors WHERE status='archived' ORDER BY archived_at DESC LIMIT 100"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
positions = conn.execute(
|
||||||
|
"SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC"
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
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"])
|
@app.route("/add_key", methods=["POST"])
|
||||||
@@ -679,6 +763,179 @@ def add_key():
|
|||||||
return redirect(url_for("keys"))
|
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/<int:pid>")
|
||||||
|
@login_required
|
||||||
|
def del_position(pid):
|
||||||
|
return close_position(pid)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/close_position/<int:pid>", 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/<int:tid>", 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/<int:tid>")
|
||||||
|
@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/<int:tid>")
|
||||||
|
@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/<int:pid>")
|
@app.route("/del_key/<int:pid>")
|
||||||
@login_required
|
@login_required
|
||||||
def del_key(pid):
|
def del_key(pid):
|
||||||
@@ -719,6 +976,13 @@ def records():
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
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(
|
return render_template(
|
||||||
"records.html",
|
"records.html",
|
||||||
reviews=review_list,
|
reviews=review_list,
|
||||||
@@ -726,6 +990,7 @@ def records():
|
|||||||
preset=preset,
|
preset=preset,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
|
prefill=prefill,
|
||||||
open_types=OPEN_TYPES,
|
open_types=OPEN_TYPES,
|
||||||
exit_triggers=EXIT_TRIGGERS,
|
exit_triggers=EXIT_TRIGGERS,
|
||||||
behavior_tags=BEHAVIOR_TAGS,
|
behavior_tags=BEHAVIOR_TAGS,
|
||||||
@@ -941,6 +1206,17 @@ def settings():
|
|||||||
webhook = request.form.get("wechat_webhook", "").strip()
|
webhook = request.form.get("wechat_webhook", "").strip()
|
||||||
set_setting("wechat_webhook", webhook)
|
set_setting("wechat_webhook", webhook)
|
||||||
flash("企业微信配置已保存")
|
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":
|
elif action == "password":
|
||||||
old_p = request.form.get("old_password", "")
|
old_p = request.form.get("old_password", "")
|
||||||
new_p = request.form.get("new_password", "")
|
new_p = request.form.get("new_password", "")
|
||||||
@@ -959,10 +1235,12 @@ def settings():
|
|||||||
|
|
||||||
webhook = get_setting("wechat_webhook")
|
webhook = get_setting("wechat_webhook")
|
||||||
username = get_setting("admin_username")
|
username = get_setting("admin_username")
|
||||||
|
live_capital = get_setting("live_capital", "0")
|
||||||
return render_template(
|
return render_template(
|
||||||
"settings.html",
|
"settings.html",
|
||||||
webhook=webhook,
|
webhook=webhook,
|
||||||
username=username,
|
username=username,
|
||||||
|
live_capital=live_capital,
|
||||||
quote_label=get_quote_source_label(),
|
quote_label=get_quote_source_label(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
+74
-9
@@ -1,12 +1,18 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var timer = null;
|
var keyTimer = null;
|
||||||
|
var posTimer = null;
|
||||||
|
|
||||||
function fmtDist(v) {
|
function fmtDist(v) {
|
||||||
if (v === null || v === undefined) return '--';
|
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');
|
var list = document.getElementById('key-monitor-list');
|
||||||
if (!list || !list.querySelector('.key-item')) return;
|
if (!list || !list.querySelector('.key-item')) return;
|
||||||
|
|
||||||
@@ -19,9 +25,7 @@
|
|||||||
var priceEl = el.querySelector('.live-price');
|
var priceEl = el.querySelector('.live-price');
|
||||||
var upEl = el.querySelector('.dist-up');
|
var upEl = el.querySelector('.dist-up');
|
||||||
var downEl = el.querySelector('.dist-down');
|
var downEl = el.querySelector('.dist-down');
|
||||||
if (priceEl) {
|
if (priceEl) priceEl.textContent = row.price != null ? row.price : '--';
|
||||||
priceEl.textContent = row.price != null ? row.price : '--';
|
|
||||||
}
|
|
||||||
if (upEl) upEl.textContent = fmtDist(row.dist_upper);
|
if (upEl) upEl.textContent = fmtDist(row.dist_upper);
|
||||||
if (downEl) downEl.textContent = fmtDist(row.dist_lower);
|
if (downEl) downEl.textContent = fmtDist(row.dist_lower);
|
||||||
});
|
});
|
||||||
@@ -29,10 +33,71 @@
|
|||||||
.catch(function () { /* ignore */ });
|
.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 (
|
||||||
|
'<div class="pos-card" data-pos-id="' + row.id + '">' +
|
||||||
|
'<div class="pos-card-head">' +
|
||||||
|
'<div><div class="title">' + row.symbol + ' <span class="badge dir">' + row.direction + '</span></div></div>' +
|
||||||
|
'<form method="post" action="/close_position/' + row.id + '" style="display:inline" onsubmit="return confirm(\'确认平仓?\')">' +
|
||||||
|
'<button type="submit" class="btn-del pos-del">平仓</button></form>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="pos-card-meta">来源 <strong>手动输入</strong> · 风险 <strong>' +
|
||||||
|
fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元</strong></div>' +
|
||||||
|
'<div class="pos-metrics">' +
|
||||||
|
'<div class="cell"><label>成交价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
||||||
|
'<div class="cell"><label>止损</label><div>' + fmtNum(row.stop_loss) + '</div></div>' +
|
||||||
|
'<div class="cell"><label>止盈</label><div>' + fmtNum(row.take_profit) + '</div></div>' +
|
||||||
|
'<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' +
|
||||||
|
'<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' +
|
||||||
|
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="pos-footer">' +
|
||||||
|
'<span>保证金 ' + fmtNum(row.margin) + '元</span>' +
|
||||||
|
'<span>仓位占比 ' + fmtNum(row.position_pct) + '%</span>' +
|
||||||
|
'<span>开仓 ' + (openT || '--') + '</span>' +
|
||||||
|
'<span>持仓 ' + (row.holding_duration || '--') + '</span>' +
|
||||||
|
'<span>张数 ' + row.lots + '</span>' +
|
||||||
|
'</div></div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<div class="empty-hint">暂无持仓,左侧录入后显示</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = rows.map(buildPosCard).join('');
|
||||||
|
})
|
||||||
|
.catch(function () { /* ignore */ });
|
||||||
|
}
|
||||||
|
|
||||||
function startPolling() {
|
function startPolling() {
|
||||||
if (timer) clearInterval(timer);
|
if (keyTimer) clearInterval(keyTimer);
|
||||||
pollPrices();
|
if (posTimer) clearInterval(posTimer);
|
||||||
timer = setInterval(pollPrices, 1000);
|
pollKeyPrices();
|
||||||
|
pollPositions();
|
||||||
|
keyTimer = setInterval(pollKeyPrices, 1000);
|
||||||
|
posTimer = setInterval(pollPositions, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', startPolling);
|
document.addEventListener('DOMContentLoaded', startPolling);
|
||||||
|
|||||||
+2
-2
@@ -25,10 +25,10 @@
|
|||||||
}
|
}
|
||||||
if (row.in_zone && distEl) {
|
if (row.in_zone && distEl) {
|
||||||
distEl.innerHTML = '<span class="text-profit" style="font-weight:600">在区间内</span>';
|
distEl.innerHTML = '<span class="text-profit" style="font-weight:600">在区间内</span>';
|
||||||
} else if (distEl && upEl && downEl) {
|
} else if (distEl) {
|
||||||
distEl.innerHTML =
|
distEl.innerHTML =
|
||||||
'距上<span class="dist-up">' + fmtDist(row.dist_upper) + '</span> ' +
|
'距上<span class="dist-up">' + fmtDist(row.dist_upper) + '</span> ' +
|
||||||
' · 距下 <span class="dist-down">' + fmtDist(row.dist_lower) + '</span>';
|
'距下<span class="dist-down">' + fmtDist(row.dist_lower) + '</span>';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -162,4 +162,6 @@
|
|||||||
bindModal();
|
bindModal();
|
||||||
recalc();
|
recalc();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.recalc = recalc;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
+35
-4
@@ -331,11 +331,41 @@
|
|||||||
.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}
|
||||||
.key-live{display:flex;flex-direction:column;align-items:center;gap:.15rem;min-width:100px;font-size:.8rem}
|
.key-live{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex:1;min-width:160px}
|
||||||
.key-live .live-price{font-size:1rem;font-weight:600;color:var(--accent)}
|
.key-live .live-price-line{font-size:.85rem;font-weight:600;color:var(--accent);white-space:nowrap}
|
||||||
.key-live .live-dist{color:var(--text-muted);font-size:.72rem;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)}
|
.key-live .live-dist span{color:var(--text-primary)}
|
||||||
.list-item.key-item{gap:.65rem}
|
.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)}
|
.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}
|
||||||
@@ -366,7 +396,8 @@
|
|||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
|
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
|
||||||
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
|
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
|
||||||
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a>
|
<a href="{{ url_for('trades') }}" class="{% if request.endpoint == 'trades' %}active{% endif %}">交易记录</a>
|
||||||
|
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">复盘</a>
|
||||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
||||||
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
|
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
+43
-2
@@ -43,8 +43,8 @@
|
|||||||
<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">
|
<div class="key-live">
|
||||||
<span class="live-price">--</span>
|
<span class="live-price-line">现价:<span class="live-price">--</span></span>
|
||||||
<span class="live-dist">距上 <span class="dist-up">--</span> · 距下 <span class="dist-down">--</span></span>
|
<span class="live-dist">距上<span class="dist-up">--</span> 距下<span class="dist-down">--</span></span>
|
||||||
</div>
|
</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>
|
||||||
@@ -79,6 +79,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="split-grid" style="margin-top:1.5rem">
|
||||||
|
<div class="card">
|
||||||
|
<h2>持仓录入</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ url_for('add_position') }}" 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="hidden" name="symbol" required>
|
||||||
|
<input type="hidden" name="symbol_name">
|
||||||
|
<input type="hidden" name="market_code" required>
|
||||||
|
<input type="hidden" name="sina_code">
|
||||||
|
<div class="symbol-dropdown"></div>
|
||||||
|
<div class="symbol-selected"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mini-field"><span>开仓时间</span><input type="datetime-local" name="open_time" required></div>
|
||||||
|
<input name="lots" type="number" step="1" min="1" value="1" placeholder="张数" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-line line-3">
|
||||||
|
<input name="entry_price" type="number" step="0.0001" placeholder="成交价格" required>
|
||||||
|
<input name="stop_loss" type="number" step="0.0001" placeholder="止损" required>
|
||||||
|
<input name="take_profit" type="number" step="0.0001" placeholder="止盈" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-line line-btn">
|
||||||
|
<button type="submit" class="btn-primary">添加持仓</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="hint" style="margin-top:.5rem">方向根据止损与成交价自动判断;风险比例依赖系统设置中的实盘资金。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>实时持仓</h2>
|
||||||
|
<div class="card-body card-scroll" id="position-live-list">
|
||||||
|
{% if not positions %}
|
||||||
|
<div class="empty-hint">暂无持仓,左侧录入后显示</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{{ url_for('static', filename='js/keys.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/keys.js') }}"></script>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
{% else %}<span class="badge active">已激活</span>{% endif %}
|
{% else %}<span class="badge active">已激活</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="key-live">
|
<div class="key-live">
|
||||||
<span class="live-price">--</span>
|
<span class="live-price-line">现价:<span class="live-price">--</span></span>
|
||||||
<span class="live-dist">距上 <span class="dist-up">--</span> · 距下 <span class="dist-down">--</span></span>
|
<span class="live-dist">距上<span class="dist-up">--</span> 距下<span class="dist-down">--</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
区间{{ p.zone_lower }}~{{ p.zone_upper }}
|
区间{{ p.zone_lower }}~{{ p.zone_upper }}
|
||||||
|
|||||||
+17
-1
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %}
|
{% block title %}复盘 - 国内期货监控系统{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="split-grid records-split">
|
<div class="split-grid records-split">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -178,4 +178,20 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
|
||||||
|
{% if prefill %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var form = document.getElementById('review-form');
|
||||||
|
if (!form) return;
|
||||||
|
var map = {{ prefill | tojson }};
|
||||||
|
Object.keys(map).forEach(function (k) {
|
||||||
|
var el = form.querySelector('[name="' + k + '"]');
|
||||||
|
if (el && map[k] != null && map[k] !== '') el.value = map[k];
|
||||||
|
});
|
||||||
|
var symInput = form.querySelector('.symbol-input');
|
||||||
|
if (symInput && map.symbol_name) symInput.value = map.symbol_name;
|
||||||
|
if (typeof recalc === 'function') recalc();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>实盘资金</h2>
|
||||||
|
<form action="{{ url_for('settings') }}" method="post" class="form-row">
|
||||||
|
<input type="hidden" name="action" value="capital">
|
||||||
|
<input name="live_capital" type="number" step="0.01" min="0" placeholder="实盘资金(元)" value="{{ live_capital }}" style="flex:1;min-width:200px;max-width:320px">
|
||||||
|
<button type="submit" class="btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
<p class="hint" style="margin-top:.75rem">用于持仓监控的风险比例、仓位占比计算,保存在数据库中。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>行情说明</h2>
|
<h2>行情说明</h2>
|
||||||
<p class="hint" style="font-size:.9rem;line-height:1.6">
|
<p class="hint" style="font-size:.9rem;line-height:1.6">
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}交易记录 - 国内期货监控系统{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<h2>交易记录</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="trade-toolbar">
|
||||||
|
<label><input type="checkbox" id="trade-edit-switch"> 修改/核对开关(开启后可编辑关键字段)</label>
|
||||||
|
</div>
|
||||||
|
<div class="trade-table-wrap card-scroll">
|
||||||
|
<table class="trade-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>品种</th><th>类型</th><th>方向</th>
|
||||||
|
<th>成交</th><th>止损(开仓)</th><th>止盈</th>
|
||||||
|
<th>基数</th><th>杠杆</th><th>持仓分钟</th>
|
||||||
|
<th>开仓时间</th><th>平仓时间</th>
|
||||||
|
<th>盈亏(元)</th><th>结果</th><th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for t in trades %}
|
||||||
|
<tr data-trade-id="{{ t.id }}">
|
||||||
|
<td><span class="cell-readonly">{{ t.symbol_name or t.symbol }}</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ t.monitor_type }}</span>
|
||||||
|
<input class="cell-edit-show" type="hidden" name="monitor_type" value="{{ t.monitor_type }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">
|
||||||
|
<span class="badge dir">{{ '做多' if t.direction == 'long' else '做空' }}</span>
|
||||||
|
</span>
|
||||||
|
<select class="cell-edit-show" name="direction" style="display:none">
|
||||||
|
<option value="long" {% if t.direction=='long' %}selected{% endif %}>做多</option>
|
||||||
|
<option value="short" {% if t.direction=='short' %}selected{% endif %}>做空</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ t.entry_price }}</span>
|
||||||
|
<input class="cell-edit-show" type="number" step="0.0001" name="entry_price" value="{{ t.entry_price }}" style="display:none">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ t.stop_loss }}</span>
|
||||||
|
<input class="cell-edit-show" type="number" step="0.0001" name="stop_loss" value="{{ t.stop_loss }}" style="display:none">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ t.take_profit }}</span>
|
||||||
|
<input class="cell-edit-show" type="number" step="0.0001" name="take_profit" value="{{ t.take_profit }}" style="display:none">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ t.lots }}手 / {{ t.margin or '-' }}</span>
|
||||||
|
<input class="cell-edit-show" type="number" step="0.01" name="margin" value="{{ t.margin or '' }}" placeholder="保证金" style="display:none">
|
||||||
|
<input type="hidden" name="lots" value="{{ t.lots }}">
|
||||||
|
</td>
|
||||||
|
<td><span class="cell-readonly">—</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ t.holding_minutes or 0 }}</span>
|
||||||
|
<input class="cell-edit-show" type="number" name="holding_minutes" value="{{ t.holding_minutes or 0 }}" style="display:none">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ (t.open_time or '')[:16].replace('T',' ') }}</span>
|
||||||
|
<input class="cell-edit-show" type="text" name="open_time" value="{{ t.open_time or '' }}" style="display:none">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">{{ (t.close_time or '')[:16].replace('T',' ') }}</span>
|
||||||
|
<input class="cell-edit-show" type="text" name="close_time" value="{{ t.close_time or '' }}" style="display:none">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide {% if t.pnl and t.pnl > 0 %}text-profit{% elif t.pnl and t.pnl < 0 %}text-loss{% endif %}">
|
||||||
|
{{ t.pnl if t.pnl is not none else '-' }}
|
||||||
|
</span>
|
||||||
|
<input class="cell-edit-show" type="number" step="0.01" name="pnl" value="{{ t.pnl or '' }}" style="display:none">
|
||||||
|
<input type="hidden" name="close_price" value="{{ t.close_price or '' }}">
|
||||||
|
<input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="cell-readonly cell-edit-hide">
|
||||||
|
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
|
||||||
|
{% elif t.result == '止损' %}<span class="badge loss">{{ t.result }}</span>
|
||||||
|
{% elif t.result == '手动平仓' %}<span class="badge result-manual">{{ t.result }}</span>
|
||||||
|
{% else %}<span class="badge result-external">{{ t.result }}</span>{% endif %}
|
||||||
|
{% if t.verified %}<span class="badge active" style="margin-left:.25rem">已核对</span>{% endif %}
|
||||||
|
</span>
|
||||||
|
<select class="cell-edit-show" name="result" style="display:none">
|
||||||
|
<option value="手动平仓" {% if t.result=='手动平仓' %}selected{% endif %}>手动平仓</option>
|
||||||
|
<option value="止盈" {% if t.result=='止盈' %}selected{% endif %}>止盈</option>
|
||||||
|
<option value="止损" {% if t.result=='止损' %}selected{% endif %}>止损</option>
|
||||||
|
<option value="外部平仓" {% if t.result=='外部平仓' %}selected{% endif %}>外部平仓</option>
|
||||||
|
<option value="时间平仓" {% if t.result=='时间平仓' %}selected{% endif %}>时间平仓</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="trade-actions">
|
||||||
|
<a href="{{ url_for('fill_review_from_trade', tid=t.id) }}" class="btn-fill">填入复盘</a>
|
||||||
|
<button type="button" class="btn-verify trade-save-btn" disabled>核对修改</button>
|
||||||
|
<a href="{{ url_for('del_trade', tid=t.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="14" class="text-muted">暂无交易记录</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="{{ url_for('static', filename='js/trades.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user