顶部导航1800宽;开单计划按日失效与历史筛选;复盘表单

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 11:54:21 +08:00
parent 471166bec3
commit 0bebdd5717
5 changed files with 487 additions and 82 deletions
+1 -1
View File
@@ -7,4 +7,4 @@ __pycache__/
venv/ venv/
.venv/ .venv/
*.log *.log
.DS_Store uploads/
+202 -11
View File
@@ -6,6 +6,9 @@ import requests
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from functools import wraps from functools import wraps
from zoneinfo import ZoneInfo
from werkzeug.utils import secure_filename
from dotenv import load_dotenv from dotenv import load_dotenv
from flask import ( from flask import (
@@ -27,6 +30,18 @@ PORT = int(os.getenv("PORT", "6600"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes") DEBUG = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes")
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db") DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
UPLOAD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads")
TZ = ZoneInfo("Asia/Shanghai")
OPEN_TYPES = ["突破开仓", "回调开仓", "追涨杀跌", "计划内开仓", "震荡摸顶底", "其他"]
EXIT_TRIGGERS = ["止盈", "止损", "手工平仓", "移动止损", "时间离场", "其他"]
BEHAVIOR_TAGS = ["怕踏空", "报复开仓", "盈利飘了", "拿不住单", "扛单", "重仓违规"]
KLINE_PERIODS = ["1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d"]
KLINE_CUTOFFS = ["平仓时间", "开仓时间", "当前时间"]
def today_str() -> str:
return datetime.now(TZ).date().isoformat()
# —————————————— 设置读写 —————————————— # —————————————— 设置读写 ——————————————
@@ -87,12 +102,27 @@ def init_db():
"ALTER TABLE order_plans ADD COLUMN market_code TEXT", "ALTER TABLE order_plans ADD COLUMN market_code TEXT",
"ALTER TABLE key_monitors ADD COLUMN market_code TEXT", "ALTER TABLE key_monitors ADD COLUMN market_code TEXT",
"ALTER TABLE trade_records ADD COLUMN market_code TEXT", "ALTER TABLE trade_records ADD COLUMN market_code TEXT",
"ALTER TABLE order_plans ADD COLUMN plan_date TEXT",
] ]
for sql in migrations: for sql in migrations:
try: try:
c.execute(sql) c.execute(sql)
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
c.execute('''CREATE TABLE IF NOT EXISTS review_records
(id INTEGER PRIMARY KEY AUTOINCREMENT,
open_time TEXT, close_time TEXT,
symbol TEXT, timeframe TEXT,
pnl REAL,
open_type TEXT, expected_rr REAL, actual_rr REAL,
exit_trigger TEXT, exit_supplement TEXT,
watch_after_breakeven TEXT, new_position_while_occupied TEXT,
screenshot TEXT,
auto_kline INTEGER DEFAULT 0,
kline_period1 TEXT, kline_period2 TEXT,
kline_count INTEGER, kline_cutoff TEXT,
behavior_tags TEXT, notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
conn.commit() conn.commit()
conn.close() conn.close()
@@ -104,6 +134,9 @@ def init_db():
if not get_setting("ths_refresh_token") and os.getenv("THS_REFRESH_TOKEN"): if not get_setting("ths_refresh_token") and os.getenv("THS_REFRESH_TOKEN"):
set_setting("ths_refresh_token", os.getenv("THS_REFRESH_TOKEN")) set_setting("ths_refresh_token", os.getenv("THS_REFRESH_TOKEN"))
os.makedirs(UPLOAD_DIR, exist_ok=True)
expire_old_plans()
def sync_admin_from_env(): def sync_admin_from_env():
""" """
@@ -178,10 +211,30 @@ def fetch_price(ths_code: str, market_code: str = "", sina_code: str = "") -> Op
# —————————————— 监控逻辑 —————————————— # —————————————— 监控逻辑 ——————————————
# —————————————— 开单计划(按日) ——————————————
def expire_old_plans():
"""当日结束后计划自动失效,保留历史。"""
today = today_str()
conn = get_db()
conn.execute(
"UPDATE order_plans SET status='expired' WHERE plan_date < ? AND status IN ('planned', 'active')",
(today,),
)
conn.execute(
"UPDATE order_plans SET plan_date=date(created_at) WHERE plan_date IS NULL OR plan_date=''"
)
conn.commit()
conn.close()
def check_order_plans(): def check_order_plans():
expire_old_plans()
today = today_str()
conn = get_db() conn = get_db()
rows = conn.execute( rows = conn.execute(
"SELECT * FROM order_plans WHERE status IN ('planned', 'active')" "SELECT * FROM order_plans WHERE plan_date=? AND status IN ('planned', 'active')",
(today,),
).fetchall() ).fetchall()
for r in rows: for r in rows:
@@ -305,6 +358,7 @@ def check_key_monitors():
def background_task(): def background_task():
while True: while True:
try: try:
expire_old_plans()
check_key_monitors() check_key_monitors()
check_order_plans() check_order_plans()
except Exception: except Exception:
@@ -361,15 +415,35 @@ def index():
@app.route("/plans") @app.route("/plans")
@login_required @login_required
def plans(): def plans():
today = today_str()
start = request.args.get("start", "")
end = request.args.get("end", "")
conn = get_db() conn = get_db()
plan_list = conn.execute( plan_list = conn.execute(
"SELECT * FROM order_plans WHERE status != 'closed' ORDER BY id DESC" "SELECT * FROM order_plans WHERE plan_date=? AND status IN ('planned', 'active') ORDER BY id DESC",
).fetchall() (today,),
closed = conn.execute(
"SELECT * FROM order_plans WHERE status='closed' ORDER BY id DESC LIMIT 20"
).fetchall() ).fetchall()
sql = "SELECT * FROM order_plans WHERE plan_date < ? OR status IN ('closed', 'expired')"
params: list = [today]
if start:
sql += " AND plan_date >= ?"
params.append(start)
if end:
sql += " AND plan_date <= ?"
params.append(end)
sql += " ORDER BY plan_date DESC, id DESC LIMIT 200"
history = conn.execute(sql, params).fetchall()
conn.close() conn.close()
return render_template("plans.html", plans=plan_list, closed=closed) return render_template(
"plans.html",
plans=plan_list,
history=history,
today=today,
start=start,
end=end,
)
@app.route("/add_plan", methods=["POST"]) @app.route("/add_plan", methods=["POST"])
@@ -391,12 +465,13 @@ def add_plan():
conn.execute( conn.execute(
"""INSERT INTO order_plans """INSERT INTO order_plans
(symbol, symbol_name, market_code, sina_code, direction, (symbol, symbol_name, market_code, sina_code, direction,
zone_upper, zone_lower, stop_loss, take_profit) zone_upper, zone_lower, stop_loss, take_profit, plan_date)
VALUES (?,?,?,?,?,?,?,?)""", VALUES (?,?,?,?,?,?,?,?,?,?)""",
( (
symbol, symbol_name, market_code, sina_code, direction, symbol, symbol_name, market_code, sina_code, direction,
float(d["zone_upper"]), float(d["zone_lower"]), float(d["zone_upper"]), float(d["zone_lower"]),
float(d["stop_loss"]), float(d["take_profit"]), float(d["stop_loss"]), float(d["take_profit"]),
today_str(),
), ),
) )
conn.commit() conn.commit()
@@ -467,12 +542,128 @@ def del_key(pid):
@app.route("/records") @app.route("/records")
@login_required @login_required
def records(): def records():
start = request.args.get("start", "")
end = request.args.get("end", "")
conn = get_db() conn = get_db()
record_list = conn.execute( sql = "SELECT * FROM review_records WHERE 1=1"
"SELECT * FROM trade_records ORDER BY id DESC" params: list = []
if start:
sql += " AND date(close_time) >= ?"
params.append(start)
if end:
sql += " AND date(close_time) <= ?"
params.append(end)
sql += " ORDER BY id DESC LIMIT 200"
review_list = conn.execute(sql, params).fetchall()
auto_list = conn.execute(
"SELECT * FROM trade_records ORDER BY id DESC LIMIT 50"
).fetchall() ).fetchall()
conn.close() conn.close()
return render_template("records.html", records=record_list)
return render_template(
"records.html",
reviews=review_list,
auto_records=auto_list,
start=start,
end=end,
open_types=OPEN_TYPES,
exit_triggers=EXIT_TRIGGERS,
behavior_tags=BEHAVIOR_TAGS,
kline_periods=KLINE_PERIODS,
kline_cutoffs=KLINE_CUTOFFS,
)
@app.route("/add_review", methods=["POST"])
@login_required
def add_review():
d = request.form
open_type = d.get("open_type", "").strip()
exit_trigger = d.get("exit_trigger", "").strip()
if not open_type:
flash("请选择开仓类型")
return redirect(url_for("records"))
if not exit_trigger:
flash("请选择离场触发")
return redirect(url_for("records"))
screenshot = ""
f = request.files.get("screenshot")
if f and f.filename:
fname = secure_filename(f.filename)
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
screenshot = f"{ts}_{fname}"
f.save(os.path.join(UPLOAD_DIR, screenshot))
tags = [t for t in BEHAVIOR_TAGS if d.get(f"tag_{t}")]
def num(key: str) -> Optional[float]:
v = d.get(key, "").strip()
if not v:
return None
return float(v)
conn = get_db()
conn.execute(
"""INSERT INTO review_records
(open_time, close_time, symbol, timeframe, pnl,
open_type, expected_rr, actual_rr, exit_trigger, exit_supplement,
watch_after_breakeven, new_position_while_occupied, screenshot,
auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff,
behavior_tags, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
d.get("open_time", "").strip(),
d.get("close_time", "").strip(),
d.get("symbol", "").strip(),
d.get("timeframe", "").strip(),
num("pnl"),
open_type,
num("expected_rr"),
num("actual_rr"),
exit_trigger,
d.get("exit_supplement", "").strip(),
d.get("watch_after_breakeven", ""),
d.get("new_position_while_occupied", ""),
screenshot,
1 if d.get("auto_kline") else 0,
d.get("kline_period1", "15m"),
d.get("kline_period2", "1h"),
int(d.get("kline_count") or 300),
d.get("kline_cutoff", "平仓时间"),
",".join(tags),
d.get("notes", "").strip(),
),
)
conn.commit()
conn.close()
flash("复盘记录已保存")
return redirect(url_for("records"))
@app.route("/del_review/<int:rid>")
@login_required
def del_review(rid):
conn = get_db()
row = conn.execute("SELECT screenshot FROM review_records WHERE id=?", (rid,)).fetchone()
if row and row["screenshot"]:
path = os.path.join(UPLOAD_DIR, row["screenshot"])
if os.path.isfile(path):
os.remove(path)
conn.execute("DELETE FROM review_records WHERE id=?", (rid,))
conn.commit()
conn.close()
flash("已删除")
return redirect(url_for("records"))
@app.route("/uploads/<path:filename>")
@login_required
def uploaded_file(filename):
from flask import send_from_directory
return send_from_directory(UPLOAD_DIR, filename)
@app.route("/del_record/<int:rid>") @app.route("/del_record/<int:rid>")
+42 -33
View File
@@ -7,75 +7,84 @@
<style> <style>
*{margin:0;padding:0;box-sizing:border-box} *{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a10;color:#eaeaea;min-height:100vh} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a10;color:#eaeaea;min-height:100vh}
.layout{display:flex;min-height:100vh} .page-wrap{max-width:1800px;margin:0 auto;min-height:100vh}
.sidebar{width:220px;background:#12121a;border-right:1px solid #242435;padding:1.5rem 0;flex-shrink:0} .topbar{background:#12121a;border-bottom:1px solid #242435;padding:0 1.5rem}
.sidebar .logo{padding:0 1.5rem 1.5rem;font-size:1.1rem;font-weight:600;background:linear-gradient(90deg,#4cc2ff,#7b42ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent} .topbar-inner{display:flex;align-items:center;gap:1.5rem;height:56px}
.nav a{display:block;padding:.75rem 1.5rem;color:#a9a9c4;text-decoration:none;font-size:.9rem;border-left:3px solid transparent;transition:.2s} .logo{font-size:1.05rem;font-weight:600;background:linear-gradient(90deg,#4cc2ff,#7b42ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;white-space:nowrap}
.nav{display:flex;gap:.25rem;flex:1;flex-wrap:wrap}
.nav a{padding:.5rem 1rem;color:#a9a9c4;text-decoration:none;font-size:.9rem;border-radius:8px;transition:.2s}
.nav a:hover{color:#fff;background:#1a1a29} .nav a:hover{color:#fff;background:#1a1a29}
.nav a.active{color:#4cc2ff;border-left-color:#4cc2ff;background:#1a1a29} .nav a.active{color:#4cc2ff;background:#1a1a29}
.main{flex:1;padding:2rem;overflow-x:auto} .user-bar{font-size:.85rem;color:#888;white-space:nowrap}
.user-bar a{color:#ff6666;text-decoration:none;margin-left:.5rem}
.main{padding:1.5rem}
.page-title{font-size:1.5rem;margin-bottom:1.5rem;color:#fff} .page-title{font-size:1.5rem;margin-bottom:1.5rem;color:#fff}
.flash{padding:1rem;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:1.5rem;text-align:center} .flash{padding:1rem;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:1.5rem;text-align:center}
.card{background:#12121a;border-radius:16px;padding:1.5rem;border:1px solid #242435;margin-bottom:1.5rem} .card{background:#12121a;border-radius:16px;padding:1.5rem;border:1px solid #242435;margin-bottom:1.5rem}
.card h2{font-size:1.15rem;margin-bottom:1rem;color:#c4c4ff;display:flex;align-items:center;gap:.5rem} .card h2{font-size:1.15rem;margin-bottom:1rem;color:#c4c4ff;display:flex;align-items:center;gap:.5rem}
.card h2:before{content:"";width:4px;height:16px;background:#4cc2ff;border-radius:2px} .card h2:before{content:"";width:4px;height:16px;background:#4cc2ff;border-radius:2px}
.form-row{display:flex;gap:.75rem;flex-wrap:wrap;margin-bottom:1rem;align-items:flex-start} .form-row{display:flex;gap:.75rem;flex-wrap:wrap;margin-bottom:1rem;align-items:flex-start}
input,select,button{padding:.7rem 1rem;border-radius:10px;border:1px solid #2e2e45;background:#1a1a29;color:#fff;font-size:.9rem;outline:none} .form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.75rem;margin-bottom:1rem}
input:focus,select:focus{border-color:#4cc2ff} .form-grid .full{grid-column:1/-1}
button.btn-primary{background:linear-gradient(90deg,#4285f4,#7b42ff);border:none;cursor:pointer;color:#fff} .field label{display:block;font-size:.8rem;color:#a9a9ff;margin-bottom:.35rem}
input,select,textarea,button{padding:.7rem 1rem;border-radius:10px;border:1px solid #2e2e45;background:#1a1a29;color:#fff;font-size:.9rem;outline:none;width:100%}
textarea{min-height:80px;resize:vertical}
input:focus,select:focus,textarea:focus{border-color:#4cc2ff}
button.btn-primary{background:linear-gradient(90deg,#4285f4,#7b42ff);border:none;cursor:pointer;color:#fff;width:auto}
button.btn-primary:hover{opacity:.9} button.btn-primary:hover{opacity:.9}
.list{display:flex;flex-direction:column;gap:.75rem} .list{display:flex;flex-direction:column;gap:.75rem}
.list-item{display:flex;justify-content:space-between;align-items:center;padding:1rem;background:#161625;border-radius:10px;gap:1rem;flex-wrap:wrap} .list-item{display:flex;justify-content:space-between;align-items:center;padding:1rem;background:#161625;border-radius:10px;gap:1rem;flex-wrap:wrap}
.btn-del{padding:.4rem .8rem;background:#291d2f;color:#ff6666;border-radius:8px;text-decoration:none;font-size:.85rem} .btn-del{padding:.4rem .8rem;background:#291d2f;color:#ff6666;border-radius:8px;text-decoration:none;font-size:.85rem}
table{width:100%;border-collapse:collapse} table{width:100%;border-collapse:collapse}
th,td{padding:.85rem;text-align:left;border-bottom:1px solid #242435;font-size:.9rem} th,td{padding:.85rem;text-align:left;border-bottom:1px solid #242435;font-size:.9rem}
th{color:#a9a9ff} th{color:#a9a9ff;white-space:nowrap}
.badge{padding:.25rem .5rem;border-radius:6px;font-size:.75rem} .badge{padding:.25rem .5rem;border-radius:6px;font-size:.75rem}
.badge.profit{background:#1e332f;color:#4cd97f} .badge.profit{background:#1e332f;color:#4cd97f}
.badge.loss{background:#331e24;color:#ff6666} .badge.loss{background:#331e24;color:#ff6666}
.badge.dir{background:#1e2533;color:#4cc2ff} .badge.dir{background:#1e2533;color:#4cc2ff}
.badge.planned{background:#29241e;color:#eac147} .badge.planned{background:#29241e;color:#eac147}
.badge.active{background:#1e332f;color:#4cd97f} .badge.active{background:#1e332f;color:#4cd97f}
.badge.expired{background:#2a2a35;color:#999}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin-bottom:1.5rem} .stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin-bottom:1.5rem}
.stat-item{background:#161625;padding:1rem;border-radius:12px;text-align:center} .stat-item{background:#161625;padding:1rem;border-radius:12px;text-align:center}
.stat-item .label{font-size:.8rem;color:#999} .stat-item .label{font-size:.8rem;color:#999}
.stat-item .value{font-size:1.4rem;font-weight:600;color:#fff;margin-top:.25rem} .stat-item .value{font-size:1.4rem;font-weight:600;color:#fff;margin-top:.25rem}
.symbol-wrap{position:relative;min-width:180px} .symbol-wrap{position:relative}
.symbol-wrap input{width:100%}
.symbol-dropdown{position:absolute;top:100%;left:0;right:0;background:#1a1a29;border:1px solid #2e2e45;border-radius:10px;margin-top:4px;z-index:100;max-height:240px;overflow-y:auto;display:none} .symbol-dropdown{position:absolute;top:100%;left:0;right:0;background:#1a1a29;border:1px solid #2e2e45;border-radius:10px;margin-top:4px;z-index:100;max-height:240px;overflow-y:auto;display:none}
.symbol-dropdown.show{display:block} .symbol-dropdown.show{display:block}
.symbol-option{padding:.65rem 1rem;cursor:pointer;font-size:.85rem;border-bottom:1px solid #242435} .symbol-option{padding:.65rem 1rem;cursor:pointer;font-size:.85rem;border-bottom:1px solid #242435}
.symbol-option:hover{background:#242435} .symbol-option:hover{background:#242435}
.symbol-option .sub{font-size:.75rem;color:#888;margin-top:2px} .symbol-option .sub{font-size:.75rem;color:#888;margin-top:2px}
.symbol-selected{font-size:.75rem;color:#4cc2ff;margin-top:4px} .symbol-selected{font-size:.75rem;color:#4cc2ff;margin-top:4px}
.user-bar{padding:1rem 1.5rem;border-top:1px solid #242435;margin-top:1rem;font-size:.8rem;color:#888} .check-row{display:flex;flex-wrap:wrap;gap:1rem;margin:.75rem 0}
.user-bar a{color:#ff6666;text-decoration:none} .check-row label{display:flex;align-items:center;gap:.4rem;font-size:.85rem;color:#ccc;cursor:pointer}
.check-row input{width:auto}
.hint{font-size:.78rem;color:#888;line-height:1.5;margin-top:.5rem}
.filter-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;margin-bottom:1rem}
.filter-row .field{width:auto;min-width:140px}
.filter-row button{width:auto}
@media(max-width:768px){ @media(max-width:768px){
.layout{flex-direction:column} .topbar-inner{flex-wrap:wrap;height:auto;padding:.75rem 0}
.sidebar{width:100%} .nav{order:3;width:100%}
.nav{display:flex;flex-wrap:wrap}
.nav a{border-left:none;border-bottom:2px solid transparent;padding:.5rem 1rem}
.nav a.active{border-bottom-color:#4cc2ff}
} }
</style> </style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
<div class="layout"> <div class="page-wrap">
<aside class="sidebar"> <header class="topbar">
<div class="logo">期货监控复盘</div> <div class="topbar-inner">
<nav class="nav"> <div class="logo">期货监控复盘</div>
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a> <nav class="nav">
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a> <a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a> <a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a> <a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a> <a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
</nav> <a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
<div class="user-bar"> </nav>
{{ session.username or '用户' }} <div class="user-bar">{{ session.username or '用户' }}<a href="{{ url_for('logout') }}">退出</a></div>
<a href="{{ url_for('logout') }}">退出</a>
</div> </div>
</aside> </header>
<main class="main"> <main class="main">
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %} {% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
+53 -17
View File
@@ -1,12 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}开单计划 - 国内期货监控系统{% endblock %} {% block title %}开单计划 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">开单计划</h1> <h1 class="page-title">开单计划 <span style="font-size:.9rem;color:#888;font-weight:normal">今日 {{ today }}</span></h1>
<div class="card"> <div class="card">
<h2>新增计划</h2> <h2>今日计划(开盘前制定,当日有效)</h2>
<form action="{{ url_for('add_plan') }}" method="post" class="form-row"> <form action="{{ url_for('add_plan') }}" method="post" class="form-row">
<div class="symbol-wrap"> <div class="symbol-wrap" style="min-width:200px">
<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">
@@ -24,12 +24,13 @@
<input name="zone_upper" type="number" step="0.0001" placeholder="决策区间上限" required> <input name="zone_upper" type="number" step="0.0001" placeholder="决策区间上限" required>
<input name="stop_loss" 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> <input name="take_profit" type="number" step="0.0001" placeholder="止盈价位" required>
<button type="submit" class="btn-primary">添加计划</button> <button type="submit" class="btn-primary">添加今日计划</button>
</form> </form>
<p class="hint">计划仅当日有效,次日 0 点自动失效并归入历史;触发止盈/止损后标记为已完成。</p>
</div> </div>
<div class="card"> <div class="card">
<h2>进行中计划</h2> <h2>今日进行中</h2>
<div class="list"> <div class="list">
{% for p in plans %} {% for p in plans %}
<div class="list-item"> <div class="list-item">
@@ -48,23 +49,58 @@
<a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除此计划?')">删除</a> <a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除此计划?')">删除</a>
</div> </div>
{% else %} {% else %}
<div style="color:#888;padding:1rem">暂无进行中的开单计划</div> <div style="color:#888;padding:1rem">今日暂无开单计划</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% if closed %}
<div class="card"> <div class="card">
<h2>最近已完成</h2> <h2>历史计划</h2>
<div class="list"> <form method="get" class="filter-row">
{% for p in closed %} <div class="field">
<div class="list-item"> <label>开始日期</label>
<div><strong>{{ p.symbol_name or p.symbol }}</strong> <span class="badge dir">{{ '做多' if p.direction == 'long' else '做空' }}</span></div> <input type="date" name="start" value="{{ start }}">
<div>区间: {{ p.zone_lower }} ~ {{ p.zone_upper }} | 损: {{ p.stop_loss }} 盈: {{ p.take_profit }}</div>
<a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
</div> </div>
{% endfor %} <div class="field">
</div> <label>结束日期</label>
<input type="date" name="end" value="{{ end }}">
</div>
<button type="submit" class="btn-primary">筛选</button>
<a href="{{ url_for('plans') }}" style="color:#888;font-size:.85rem;padding:.7rem">重置</a>
</form>
<table>
<thead>
<tr>
<th>日期</th>
<th>品种</th>
<th>方向</th>
<th>区间</th>
<th>止损</th>
<th>止盈</th>
<th>状态</th>
<th></th>
</tr>
</thead>
<tbody>
{% for p in history %}
<tr>
<td>{{ p.plan_date or '' }}</td>
<td>{{ p.symbol_name or p.symbol }}</td>
<td><span class="badge dir">{{ '做多' if p.direction == 'long' else '做空' }}</span></td>
<td>{{ p.zone_lower }} ~ {{ p.zone_upper }}</td>
<td>{{ p.stop_loss }}</td>
<td>{{ p.take_profit }}</td>
<td>
{% if p.status == 'closed' %}<span class="badge profit">已完成</span>
{% elif p.status == 'expired' %}<span class="badge expired">已失效</span>
{% else %}<span class="badge">{{ p.status }}</span>{% endif %}
</td>
<td><a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tr>
{% else %}
<tr><td colspan="8" style="color:#888">暂无历史记录</td></tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
+189 -20
View File
@@ -1,47 +1,216 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %} {% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">交易记录与复盘</h1> <h1 class="page-title">交易复盘记录上传(含截图)</h1>
<div class="card"> <div class="card">
<h2>全部记录</h2> <form action="{{ url_for('add_review') }}" method="post" enctype="multipart/form-data">
<div class="form-grid">
<div class="field">
<label>开仓时间</label>
<input type="datetime-local" name="open_time" required>
</div>
<div class="field">
<label>平仓时间</label>
<input type="datetime-local" name="close_time" required>
</div>
<div class="field">
<label>品种</label>
<input name="symbol" placeholder="如 ag2608" required>
</div>
<div class="field">
<label>周期</label>
<input name="timeframe" placeholder="5m" value="5m">
</div>
<div class="field">
<label>盈亏(U)</label>
<input name="pnl" type="number" step="0.01" placeholder="盈亏金额">
</div>
<div class="field full">
<label>开仓类型(必选)</label>
<select name="open_type" required>
<option value="">请选择</option>
{% for t in open_types %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>预期 RR</label>
<input name="expected_rr" type="number" step="0.01" placeholder="预期盈亏比">
</div>
<div class="field">
<label>实际 RR</label>
<input name="actual_rr" type="number" step="0.01" placeholder="实际盈亏比">
</div>
<div class="field">
<label>离场触发(必选)</label>
<select name="exit_trigger" required>
<option value="">请选择</option>
{% for t in exit_triggers %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>离场补充(仅手工平仓必填)</label>
<input name="exit_supplement" placeholder="手工平仓说明">
</div>
<div class="field">
<label>保本后盯盘</label>
<select name="watch_after_breakeven">
<option value="否"></option>
<option value="是"></option>
</select>
</div>
<div class="field">
<label>占用时新开仓</label>
<select name="new_position_while_occupied">
<option value="否"></option>
<option value="是"></option>
</select>
</div>
<div class="field">
<label>截图上传</label>
<input type="file" name="screenshot" accept="image/*">
</div>
</div>
<div class="check-row">
<label><input type="checkbox" name="auto_kline" value="1"> 保存时自动生成 K 线图并作为截图</label>
</div>
<div class="form-grid">
<div class="field">
<label>周期1</label>
<select name="kline_period1">
{% for p in kline_periods %}
<option value="{{ p }}" {% if p == '15m' %}selected{% endif %}>{{ p }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>周期2</label>
<select name="kline_period2">
{% for p in kline_periods %}
<option value="{{ p }}" {% if p == '1h' %}selected{% endif %}>{{ p }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label>K线数</label>
<input name="kline_count" type="number" value="300" min="50" max="1000">
</div>
<div class="field">
<label>K线截止</label>
<select name="kline_cutoff">
{% for c in kline_cutoffs %}
<option value="{{ c }}">{{ c }}</option>
{% endfor %}
</select>
</div>
</div>
<p class="hint">勾选自动生成时,将按所选周期上下排列 K 线图;截止时间为平仓/开仓/当前,用于截取行情片段。</p>
<div class="check-row">
{% for tag in behavior_tags %}
<label><input type="checkbox" name="tag_{{ tag }}" value="1"> {{ tag }}</label>
{% endfor %}
</div>
<div class="field" style="margin-top:1rem">
<label>备注</label>
<textarea name="notes" placeholder="复盘备注"></textarea>
</div>
<button type="submit" class="btn-primary" style="margin-top:1rem">保存复盘记录</button>
</form>
</div>
<div class="card">
<h2>复盘历史</h2>
<form method="get" class="filter-row">
<div class="field">
<label>开始日期</label>
<input type="date" name="start" value="{{ start }}">
</div>
<div class="field">
<label>结束日期</label>
<input type="date" name="end" value="{{ end }}">
</div>
<button type="submit" class="btn-primary">筛选</button>
<a href="{{ url_for('records') }}" style="color:#888;font-size:.85rem;padding:.7rem">重置</a>
</form>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>开仓</th>
<th>平仓</th>
<th>品种</th> <th>品种</th>
<th>类型</th> <th>周期</th>
<th>方向</th> <th>盈亏</th>
<th>触发价</th> <th>开仓类型</th>
<th>止损</th> <th>预期RR</th>
<th>止盈</th> <th>实际RR</th>
<th>结果</th> <th>离场</th>
<th>时间</th> <th>行为标签</th>
<th>截图</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for r in records %} {% for r in reviews %}
<tr>
<td>{{ r.open_time[:16] if r.open_time else '' }}</td>
<td>{{ r.close_time[:16] if r.close_time else '' }}</td>
<td>{{ r.symbol }}</td>
<td>{{ r.timeframe }}</td>
<td>
{% if r.pnl and r.pnl > 0 %}<span class="badge profit">{{ r.pnl }}</span>
{% elif r.pnl and r.pnl < 0 %}<span class="badge loss">{{ r.pnl }}</span>
{% else %}{{ r.pnl or '-' }}{% endif %}
</td>
<td>{{ r.open_type }}</td>
<td>{{ r.expected_rr or '-' }}</td>
<td>{{ r.actual_rr or '-' }}</td>
<td>{{ r.exit_trigger }}</td>
<td style="font-size:.8rem">{{ r.behavior_tags or '-' }}</td>
<td>
{% if r.screenshot %}
<a href="{{ url_for('uploaded_file', filename=r.screenshot) }}" target="_blank" style="color:#4cc2ff">查看</a>
{% else %}-{% endif %}
</td>
<td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tr>
{% else %}
<tr><td colspan="12" style="color:#888">暂无复盘记录</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if auto_records %}
<div class="card">
<h2>系统自动记录(止盈/止损)</h2>
<table>
<thead>
<tr><th>品种</th><th>类型</th><th>方向</th><th>触发价</th><th>结果</th><th>时间</th></tr>
</thead>
<tbody>
{% for r in auto_records %}
<tr> <tr>
<td>{{ r.symbol_name or r.symbol }}</td> <td>{{ r.symbol_name or r.symbol }}</td>
<td>{{ r.monitor_type }}</td> <td>{{ r.monitor_type }}</td>
<td><span class="badge dir">{{ '做多' if r.direction == 'long' else '做空' }}</span></td> <td><span class="badge dir">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ r.trigger_price }}</td> <td>{{ r.trigger_price }}</td>
<td>{{ r.stop_loss }}</td>
<td>{{ r.take_profit }}</td>
<td> <td>
{% if r.result == '止盈' %} {% if r.result == '止盈' %}<span class="badge profit">止盈</span>
<span class="badge profit"></span> {% else %}<span class="badge loss"></span>{% endif %}
{% else %}
<span class="badge loss">止损</span>
{% endif %}
</td> </td>
<td>{{ r.created_at[:16] if r.created_at else '' }}</td> <td>{{ r.created_at[:16] if r.created_at else '' }}</td>
<td><a href="{{ url_for('del_record', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tr> </tr>
{% else %}
<tr><td colspan="9" style="color:#888">暂无交易记录</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}