双栏布局:开单计划/关键位/复盘;复盘字段与情绪单
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user