复盘盈亏比自动计算与K线自动生成;居中页头导航

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 12:58:24 +08:00
parent 105f630388
commit a35a08d2f6
10 changed files with 352 additions and 56 deletions
+44 -5
View File
@@ -18,6 +18,7 @@ from flask import (
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
from symbols import search_symbols, ths_to_codes from symbols import search_symbols, ths_to_codes
from kline_chart import generate_review_kline_chart
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")) load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env"))
@@ -61,6 +62,23 @@ def calc_holding_duration(open_time: str, close_time: str) -> str:
return "" return ""
def calc_rr_ratio(direction: str, entry: float, stop: float, target: float) -> Optional[float]:
"""盈亏比 = 盈利空间 / 风险空间。"""
if entry is None or stop is None or target is None:
return None
if direction == "long":
risk = entry - stop
if risk <= 0:
return None
return round((target - entry) / risk, 2)
if direction == "short":
risk = stop - entry
if risk <= 0:
return None
return round((entry - target) / risk, 2)
return None
def calc_theoretical_pnl(direction: str, entry: float, target: float, lots: float) -> Optional[float]: 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: if entry is None or target is None or lots is None:
return None return None
@@ -680,8 +698,29 @@ def add_review():
lots = num("lots") or 1.0 lots = num("lots") or 1.0
holding = calc_holding_duration(open_time, close_time) holding = calc_holding_duration(open_time, close_time)
initial_pnl = calc_theoretical_pnl(direction, entry_price, take_profit, lots) initial_pnl = calc_rr_ratio(direction, entry_price, stop_loss, take_profit)
actual_pnl = calc_theoretical_pnl(direction, entry_price, close_price, lots) actual_pnl = calc_rr_ratio(direction, entry_price, stop_loss, close_price)
auto_kline = bool(d.get("auto_kline"))
if auto_kline and not screenshot:
try:
generated = generate_review_kline_chart(
symbol=d.get("symbol", "").strip(),
periods=[d.get("kline_period1", "15m"), d.get("kline_period2", "1h")],
count=int(d.get("kline_count") or 300),
cutoff_label=d.get("kline_cutoff", "平仓时间"),
open_time=open_time,
close_time=close_time,
entry_price=entry_price,
stop_loss=stop_loss,
take_profit=take_profit,
close_price=close_price,
upload_dir=UPLOAD_DIR,
)
if generated:
screenshot = generated
except Exception as exc:
app.logger.warning("auto kline failed: %s", exc)
conn = get_db() conn = get_db()
conn.execute( conn.execute(
@@ -702,14 +741,14 @@ def add_review():
entry_price, stop_loss, take_profit, close_price, lots, entry_price, stop_loss, take_profit, close_price, lots,
holding, initial_pnl, actual_pnl, num("pnl"), holding, initial_pnl, actual_pnl, num("pnl"),
open_type, open_type,
num("expected_rr"), None,
num("actual_rr"), None,
exit_trigger, exit_trigger,
d.get("exit_supplement", "").strip(), d.get("exit_supplement", "").strip(),
d.get("watch_after_breakeven", ""), d.get("watch_after_breakeven", ""),
d.get("new_position_while_occupied", ""), d.get("new_position_while_occupied", ""),
screenshot, screenshot,
1 if d.get("auto_kline") else 0, 1 if auto_kline else 0,
d.get("kline_period1", "15m"), d.get("kline_period1", "15m"),
d.get("kline_period2", "1h"), d.get("kline_period2", "1h"),
int(d.get("kline_count") or 300), int(d.get("kline_count") or 300),
+257
View File
@@ -0,0 +1,257 @@
"""复盘 K 线:新浪拉取 + matplotlib 生成截图。"""
import json
import logging
import os
import re
from datetime import datetime
from typing import Optional
from zoneinfo import ZoneInfo
import requests
from symbols import ths_to_codes
logger = logging.getLogger(__name__)
TZ = ZoneInfo("Asia/Shanghai")
PERIOD_MINUTES = {
"1m": "1",
"3m": "3",
"5m": "5",
"15m": "15",
"30m": "30",
"1h": "60",
"4h": "240",
}
def ths_to_sina_chart_symbol(symbol: str) -> Optional[str]:
"""ag2608 -> AG2608(新浪 K 线接口合约代码)。"""
code = (symbol or "").strip()
if not code:
return None
codes = ths_to_codes(code)
if codes:
sina = codes.get("sina_code", "")
if sina.startswith("nf_"):
return sina[3:]
if sina.startswith("CFF_RE_"):
return sina[7:]
ths = codes.get("ths_code", "")
return ths.upper() if ths else None
m = re.match(r"^([A-Za-z]+)(\d+)$", code)
if m:
return m.group(1).upper() + m.group(2)
return None
def _parse_jsonp(text: str) -> Optional[list]:
m = re.search(r"\((.*)\)\s*;?\s*$", text.strip(), re.DOTALL)
if not m:
return None
try:
data = json.loads(m.group(1))
return data if isinstance(data, list) else None
except json.JSONDecodeError:
return None
def fetch_sina_klines(symbol: str, period: str) -> list:
"""拉取新浪期货分钟 K 线。"""
chart_sym = ths_to_sina_chart_symbol(symbol)
if not chart_sym:
return []
if period == "1d":
return _fetch_sina_daily(chart_sym)
typ = PERIOD_MINUTES.get(period)
if not typ:
return []
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
url = (
"https://stock2.finance.sina.com.cn/futures/api/jsonp.php/"
f"var_{chart_sym}_{typ}_{ts}=/InnerFuturesNewService.getFewMinLine"
f"?symbol={chart_sym}&type={typ}"
)
try:
resp = requests.get(
url,
timeout=20,
headers={"Referer": "https://finance.sina.com.cn"},
)
bars = _parse_jsonp(resp.text)
return bars or []
except Exception as exc:
logger.warning("fetch kline failed %s %s: %s", chart_sym, period, exc)
return []
def _fetch_sina_daily(chart_sym: str) -> list:
url = (
"https://stock2.finance.sina.com.cn/futures/api/json.php/"
f"IndexService.getInnerFuturesDailyKLine?symbol={chart_sym}"
)
try:
resp = requests.get(url, timeout=20, headers={"Referer": "https://finance.sina.com.cn"})
raw = resp.json()
if not raw:
return []
out = []
for row in raw:
if isinstance(row, list) and len(row) >= 5:
out.append({
"d": row[0],
"o": row[1],
"h": row[2],
"l": row[3],
"c": row[4],
})
return out
except Exception as exc:
logger.warning("fetch daily kline failed %s: %s", chart_sym, exc)
return []
def _parse_dt(value: str) -> Optional[datetime]:
if not value:
return None
v = value.strip().replace("T", " ")
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
try:
return datetime.strptime(v, fmt).replace(tzinfo=TZ)
except ValueError:
continue
try:
return datetime.fromisoformat(value.strip()).replace(tzinfo=TZ)
except ValueError:
return None
def _bar_datetime(bar: dict) -> Optional[datetime]:
d = bar.get("d")
if not d:
return None
try:
return datetime.strptime(d, "%Y-%m-%d %H:%M:%S").replace(tzinfo=TZ)
except ValueError:
return None
def _select_bars(
bars: list,
cutoff: datetime,
count: int,
) -> list:
filtered = []
for bar in bars:
dt = _bar_datetime(bar)
if dt and dt <= cutoff:
filtered.append(bar)
if not filtered:
filtered = bars
if count > 0 and len(filtered) > count:
filtered = filtered[-count:]
return filtered
def generate_review_kline_chart(
symbol: str,
periods: list[str],
count: int,
cutoff_label: str,
open_time: str,
close_time: str,
entry_price: Optional[float],
stop_loss: Optional[float],
take_profit: Optional[float],
close_price: Optional[float],
upload_dir: str,
) -> Optional[str]:
"""生成双周期 K 线复盘图,返回 uploads 目录下的文件名。"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
now = datetime.now(TZ)
if cutoff_label == "开仓时间":
cutoff = _parse_dt(open_time) or now
elif cutoff_label == "当前时间":
cutoff = now
else:
cutoff = _parse_dt(close_time) or now
open_dt = _parse_dt(open_time)
close_dt = _parse_dt(close_time)
valid_periods = [p for p in periods if p]
if not valid_periods:
valid_periods = ["15m", "1h"]
fig, axes = plt.subplots(
len(valid_periods), 1,
figsize=(14, 4.5 * len(valid_periods)),
facecolor="#0a0a10",
squeeze=False,
)
plotted = False
for idx, period in enumerate(valid_periods):
ax = axes[idx, 0]
bars = fetch_sina_klines(symbol, period)
bars = _select_bars(bars, cutoff, count)
if not bars:
ax.set_facecolor("#12121a")
ax.text(0.5, 0.5, f"No {period} data", ha="center", va="center", color="#888")
ax.set_xticks([])
ax.set_yticks([])
continue
times = [_bar_datetime(b) for b in bars]
closes = [float(b["c"]) for b in bars]
highs = [float(b["h"]) for b in bars]
lows = [float(b["l"]) for b in bars]
ax.set_facecolor("#12121a")
ax.plot(times, closes, color="#4cc2ff", linewidth=1.2)
ax.fill_between(
times, lows, highs,
color="#4cc2ff", alpha=0.12,
)
levels = [
(entry_price, "#eac147", "Entry"),
(stop_loss, "#ff6666", "SL"),
(take_profit, "#4cd97f", "TP"),
(close_price, "#c4c4ff", "Close"),
]
for price, color, label in levels:
if price is not None:
ax.axhline(price, color=color, linewidth=0.9, linestyle="--", alpha=0.85)
ax.text(times[-1], price, label, color=color, fontsize=8, va="bottom")
if open_dt:
ax.axvline(open_dt, color="#888", linewidth=0.8, linestyle=":", alpha=0.7)
if close_dt:
ax.axvline(close_dt, color="#aaa", linewidth=0.8, linestyle=":", alpha=0.7)
chart_sym = ths_to_sina_chart_symbol(symbol) or symbol
ax.set_title(f"{chart_sym} {period}", color="#eaeaea", fontsize=11, pad=8)
ax.tick_params(colors="#888", labelsize=8)
for spine in ax.spines.values():
spine.set_color("#2e2e45")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d %H:%M"))
ax.grid(True, color="#1e1e30", linewidth=0.5)
plotted = True
if not plotted:
plt.close(fig)
return None
fig.tight_layout()
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
chart_sym = ths_to_sina_chart_symbol(symbol) or "chart"
filename = f"{ts}_kline_{chart_sym}.png"
path = os.path.join(upload_dir, filename)
fig.savefig(path, dpi=120, facecolor=fig.get_facecolor())
plt.close(fig)
return filename
+1
View File
@@ -2,3 +2,4 @@ Flask==3.0.3
requests==2.32.3 requests==2.32.3
python-dotenv==1.0.1 python-dotenv==1.0.1
Werkzeug==3.0.3 Werkzeug==3.0.3
matplotlib==3.9.2
+18 -11
View File
@@ -4,12 +4,21 @@
return isNaN(n) ? null : n; return isNaN(n) ? null : n;
} }
function calcPnl(direction, entry, target, lots) { function calcRR(direction, entry, stop, target) {
if (!entry || !target || !lots) return ''; if (!entry || !stop || !target) return '';
if (direction === 'long') return ((target - entry) * lots).toFixed(2); var risk, reward;
if (direction === 'short') return ((entry - target) * lots).toFixed(2); if (direction === 'long') {
risk = entry - stop;
reward = target - entry;
} else if (direction === 'short') {
risk = stop - entry;
reward = entry - target;
} else {
return ''; return '';
} }
if (risk <= 0) return '';
return (reward / risk).toFixed(2);
}
function calcDuration(openVal, closeVal) { function calcDuration(openVal, closeVal) {
if (!openVal || !closeVal) return ''; if (!openVal || !closeVal) return '';
@@ -30,16 +39,15 @@
var sl = parseNum(form.querySelector('[name="stop_loss"]').value); var sl = parseNum(form.querySelector('[name="stop_loss"]').value);
var tp = parseNum(form.querySelector('[name="take_profit"]').value); var tp = parseNum(form.querySelector('[name="take_profit"]').value);
var close = parseNum(form.querySelector('[name="close_price"]').value); var close = parseNum(form.querySelector('[name="close_price"]').value);
var lots = parseNum(form.querySelector('[name="lots"]').value) || 1;
var openT = form.querySelector('[name="open_time"]').value; var openT = form.querySelector('[name="open_time"]').value;
var closeT = form.querySelector('[name="close_time"]').value; var closeT = form.querySelector('[name="close_time"]').value;
var hold = document.getElementById('holding_duration'); var hold = document.getElementById('holding_duration');
var initP = document.getElementById('initial_pnl'); var initR = document.getElementById('initial_rr');
var actP = document.getElementById('actual_pnl'); var actR = document.getElementById('actual_rr');
if (hold) hold.value = calcDuration(openT, closeT); if (hold) hold.value = calcDuration(openT, closeT);
if (initP) initP.value = calcPnl(dir, entry, tp, lots); if (initR) initR.value = calcRR(dir, entry, sl, tp);
if (actP) actP.value = calcPnl(dir, entry, close, lots); if (actR) actR.value = calcRR(dir, entry, sl, close);
} }
function bindForm() { function bindForm() {
@@ -62,9 +70,8 @@
['止盈', data.take_profit], ['平仓价', data.close_price], ['止盈', data.take_profit], ['平仓价', data.close_price],
['张数', data.lots], ['开仓时间', data.open_time], ['张数', data.lots], ['开仓时间', data.open_time],
['平仓时间', data.close_time], ['持仓时长', data.holding_duration], ['平仓时间', data.close_time], ['持仓时长', data.holding_duration],
['初始盈亏', data.initial_pnl], ['实际盈亏', data.actual_pnl], ['初始盈亏', data.initial_pnl], ['实际盈亏', data.actual_pnl],
['盈亏金额', data.pnl], ['开仓类型', data.open_type], ['盈亏金额', data.pnl], ['开仓类型', data.open_type],
['预期RR', data.expected_rr], ['实际RR', data.actual_rr],
['离场触发', data.exit_trigger], ['离场补充', data.exit_supplement], ['离场触发', data.exit_trigger], ['离场补充', data.exit_supplement],
['情绪单', data.is_emotion ? '是' : '否'], ['情绪单', data.is_emotion ? '是' : '否'],
['行为标签', data.behavior_tags], ['备注', data.notes] ['行为标签', data.behavior_tags], ['备注', data.notes]
+19 -17
View File
@@ -8,17 +8,16 @@
*{margin:0;padding:0;box-sizing:border-box} *{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a10;color:#eaeaea;min-height:100vh} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a10;color:#eaeaea;min-height:100vh}
.page-wrap{max-width:1800px;margin:0 auto;min-height:100vh} .page-wrap{max-width:1800px;margin:0 auto;min-height:100vh}
.topbar{background:#12121a;border-bottom:1px solid #242435;padding:0 1.5rem} .site-header{text-align:center;padding:1.5rem 1rem 1.25rem;border-bottom:1px solid #1a1a28;position:relative}
.topbar-inner{display:flex;align-items:center;gap:1.5rem;height:56px} .site-title{font-size:1.75rem;font-weight:700;color:#fff;margin-bottom:.55rem;line-height:1.3}
.logo{font-size:1.05rem;font-weight:600;background:linear-gradient(90deg,#4cc2ff,#7b42ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;white-space:nowrap} .site-badge{display:inline-block;padding:.22rem .85rem;border-radius:999px;border:1px solid #2d6a4f;background:#0d2818;color:#4cd97f;font-size:.75rem;margin-bottom:1.15rem}
.nav{display:flex;gap:.25rem;flex:1;flex-wrap:wrap} .site-nav{display:flex;justify-content:center;gap:.45rem;flex-wrap:wrap}
.nav a{padding:.5rem 1rem;color:#a9a9c4;text-decoration:none;font-size:.9rem;border-radius:8px;transition:.2s} .site-nav a{padding:.55rem 1.15rem;border-radius:8px;border:1px solid #2a2a40;background:#161625;color:#e8e8f0;text-decoration:none;font-size:.88rem;transition:.2s;white-space:nowrap}
.nav a:hover{color:#fff;background:#1a1a29} .site-nav a:hover{background:#1e2533;border-color:#3a3a55;color:#fff}
.nav a.active{color:#4cc2ff;background:#1a1a29} .site-nav a.active{background:#2d5aa8;border-color:#3d6ec4;color:#fff}
.user-bar{font-size:.85rem;color:#888;white-space:nowrap} .user-bar{position:absolute;top:1rem;right:1.5rem;font-size:.8rem;color:#888;white-space:nowrap}
.user-bar a{color:#ff6666;text-decoration:none;margin-left:.5rem} .user-bar a{color:#ff6666;text-decoration:none;margin-left:.5rem}
.main{padding:1.5rem} .main{padding:1.5rem}
.page-title{font-size:1.5rem;margin-bottom:1.5rem;color:#fff}
.flash{padding:1rem;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:1.5rem;text-align:center} .flash{padding:1rem;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:1.5rem;text-align:center}
.card{background:#12121a;border-radius:16px;padding:1.5rem;border:1px solid #242435;margin-bottom:1.5rem} .card{background:#12121a;border-radius:16px;padding:1.5rem;border:1px solid #242435;margin-bottom:1.5rem}
.card h2{font-size:1.15rem;margin-bottom:1rem;color:#c4c4ff;display:flex;align-items:center;gap:.5rem} .card h2{font-size:1.15rem;margin-bottom:1rem;color:#c4c4ff;display:flex;align-items:center;gap:.5rem}
@@ -26,6 +25,7 @@
.form-row{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center} .form-row{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center}
.form-compact{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem} .form-compact{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem}
.form-compact .form-line{display:grid;gap:.5rem;align-items:center} .form-compact .form-line{display:grid;gap:.5rem;align-items:center}
.form-compact .line-2{grid-template-columns:repeat(2,1fr)}
.form-compact .line-3{grid-template-columns:repeat(3,1fr)} .form-compact .line-3{grid-template-columns:repeat(3,1fr)}
.form-compact .line-4{grid-template-columns:repeat(4,1fr)} .form-compact .line-4{grid-template-columns:repeat(4,1fr)}
.form-compact .line-5{grid-template-columns:repeat(5,1fr)} .form-compact .line-5{grid-template-columns:repeat(5,1fr)}
@@ -110,26 +110,28 @@
.split-grid .card{min-height:auto} .split-grid .card{min-height:auto}
} }
@media(max-width:768px){ @media(max-width:768px){
.topbar-inner{flex-wrap:wrap;height:auto;padding:.75rem 0} .site-header{padding:1.25rem .75rem 1rem}
.nav{order:3;width:100%} .site-title{font-size:1.35rem}
.user-bar{position:static;text-align:center;margin-bottom:.75rem}
.site-nav{gap:.35rem}
.site-nav a{padding:.45rem .75rem;font-size:.82rem}
} }
</style> </style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
<div class="page-wrap"> <div class="page-wrap">
<header class="topbar"> <header class="site-header">
<div class="topbar-inner"> <div class="user-bar">{{ session.username or '用户' }}<a href="{{ url_for('logout') }}">退出</a></div>
<div class="logo">期货监控复盘</div> <h1 class="site-title">国内期货 | 交易监控 + 复盘一体化</h1>
<nav class="nav"> <div class="site-badge">新浪行情</div>
<nav class="site-nav">
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a> <a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a> <a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a> <a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a> <a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a> <a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
</nav> </nav>
<div class="user-bar">{{ session.username or '用户' }}<a href="{{ url_for('logout') }}">退出</a></div>
</div>
</header> </header>
<main class="main"> <main class="main">
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %} {% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
-2
View File
@@ -1,8 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}关键位监控 - 国内期货监控系统{% endblock %} {% block title %}关键位监控 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">关键位监控</h1>
<div class="split-grid"> <div class="split-grid">
<div class="card"> <div class="card">
<h2>新增监控</h2> <h2>新增监控</h2>
+1 -3
View File
@@ -1,11 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}开单计划 - 国内期货监控系统{% endblock %} {% block title %}开单计划 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">开单计划 <span style="font-size:.9rem;color:#888;font-weight:normal">今日 {{ today }}</span></h1>
<div class="split-grid"> <div class="split-grid">
<div class="card"> <div class="card">
<h2>今日计划</h2> <h2>今日计划 <span style="font-size:.8rem;color:#888;font-weight:normal">今日 {{ today }}</span></h2>
<div class="card-body"> <div class="card-body">
<p class="hint" style="margin-bottom:.75rem">开盘前制定,当日有效;下方为进行中计划。</p> <p class="hint" style="margin-bottom:.75rem">开盘前制定,当日有效;下方为进行中计划。</p>
<form action="{{ url_for('add_plan') }}" method="post" class="form-compact"> <form action="{{ url_for('add_plan') }}" method="post" class="form-compact">
+5 -9
View File
@@ -1,8 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %} {% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title page-title-sm">交易记录与复盘</h1>
<div class="split-grid records-split"> <div class="split-grid records-split">
<div class="card"> <div class="card">
<h2>复盘上传</h2> <h2>复盘上传</h2>
@@ -30,11 +28,9 @@
<input id="holding_duration" type="text" readonly class="calc-readonly" placeholder="持仓时长(自动)"> <input id="holding_duration" type="text" readonly class="calc-readonly" placeholder="持仓时长(自动)">
<input name="pnl" type="number" step="0.01" placeholder="盈亏金额(手动)"> <input name="pnl" type="number" step="0.01" placeholder="盈亏金额(手动)">
</div> </div>
<div class="form-line line-4"> <div class="form-line line-2">
<input id="initial_pnl" type="text" readonly class="calc-readonly" placeholder="初始盈亏(自动)"> <input id="initial_rr" type="text" readonly class="calc-readonly" placeholder="初始盈亏(自动)">
<input id="actual_pnl" type="text" readonly class="calc-readonly" placeholder="实际盈亏(自动)"> <input id="actual_rr" type="text" readonly class="calc-readonly" placeholder="实际盈亏(自动)">
<input name="expected_rr" type="number" step="0.01" placeholder="预期RR">
<input name="actual_rr" type="number" step="0.01" placeholder="实际RR">
</div> </div>
<div class="form-line line-4"> <div class="form-line line-4">
<select name="open_type" required> <select name="open_type" required>
@@ -61,7 +57,7 @@
<button type="submit" class="btn-primary">保存</button> <button type="submit" class="btn-primary">保存</button>
</div> </div>
<div class="kline-row"> <div class="kline-row">
<label><input type="checkbox" name="auto_kline" value="1"> 自动K线</label> <label><input type="checkbox" name="auto_kline" value="1" checked> 自动K线</label>
<select name="kline_period1" title="周期1">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='15m' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select> <select name="kline_period1" title="周期1">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='15m' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
<select name="kline_period2" title="周期2">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='1h' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select> <select name="kline_period2" title="周期2">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='1h' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
<input name="kline_count" type="number" value="300" placeholder="K线数" title="K线数"> <input name="kline_count" type="number" value="300" placeholder="K线数" title="K线数">
@@ -121,7 +117,7 @@
"open_time": r.open_time, "close_time": r.close_time, "open_time": r.open_time, "close_time": r.close_time,
"holding_duration": r.holding_duration, "initial_pnl": r.initial_pnl, "holding_duration": r.holding_duration, "initial_pnl": r.initial_pnl,
"actual_pnl": r.actual_pnl, "pnl": r.pnl, "actual_pnl": r.actual_pnl, "pnl": r.pnl,
"open_type": r.open_type, "expected_rr": r.expected_rr, "actual_rr": r.actual_rr, "open_type": r.open_type,
"exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement, "exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement,
"is_emotion": r.is_emotion, "behavior_tags": r.behavior_tags, "is_emotion": r.is_emotion, "behavior_tags": r.behavior_tags,
"notes": r.notes, "screenshot": r.screenshot "notes": r.notes, "screenshot": r.screenshot
-1
View File
@@ -1,7 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}系统设置 - 国内期货监控系统{% endblock %} {% block title %}系统设置 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">系统设置</h1>
<div class="card"> <div class="card">
<h2>行情说明</h2> <h2>行情说明</h2>
-1
View File
@@ -1,7 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}统计分析 - 国内期货监控系统{% endblock %} {% block title %}统计分析 - 国内期货监控系统{% endblock %}
{% block content %} {% block content %}
<h1 class="page-title">统计分析</h1>
<div class="stat-grid"> <div class="stat-grid">
<div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div> <div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div>