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

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 threading
import requests
from datetime import datetime
from datetime import datetime, timedelta
from typing import Optional
from functools import wraps
from zoneinfo import ZoneInfo
@@ -44,6 +44,46 @@ def today_str() -> str:
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():
"""当日结束后计划自动失效,保留历史。"""
today = today_str()
@@ -118,6 +158,18 @@ def init_db():
"ALTER TABLE key_monitors 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 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:
try:
@@ -309,7 +361,9 @@ def check_order_plans():
def check_key_monitors():
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:
sym = r["symbol"]
@@ -493,9 +547,14 @@ def del_plan(pid):
@login_required
def keys():
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()
return render_template("keys.html", keys=key_list)
return render_template("keys.html", keys=key_list, history=history)
@app.route("/add_key", methods=["POST"])
@@ -530,18 +589,24 @@ def add_key():
@login_required
def del_key(pid):
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.close()
flash("删除")
flash("移入监控历史")
return redirect(url_for("keys"))
@app.route("/records")
@login_required
def records():
preset = request.args.get("preset", "")
start = request.args.get("start", "")
end = request.args.get("end", "")
if preset:
start, end = parse_review_date_filter(preset, start, end)
conn = get_db()
sql = "SELECT * FROM review_records WHERE 1=1"
@@ -556,7 +621,7 @@ def records():
review_list = conn.execute(sql, params).fetchall()
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()
conn.close()
@@ -564,6 +629,7 @@ def records():
"records.html",
reviews=review_list,
auto_records=auto_list,
preset=preset,
start=start,
end=end,
open_types=OPEN_TYPES,
@@ -596,6 +662,7 @@ def add_review():
f.save(os.path.join(UPLOAD_DIR, screenshot))
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]:
v = d.get(key, "").strip()
@@ -603,21 +670,37 @@ def add_review():
return None
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.execute(
"""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,
watch_after_breakeven, new_position_while_occupied, screenshot,
auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff,
behavior_tags, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
behavior_tags, is_emotion, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
d.get("open_time", "").strip(),
d.get("close_time", "").strip(),
open_time, close_time,
d.get("symbol", "").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,
num("expected_rr"),
num("actual_rr"),
@@ -632,6 +715,7 @@ def add_review():
int(d.get("kline_count") or 300),
d.get("kline_cutoff", "平仓时间"),
",".join(tags),
is_emotion,
d.get("notes", "").strip(),
),
)