顶部导航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
+202 -11
View File
@@ -6,6 +6,9 @@ import requests
from datetime import datetime
from typing import Optional
from functools import wraps
from zoneinfo import ZoneInfo
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
from flask import (
@@ -27,6 +30,18 @@ PORT = int(os.getenv("PORT", "6600"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes")
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 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",
]
for sql in migrations:
try:
c.execute(sql)
except sqlite3.OperationalError:
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.close()
@@ -104,6 +134,9 @@ def init_db():
if not get_setting("ths_refresh_token") and 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():
"""
@@ -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():
expire_old_plans()
today = today_str()
conn = get_db()
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()
for r in rows:
@@ -305,6 +358,7 @@ def check_key_monitors():
def background_task():
while True:
try:
expire_old_plans()
check_key_monitors()
check_order_plans()
except Exception:
@@ -361,15 +415,35 @@ def index():
@app.route("/plans")
@login_required
def plans():
today = today_str()
start = request.args.get("start", "")
end = request.args.get("end", "")
conn = get_db()
plan_list = conn.execute(
"SELECT * FROM order_plans WHERE status != 'closed' ORDER BY id DESC"
).fetchall()
closed = conn.execute(
"SELECT * FROM order_plans WHERE status='closed' ORDER BY id DESC LIMIT 20"
"SELECT * FROM order_plans WHERE plan_date=? AND status IN ('planned', 'active') ORDER BY id DESC",
(today,),
).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()
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"])
@@ -391,12 +465,13 @@ def add_plan():
conn.execute(
"""INSERT INTO order_plans
(symbol, symbol_name, market_code, sina_code, direction,
zone_upper, zone_lower, stop_loss, take_profit)
VALUES (?,?,?,?,?,?,?,?)""",
zone_upper, zone_lower, stop_loss, take_profit, plan_date)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
(
symbol, symbol_name, market_code, sina_code, direction,
float(d["zone_upper"]), float(d["zone_lower"]),
float(d["stop_loss"]), float(d["take_profit"]),
today_str(),
),
)
conn.commit()
@@ -467,12 +542,128 @@ def del_key(pid):
@app.route("/records")
@login_required
def records():
start = request.args.get("start", "")
end = request.args.get("end", "")
conn = get_db()
record_list = conn.execute(
"SELECT * FROM trade_records ORDER BY id DESC"
sql = "SELECT * FROM review_records WHERE 1=1"
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()
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>")