diff --git a/.gitignore b/.gitignore index 5110bc3..1ec9647 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ __pycache__/ venv/ .venv/ *.log -.DS_Store +uploads/ diff --git a/app.py b/app.py index 08e0c61..46c8fe4 100644 --- a/app.py +++ b/app.py @@ -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/") +@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/") +@login_required +def uploaded_file(filename): + from flask import send_from_directory + return send_from_directory(UPLOAD_DIR, filename) @app.route("/del_record/") diff --git a/templates/base.html b/templates/base.html index d56200f..7e04c38 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,75 +7,84 @@ {% block extra_css %}{% endblock %} -
- +
{% with msg=get_flashed_messages() %}{% if msg %}
{{ msg[0] }}
{% endif %}{% endwith %} {% block content %}{% endblock %} diff --git a/templates/plans.html b/templates/plans.html index 1d05ead..6e24810 100644 --- a/templates/plans.html +++ b/templates/plans.html @@ -1,12 +1,12 @@ {% extends "base.html" %} {% block title %}开单计划 - 国内期货监控系统{% endblock %} {% block content %} -

开单计划

+

开单计划 今日 {{ today }}

-

新增计划

+

今日计划(开盘前制定,当日有效)

-
+
@@ -24,12 +24,13 @@ - + +

计划仅当日有效,次日 0 点自动失效并归入历史;触发止盈/止损后标记为已完成。

-

进行中计划

+

今日进行中

{% for p in plans %}
@@ -48,23 +49,58 @@ 删除
{% else %} -
暂无进行中的开单计划
+
今日暂无开单计划
{% endfor %}
-{% if closed %}
-

最近已完成

-
- {% for p in closed %} -
-
{{ p.symbol_name or p.symbol }} {{ '做多' if p.direction == 'long' else '做空' }}
-
区间: {{ p.zone_lower }} ~ {{ p.zone_upper }} | 损: {{ p.stop_loss }} 盈: {{ p.take_profit }}
- 删除 +

历史计划

+
+
+ +
- {% endfor %} -
+
+ + +
+ + 重置 + + + + + + + + + + + + + + + + {% for p in history %} + + + + + + + + + + + {% else %} + + {% endfor %} + +
日期品种方向区间止损止盈状态
{{ p.plan_date or '' }}{{ p.symbol_name or p.symbol }}{{ '做多' if p.direction == 'long' else '做空' }}{{ p.zone_lower }} ~ {{ p.zone_upper }}{{ p.stop_loss }}{{ p.take_profit }} + {% if p.status == 'closed' %}已完成 + {% elif p.status == 'expired' %}已失效 + {% else %}{{ p.status }}{% endif %} +
暂无历史记录
-{% endif %} {% endblock %} diff --git a/templates/records.html b/templates/records.html index 83aee73..c19b041 100644 --- a/templates/records.html +++ b/templates/records.html @@ -1,47 +1,216 @@ {% extends "base.html" %} {% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %} {% block content %} -

交易记录与复盘

+

交易复盘记录上传(含截图)

-

全部记录

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

勾选自动生成时,将按所选周期上下排列 K 线图;截止时间为平仓/开仓/当前,用于截取行情片段。

+ +
+ {% for tag in behavior_tags %} + + {% endfor %} +
+ +
+ + +
+ + +
+
+ +
+

复盘历史

+
+
+ + +
+
+ + +
+ + 重置 +
+ + - - - - - - - + + + + + + + + - {% for r in records %} + {% for r in reviews %} + + + + + + + + + + + + + + + {% else %} + + {% endfor %} + +
开仓平仓 品种类型方向触发价止损止盈结果时间周期盈亏开仓类型预期RR实际RR离场行为标签截图
{{ r.open_time[:16] if r.open_time else '' }}{{ r.close_time[:16] if r.close_time else '' }}{{ r.symbol }}{{ r.timeframe }} + {% if r.pnl and r.pnl > 0 %}{{ r.pnl }} + {% elif r.pnl and r.pnl < 0 %}{{ r.pnl }} + {% else %}{{ r.pnl or '-' }}{% endif %} + {{ r.open_type }}{{ r.expected_rr or '-' }}{{ r.actual_rr or '-' }}{{ r.exit_trigger }}{{ r.behavior_tags or '-' }} + {% if r.screenshot %} + 查看 + {% else %}-{% endif %} +
暂无复盘记录
+
+ +{% if auto_records %} +
+

系统自动记录(止盈/止损)

+ + + + + + {% for r in auto_records %} - - - - {% else %} - {% endfor %}
品种类型方向触发价结果时间
{{ r.symbol_name or r.symbol }} {{ r.monitor_type }} {{ '做多' if r.direction == 'long' else '做空' }} {{ r.trigger_price }}{{ r.stop_loss }}{{ r.take_profit }} - {% if r.result == '止盈' %} - 止盈 - {% else %} - 止损 - {% endif %} + {% if r.result == '止盈' %}止盈 + {% else %}止损{% endif %} {{ r.created_at[:16] if r.created_at else '' }}
暂无交易记录
+{% endif %} {% endblock %}