双栏布局:开单计划/关键位/复盘;复盘字段与情绪单

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 12:27:20 +08:00
parent 9fc41b6a46
commit 9b4bbbe8a3
6 changed files with 511 additions and 338 deletions
+97 -13
View File
@@ -3,7 +3,7 @@ import sqlite3
import time import time
import threading import threading
import requests import requests
from datetime import datetime from datetime import datetime, timedelta
from typing import Optional from typing import Optional
from functools import wraps from functools import wraps
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -44,6 +44,46 @@ def today_str() -> str:
return datetime.now(TZ).date().isoformat() return datetime.now(TZ).date().isoformat()
def calc_holding_duration(open_time: str, close_time: str) -> str:
try:
o = datetime.fromisoformat(open_time.strip())
c = datetime.fromisoformat(close_time.strip())
delta = c - o
if delta.total_seconds() < 0:
return ""
secs = int(delta.total_seconds())
h, rem = divmod(secs, 3600)
m, _ = divmod(rem, 60)
if h:
return f"{h}小时{m}分钟"
return f"{m}分钟"
except Exception:
return ""
def calc_theoretical_pnl(direction: str, entry: float, target: float, lots: float) -> Optional[float]:
if entry is None or target is None or lots is None:
return None
if direction == "long":
return round((target - entry) * lots, 2)
if direction == "short":
return round((entry - target) * lots, 2)
return None
def parse_review_date_filter(preset: str, start: str, end: str) -> tuple[str, str]:
today = datetime.now(TZ).date()
if preset == "today":
s = today.isoformat()
return s, s
if preset == "week":
monday = today - timedelta(days=today.weekday())
return monday.isoformat(), today.isoformat()
if preset == "month":
return today.replace(day=1).isoformat(), today.isoformat()
return start.strip(), end.strip()
def expire_old_plans(): def expire_old_plans():
"""当日结束后计划自动失效,保留历史。""" """当日结束后计划自动失效,保留历史。"""
today = today_str() today = today_str()
@@ -118,6 +158,18 @@ def init_db():
"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", "ALTER TABLE order_plans ADD COLUMN plan_date TEXT",
"ALTER TABLE key_monitors ADD COLUMN status TEXT DEFAULT 'active'",
"ALTER TABLE key_monitors ADD COLUMN archived_at TEXT",
"ALTER TABLE review_records ADD COLUMN direction TEXT",
"ALTER TABLE review_records ADD COLUMN entry_price REAL",
"ALTER TABLE review_records ADD COLUMN stop_loss REAL",
"ALTER TABLE review_records ADD COLUMN take_profit REAL",
"ALTER TABLE review_records ADD COLUMN close_price REAL",
"ALTER TABLE review_records ADD COLUMN lots REAL",
"ALTER TABLE review_records ADD COLUMN holding_duration TEXT",
"ALTER TABLE review_records ADD COLUMN initial_pnl REAL",
"ALTER TABLE review_records ADD COLUMN actual_pnl REAL",
"ALTER TABLE review_records ADD COLUMN is_emotion INTEGER DEFAULT 0",
] ]
for sql in migrations: for sql in migrations:
try: try:
@@ -309,7 +361,9 @@ def check_order_plans():
def check_key_monitors(): def check_key_monitors():
conn = get_db() conn = get_db()
rows = conn.execute("SELECT * FROM key_monitors").fetchall() rows = conn.execute(
"SELECT * FROM key_monitors WHERE status='active' OR status IS NULL"
).fetchall()
for r in rows: for r in rows:
sym = r["symbol"] sym = r["symbol"]
@@ -493,9 +547,14 @@ def del_plan(pid):
@login_required @login_required
def keys(): def keys():
conn = get_db() conn = get_db()
key_list = conn.execute("SELECT * FROM key_monitors ORDER BY id DESC").fetchall() key_list = conn.execute(
"SELECT * FROM key_monitors WHERE status='active' OR status IS NULL ORDER BY id DESC"
).fetchall()
history = conn.execute(
"SELECT * FROM key_monitors WHERE status='archived' ORDER BY archived_at DESC LIMIT 100"
).fetchall()
conn.close() conn.close()
return render_template("keys.html", keys=key_list) return render_template("keys.html", keys=key_list, history=history)
@app.route("/add_key", methods=["POST"]) @app.route("/add_key", methods=["POST"])
@@ -530,18 +589,24 @@ def add_key():
@login_required @login_required
def del_key(pid): def del_key(pid):
conn = get_db() conn = get_db()
conn.execute("DELETE FROM key_monitors WHERE id=?", (pid,)) conn.execute(
"UPDATE key_monitors SET status='archived', archived_at=? WHERE id=?",
(datetime.now(TZ).isoformat(), pid),
)
conn.commit() conn.commit()
conn.close() conn.close()
flash("删除") flash("移入监控历史")
return redirect(url_for("keys")) return redirect(url_for("keys"))
@app.route("/records") @app.route("/records")
@login_required @login_required
def records(): def records():
preset = request.args.get("preset", "")
start = request.args.get("start", "") start = request.args.get("start", "")
end = request.args.get("end", "") end = request.args.get("end", "")
if preset:
start, end = parse_review_date_filter(preset, start, end)
conn = get_db() conn = get_db()
sql = "SELECT * FROM review_records WHERE 1=1" sql = "SELECT * FROM review_records WHERE 1=1"
@@ -556,7 +621,7 @@ def records():
review_list = conn.execute(sql, params).fetchall() review_list = conn.execute(sql, params).fetchall()
auto_list = conn.execute( auto_list = conn.execute(
"SELECT * FROM trade_records ORDER BY id DESC LIMIT 50" "SELECT * FROM trade_records ORDER BY id DESC LIMIT 30"
).fetchall() ).fetchall()
conn.close() conn.close()
@@ -564,6 +629,7 @@ def records():
"records.html", "records.html",
reviews=review_list, reviews=review_list,
auto_records=auto_list, auto_records=auto_list,
preset=preset,
start=start, start=start,
end=end, end=end,
open_types=OPEN_TYPES, open_types=OPEN_TYPES,
@@ -596,6 +662,7 @@ def add_review():
f.save(os.path.join(UPLOAD_DIR, screenshot)) f.save(os.path.join(UPLOAD_DIR, screenshot))
tags = [t for t in BEHAVIOR_TAGS if d.get(f"tag_{t}")] tags = [t for t in BEHAVIOR_TAGS if d.get(f"tag_{t}")]
is_emotion = 1 if tags else 0
def num(key: str) -> Optional[float]: def num(key: str) -> Optional[float]:
v = d.get(key, "").strip() v = d.get(key, "").strip()
@@ -603,21 +670,37 @@ def add_review():
return None return None
return float(v) return float(v)
open_time = d.get("open_time", "").strip()
close_time = d.get("close_time", "").strip()
direction = d.get("direction", "").strip()
entry_price = num("entry_price")
stop_loss = num("stop_loss")
take_profit = num("take_profit")
close_price = num("close_price")
lots = num("lots") or 1.0
holding = calc_holding_duration(open_time, close_time)
initial_pnl = calc_theoretical_pnl(direction, entry_price, take_profit, lots)
actual_pnl = calc_theoretical_pnl(direction, entry_price, close_price, lots)
conn = get_db() conn = get_db()
conn.execute( conn.execute(
"""INSERT INTO review_records """INSERT INTO review_records
(open_time, close_time, symbol, timeframe, pnl, (open_time, close_time, symbol, timeframe, direction,
entry_price, stop_loss, take_profit, close_price, lots,
holding_duration, initial_pnl, actual_pnl, pnl,
open_type, expected_rr, actual_rr, exit_trigger, exit_supplement, open_type, expected_rr, actual_rr, exit_trigger, exit_supplement,
watch_after_breakeven, new_position_while_occupied, screenshot, watch_after_breakeven, new_position_while_occupied, screenshot,
auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff, auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff,
behavior_tags, notes) behavior_tags, is_emotion, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
( (
d.get("open_time", "").strip(), open_time, close_time,
d.get("close_time", "").strip(),
d.get("symbol", "").strip(), d.get("symbol", "").strip(),
d.get("timeframe", "").strip(), d.get("timeframe", "").strip(),
num("pnl"), direction,
entry_price, stop_loss, take_profit, close_price, lots,
holding, initial_pnl, actual_pnl, num("pnl"),
open_type, open_type,
num("expected_rr"), num("expected_rr"),
num("actual_rr"), num("actual_rr"),
@@ -632,6 +715,7 @@ def add_review():
int(d.get("kline_count") or 300), int(d.get("kline_count") or 300),
d.get("kline_cutoff", "平仓时间"), d.get("kline_cutoff", "平仓时间"),
",".join(tags), ",".join(tags),
is_emotion,
d.get("notes", "").strip(), d.get("notes", "").strip(),
), ),
) )
+106
View File
@@ -0,0 +1,106 @@
(function () {
function parseNum(v) {
var n = parseFloat(v);
return isNaN(n) ? null : n;
}
function calcPnl(direction, entry, target, lots) {
if (!entry || !target || !lots) return '';
if (direction === 'long') return ((target - entry) * lots).toFixed(2);
if (direction === 'short') return ((entry - target) * lots).toFixed(2);
return '';
}
function calcDuration(openVal, closeVal) {
if (!openVal || !closeVal) return '';
var o = new Date(openVal);
var c = new Date(closeVal);
var secs = Math.floor((c - o) / 1000);
if (secs < 0) return '';
var h = Math.floor(secs / 3600);
var m = Math.floor((secs % 3600) / 60);
return h ? h + '小时' + m + '分钟' : m + '分钟';
}
function recalc() {
var form = document.getElementById('review-form');
if (!form) return;
var dir = form.querySelector('[name="direction"]').value;
var entry = parseNum(form.querySelector('[name="entry_price"]').value);
var sl = parseNum(form.querySelector('[name="stop_loss"]').value);
var tp = parseNum(form.querySelector('[name="take_profit"]').value);
var close = parseNum(form.querySelector('[name="close_price"]').value);
var lots = parseNum(form.querySelector('[name="lots"]').value) || 1;
var openT = form.querySelector('[name="open_time"]').value;
var closeT = form.querySelector('[name="close_time"]').value;
var hold = document.getElementById('holding_duration');
var initP = document.getElementById('initial_pnl');
var actP = document.getElementById('actual_pnl');
if (hold) hold.value = calcDuration(openT, closeT);
if (initP) initP.value = calcPnl(dir, entry, tp, lots);
if (actP) actP.value = calcPnl(dir, entry, close, lots);
}
function bindForm() {
var form = document.getElementById('review-form');
if (!form) return;
form.querySelectorAll('input, select').forEach(function (el) {
el.addEventListener('input', recalc);
el.addEventListener('change', recalc);
});
}
function showModal(data) {
var mask = document.getElementById('review-modal');
var body = document.getElementById('review-modal-body');
if (!mask || !body) return;
var html = '<div class="modal-grid">';
var fields = [
['品种', data.symbol], ['方向', data.direction],
['成交价', data.entry_price], ['止损', data.stop_loss],
['止盈', data.take_profit], ['平仓价', data.close_price],
['张数', data.lots], ['开仓时间', data.open_time],
['平仓时间', data.close_time], ['持仓时长', data.holding_duration],
['初始盈亏', data.initial_pnl], ['实际盈亏', data.actual_pnl],
['盈亏金额', data.pnl], ['开仓类型', data.open_type],
['预期RR', data.expected_rr], ['实际RR', data.actual_rr],
['离场触发', data.exit_trigger], ['离场补充', data.exit_supplement],
['情绪单', data.is_emotion ? '是' : '否'],
['行为标签', data.behavior_tags], ['备注', data.notes]
];
fields.forEach(function (pair) {
html += '<div class="item"><label>' + pair[0] + '</label><div>' + (pair[1] || '-') + '</div></div>';
});
html += '</div>';
if (data.screenshot) {
html += '<div style="margin-top:1rem"><img src="/uploads/' + data.screenshot + '" style="max-width:100%;border-radius:8px"></div>';
}
body.innerHTML = html;
mask.classList.add('show');
}
function bindModal() {
var mask = document.getElementById('review-modal');
if (!mask) return;
mask.querySelector('.modal-close').addEventListener('click', function () {
mask.classList.remove('show');
});
mask.addEventListener('click', function (e) {
if (e.target === mask) mask.classList.remove('show');
});
document.querySelectorAll('.review-view-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
try {
showModal(JSON.parse(btn.getAttribute('data-review')));
} catch (e) { /* ignore */ }
});
});
}
document.addEventListener('DOMContentLoaded', function () {
bindForm();
bindModal();
recalc();
});
})();
+22
View File
@@ -63,6 +63,28 @@
.filter-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;margin-bottom:1rem} .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 .field{width:auto;min-width:140px}
.filter-row button{width:auto} .filter-row button{width:auto}
.split-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:stretch;margin-bottom:1.5rem}
.split-grid .card{margin-bottom:0;height:100%;min-height:560px;display:flex;flex-direction:column}
.split-grid .card-body{flex:1;overflow:auto}
.card-scroll{max-height:420px;overflow-y:auto}
.preset-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}
.preset-tabs a{padding:.45rem .85rem;border-radius:8px;border:1px solid #2e2e45;color:#a9a9c4;text-decoration:none;font-size:.85rem}
.preset-tabs a.active,.preset-tabs a:hover{background:#1e2533;color:#4cc2ff;border-color:#4cc2ff}
.btn-link{color:#4cc2ff;cursor:pointer;font-size:.85rem;background:none;border:none;padding:0}
.btn-link:hover{text-decoration:underline}
.modal-mask{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:1000;display:none;align-items:center;justify-content:center;padding:1rem}
.modal-mask.show{display:flex}
.modal-box{background:#12121a;border:1px solid #242435;border-radius:16px;max-width:900px;width:100%;max-height:90vh;overflow:auto;padding:1.5rem}
.modal-box h3{margin-bottom:1rem;color:#c4c4ff}
.modal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem;font-size:.9rem}
.modal-grid .item label{color:#888;font-size:.75rem;display:block}
.modal-grid .item div{margin-top:.2rem}
.modal-close{float:right;color:#888;cursor:pointer;font-size:1.2rem}
.calc-readonly{background:#161625;color:#4cc2ff}
@media(max-width:1100px){
.split-grid{grid-template-columns:1fr}
.split-grid .card{min-height:auto}
}
@media(max-width:768px){ @media(max-width:768px){
.topbar-inner{flex-wrap:wrap;height:auto;padding:.75rem 0} .topbar-inner{flex-wrap:wrap;height:auto;padding:.75rem 0}
.nav{order:3;width:100%} .nav{order:3;width:100%}
+36 -13
View File
@@ -3,11 +3,13 @@
{% block content %} {% block content %}
<h1 class="page-title">关键位监控</h1> <h1 class="page-title">关键位监控</h1>
<div class="split-grid">
<div class="card"> <div class="card">
<h2>新增监控</h2> <h2>新增监控</h2>
<div class="card-body">
<form action="{{ url_for('add_key') }}" method="post" class="form-row"> <form action="{{ url_for('add_key') }}" method="post" class="form-row">
<div class="symbol-wrap"> <div class="symbol-wrap" style="min-width:180px;flex:1">
<input type="text" class="symbol-input" placeholder="输入中文名或同花顺代码" autocomplete="off" required> <input type="text" class="symbol-input" placeholder="中文名或同花顺代码" autocomplete="off" required>
<input type="hidden" name="symbol" required> <input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name"> <input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required> <input type="hidden" name="market_code" required>
@@ -22,32 +24,53 @@
<option value="关键支撑位">关键支撑位</option> <option value="关键支撑位">关键支撑位</option>
</select> </select>
<select name="direction" required> <select name="direction" required>
<option value="">选择方向</option> <option value="">方向</option>
<option value="long">做多</option> <option value="long">做多</option>
<option value="short">做空</option> <option value="short">做空</option>
</select> </select>
<input name="upper" type="number" step="0.0001" placeholder="上沿/阻力" required> <input name="upper" type="number" step="0.0001" placeholder="上沿/阻力" required>
<input name="lower" type="number" step="0.0001" placeholder="下沿/支撑" required> <input name="lower" type="number" step="0.0001" placeholder="下沿/支撑" required>
<button type="submit" class="btn-primary">添加监控</button> <button type="submit" class="btn-primary">添加</button>
</form> </form>
</div> <h3 style="font-size:.95rem;color:#a9a9ff;margin:1rem 0 .75rem">监控列表</h3>
<div class="list card-scroll">
<div class="card">
<h2>监控列表</h2>
<div class="list">
{% for k in keys %} {% for k in keys %}
<div class="list-item"> <div class="list-item">
<div> <div>
<strong>{{ k.symbol_name or k.symbol }}</strong> | {{ k.monitor_type }} <strong>{{ k.symbol_name or k.symbol }}</strong> | {{ k.monitor_type }}
<span class="badge dir">{{ '做多' if k.direction == 'long' else '做空' }}</span> <span class="badge dir">{{ '做多' if k.direction == 'long' else '做空' }}</span>
</div> </div>
<div>: {{ k.upper }} | 下: {{ k.lower }}</div> <div>上 {{ k.upper }} | 下 {{ k.lower }}</div>
<div style="font-size:.8rem;color:#888">同花顺: {{ k.symbol }}</div> <a href="{{ url_for('del_key', pid=k.id) }}" class="btn-del" onclick="return confirm('移入历史?')">删除</a>
<a href="{{ url_for('del_key', pid=k.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
</div> </div>
{% else %} {% else %}
<div style="color:#888;padding:1rem">暂无关键位监控</div> <div style="color:#888;padding:1rem">暂无监控</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div>
<div class="card">
<h2>监控历史</h2>
<div class="card-body card-scroll">
<table>
<thead><tr><th>品种</th><th>类型</th><th>方向</th><th>上沿</th><th>下沿</th><th>归档时间</th></tr></thead>
<tbody>
{% for k in history %}
<tr>
<td>{{ k.symbol_name or k.symbol }}</td>
<td>{{ k.monitor_type }}</td>
<td><span class="badge dir">{{ '多' if k.direction == 'long' else '空' }}</span></td>
<td>{{ k.upper }}</td>
<td>{{ k.lower }}</td>
<td>{{ k.archived_at[:16] if k.archived_at else '' }}</td>
</tr>
{% else %}
<tr><td colspan="6" style="color:#888">暂无历史</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %} {% endblock %}
+33 -53
View File
@@ -3,11 +3,14 @@
{% block content %} {% block content %}
<h1 class="page-title">开单计划 <span style="font-size:.9rem;color:#888;font-weight:normal">今日 {{ today }}</span></h1> <h1 class="page-title">开单计划 <span style="font-size:.9rem;color:#888;font-weight:normal">今日 {{ today }}</span></h1>
<div class="split-grid">
<div class="card"> <div class="card">
<h2>今日计划(开盘前制定,当日有效)</h2> <h2>今日计划</h2>
<div class="card-body">
<p class="hint" style="margin-bottom:1rem">开盘前制定,当日有效;下方为进行中计划。</p>
<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" style="min-width:200px"> <div class="symbol-wrap" style="min-width:180px;flex:1">
<input type="text" class="symbol-input" placeholder="输入中文名或同花顺代码" autocomplete="off" required> <input type="text" class="symbol-input" placeholder="中文名或同花顺代码" autocomplete="off" required>
<input type="hidden" name="symbol" required> <input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name"> <input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required> <input type="hidden" name="market_code" required>
@@ -16,91 +19,68 @@
<div class="symbol-selected"></div> <div class="symbol-selected"></div>
</div> </div>
<select name="direction" required> <select name="direction" required>
<option value="">选择方向</option> <option value="">方向</option>
<option value="long">做多</option> <option value="long">做多</option>
<option value="short">做空</option> <option value="short">做空</option>
</select> </select>
<input name="zone_lower" type="number" step="0.0001" placeholder="决策区间下限" required> <input name="zone_lower" type="number" step="0.0001" placeholder="区间下限" required>
<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> <h3 style="font-size:.95rem;color:#a9a9ff;margin:1rem 0 .75rem">进行中</h3>
</div> <div class="list card-scroll">
<div class="card">
<h2>今日进行中</h2>
<div class="list">
{% for p in plans %} {% for p in plans %}
<div class="list-item"> <div class="list-item">
<div> <div>
<strong>{{ p.symbol_name or p.symbol }}</strong> <strong>{{ p.symbol_name or p.symbol }}</strong>
<span class="badge dir">{{ '做多' if p.direction == 'long' else '做空' }}</span> <span class="badge dir">{{ '做多' if p.direction == 'long' else '做空' }}</span>
{% if p.status == 'planned' %} {% if p.status == 'planned' %}<span class="badge planned">待触发</span>
<span class="badge planned">待触发</span> {% else %}<span class="badge active">已激活</span>{% endif %}
{% else %}
<span class="badge active">已激活</span>
{% endif %}
</div> </div>
<div>区间: {{ p.zone_lower }} ~ {{ p.zone_upper }}</div> <div>区间 {{ p.zone_lower }}~{{ p.zone_upper }} | 损{{ p.stop_loss }} 盈{{ p.take_profit }}</div>
<div>止损: {{ p.stop_loss }} | 止盈: {{ p.take_profit }}</div> <a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
<div style="font-size:.8rem;color:#888">同花顺: {{ p.symbol }}</div>
<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>
</div>
<div class="card"> <div class="card">
<h2>历史计划</h2> <h2>历史计划</h2>
<div class="card-body">
<form method="get" class="filter-row"> <form method="get" class="filter-row">
<div class="field"> <div class="field"><label>开始</label><input type="date" name="start" value="{{ start }}"></div>
<label>开始日期</label> <div class="field"><label>结束</label><input type="date" name="end" value="{{ end }}"></div>
<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> <button type="submit" class="btn-primary">筛选</button>
<a href="{{ url_for('plans') }}" style="color:#888;font-size:.85rem;padding:.7rem">重置</a> <a href="{{ url_for('plans') }}" style="color:#888;font-size:.85rem;padding:.7rem">重置</a>
</form> </form>
<div class="card-scroll">
<table> <table>
<thead> <thead><tr><th>日期</th><th>品种</th><th>方向</th><th>区间</th><th>状态</th></tr></thead>
<tr>
<th>日期</th>
<th>品种</th>
<th>方向</th>
<th>区间</th>
<th>止损</th>
<th>止盈</th>
<th>状态</th>
<th></th>
</tr>
</thead>
<tbody> <tbody>
{% for p in history %} {% for p in history %}
<tr> <tr>
<td>{{ p.plan_date or '' }}</td> <td>{{ p.plan_date or '' }}</td>
<td>{{ p.symbol_name or p.symbol }}</td> <td>{{ p.symbol_name or p.symbol }}</td>
<td><span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span></td> <td><span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span></td>
<td>{{ p.zone_lower }}~{{ p.zone_upper }}</td> <td>{{ p.zone_lower }}~{{ p.zone_upper }}</td>
<td>{{ p.stop_loss }}</td>
<td>{{ p.take_profit }}</td>
<td> <td>
{% if p.status == 'closed' %}<span class="badge profit">完成</span> {% if p.status == 'closed' %}<span class="badge profit">完成</span>
{% elif p.status == 'expired' %}<span class="badge expired">失效</span> {% elif p.status == 'expired' %}<span class="badge expired">失效</span>
{% else %}<span class="badge">{{ p.status }}</span>{% endif %} {% else %}{{ p.status }}{% endif %}
</td> </td>
<td><a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="8" style="color:#888">暂无历史记录</td></tr> <tr><td colspan="5" style="color:#888">暂无历史</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
+88 -130
View File
@@ -1,211 +1,166 @@
{% 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="split-grid">
<div class="card"> <div class="card">
<form action="{{ url_for('add_review') }}" method="post" enctype="multipart/form-data"> <h2>交易复盘记录上传(含截图)</h2>
<div class="card-body card-scroll">
<form id="review-form" action="{{ url_for('add_review') }}" method="post" enctype="multipart/form-data">
<div class="form-grid"> <div class="form-grid">
<div class="field"><label>品种</label><input name="symbol" placeholder="ag2608" required></div>
<div class="field"> <div class="field">
<label>开仓时间</label> <label>方向</label>
<input type="datetime-local" name="open_time" required> <select name="direction" required>
</div> <option value="">请选择</option>
<div class="field"> <option value="long">做多</option>
<label>平仓时间</label> <option value="short">做空</option>
<input type="datetime-local" name="close_time" required> </select>
</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>
<div class="field"><label>成交价</label><input name="entry_price" type="number" step="0.0001" required></div>
<div class="field"><label>止损</label><input name="stop_loss" type="number" step="0.0001" required></div>
<div class="field"><label>止盈</label><input name="take_profit" type="number" step="0.0001" required></div>
<div class="field"><label>平仓价格</label><input name="close_price" type="number" step="0.0001" required></div>
<div class="field"><label>张数</label><input name="lots" type="number" step="1" min="1" value="1" required></div>
<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 id="holding_duration" type="text" readonly class="calc-readonly" placeholder="自动计算"></div>
<div class="field"><label>初始盈亏(自动)</label><input id="initial_pnl" type="text" readonly class="calc-readonly" placeholder="按止盈测算"></div>
<div class="field"><label>实际盈亏(自动)</label><input id="actual_pnl" type="text" readonly class="calc-readonly" placeholder="按平仓价测算"></div>
<div class="field"><label>盈亏金额(手动)</label><input name="pnl" type="number" step="0.01" placeholder="实际盈亏金额"></div>
<div class="field"><label>周期</label><input name="timeframe" value="5m"></div>
<div class="field full"> <div class="field full">
<label>开仓类型(必选)</label> <label>开仓类型(必选)</label>
<select name="open_type" required> <select name="open_type" required>
<option value="">请选择</option> <option value="">请选择</option>
{% for t in open_types %} {% for t in open_types %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select> </select>
</div> </div>
<div class="field"> <div class="field"><label>预期 RR</label><input name="expected_rr" type="number" step="0.01"></div>
<label>预期 RR</label> <div class="field"><label>实际 RR</label><input name="actual_rr" type="number" step="0.01"></div>
<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"> <div class="field">
<label>离场触发(必选)</label> <label>离场触发(必选)</label>
<select name="exit_trigger" required> <select name="exit_trigger" required>
<option value="">请选择</option> <option value="">请选择</option>
{% for t in exit_triggers %} {% for t in exit_triggers %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select> </select>
</div> </div>
<div class="field"> <div class="field"><label>离场补充</label><input name="exit_supplement" placeholder="手工平仓说明"></div>
<label>离场补充(仅手工平仓必填)</label>
<input name="exit_supplement" placeholder="手工平仓说明">
</div>
<div class="field"> <div class="field">
<label>保本后盯盘</label> <label>保本后盯盘</label>
<select name="watch_after_breakeven"> <select name="watch_after_breakeven"><option value="否"></option><option value="是"></option></select>
<option value="否"></option>
<option value="是"></option>
</select>
</div> </div>
<div class="field"> <div class="field">
<label>占用时新开仓</label> <label>占用时新开仓</label>
<select name="new_position_while_occupied"> <select name="new_position_while_occupied"><option value="否"></option><option value="是"></option></select>
<option value="否"></option>
<option value="是"></option>
</select>
</div>
<div class="field">
<label>截图上传</label>
<input type="file" name="screenshot" accept="image/*">
</div> </div>
<div class="field"><label>截图上传</label><input type="file" name="screenshot" accept="image/*"></div>
</div> </div>
<div class="check-row"> <div class="check-row"><label><input type="checkbox" name="auto_kline" value="1"> 保存时自动生成 K 线图并作为截图</label></div>
<label><input type="checkbox" name="auto_kline" value="1"> 保存时自动生成 K 线图并作为截图</label>
</div>
<div class="form-grid"> <div class="form-grid">
<div class="field"> <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>
<label>周期1</label> <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>
<select name="kline_period1"> <div class="field"><label>K线数</label><input name="kline_count" type="number" value="300"></div>
{% for p in kline_periods %} <div class="field"><label>K线截止</label><select name="kline_cutoff">{% for c in kline_cutoffs %}<option value="{{ c }}">{{ c }}</option>{% endfor %}</select></div>
<option value="{{ p }}" {% if p == '15m' %}selected{% endif %}>{{ p }}</option>
{% endfor %}
</select>
</div> </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>
<p class="hint">下方勾选行为标签的均为<strong>情绪单</strong></p>
<div class="check-row"> <div class="check-row">
{% for tag in behavior_tags %} {% for tag in behavior_tags %}
<label><input type="checkbox" name="tag_{{ tag }}" value="1"> {{ tag }}</label> <label><input type="checkbox" name="tag_{{ tag }}" value="1"> {{ tag }}</label>
{% endfor %} {% endfor %}
</div> </div>
<div class="field" style="margin-top:.75rem"><label>备注</label><textarea name="notes"></textarea></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> <button type="submit" class="btn-primary" style="margin-top:1rem">保存复盘记录</button>
</form> </form>
</div> </div>
</div>
<div class="card"> <div class="card">
<h2>复盘历史</h2> <h2>复盘历史</h2>
<div class="card-body">
<div class="preset-tabs">
<a href="{{ url_for('records', preset='today') }}" class="{% if preset=='today' %}active{% endif %}">本日</a>
<a href="{{ url_for('records', preset='week') }}" class="{% if preset=='week' %}active{% endif %}">本周</a>
<a href="{{ url_for('records', preset='month') }}" class="{% if preset=='month' %}active{% endif %}">本月</a>
<a href="{{ url_for('records', preset='custom') }}" class="{% if preset=='custom' %}active{% endif %}">自定义</a>
</div>
{% if preset == 'custom' or start or end %}
<form method="get" class="filter-row"> <form method="get" class="filter-row">
<div class="field"> <input type="hidden" name="preset" value="custom">
<label>开始日期</label> <div class="field"><label>开始</label><input type="date" name="start" value="{{ start }}"></div>
<input type="date" name="start" value="{{ start }}"> <div class="field"><label>结束</label><input type="date" name="end" value="{{ end }}"></div>
</div>
<div class="field">
<label>结束日期</label>
<input type="date" name="end" value="{{ end }}">
</div>
<button type="submit" class="btn-primary">筛选</button> <button type="submit" class="btn-primary">筛选</button>
<a href="{{ url_for('records') }}" style="color:#888;font-size:.85rem;padding:.7rem">重置</a>
</form> </form>
{% endif %}
<div class="card-scroll">
<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>盈亏</th>
<th>开仓类型</th>
<th>预期RR</th>
<th>实际RR</th>
<th>离场</th>
<th>行为标签</th>
<th>截图</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for r in reviews %} {% for r in reviews %}
<tr> <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.close_time[:16] if r.close_time else '' }}</td>
<td>{{ r.symbol }}</td> <td>{{ r.symbol }}</td>
<td>{{ r.timeframe }}</td> <td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td>
<td> <td>
{% if r.pnl and r.pnl > 0 %}<span class="badge profit">{{ r.pnl }}</span> {% 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> {% elif r.pnl and r.pnl < 0 %}<span class="badge loss">{{ r.pnl }}</span>
{% else %}{{ r.pnl or '-' }}{% endif %} {% else %}{{ r.actual_pnl or '-' }}{% endif %}
</td> </td>
<td>{{ r.open_type }}</td> <td>{% if r.is_emotion %}<span class="badge loss">情绪</span>{% else %}-{% endif %}</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> <td>
{% if r.screenshot %} <button type="button" class="btn-link review-view-btn" data-review='{{ {
<a href="{{ url_for('uploaded_file', filename=r.screenshot) }}" target="_blank" style="color:#4cc2ff">查看</a> "symbol": r.symbol, "direction": "做多" if r.direction=="long" else "做空",
{% else %}-{% endif %} "entry_price": r.entry_price, "stop_loss": r.stop_loss, "take_profit": r.take_profit,
"close_price": r.close_price, "lots": r.lots,
"open_time": r.open_time, "close_time": r.close_time,
"holding_duration": r.holding_duration, "initial_pnl": r.initial_pnl,
"actual_pnl": r.actual_pnl, "pnl": r.pnl,
"open_type": r.open_type, "expected_rr": r.expected_rr, "actual_rr": r.actual_rr,
"exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement,
"is_emotion": r.is_emotion, "behavior_tags": r.behavior_tags,
"notes": r.notes, "screenshot": r.screenshot
} | tojson }}'>放大查看</button>
</td> </td>
<td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td> <td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="12" style="color:#888">暂无复盘记录</td></tr> <tr><td colspan="7" style="color:#888">暂无复盘记录</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</div>
</div>
<div id="review-modal" class="modal-mask">
<div class="modal-box">
<span class="modal-close"></span>
<h3>复盘详情</h3>
<div id="review-modal-body"></div>
</div>
</div>
{% if auto_records %} {% if auto_records %}
<div class="card"> <div class="card" style="margin-top:1.5rem">
<h2>系统自动记录(止盈/止损)</h2> <h2>系统自动记录(止盈/止损)</h2>
<table> <table>
<thead> <thead><tr><th>品种</th><th>类型</th><th>方向</th><th>触发价</th><th>结果</th><th>时间</th></tr></thead>
<tr><th>品种</th><th>类型</th><th>方向</th><th>触发价</th><th>结果</th><th>时间</th></tr>
</thead>
<tbody> <tbody>
{% for r in auto_records %} {% 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> <td>{% if r.result == '止盈' %}<span class="badge profit">止盈</span>{% else %}<span class="badge loss">止损</span>{% endif %}</td>
{% if r.result == '止盈' %}<span class="badge profit">止盈</span>
{% else %}<span class="badge loss">止损</span>{% endif %}
</td>
<td>{{ r.created_at[:16] if r.created_at else '' }}</td> <td>{{ r.created_at[:16] if r.created_at else '' }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -214,3 +169,6 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
{% endblock %}