复盘盈亏比自动计算与K线自动生成;居中页头导航
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
+19
-12
@@ -4,11 +4,20 @@
|
|||||||
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') {
|
||||||
return '';
|
risk = entry - stop;
|
||||||
|
reward = target - entry;
|
||||||
|
} else if (direction === 'short') {
|
||||||
|
risk = stop - entry;
|
||||||
|
reward = entry - target;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (risk <= 0) return '';
|
||||||
|
return (reward / risk).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcDuration(openVal, closeVal) {
|
function calcDuration(openVal, closeVal) {
|
||||||
@@ -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]
|
||||||
|
|||||||
+25
-23
@@ -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>
|
||||||
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
|
<nav class="site-nav">
|
||||||
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
|
<a href="{{ url_for('plans') }}" class="{% if request.endpoint == 'plans' %}active{% endif %}">开单计划</a>
|
||||||
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a>
|
<a href="{{ url_for('keys') }}" class="{% if request.endpoint == 'keys' %}active{% endif %}">关键位监控</a>
|
||||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
<a href="{{ url_for('records') }}" class="{% if request.endpoint == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||||
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
|
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
||||||
</nav>
|
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
|
||||||
<div class="user-bar">{{ session.username or '用户' }}<a href="{{ url_for('logout') }}">退出</a></div>
|
</nav>
|
||||||
</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 %}
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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,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,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>
|
||||||
|
|||||||
Reference in New Issue
Block a user