顶部导航1800宽;开单计划按日失效与历史筛选;复盘表单
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user