Normalize fullwidth punctuation to ASCII across codebase.

Add scripts/normalize_ambiguous_unicode.py; fix corrupted patch_instance_theme_templates.py. Preserves curly quotes in string literals; removes Git homoglyph warnings on .env.example.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-08 23:42:26 +08:00
parent aaa72c7961
commit b733e551a0
392 changed files with 71522 additions and 71369 deletions
+187 -187
View File
@@ -1,187 +1,187 @@
"""实盘/关键位放大 K 线订单元数据与交易所浮盈价格展示精度"""
from __future__ import annotations
from typing import Any, Callable, Optional
from lib.hub.hub_ohlcv_lib import (
normalize_price_tick,
price_tick_from_market,
round_ohlcv_bars_to_tick,
)
from lib.trade.order_monitor_display_lib import (
apply_order_live_price_display,
apply_order_price_display_fields,
)
def resolve_kline_price_tick(
exchange: Any,
exchange_symbol: str,
*,
ensure_markets_fn: Callable[[], None],
) -> Optional[float]:
"""交易所最小价格变动单位供 lightweight-charts 右侧刻度与标记线对齐"""
if not exchange_symbol:
return None
try:
ensure_markets_fn()
return normalize_price_tick(price_tick_from_market(exchange, exchange_symbol))
except Exception:
return None
def align_candles_to_price_tick(
candles: list[dict[str, Any]],
price_tick: Optional[float],
) -> None:
if price_tick is not None and candles:
round_ohlcv_bars_to_tick(candles, price_tick)
def kline_api_price_fields(
exchange: Any,
exchange_symbol: str,
candles: list[dict[str, Any]],
*,
ensure_markets_fn: Callable[[], None],
) -> dict[str, Any]:
tick = resolve_kline_price_tick(
exchange, exchange_symbol, ensure_markets_fn=ensure_markets_fn
)
align_candles_to_price_tick(candles, tick)
return {"price_tick": tick}
def load_swap_positions_for_order_kline(
exchange: Any,
*,
private_configured: bool,
ensure_markets_fn: Callable[[], None],
settle: str = "usdt",
) -> list:
if not private_configured:
return []
try:
ensure_markets_fn()
try:
return exchange.fetch_positions(None, {"settle": settle}) or []
except Exception:
return exchange.fetch_positions() or []
except Exception:
return []
def metrics_for_order_item(
order_item: dict[str, Any],
positions: list,
*,
resolve_ex_sym_fn: Callable[[Any], str],
select_live_fn: Callable[[list, str, str], Any],
parse_metrics_fn: Callable[..., Optional[dict]],
) -> Optional[dict]:
if not positions:
return None
ex_sym = resolve_ex_sym_fn(order_item)
direction = order_item.get("direction") or "long"
prow = select_live_fn(positions, ex_sym, direction)
if not prow:
return None
lev = order_item.get("leverage")
return parse_metrics_fn(prow, order_leverage=lev)
def build_order_kline_order_payload(
order_item: dict[str, Any],
*,
ticker_price: Any,
format_price_fn: Callable[[Any, Any], str],
calc_pnl_fn: Callable[..., float],
calc_rr_ratio_fn: Callable[..., Optional[float]],
ex_metrics: Optional[dict] = None,
) -> dict[str, Any]:
sym = order_item.get("symbol") or ""
direction = order_item.get("direction") or "long"
margin = float(order_item.get("margin_capital") or 0)
leverage = float(order_item.get("leverage") or 0)
entry = float(order_item.get("trigger_price") or 0)
float_pnl = 0.0
float_pct = 0.0
if ticker_price and entry > 0:
float_pnl = float(
calc_pnl_fn(direction, entry, ticker_price, margin, leverage)
)
float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0.0
px_for_fmt = ticker_price
mark_raw = None
if ex_metrics and ex_metrics.get("mark_price") is not None:
mark_raw = ex_metrics["mark_price"]
try:
px_for_fmt = float(mark_raw)
except (TypeError, ValueError):
pass
if ex_metrics and ex_metrics.get("unrealized_pnl") is not None:
float_pnl = round(float(ex_metrics["unrealized_pnl"]), 2)
denom = ex_metrics.get("initial_margin") or margin
float_pct = (
round((float_pnl / float(denom)) * 100, 4)
if denom and float(denom) > 0
else float_pct
)
payload: dict[str, Any] = {
"id": order_item["id"],
"symbol": sym,
"direction": direction,
"trigger_price": order_item.get("trigger_price"),
"stop_loss": order_item.get("stop_loss"),
"take_profit": order_item.get("take_profit"),
"trigger_price_display": format_price_fn(sym, order_item.get("trigger_price")),
"stop_loss_display": format_price_fn(sym, order_item.get("stop_loss")),
"take_profit_display": format_price_fn(sym, order_item.get("take_profit")),
"margin_capital": order_item.get("margin_capital"),
"leverage": order_item.get("leverage"),
"position_ratio": order_item.get("position_ratio"),
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
"current_price": round(float(px_for_fmt), 8) if px_for_fmt is not None else None,
"float_pnl": round(float(float_pnl), 2),
"float_pct": float_pct,
}
apply_order_price_display_fields(
payload,
direction=direction,
entry_price=order_item.get("trigger_price"),
initial_stop_loss=order_item.get("initial_stop_loss"),
stop_loss=order_item.get("stop_loss"),
take_profit=order_item.get("take_profit"),
calc_rr_ratio_fn=calc_rr_ratio_fn,
)
apply_order_live_price_display(
payload,
sym,
ticker_price,
mark_raw,
format_price_fn,
)
payload["current_price_display"] = payload.get("price_display") or (
format_price_fn(sym, px_for_fmt) if px_for_fmt is not None else None
)
return payload
def enrich_key_kline_response(
*,
symbol: str,
current_price: Any,
key_info: Optional[dict[str, Any]],
format_price_fn: Callable[[Any, Any], str],
) -> tuple[Any, Optional[dict[str, Any]]]:
price_display = format_price_fn(symbol, current_price) if current_price is not None else None
if key_info is None:
return price_display, None
enriched = dict(key_info)
enriched["upper_display"] = format_price_fn(symbol, key_info.get("upper"))
enriched["lower_display"] = format_price_fn(symbol, key_info.get("lower"))
return price_display, enriched
"""实盘/关键位放大 K 线:订单元数据与交易所浮盈,价格展示精度."""
from __future__ import annotations
from typing import Any, Callable, Optional
from lib.hub.hub_ohlcv_lib import (
normalize_price_tick,
price_tick_from_market,
round_ohlcv_bars_to_tick,
)
from lib.trade.order_monitor_display_lib import (
apply_order_live_price_display,
apply_order_price_display_fields,
)
def resolve_kline_price_tick(
exchange: Any,
exchange_symbol: str,
*,
ensure_markets_fn: Callable[[], None],
) -> Optional[float]:
"""交易所最小价格变动单位,供 lightweight-charts 右侧刻度与标记线对齐."""
if not exchange_symbol:
return None
try:
ensure_markets_fn()
return normalize_price_tick(price_tick_from_market(exchange, exchange_symbol))
except Exception:
return None
def align_candles_to_price_tick(
candles: list[dict[str, Any]],
price_tick: Optional[float],
) -> None:
if price_tick is not None and candles:
round_ohlcv_bars_to_tick(candles, price_tick)
def kline_api_price_fields(
exchange: Any,
exchange_symbol: str,
candles: list[dict[str, Any]],
*,
ensure_markets_fn: Callable[[], None],
) -> dict[str, Any]:
tick = resolve_kline_price_tick(
exchange, exchange_symbol, ensure_markets_fn=ensure_markets_fn
)
align_candles_to_price_tick(candles, tick)
return {"price_tick": tick}
def load_swap_positions_for_order_kline(
exchange: Any,
*,
private_configured: bool,
ensure_markets_fn: Callable[[], None],
settle: str = "usdt",
) -> list:
if not private_configured:
return []
try:
ensure_markets_fn()
try:
return exchange.fetch_positions(None, {"settle": settle}) or []
except Exception:
return exchange.fetch_positions() or []
except Exception:
return []
def metrics_for_order_item(
order_item: dict[str, Any],
positions: list,
*,
resolve_ex_sym_fn: Callable[[Any], str],
select_live_fn: Callable[[list, str, str], Any],
parse_metrics_fn: Callable[..., Optional[dict]],
) -> Optional[dict]:
if not positions:
return None
ex_sym = resolve_ex_sym_fn(order_item)
direction = order_item.get("direction") or "long"
prow = select_live_fn(positions, ex_sym, direction)
if not prow:
return None
lev = order_item.get("leverage")
return parse_metrics_fn(prow, order_leverage=lev)
def build_order_kline_order_payload(
order_item: dict[str, Any],
*,
ticker_price: Any,
format_price_fn: Callable[[Any, Any], str],
calc_pnl_fn: Callable[..., float],
calc_rr_ratio_fn: Callable[..., Optional[float]],
ex_metrics: Optional[dict] = None,
) -> dict[str, Any]:
sym = order_item.get("symbol") or ""
direction = order_item.get("direction") or "long"
margin = float(order_item.get("margin_capital") or 0)
leverage = float(order_item.get("leverage") or 0)
entry = float(order_item.get("trigger_price") or 0)
float_pnl = 0.0
float_pct = 0.0
if ticker_price and entry > 0:
float_pnl = float(
calc_pnl_fn(direction, entry, ticker_price, margin, leverage)
)
float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0.0
px_for_fmt = ticker_price
mark_raw = None
if ex_metrics and ex_metrics.get("mark_price") is not None:
mark_raw = ex_metrics["mark_price"]
try:
px_for_fmt = float(mark_raw)
except (TypeError, ValueError):
pass
if ex_metrics and ex_metrics.get("unrealized_pnl") is not None:
float_pnl = round(float(ex_metrics["unrealized_pnl"]), 2)
denom = ex_metrics.get("initial_margin") or margin
float_pct = (
round((float_pnl / float(denom)) * 100, 4)
if denom and float(denom) > 0
else float_pct
)
payload: dict[str, Any] = {
"id": order_item["id"],
"symbol": sym,
"direction": direction,
"trigger_price": order_item.get("trigger_price"),
"stop_loss": order_item.get("stop_loss"),
"take_profit": order_item.get("take_profit"),
"trigger_price_display": format_price_fn(sym, order_item.get("trigger_price")),
"stop_loss_display": format_price_fn(sym, order_item.get("stop_loss")),
"take_profit_display": format_price_fn(sym, order_item.get("take_profit")),
"margin_capital": order_item.get("margin_capital"),
"leverage": order_item.get("leverage"),
"position_ratio": order_item.get("position_ratio"),
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
"current_price": round(float(px_for_fmt), 8) if px_for_fmt is not None else None,
"float_pnl": round(float(float_pnl), 2),
"float_pct": float_pct,
}
apply_order_price_display_fields(
payload,
direction=direction,
entry_price=order_item.get("trigger_price"),
initial_stop_loss=order_item.get("initial_stop_loss"),
stop_loss=order_item.get("stop_loss"),
take_profit=order_item.get("take_profit"),
calc_rr_ratio_fn=calc_rr_ratio_fn,
)
apply_order_live_price_display(
payload,
sym,
ticker_price,
mark_raw,
format_price_fn,
)
payload["current_price_display"] = payload.get("price_display") or (
format_price_fn(sym, px_for_fmt) if px_for_fmt is not None else None
)
return payload
def enrich_key_kline_response(
*,
symbol: str,
current_price: Any,
key_info: Optional[dict[str, Any]],
format_price_fn: Callable[[Any, Any], str],
) -> tuple[Any, Optional[dict[str, Any]]]:
price_display = format_price_fn(symbol, current_price) if current_price is not None else None
if key_info is None:
return price_display, None
enriched = dict(key_info)
enriched["upper_display"] = format_price_fn(symbol, key_info.get("upper"))
enriched["lower_display"] = format_price_fn(symbol, key_info.get("lower"))
return price_display, enriched
+1 -1
View File
@@ -1,4 +1,4 @@
"""实例顶栏 / 系统设置区块显示开关存 SQLite即时生效)。"""
"""实例顶栏 / 系统设置区块显示开关(存 SQLite,即时生效)."""
from __future__ import annotations
from typing import Any, Callable, Optional
+155 -155
View File
@@ -1,155 +1,155 @@
"""embed 壳/片段按 tab 裁剪 render_main_page 的数据加载降内存与 API 压力"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
EMBED_STRATEGY_PAGES = frozenset({"strategy", "strategy_trend", "strategy_roll", "strategy_records"})
_WIN_EPS = 1e-9
@dataclass(frozen=True)
class EmbedRenderPlan:
exchange_capitals: bool
records_rows: bool
records_summary: bool
key_history: bool
key_list: bool
orders: bool
stats_bundle: bool
strategy: bool
orphan_live: bool
def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
if embed_mode not in ("fragment", "shell"):
return EmbedRenderPlan(
exchange_capitals=True,
records_rows=True,
records_summary=False,
key_history=True,
key_list=True,
orders=True,
stats_bundle=True,
strategy=True,
orphan_live=True,
)
is_shell = embed_mode == "shell"
is_strategy = page in EMBED_STRATEGY_PAGES
is_settings_like = page in ("settings", "risk_policy", "env_config")
return EmbedRenderPlan(
exchange_capitals=is_shell,
records_rows=page == "records",
records_summary=is_shell and page != "records" and not is_settings_like,
key_history=page == "key_monitor",
key_list=page in ("key_monitor", "trade") or is_strategy,
orders=page == "trade" or is_strategy,
stats_bundle=page == "stats",
strategy=is_strategy,
orphan_live=page == "trade" and is_shell,
)
def profit_loss_ratio_from_averages(avg_win: float | None, avg_loss: float | None) -> float | None:
"""盈亏比 = 平均盈利 / |平均亏损|"""
if avg_win is None or avg_loss is None:
return None
try:
aw = float(avg_win)
al = float(avg_loss)
except (TypeError, ValueError):
return None
if al == 0:
return None
return round(aw / abs(al), 2)
def profit_loss_ratio_from_trades(trades: list[dict[str, Any]] | None) -> float | None:
wins: list[float] = []
losses: list[float] = []
for row in trades or []:
if not isinstance(row, dict):
continue
try:
pnl = float(row.get("effective_pnl_amount") or row.get("pnl_amount") or 0)
except (TypeError, ValueError):
continue
if pnl > _WIN_EPS:
wins.append(pnl)
elif pnl < -_WIN_EPS:
losses.append(pnl)
avg_win = sum(wins) / len(wins) if wins else None
avg_loss = sum(losses) / len(losses) if losses else None
return profit_loss_ratio_from_averages(avg_win, avg_loss)
def options_funding_label(
funding_usdc: float | None,
funding_usdt: float | None = None,
) -> str:
parts: list[str] = []
if funding_usdc is not None:
parts.append(f"{float(funding_usdc):.2f} USDC")
if funding_usdt is not None:
parts.append(f"{float(funding_usdt):.2f} USDT")
return " · ".join(parts) if parts else ""
def total_funds_usdt(
funding_usdt: float | None,
trading_usdt: float | None,
options_trading_usdc: float | None = None,
options_funding_usdc: float | None = None,
options_funding_usdt: float | None = None,
) -> float | None:
if funding_usdt is None:
return None
try:
total = float(funding_usdt) + float(trading_usdt or 0)
if options_funding_usdc is not None:
total += float(options_funding_usdc)
if options_funding_usdt is not None:
total += float(options_funding_usdt)
if options_trading_usdc is not None:
total += float(options_trading_usdc)
return round(total, 2)
except (TypeError, ValueError):
return None
def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]:
"""顶栏统计用 COUNT避免 embed 壳拉 1000 行交易记录"""
from lib.trade.trade_result_lib import sql_effective_pnl_expr
pnl_sql = sql_effective_pnl_expr()
row = conn.execute(
f"""
SELECT
COUNT(*) AS total,
SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins,
AVG(CASE WHEN {pnl_sql} > 0 THEN {pnl_sql} END) AS avg_win,
AVG(CASE WHEN {pnl_sql} < 0 THEN {pnl_sql} END) AS avg_loss
FROM trade_records
WHERE {tr_ts} >= ? AND {tr_ts} <= ?
AND COALESCE(result, '') != '错过'
AND COALESCE(reviewed_result, '') != '错过'
""",
(start_bj, end_bj),
).fetchone()
total = int(row["total"] or 0) if row else 0
wins = int(row["wins"] or 0) if row else 0
rate = round(wins / total * 100, 2) if total else 0
avg_win = float(row["avg_win"]) if row and row["avg_win"] is not None else None
avg_loss = float(row["avg_loss"]) if row and row["avg_loss"] is not None else None
return {
"records": [],
"total": total,
"rate": rate,
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
}
def minimal_stats_bundle(reset_hour: int) -> dict[str, Any]:
return {"stats_reset_hour": reset_hour, "segments": []}
"""embed 壳/片段:按 tab 裁剪 render_main_page 的数据加载,降内存与 API 压力."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
EMBED_STRATEGY_PAGES = frozenset({"strategy", "strategy_trend", "strategy_roll", "strategy_records"})
_WIN_EPS = 1e-9
@dataclass(frozen=True)
class EmbedRenderPlan:
exchange_capitals: bool
records_rows: bool
records_summary: bool
key_history: bool
key_list: bool
orders: bool
stats_bundle: bool
strategy: bool
orphan_live: bool
def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
if embed_mode not in ("fragment", "shell"):
return EmbedRenderPlan(
exchange_capitals=True,
records_rows=True,
records_summary=False,
key_history=True,
key_list=True,
orders=True,
stats_bundle=True,
strategy=True,
orphan_live=True,
)
is_shell = embed_mode == "shell"
is_strategy = page in EMBED_STRATEGY_PAGES
is_settings_like = page in ("settings", "risk_policy", "env_config")
return EmbedRenderPlan(
exchange_capitals=is_shell,
records_rows=page == "records",
records_summary=is_shell and page != "records" and not is_settings_like,
key_history=page == "key_monitor",
key_list=page in ("key_monitor", "trade") or is_strategy,
orders=page == "trade" or is_strategy,
stats_bundle=page == "stats",
strategy=is_strategy,
orphan_live=page == "trade" and is_shell,
)
def profit_loss_ratio_from_averages(avg_win: float | None, avg_loss: float | None) -> float | None:
"""盈亏比 = 平均盈利 / |平均亏损|."""
if avg_win is None or avg_loss is None:
return None
try:
aw = float(avg_win)
al = float(avg_loss)
except (TypeError, ValueError):
return None
if al == 0:
return None
return round(aw / abs(al), 2)
def profit_loss_ratio_from_trades(trades: list[dict[str, Any]] | None) -> float | None:
wins: list[float] = []
losses: list[float] = []
for row in trades or []:
if not isinstance(row, dict):
continue
try:
pnl = float(row.get("effective_pnl_amount") or row.get("pnl_amount") or 0)
except (TypeError, ValueError):
continue
if pnl > _WIN_EPS:
wins.append(pnl)
elif pnl < -_WIN_EPS:
losses.append(pnl)
avg_win = sum(wins) / len(wins) if wins else None
avg_loss = sum(losses) / len(losses) if losses else None
return profit_loss_ratio_from_averages(avg_win, avg_loss)
def options_funding_label(
funding_usdc: float | None,
funding_usdt: float | None = None,
) -> str:
parts: list[str] = []
if funding_usdc is not None:
parts.append(f"{float(funding_usdc):.2f} USDC")
if funding_usdt is not None:
parts.append(f"{float(funding_usdt):.2f} USDT")
return " · ".join(parts) if parts else ""
def total_funds_usdt(
funding_usdt: float | None,
trading_usdt: float | None,
options_trading_usdc: float | None = None,
options_funding_usdc: float | None = None,
options_funding_usdt: float | None = None,
) -> float | None:
if funding_usdt is None:
return None
try:
total = float(funding_usdt) + float(trading_usdt or 0)
if options_funding_usdc is not None:
total += float(options_funding_usdc)
if options_funding_usdt is not None:
total += float(options_funding_usdt)
if options_trading_usdc is not None:
total += float(options_trading_usdc)
return round(total, 2)
except (TypeError, ValueError):
return None
def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]:
"""顶栏统计用 COUNT,避免 embed 壳拉 1000 行交易记录."""
from lib.trade.trade_result_lib import sql_effective_pnl_expr
pnl_sql = sql_effective_pnl_expr()
row = conn.execute(
f"""
SELECT
COUNT(*) AS total,
SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins,
AVG(CASE WHEN {pnl_sql} > 0 THEN {pnl_sql} END) AS avg_win,
AVG(CASE WHEN {pnl_sql} < 0 THEN {pnl_sql} END) AS avg_loss
FROM trade_records
WHERE {tr_ts} >= ? AND {tr_ts} <= ?
AND COALESCE(result, '') != '错过'
AND COALESCE(reviewed_result, '') != '错过'
""",
(start_bj, end_bj),
).fetchone()
total = int(row["total"] or 0) if row else 0
wins = int(row["wins"] or 0) if row else 0
rate = round(wins / total * 100, 2) if total else 0
avg_win = float(row["avg_win"]) if row and row["avg_win"] is not None else None
avg_loss = float(row["avg_loss"]) if row and row["avg_loss"] is not None else None
return {
"records": [],
"total": total,
"rate": rate,
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
}
def minimal_stats_bundle(reset_hour: int) -> dict[str, Any]:
return {"stats_reset_hour": reset_hour, "segments": []}
+186 -186
View File
@@ -1,186 +1,186 @@
"""中控 iframe壳常驻 + tab 内容 API/embed/api/embed/page/<tab>)。"""
from __future__ import annotations
from lib.paths import embed_templates_dir
import os
from typing import Callable
from urllib.parse import parse_qsl, urlencode, urlsplit
from flask import Flask, Response, jsonify, redirect, request, session
from jinja2 import ChoiceLoader, FileSystemLoader
EMBED_TABS: tuple[str, ...] = (
"key_monitor",
"trade",
"strategy",
"strategy_records",
"options",
"records",
"stats",
"risk_policy",
"env_config",
"settings",
)
PATH_TO_EMBED_TAB: dict[str, str] = {
"/": "trade",
"/trade": "trade",
"/key_monitor": "key_monitor",
"/strategy": "strategy",
"/strategy/trend": "strategy",
"/strategy/roll": "strategy",
"/strategy/records": "strategy_records",
"/options": "options",
"/records": "records",
"/stats": "stats",
"/risk_policy": "risk_policy",
"/env_config": "env_config",
"/settings": "settings",
}
ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = {
"gate": "order_monitor_rule_tips_gate.html",
"binance": "order_monitor_rule_tips_binance.html",
"okx": "order_monitor_rule_tips_okx.html",
}
def order_rule_tips_template(exchange_key: str) -> str:
ex = (exchange_key or "").strip().lower()
return ORDER_RULE_TIPS_BY_EXCHANGE.get(ex, "order_monitor_rule_tips_gate.html")
def include_transfer_block(exchange_key: str) -> bool:
"""三所 standalone / embed 壳均在顶栏展示划转区块"""
return (exchange_key or "").strip().lower() in ORDER_RULE_TIPS_BY_EXCHANGE
def ui_open_guard_enabled(exchange_key: str) -> bool:
return (exchange_key or "").strip().lower() == "okx"
def ui_orphan_recovery_enabled(exchange_key: str) -> bool:
return (exchange_key or "").strip().lower() == "binance"
def path_to_embed_tab(path: str) -> str | None:
p = (path or "/").strip()
if not p.startswith("/"):
p = "/" + p
base = urlsplit(p).path.rstrip("/") or "/"
return PATH_TO_EMBED_TAB.get(base)
def embed_shell_enabled() -> bool:
return (os.getenv("HUB_EMBED_SHELL") or "1").strip().lower() in ("1", "true", "yes", "on")
def redirect_to_embed_shell_if_enabled(page: str):
"""直连 /trade 等整页路由时重定向到 embed 壳顶栏常驻tab 软切换)。"""
if not embed_shell_enabled():
return None
if (request.args.get("embed") or "").strip() == "1":
return None
if (request.path or "").rstrip("/") == "/embed":
return None
q = {k: v for k, v in request.args.items()}
q["tab"] = page
q["embed"] = "1"
return redirect("/embed?" + urlencode(q))
def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str:
"""embed=1 打开时/trade → /embed?tab=trade&embed=1"""
if not embed_shell_enabled():
split = urlsplit(path or "/")
q = dict(parse_qsl(split.query, keep_blank_values=True))
q["embed"] = "1"
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
q["hub_theme"] = ht
dest = split.path or "/"
if q:
return f"{dest}?{urlencode(q)}"
return dest + "?embed=1"
split = urlsplit(path or "/")
tab = path_to_embed_tab(split.path)
q = dict(parse_qsl(split.query, keep_blank_values=True))
if tab:
q["tab"] = tab
q["embed"] = "1"
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
q["hub_theme"] = ht
return f"/embed?{urlencode(q)}"
q["embed"] = "1"
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
q["hub_theme"] = ht
dest = split.path or "/"
if split.query:
dest += "?" + split.query
if "embed=1" not in dest:
sep = "&" if "?" in dest else "?"
dest += f"{sep}embed=1"
if ht in ("light", "dark") and "hub_theme=" not in dest:
sep = "&" if "?" in dest else "?"
dest += f"{sep}hub_theme={ht}"
return dest
def attach_embed_templates(app: Flask, repo_root: str) -> None:
embed_dir = embed_templates_dir(repo_root)
if not os.path.isdir(embed_dir):
return
existing = app.jinja_loader
loaders = [FileSystemLoader(embed_dir)]
if existing is not None:
if isinstance(existing, ChoiceLoader):
loaders = list(existing.loaders) + loaders
else:
loaders.insert(0, existing)
app.jinja_loader = ChoiceLoader(loaders)
def register_embed_routes(
app: Flask,
login_required: Callable,
render_main_page_fn: Callable,
) -> None:
from lib.instance.instance_live_push_lib import register_instance_live_routes
app.config["RENDER_MAIN_PAGE_FN"] = render_main_page_fn
register_instance_live_routes(app, login_required)
@login_required
@app.route("/embed")
def embed_shell_page():
tab = (request.args.get("tab") or "trade").strip()
if tab not in EMBED_TABS:
tab = "trade"
session["hub_embed_shell"] = True
return render_main_page_fn(tab, embed_mode="shell")
@login_required
@app.route("/api/embed/page/<tab>")
def api_embed_page(tab: str):
tab = (tab or "").strip()
if tab not in EMBED_TABS:
return jsonify({"ok": False, "msg": "unknown tab"}), 404
allowed_fn = app.config.get("INSTANCE_TAB_ALLOWED_FN")
if callable(allowed_fn) and not allowed_fn(tab):
return jsonify({"ok": False, "msg": "tab disabled"}), 403
html = render_main_page_fn(tab, embed_mode="fragment")
if isinstance(html, Response):
html = html.get_data(as_text=True)
return jsonify({"ok": True, "page": tab, "html": html})
def embed_context_extras(exchange_key: str) -> dict:
return {
"order_rule_tips_tpl": order_rule_tips_template(exchange_key),
"include_transfer_block": include_transfer_block(exchange_key),
"ui_open_guard_enabled": ui_open_guard_enabled(exchange_key),
"ui_orphan_recovery_enabled": ui_orphan_recovery_enabled(exchange_key),
}
"""中控 iframe:壳常驻 + tab 内容 API(/embed,/api/embed/page/<tab>)."""
from __future__ import annotations
from lib.paths import embed_templates_dir
import os
from typing import Callable
from urllib.parse import parse_qsl, urlencode, urlsplit
from flask import Flask, Response, jsonify, redirect, request, session
from jinja2 import ChoiceLoader, FileSystemLoader
EMBED_TABS: tuple[str, ...] = (
"key_monitor",
"trade",
"strategy",
"strategy_records",
"options",
"records",
"stats",
"risk_policy",
"env_config",
"settings",
)
PATH_TO_EMBED_TAB: dict[str, str] = {
"/": "trade",
"/trade": "trade",
"/key_monitor": "key_monitor",
"/strategy": "strategy",
"/strategy/trend": "strategy",
"/strategy/roll": "strategy",
"/strategy/records": "strategy_records",
"/options": "options",
"/records": "records",
"/stats": "stats",
"/risk_policy": "risk_policy",
"/env_config": "env_config",
"/settings": "settings",
}
ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = {
"gate": "order_monitor_rule_tips_gate.html",
"binance": "order_monitor_rule_tips_binance.html",
"okx": "order_monitor_rule_tips_okx.html",
}
def order_rule_tips_template(exchange_key: str) -> str:
ex = (exchange_key or "").strip().lower()
return ORDER_RULE_TIPS_BY_EXCHANGE.get(ex, "order_monitor_rule_tips_gate.html")
def include_transfer_block(exchange_key: str) -> bool:
"""三所 standalone / embed 壳均在顶栏展示划转区块."""
return (exchange_key or "").strip().lower() in ORDER_RULE_TIPS_BY_EXCHANGE
def ui_open_guard_enabled(exchange_key: str) -> bool:
return (exchange_key or "").strip().lower() == "okx"
def ui_orphan_recovery_enabled(exchange_key: str) -> bool:
return (exchange_key or "").strip().lower() == "binance"
def path_to_embed_tab(path: str) -> str | None:
p = (path or "/").strip()
if not p.startswith("/"):
p = "/" + p
base = urlsplit(p).path.rstrip("/") or "/"
return PATH_TO_EMBED_TAB.get(base)
def embed_shell_enabled() -> bool:
return (os.getenv("HUB_EMBED_SHELL") or "1").strip().lower() in ("1", "true", "yes", "on")
def redirect_to_embed_shell_if_enabled(page: str):
"""直连 /trade 等整页路由时,重定向到 embed 壳(顶栏常驻,tab 软切换)."""
if not embed_shell_enabled():
return None
if (request.args.get("embed") or "").strip() == "1":
return None
if (request.path or "").rstrip("/") == "/embed":
return None
q = {k: v for k, v in request.args.items()}
q["tab"] = page
q["embed"] = "1"
return redirect("/embed?" + urlencode(q))
def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str:
"""embed=1 打开时:/trade → /embed?tab=trade&embed=1"""
if not embed_shell_enabled():
split = urlsplit(path or "/")
q = dict(parse_qsl(split.query, keep_blank_values=True))
q["embed"] = "1"
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
q["hub_theme"] = ht
dest = split.path or "/"
if q:
return f"{dest}?{urlencode(q)}"
return dest + "?embed=1"
split = urlsplit(path or "/")
tab = path_to_embed_tab(split.path)
q = dict(parse_qsl(split.query, keep_blank_values=True))
if tab:
q["tab"] = tab
q["embed"] = "1"
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
q["hub_theme"] = ht
return f"/embed?{urlencode(q)}"
q["embed"] = "1"
ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
q["hub_theme"] = ht
dest = split.path or "/"
if split.query:
dest += "?" + split.query
if "embed=1" not in dest:
sep = "&" if "?" in dest else "?"
dest += f"{sep}embed=1"
if ht in ("light", "dark") and "hub_theme=" not in dest:
sep = "&" if "?" in dest else "?"
dest += f"{sep}hub_theme={ht}"
return dest
def attach_embed_templates(app: Flask, repo_root: str) -> None:
embed_dir = embed_templates_dir(repo_root)
if not os.path.isdir(embed_dir):
return
existing = app.jinja_loader
loaders = [FileSystemLoader(embed_dir)]
if existing is not None:
if isinstance(existing, ChoiceLoader):
loaders = list(existing.loaders) + loaders
else:
loaders.insert(0, existing)
app.jinja_loader = ChoiceLoader(loaders)
def register_embed_routes(
app: Flask,
login_required: Callable,
render_main_page_fn: Callable,
) -> None:
from lib.instance.instance_live_push_lib import register_instance_live_routes
app.config["RENDER_MAIN_PAGE_FN"] = render_main_page_fn
register_instance_live_routes(app, login_required)
@login_required
@app.route("/embed")
def embed_shell_page():
tab = (request.args.get("tab") or "trade").strip()
if tab not in EMBED_TABS:
tab = "trade"
session["hub_embed_shell"] = True
return render_main_page_fn(tab, embed_mode="shell")
@login_required
@app.route("/api/embed/page/<tab>")
def api_embed_page(tab: str):
tab = (tab or "").strip()
if tab not in EMBED_TABS:
return jsonify({"ok": False, "msg": "unknown tab"}), 404
allowed_fn = app.config.get("INSTANCE_TAB_ALLOWED_FN")
if callable(allowed_fn) and not allowed_fn(tab):
return jsonify({"ok": False, "msg": "tab disabled"}), 403
html = render_main_page_fn(tab, embed_mode="fragment")
if isinstance(html, Response):
html = html.get_data(as_text=True)
return jsonify({"ok": True, "page": tab, "html": html})
def embed_context_extras(exchange_key: str) -> dict:
return {
"order_rule_tips_tpl": order_rule_tips_template(exchange_key),
"include_transfer_block": include_transfer_block(exchange_key),
"ui_open_guard_enabled": ui_open_guard_enabled(exchange_key),
"ui_orphan_recovery_enabled": ui_orphan_recovery_enabled(exchange_key),
}
+5 -5
View File
@@ -1,4 +1,4 @@
"""实例页持仓未实现盈亏实时盈亏汇总"""
"""实例页:持仓未实现盈亏(实时盈亏)汇总."""
from __future__ import annotations
from collections.abc import Callable
@@ -8,7 +8,7 @@ from lib.hub.hub_position_metrics import parse_position_unrealized_pnl
def position_row_contracts(pos: dict[str, Any]) -> float:
"""持仓张数与三所 app 内 _position_row_effective_contracts 规则一致"""
"""持仓张数:与三所 app 内 _position_row_effective_contracts 规则一致."""
if not isinstance(pos, dict):
return 0.0
info = pos.get("info") or {}
@@ -67,7 +67,7 @@ def sum_unrealized_pnl_from_metrics(
rows: list[dict[str, Any]] | list[Any],
get_metrics_fn: Callable[[str, str], dict[str, Any] | None],
) -> float | None:
"""按活跃监控单逐笔拉交易所 metrics 汇总与持仓卡浮盈亏一致)。"""
"""按活跃监控单逐笔拉交易所 metrics 汇总(与持仓卡浮盈亏一致)."""
total = 0.0
found = False
for row in rows or []:
@@ -103,7 +103,7 @@ def resolve_instance_unrealized_pnl(
active_rows: list[Any] | None,
get_metrics_fn: Callable[[str, str], dict[str, Any] | None] | None,
) -> float | None:
"""先全量持仓汇总失败或无数据时回退到活跃监控单 metrics"""
"""先全量持仓汇总,失败或无数据时回退到活跃监控单 metrics."""
total = fetch_unrealized_pnl(fetch_positions_fn)
if total is not None:
return total
@@ -113,7 +113,7 @@ def resolve_instance_unrealized_pnl(
def merge_unrealized_pnl_components(*parts: float | None) -> float | None:
"""合并永续与期权等多路未实现盈亏任一路有值即参与合计)。"""
"""合并永续与期权等多路未实现盈亏(任一路有值即参与合计)."""
total = 0.0
found = False
for part in parts:
+1 -1
View File
@@ -1,4 +1,4 @@
"""实例 embed 壳后台定时 tick + SSE 通知前端拉 JSON 快照对齐中控 dashboard)。"""
"""实例 embed 壳:后台定时 tick + SSE 通知前端拉 JSON 快照(对齐中控 dashboard)."""
from __future__ import annotations
import json
+2 -2
View File
@@ -1,4 +1,4 @@
"""中控 iframe 内软导航服务端跳过重型同步避免切 tab 等待数秒"""
"""中控 iframe 内软导航:服务端跳过重型同步,避免切 tab 等待数秒."""
from __future__ import annotations
@@ -6,7 +6,7 @@ from flask import Request
def request_is_hub_soft_nav(req: Request | None = None) -> bool:
"""embed=1 且带 X-Instance-Soft-Nav 头实例页内 fetch 换页非整页刷新"""
"""embed=1 且带 X-Instance-Soft-Nav 头:实例页内 fetch 换页,非整页刷新."""
try:
from flask import request as flask_request
+1 -1
View File
@@ -1,4 +1,4 @@
"""PM2 重启当前实例仅 Linux 部署环境)。"""
"""PM2 重启当前实例(仅 Linux 部署环境)."""
from __future__ import annotations
import os
+3 -3
View File
@@ -1,4 +1,4 @@
"""实例「系统设置」页从 .env 汇总风控说明三所共用)。"""
"""实例「系统设置」页:从 .env 汇总风控说明(三所共用)."""
from __future__ import annotations
import os
@@ -82,7 +82,7 @@ def build_instance_settings_view(
_row(
"单日开仓提醒",
f"{alert_threshold}",
"达次数推送企业微信不拦单",
"达次数推送企业微信,不拦单",
),
_row(
"单日开仓硬上限",
@@ -144,7 +144,7 @@ def build_instance_settings_view(
_row("期权模块", "已启用"),
_row(
"期权 API",
f"已配置{opt_key[-4:]}" if len(opt_key) >= 4 else "未配置",
f"已配置({opt_key[-4:]})" if len(opt_key) >= 4 else "未配置",
),
_row(
"子账户",
+1 -1
View File
@@ -1,4 +1,4 @@
"""实例系统设置 API导航开关env 读写改密PM2 重启"""
"""实例系统设置 API:导航开关,env 读写,改密,PM2 重启."""
from __future__ import annotations
import os
+8 -8
View File
@@ -1,4 +1,4 @@
"""交易复盘 / 订单 K 线拼图Binance / Gate / OKX 共用)。"""
"""交易复盘 / 订单 K 线拼图(Binance / Gate / OKX 共用)."""
import math
@@ -148,11 +148,11 @@ def _to_int_ms(value):
def trade_review_fetch_window(entry_ts_ms, exit_ts_ms, timeframe, limit, anchor=None, now_ms=None):
"""
复盘 K 线窗口anchor=close):
- 有开/平仓从开仓前若干根起到平仓 K 线止覆盖整笔交易 + 入场前背景
- 仅开仓以开仓时间为终点向前 limit 根
- 仅平仓以平仓时间为终点向前 limit 根
anchor=now以当前时间为终点向前 limit 根可看平仓后走势
复盘 K 线窗口(anchor=close):
- 有开/平仓:从开仓前若干根起,到平仓 K 线止(覆盖整笔交易 + 入场前背景)
- 仅开仓:以开仓时间为终点向前 limit 根
- 仅平仓:以平仓时间为终点向前 limit 根
anchor=now:以当前时间为终点向前 limit 根(可看平仓后走势)
"""
period = timeframe_period_ms(timeframe)
lim = max(2, int(limit))
@@ -224,7 +224,7 @@ def trim_rows_for_trade_review(rows, window):
def parse_journal_chart_timeframes(tf1, tf2, fallback_tfs=None):
"""复盘表单最多两个周期去重保序"""
"""复盘表单:最多两个周期,去重保序."""
out = []
for raw in (tf1, tf2):
tf = normalize_chart_timeframe(raw)
@@ -274,7 +274,7 @@ def render_candles_subplot(
price_levels=None,
):
if not Image or not ImageDraw:
raise RuntimeError("缺少依赖Pillowpip install Pillow")
raise RuntimeError("缺少依赖:Pillow(pip install Pillow)")
img = Image.new("RGB", (width, height), bg_rgb)
draw = ImageDraw.Draw(img)
font = _load_font(14)
+208 -208
View File
@@ -1,208 +1,208 @@
"""复盘记录多周期截图上传存储与读取三所共用)。"""
from __future__ import annotations
import json
import os
import re
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
JOURNAL_UPLOAD_TFS: tuple[str, ...] = ("5m", "15m", "1h", "4h")
JOURNAL_UPLOAD_ALLOWED_EXT = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"})
_JOURNAL_DRAFT_ID_RE = re.compile(r"^[a-f0-9]{32}$")
_JOURNAL_SLOT_FILE_RE = re.compile(
r"^journal_([a-f0-9]{32})_(5m|15m|1h|4h)\.(png|jpg|jpeg|webp|gif|bmp)$",
re.I,
)
def journal_upload_field_name(tf: str) -> str:
return f"screenshot_{tf}"
def uploaded_screenshot_field_name(tf: str) -> str:
return f"uploaded_screenshot_{tf}"
def normalize_journal_draft_id(raw: Any) -> Optional[str]:
s = str(raw or "").strip().lower()
if _JOURNAL_DRAFT_ID_RE.match(s):
return s
return None
def _safe_ext(filename: str) -> str:
ext = os.path.splitext(str(filename or ""))[1].lower()
return ext if ext in JOURNAL_UPLOAD_ALLOWED_EXT else ".png"
def build_journal_slot_filename(
entry_id: str,
tf: str,
ext: str,
*,
secure_filename_fn: Callable[[str], str],
) -> str:
ext = ext if ext.startswith(".") else f".{ext}"
ext = _safe_ext(f"x{ext}")
fname = secure_filename_fn(f"journal_{entry_id}_{tf}{ext}")
return fname or ""
def is_valid_preuploaded_journal_file(filename: str, entry_id: str, tf: str) -> bool:
fn = os.path.basename(str(filename or "").strip())
if not fn or fn != str(filename or "").strip():
return False
m = _JOURNAL_SLOT_FILE_RE.match(fn)
if not m:
return False
return m.group(1) == entry_id.lower() and m.group(2) == tf
def save_journal_slot_file(
file,
entry_id: str,
tf: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> Optional[Dict[str, str]]:
if tf not in JOURNAL_UPLOAD_TFS or not entry_id or not upload_folder:
return None
if not file or not getattr(file, "filename", None):
return None
ext = _safe_ext(file.filename)
fname = build_journal_slot_filename(
entry_id, tf, ext, secure_filename_fn=secure_filename_fn
)
if not fname:
return None
os.makedirs(upload_folder, exist_ok=True)
path = os.path.join(upload_folder, fname)
file.save(path)
return {"tf": tf, "file": fname}
def collect_journal_slot_images(
form,
files,
entry_id: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> List[Dict[str, str]]:
"""优先使用即时上传 hidden 字段否则回退到表单 multipart"""
saved: List[Dict[str, str]] = []
if not entry_id or not upload_folder:
return saved
for tf in JOURNAL_UPLOAD_TFS:
pre = ""
if form is not None:
pre = str(form.get(uploaded_screenshot_field_name(tf)) or "").strip()
if pre and is_valid_preuploaded_journal_file(pre, entry_id, tf):
path = os.path.join(upload_folder, os.path.basename(pre))
if os.path.isfile(path):
saved.append({"tf": tf, "file": os.path.basename(pre)})
continue
f = files.get(journal_upload_field_name(tf)) if files else None
item = save_journal_slot_file(
f,
entry_id,
tf,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
if item:
saved.append(item)
return saved
def save_journal_slot_uploads(
files,
entry_id: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> List[Dict[str, str]]:
"""保存四槽位手动截图返回 [{"tf":"5m","file":"journal_xxx_5m.png"}, ...]"""
return collect_journal_slot_images(
None,
files,
entry_id,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]:
if not items:
return None
return json.dumps(list(items), ensure_ascii=False, separators=(",", ":"))
def parse_images_json(raw: Any) -> List[Dict[str, str]]:
if not raw:
return []
if isinstance(raw, list):
data = raw
else:
try:
data = json.loads(str(raw))
except (TypeError, ValueError, json.JSONDecodeError):
return []
if not isinstance(data, list):
return []
out: List[Dict[str, str]] = []
for item in data:
if not isinstance(item, dict):
continue
tf = str(item.get("tf") or "").strip()
file = str(item.get("file") or "").strip()
if file:
out.append({"tf": tf, "file": file})
return out
def primary_journal_image(
manual_images: Sequence[Mapping[str, str]],
*,
fallback: Optional[str] = None,
) -> Optional[str]:
if manual_images:
return str(manual_images[0].get("file") or "").strip() or None
return fallback
def enrich_journal_api_item(item: Dict[str, Any]) -> Dict[str, Any]:
"""API 输出解析 images_json兼容旧单图 image 字段"""
images = parse_images_json(item.get("images_json"))
if not images and item.get("image"):
images = [{"tf": "", "file": str(item["image"]).strip()}]
item["images"] = images
return item
def journal_image_paths(row: Any, upload_folder: str) -> List[str]:
"""删除 / AI 附图收集本条复盘所有本地图片路径去重)。"""
upload_folder = os.path.abspath(upload_folder or "")
paths: List[str] = []
seen = set()
def _add(name: Optional[str]) -> None:
if not name:
return
p = os.path.abspath(os.path.join(upload_folder, str(name).strip()))
if os.path.isfile(p) and p not in seen:
seen.add(p)
paths.append(p)
try:
keys = row.keys() if hasattr(row, "keys") else ()
except Exception:
keys = ()
if "images_json" in keys and row["images_json"]:
for img in parse_images_json(row["images_json"]):
_add(img.get("file"))
if "image" in keys:
_add(row["image"])
return paths
"""复盘记录:多周期截图上传,存储与读取(三所共用)."""
from __future__ import annotations
import json
import os
import re
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
JOURNAL_UPLOAD_TFS: tuple[str, ...] = ("5m", "15m", "1h", "4h")
JOURNAL_UPLOAD_ALLOWED_EXT = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"})
_JOURNAL_DRAFT_ID_RE = re.compile(r"^[a-f0-9]{32}$")
_JOURNAL_SLOT_FILE_RE = re.compile(
r"^journal_([a-f0-9]{32})_(5m|15m|1h|4h)\.(png|jpg|jpeg|webp|gif|bmp)$",
re.I,
)
def journal_upload_field_name(tf: str) -> str:
return f"screenshot_{tf}"
def uploaded_screenshot_field_name(tf: str) -> str:
return f"uploaded_screenshot_{tf}"
def normalize_journal_draft_id(raw: Any) -> Optional[str]:
s = str(raw or "").strip().lower()
if _JOURNAL_DRAFT_ID_RE.match(s):
return s
return None
def _safe_ext(filename: str) -> str:
ext = os.path.splitext(str(filename or ""))[1].lower()
return ext if ext in JOURNAL_UPLOAD_ALLOWED_EXT else ".png"
def build_journal_slot_filename(
entry_id: str,
tf: str,
ext: str,
*,
secure_filename_fn: Callable[[str], str],
) -> str:
ext = ext if ext.startswith(".") else f".{ext}"
ext = _safe_ext(f"x{ext}")
fname = secure_filename_fn(f"journal_{entry_id}_{tf}{ext}")
return fname or ""
def is_valid_preuploaded_journal_file(filename: str, entry_id: str, tf: str) -> bool:
fn = os.path.basename(str(filename or "").strip())
if not fn or fn != str(filename or "").strip():
return False
m = _JOURNAL_SLOT_FILE_RE.match(fn)
if not m:
return False
return m.group(1) == entry_id.lower() and m.group(2) == tf
def save_journal_slot_file(
file,
entry_id: str,
tf: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> Optional[Dict[str, str]]:
if tf not in JOURNAL_UPLOAD_TFS or not entry_id or not upload_folder:
return None
if not file or not getattr(file, "filename", None):
return None
ext = _safe_ext(file.filename)
fname = build_journal_slot_filename(
entry_id, tf, ext, secure_filename_fn=secure_filename_fn
)
if not fname:
return None
os.makedirs(upload_folder, exist_ok=True)
path = os.path.join(upload_folder, fname)
file.save(path)
return {"tf": tf, "file": fname}
def collect_journal_slot_images(
form,
files,
entry_id: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> List[Dict[str, str]]:
"""优先使用即时上传 hidden 字段;否则回退到表单 multipart."""
saved: List[Dict[str, str]] = []
if not entry_id or not upload_folder:
return saved
for tf in JOURNAL_UPLOAD_TFS:
pre = ""
if form is not None:
pre = str(form.get(uploaded_screenshot_field_name(tf)) or "").strip()
if pre and is_valid_preuploaded_journal_file(pre, entry_id, tf):
path = os.path.join(upload_folder, os.path.basename(pre))
if os.path.isfile(path):
saved.append({"tf": tf, "file": os.path.basename(pre)})
continue
f = files.get(journal_upload_field_name(tf)) if files else None
item = save_journal_slot_file(
f,
entry_id,
tf,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
if item:
saved.append(item)
return saved
def save_journal_slot_uploads(
files,
entry_id: str,
upload_folder: str,
*,
secure_filename_fn: Callable[[str], str],
) -> List[Dict[str, str]]:
"""保存四槽位手动截图,返回 [{"tf":"5m","file":"journal_xxx_5m.png"}, ...]."""
return collect_journal_slot_images(
None,
files,
entry_id,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]:
if not items:
return None
return json.dumps(list(items), ensure_ascii=False, separators=(",", ":"))
def parse_images_json(raw: Any) -> List[Dict[str, str]]:
if not raw:
return []
if isinstance(raw, list):
data = raw
else:
try:
data = json.loads(str(raw))
except (TypeError, ValueError, json.JSONDecodeError):
return []
if not isinstance(data, list):
return []
out: List[Dict[str, str]] = []
for item in data:
if not isinstance(item, dict):
continue
tf = str(item.get("tf") or "").strip()
file = str(item.get("file") or "").strip()
if file:
out.append({"tf": tf, "file": file})
return out
def primary_journal_image(
manual_images: Sequence[Mapping[str, str]],
*,
fallback: Optional[str] = None,
) -> Optional[str]:
if manual_images:
return str(manual_images[0].get("file") or "").strip() or None
return fallback
def enrich_journal_api_item(item: Dict[str, Any]) -> Dict[str, Any]:
"""API 输出:解析 images_json,兼容旧单图 image 字段."""
images = parse_images_json(item.get("images_json"))
if not images and item.get("image"):
images = [{"tf": "", "file": str(item["image"]).strip()}]
item["images"] = images
return item
def journal_image_paths(row: Any, upload_folder: str) -> List[str]:
"""删除 / AI 附图:收集本条复盘所有本地图片路径(去重)."""
upload_folder = os.path.abspath(upload_folder or "")
paths: List[str] = []
seen = set()
def _add(name: Optional[str]) -> None:
if not name:
return
p = os.path.abspath(os.path.join(upload_folder, str(name).strip()))
if os.path.isfile(p) and p not in seen:
seen.add(p)
paths.append(p)
try:
keys = row.keys() if hasattr(row, "keys") else ()
except Exception:
keys = ()
if "images_json" in keys and row["images_json"]:
for img in parse_images_json(row["images_json"]):
_add(img.get("file"))
if "image" in keys:
_add(row["image"])
return paths
+43 -43
View File
@@ -1,43 +1,43 @@
"""复盘截图即时上传 API三所共用)。"""
from __future__ import annotations
from typing import Any, Callable, Dict, Tuple
from lib.instance.journal_images_lib import (
JOURNAL_UPLOAD_TFS,
normalize_journal_draft_id,
save_journal_slot_file,
)
def handle_journal_upload_slot(
request: Any,
*,
upload_folder: str,
secure_filename_fn: Callable[[str], str],
) -> Tuple[Dict[str, Any], int]:
"""POST multipart: journal_draft_id, tf, file → {ok, file}"""
draft_id = normalize_journal_draft_id(
request.form.get("journal_draft_id") if request.form else None
)
tf = str((request.form.get("tf") if request.form else None) or "").strip()
if not draft_id:
return {"ok": False, "error": "invalid draft_id"}, 400
if tf not in JOURNAL_UPLOAD_TFS:
return {"ok": False, "error": "invalid tf"}, 400
f = request.files.get("file") if request.files else None
if not f or not getattr(f, "filename", None):
return {"ok": False, "error": "no file"}, 400
item = save_journal_slot_file(
f,
draft_id,
tf,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
if not item:
return {"ok": False, "error": "save failed"}, 500
return {"ok": True, "tf": tf, "file": item["file"]}, 200
"""复盘截图即时上传 API(三所共用)."""
from __future__ import annotations
from typing import Any, Callable, Dict, Tuple
from lib.instance.journal_images_lib import (
JOURNAL_UPLOAD_TFS,
normalize_journal_draft_id,
save_journal_slot_file,
)
def handle_journal_upload_slot(
request: Any,
*,
upload_folder: str,
secure_filename_fn: Callable[[str], str],
) -> Tuple[Dict[str, Any], int]:
"""POST multipart: journal_draft_id, tf, file → {ok, file}."""
draft_id = normalize_journal_draft_id(
request.form.get("journal_draft_id") if request.form else None
)
tf = str((request.form.get("tf") if request.form else None) or "").strip()
if not draft_id:
return {"ok": False, "error": "invalid draft_id"}, 400
if tf not in JOURNAL_UPLOAD_TFS:
return {"ok": False, "error": "invalid tf"}, 400
f = request.files.get("file") if request.files else None
if not f or not getattr(f, "filename", None):
return {"ok": False, "error": "no file"}, 400
item = save_journal_slot_file(
f,
draft_id,
tf,
upload_folder,
secure_filename_fn=secure_filename_fn,
)
if not item:
return {"ok": False, "error": "save failed"}, 500
return {"ok": True, "tf": tf, "file": item["file"]}, 200
+2 -2
View File
@@ -1,4 +1,4 @@
"""env 运行时覆盖热生效项优先读 SQLite再回退 os.environ"""
"""env 运行时覆盖:热生效项优先读 SQLite,再回退 os.environ."""
from __future__ import annotations
import os
@@ -42,7 +42,7 @@ def set_config_overrides(get_db: Callable, mapping: dict[str, str]) -> None:
def apply_env_reload(env_path: str, get_db: Callable, changed_keys: list[str], groups: list[dict]) -> dict[str, bool]:
"""写盘后同步 os.environ并将可热生效项写入 runtime 覆盖"""
"""写盘后同步 os.environ,并将可热生效项写入 runtime 覆盖."""
load_env_file_into_environ(env_path)
hot: dict[str, str] = {}
field_map = {}
+1 -1
View File
@@ -1,4 +1,4 @@
"""实例 SQLite 运行时配置导航开关env 热覆盖等)。"""
"""实例 SQLite 运行时配置(导航开关,env 热覆盖等)."""
from __future__ import annotations
import sqlite3
@@ -1,7 +1,7 @@
{# 系统设置 · 导航显示开关SSR 预渲染保存仍走 API #}
{# 系统设置 · 导航显示开关(SSR 预渲染,保存仍走 API) #}
<div class="settings-tab-inner" id="display-prefs-card">
<h2>导航显示</h2>
<p class="settings-env-hint">以下开关控制顶栏导航与系统设置内区块是否显示保存后立即生效关键位监控实盘下单系统设置为固定项</p>
<p class="settings-env-hint">以下开关控制顶栏导航与系统设置内区块是否显示,保存后立即生效.关键位监控,实盘下单,系统设置为固定项.</p>
<div id="display-prefs-form" class="display-prefs-form" data-prefs-ssr="1">
{% if display_meta %}
{% for group in display_meta %}
File diff suppressed because it is too large Load Diff
+438 -438
View File
@@ -1,438 +1,438 @@
{# Hub iframe tab fragment — shared via embed_templates #}
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
<div class="sub">{{ s.range_label }}</div>
<div class="stats-detail">
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ funds_fmt(s.worst_day_pnl) }}U{% else %}-{% endif %}</div></div>
</div>
</div>
{% endmacro %}
<div class="grid">
{% if page == 'key_monitor' %}
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">实盘下单监控</h2>
{% if focus_order_id %}
<a href="/order_focus?order_id={{ focus_order_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(100根)</a>
{% else %}
<span class="btn-del" style="background:#2f2f44;color:#9aa;cursor:not-allowed">暂无持仓可放大</span>
{% endif %}
</div>
{% include order_rule_tips_tpl %}
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
{{ trade_policy_symbol('symbol', 'order-symbol') }}
{{ trade_policy_direction('direction', 'order-direction') }}
<select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损固定盈亏比</option>
<option value="price">止盈止损价格模式</option>
<option value="pct">止盈止损百分比模式</option>
</select>
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
{{ order_entry_type_fields() }}
{% from 'order_leverage_fields.html' import order_leverage_fields with context %}
{{ order_leverage_fields() }}
{% if not intraday_discipline %}
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本关闭则仅保留初始止损与交易所挂单
</label>
<span id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</span>
{% else %}
<input type="hidden" name="breakeven_enabled" value="0">
{% endif %}
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图各周期100根含开平仓标记
</label>
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
<button type="submit">{{ open_position_button_label }}</button>
</form>
{% include 'order_plan_preview_bar.html' %}
</div>
<div class="card">
<h2 style="margin-bottom:8px">实时持仓</h2>
<div class="panel-scroll pos-list pos-list-live">
{% for o in order %}
<div class="pos-card" id="order-row-{{ o.id }}"
data-monitor-id="{{ o.id }}"
data-symbol="{{ o.symbol }}"
data-direction="{{ o.direction }}"
data-plan-sl="{% if o.stop_loss %}{{ price_fmt(o.symbol, o.stop_loss) }}{% endif %}"
data-plan-tp="{% if o.take_profit %}{{ price_fmt(o.symbol, o.take_profit) }}{% endif %}"
data-entry="{% if o.trigger_price %}{{ price_fmt(o.symbol, o.trigger_price) }}{% endif %}">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
{% if o.time_close_enabled %}
<span class="pos-symbol-time-close pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
{% endif %}
{% include 'force_close_order_badge.html' %}
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<div class="pos-head-actions">
{% if not intraday_discipline %}
<button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button>
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓继续')">平仓</a>
{% endif %}
</div>
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% elif intraday_discipline %}开仓: —{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}">
{% if intraday_discipline %}
{% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
<div class="pos-cell">
<span class="pos-label">成交价</span>
<span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止损</span>
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止盈</span>
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈亏比</span>
<span class="pos-value" id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%g'|format(o.rr_ratio) }}:1{% else %}-:1{% endif %}</span>
</div>
<div class="pos-cell">
<span class="pos-label">张数</span>
<span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈利金额</span>
<span class="pos-value pos-tp-profit" id="order-tp-profit-{{ o.id }}"></span>
</div>
<div class="pos-cell">
<span class="pos-label">标记价</span>
<span class="pos-value" id="order-price-{{ o.id }}">-</span>
</div>
<div class="pos-cell">
<span class="pos-label">浮盈亏</span>
<span class="pos-value" id="order-pnl-{{ o.id }}">-</span>
</div>
</div>
<div class="pos-footer">
<span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span>
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
<span>杠杆: {{ o.leverage or '-' }}x</span>
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
<span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span>
<span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}"></span></span>
</div>
<div class="pos-ex-orders">
<div class="pos-ex-orders-title">交易所止盈止损</div>
<div class="pos-ex-order-row">
<span class="pos-ex-order-main" id="ex-sl-text-{{ o.id }}">止损加载中…</span>
<button type="button" class="pos-ex-cancel-btn" id="ex-sl-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'sl')">撤单</button>
</div>
<div class="pos-ex-order-row">
<span class="pos-ex-order-main" id="ex-tp-text-{{ o.id }}">止盈加载中…</span>
<button type="button" class="pos-ex-cancel-btn" id="ex-tp-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'tp')">撤单</button>
</div>
</div>
</div>
{% else %}
<div class="pos-empty">暂无持仓</div>
{% endfor %}
</div>
</div>
<div id="tpsl-modal" class="tpsl-modal-backdrop" onclick="if(event.target===this)closeTpslEntrustModal()">
<div class="tpsl-modal" onclick="event.stopPropagation()">
<h3 id="tpsl-modal-title">挂止盈止损</h3>
<p style="font-size:.78rem;color:#8892b0;margin:0 0 10px">将先撤销该合约已有 TP/SL再按下列价格重挂</p>
<div class="form-row">
<select id="tpsl-modal-mode" onchange="toggleTpslModalMode()">
<option value="price">价格模式</option>
<option value="pct">百分比模式</option>
</select>
</div>
<div class="form-row">
<input id="tpsl-modal-sl" step="any" placeholder="止损价格">
<input id="tpsl-modal-tp" step="any" placeholder="止盈价格">
</div>
<div class="form-row">
<input id="tpsl-modal-sl-pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
<input id="tpsl-modal-tp-pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
</div>
<div class="tpsl-modal-actions">
<button type="button" class="tpsl-modal-cancel" onclick="closeTpslEntrustModal()">取消</button>
<button type="button" class="tpsl-modal-submit" onclick="submitTpslEntrust()">先撤后挂</button>
</div>
</div>
</div>
</div>
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
{% include 'strategy_trading_page.html' %}
{% elif page == 'strategy_records' %}
{% include 'strategy_records_page.html' %}
{% elif page == 'options' %}
{% include 'options_panel.html' %}
{% endif %}
{% if page == 'records' %}
<div class="card full records-card">
<h2>交易记录</h2>
<div class="form-row" style="margin-bottom:10px;gap:8px">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input id="review-mode-toggle" type="checkbox">
修改/核对开关开启后可编辑关键字段
</label>
</div>
<div class="table-wrap">
<table>
<tr><th>品种</th><th>类型</th><th>开仓类型</th><th>方向</th><th>成交</th><th>止损(开仓)</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
{% for r in record %}
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td>{{ r.effective_entry_reason or '-' }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ funds_fmt(r.effective_pnl_amount or 0) }}</span>{% if r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% elif r.display_pnl_source != 'reviewed' %}<span style="font-size:.68rem;color:#8892b0"></span>{% endif %}</td>
<td>
{% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
{% elif effective_result in ["止损","强制清仓","手动平仓"] %}<span class="badge loss">{{ effective_result }}</span>
{% elif effective_result == "时间平仓" %}<span class="badge miss">{{ effective_result }}</span>
{% else %}<span class="badge">{{ effective_result or '-' }}</span>{% endif %}
</td>
<td>
<button
type="button"
class="table-del"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='fillJournalFromTrade({{ {
"symbol": r.symbol,
"monitor_type": r.monitor_type,
"key_signal_type": r.key_signal_type or "",
"direction": r.direction,
"trigger_price": r.trigger_price,
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"risk_amount": r.risk_amount,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
>填入复盘</button>
<button
type="button"
class="table-del review-edit-btn"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='editTradeRecordReview({{ {
"id": r.id,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"miss_reason": r.effective_miss_reason,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
disabled
>核对修改</button>
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card full journal-card">
<h2>交易复盘记录上传含截图</h2>
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
<input type="hidden" name="exit_price_hint" id="exit-price-hint">
<input type="hidden" name="direction_hint" id="direction-hint">
{% from 'journal_form_fields.html' import journal_form_fields %}
{{ journal_form_fields(entry_reason_options) }}
{% from 'journal_upload_slots.html' import journal_upload_slots %}
{{ journal_upload_slots() }}
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="journal_exchange_chart" value="true">
保存时自动生成 K 线图并作为截图
</label>
<label style="font-size:.82rem;color:#9aa">周期1</label>
<select name="journal_chart_tf1" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">周期2</label>
<select name="journal_chart_tf2" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线数</label>
<select name="journal_chart_limit" style="min-width:72px">
{% for n in [100, 150, 200, 250, 300, 400, 500] %}
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线截止</label>
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
</select>
</div>
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:2px;margin-bottom:0">双周期上下排列截止=平仓时间开仓前背景至平仓截止=当前时间最近 N 根至此刻可看平仓后走势);标注开仓平仓与止损位</div>
<div class="mood-grid">
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
</div>
<textarea name="note" rows="2" placeholder="备注"></textarea>
<button type="submit" style="margin-top:8px">保存复盘记录</button>
</form>
</div>
<div class="card full review-card" id="review-card">
<div class="review-card-head">
<h2>AI复盘按交易记录</h2>
<button type="button" class="review-card-fs-btn" id="review-card-fs-btn" onclick="toggleReviewCardFullscreen()">全屏</button>
</div>
<div class="form-row">
<input type="date" id="day_date">
<button type="button" id="gen-daily-btn" onclick="genDaily()">生成日复盘</button>
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
<input type="date" id="week_start">
<input type="date" id="week_end">
<button type="button" id="gen-weekly-btn" onclick="genWeekly()">生成周复盘</button>
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
</div>
<div class="ai-result-wrap" id="daily_result_wrap" style="display:none">
<div id="daily_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('日复盘结果', 'daily_result')">全屏查看</button>
</div>
</div>
<div class="ai-result-wrap" id="weekly_result_wrap" style="display:none">
<div id="weekly_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('周复盘结果', 'weekly_result')">全屏查看</button>
</div>
</div>
<div class="panel-list" style="margin-top:10px">
<div class="panel-item">
<strong>交易复盘记录</strong>
<div id="journal-list"></div>
</div>
<div class="panel-item">
<strong>AI历史复盘</strong>
<div id="review-list"></div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if page == 'env_config' %}
{% include 'env_config_panel.html' %}
{% endif %}
{% if page == 'risk_policy' %}
{% include 'risk_policy_panel.html' %}
{% endif %}
{% if page == 'settings' %}
{% include 'settings_panel.html' %}
{% endif %}
{% if page == 'stats' %}
<div class="card stats-card full" id="stats-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
<h2 style="margin-bottom:0">数据统计</h2>
<button type="button" class="stats-toggle" id="stats-toggle-btn" onclick="toggleStatsCard()">折叠</button>
</div>
<div class="stats-content" id="stats-content">
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入与顶栏 UTC 列表窗无关)。历史总开仓累计):
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong>
</div>
<div class="form-row" style="margin-bottom:14px;align-items:center">
<label style="display:flex;align-items:center;gap:8px;font-size:.88rem;color:#cfd3ef">
统计品类
<select id="stats-segment-select" onchange="switchStatsSegment()" style="min-width:200px">
{% for seg in stats_bundle.segments %}
<option value="{{ seg.key }}">{{ seg.title }}</option>
{% endfor %}
</select>
</label>
</div>
{% for seg in stats_bundle.segments %}
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
{{ period_stats("日统计", seg.day) }}
{{ period_stats("周统计", seg.week) }}
{{ period_stats("月统计", seg.month) }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{# Hub iframe tab fragment — shared via embed_templates #}
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
<div class="sub">{{ s.range_label }}</div>
<div class="stats-detail">
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}</div></div>
</div>
</div>
{% endmacro %}
<div class="grid">
{% if page == 'key_monitor' %}
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">实盘下单监控</h2>
{% if focus_order_id %}
<a href="/order_focus?order_id={{ focus_order_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(100根)</a>
{% else %}
<span class="btn-del" style="background:#2f2f44;color:#9aa;cursor:not-allowed">暂无持仓可放大</span>
{% endif %}
</div>
{% include order_rule_tips_tpl %}
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
{{ trade_policy_symbol('symbol', 'order-symbol') }}
{{ trade_policy_direction('direction', 'order-direction') }}
<select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<option value="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
{{ order_entry_type_fields() }}
{% from 'order_leverage_fields.html' import order_leverage_fields with context %}
{{ order_leverage_fields() }}
{% if not intraday_discipline %}
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<span id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</span>
{% else %}
<input type="hidden" name="breakeven_enabled" value="0">
{% endif %}
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
<button type="submit">{{ open_position_button_label }}</button>
</form>
{% include 'order_plan_preview_bar.html' %}
</div>
<div class="card">
<h2 style="margin-bottom:8px">实时持仓</h2>
<div class="panel-scroll pos-list pos-list-live">
{% for o in order %}
<div class="pos-card" id="order-row-{{ o.id }}"
data-monitor-id="{{ o.id }}"
data-symbol="{{ o.symbol }}"
data-direction="{{ o.direction }}"
data-plan-sl="{% if o.stop_loss %}{{ price_fmt(o.symbol, o.stop_loss) }}{% endif %}"
data-plan-tp="{% if o.take_profit %}{{ price_fmt(o.symbol, o.take_profit) }}{% endif %}"
data-entry="{% if o.trigger_price %}{{ price_fmt(o.symbol, o.trigger_price) }}{% endif %}">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
{% if o.time_close_enabled %}
<span class="pos-symbol-time-close pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
{% endif %}
{% include 'force_close_order_badge.html' %}
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<div class="pos-head-actions">
{% if not intraday_discipline %}
<button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button>
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
{% endif %}
</div>
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% elif intraday_discipline %}开仓: —{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}">
{% if intraday_discipline %}
{% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
<div class="pos-cell">
<span class="pos-label">成交价</span>
<span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止损</span>
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止盈</span>
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈亏比</span>
<span class="pos-value" id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%g'|format(o.rr_ratio) }}:1{% else %}-:1{% endif %}</span>
</div>
<div class="pos-cell">
<span class="pos-label">张数</span>
<span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈利金额</span>
<span class="pos-value pos-tp-profit" id="order-tp-profit-{{ o.id }}"></span>
</div>
<div class="pos-cell">
<span class="pos-label">标记价</span>
<span class="pos-value" id="order-price-{{ o.id }}">-</span>
</div>
<div class="pos-cell">
<span class="pos-label">浮盈亏</span>
<span class="pos-value" id="order-pnl-{{ o.id }}">-</span>
</div>
</div>
<div class="pos-footer">
<span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span>
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
<span>杠杆: {{ o.leverage or '-' }}x</span>
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
<span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span>
<span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}"></span></span>
</div>
<div class="pos-ex-orders">
<div class="pos-ex-orders-title">交易所止盈止损</div>
<div class="pos-ex-order-row">
<span class="pos-ex-order-main" id="ex-sl-text-{{ o.id }}">止损:加载中…</span>
<button type="button" class="pos-ex-cancel-btn" id="ex-sl-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'sl')">撤单</button>
</div>
<div class="pos-ex-order-row">
<span class="pos-ex-order-main" id="ex-tp-text-{{ o.id }}">止盈:加载中…</span>
<button type="button" class="pos-ex-cancel-btn" id="ex-tp-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'tp')">撤单</button>
</div>
</div>
</div>
{% else %}
<div class="pos-empty">暂无持仓</div>
{% endfor %}
</div>
</div>
<div id="tpsl-modal" class="tpsl-modal-backdrop" onclick="if(event.target===this)closeTpslEntrustModal()">
<div class="tpsl-modal" onclick="event.stopPropagation()">
<h3 id="tpsl-modal-title">挂止盈止损</h3>
<p style="font-size:.78rem;color:#8892b0;margin:0 0 10px">将先撤销该合约已有 TP/SL,再按下列价格重挂.</p>
<div class="form-row">
<select id="tpsl-modal-mode" onchange="toggleTpslModalMode()">
<option value="price">价格模式</option>
<option value="pct">百分比模式</option>
</select>
</div>
<div class="form-row">
<input id="tpsl-modal-sl" step="any" placeholder="止损价格">
<input id="tpsl-modal-tp" step="any" placeholder="止盈价格">
</div>
<div class="form-row">
<input id="tpsl-modal-sl-pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
<input id="tpsl-modal-tp-pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
</div>
<div class="tpsl-modal-actions">
<button type="button" class="tpsl-modal-cancel" onclick="closeTpslEntrustModal()">取消</button>
<button type="button" class="tpsl-modal-submit" onclick="submitTpslEntrust()">先撤后挂</button>
</div>
</div>
</div>
</div>
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
{% include 'strategy_trading_page.html' %}
{% elif page == 'strategy_records' %}
{% include 'strategy_records_page.html' %}
{% elif page == 'options' %}
{% include 'options_panel.html' %}
{% endif %}
{% if page == 'records' %}
<div class="card full records-card">
<h2>交易记录</h2>
<div class="form-row" style="margin-bottom:10px;gap:8px">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input id="review-mode-toggle" type="checkbox">
修改/核对开关(开启后可编辑关键字段)
</label>
</div>
<div class="table-wrap">
<table>
<tr><th>品种</th><th>类型</th><th>开仓类型</th><th>方向</th><th>成交</th><th>止损(开仓)</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
{% for r in record %}
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td>{{ r.effective_entry_reason or '-' }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ funds_fmt(r.effective_pnl_amount or 0) }}</span>{% if r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% elif r.display_pnl_source != 'reviewed' %}<span style="font-size:.68rem;color:#8892b0"></span>{% endif %}</td>
<td>
{% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
{% elif effective_result in ["止损","强制清仓","手动平仓"] %}<span class="badge loss">{{ effective_result }}</span>
{% elif effective_result == "时间平仓" %}<span class="badge miss">{{ effective_result }}</span>
{% else %}<span class="badge">{{ effective_result or '-' }}</span>{% endif %}
</td>
<td>
<button
type="button"
class="table-del"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='fillJournalFromTrade({{ {
"symbol": r.symbol,
"monitor_type": r.monitor_type,
"key_signal_type": r.key_signal_type or "",
"direction": r.direction,
"trigger_price": r.trigger_price,
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"risk_amount": r.risk_amount,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
>填入复盘</button>
<button
type="button"
class="table-del review-edit-btn"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='editTradeRecordReview({{ {
"id": r.id,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"miss_reason": r.effective_miss_reason,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
disabled
>核对修改</button>
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card full journal-card">
<h2>交易复盘记录上传(含截图)</h2>
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
<input type="hidden" name="exit_price_hint" id="exit-price-hint">
<input type="hidden" name="direction_hint" id="direction-hint">
{% from 'journal_form_fields.html' import journal_form_fields %}
{{ journal_form_fields(entry_reason_options) }}
{% from 'journal_upload_slots.html' import journal_upload_slots %}
{{ journal_upload_slots() }}
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="journal_exchange_chart" value="true">
保存时自动生成 K 线图并作为截图
</label>
<label style="font-size:.82rem;color:#9aa">周期1</label>
<select name="journal_chart_tf1" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">周期2</label>
<select name="journal_chart_tf2" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线数</label>
<select name="journal_chart_limit" style="min-width:72px">
{% for n in [100, 150, 200, 250, 300, 400, 500] %}
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线截止</label>
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
</select>
</div>
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:2px;margin-bottom:0">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓,平仓与止损位</div>
<div class="mood-grid">
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
</div>
<textarea name="note" rows="2" placeholder="备注"></textarea>
<button type="submit" style="margin-top:8px">保存复盘记录</button>
</form>
</div>
<div class="card full review-card" id="review-card">
<div class="review-card-head">
<h2>AI复盘(按交易记录)</h2>
<button type="button" class="review-card-fs-btn" id="review-card-fs-btn" onclick="toggleReviewCardFullscreen()">全屏</button>
</div>
<div class="form-row">
<input type="date" id="day_date">
<button type="button" id="gen-daily-btn" onclick="genDaily()">生成日复盘</button>
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
<input type="date" id="week_start">
<input type="date" id="week_end">
<button type="button" id="gen-weekly-btn" onclick="genWeekly()">生成周复盘</button>
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
</div>
<div class="ai-result-wrap" id="daily_result_wrap" style="display:none">
<div id="daily_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('日复盘结果', 'daily_result')">全屏查看</button>
</div>
</div>
<div class="ai-result-wrap" id="weekly_result_wrap" style="display:none">
<div id="weekly_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('周复盘结果', 'weekly_result')">全屏查看</button>
</div>
</div>
<div class="panel-list" style="margin-top:10px">
<div class="panel-item">
<strong>交易复盘记录</strong>
<div id="journal-list"></div>
</div>
<div class="panel-item">
<strong>AI历史复盘</strong>
<div id="review-list"></div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if page == 'env_config' %}
{% include 'env_config_panel.html' %}
{% endif %}
{% if page == 'risk_policy' %}
{% include 'risk_policy_panel.html' %}
{% endif %}
{% if page == 'settings' %}
{% include 'settings_panel.html' %}
{% endif %}
{% if page == 'stats' %}
<div class="card stats-card full" id="stats-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
<h2 style="margin-bottom:0">数据统计</h2>
<button type="button" class="stats-toggle" id="stats-toggle-btn" onclick="toggleStatsCard()">折叠</button>
</div>
<div class="stats-content" id="stats-content">
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入(与顶栏 UTC 列表窗无关).历史总开仓(累计):
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong>
</div>
<div class="form-row" style="margin-bottom:14px;align-items:center">
<label style="display:flex;align-items:center;gap:8px;font-size:.88rem;color:#cfd3ef">
统计品类
<select id="stats-segment-select" onchange="switchStatsSegment()" style="min-width:200px">
{% for seg in stats_bundle.segments %}
<option value="{{ seg.key }}">{{ seg.title }}</option>
{% endfor %}
</select>
</label>
</div>
{% for seg in stats_bundle.segments %}
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
{{ period_stats("日统计", seg.day) }}
{{ period_stats("周统计", seg.week) }}
{{ period_stats("月统计", seg.month) }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
+4 -4
View File
@@ -1,10 +1,10 @@
{# env配置CSS Tab无需 JS+ 双列表单 #}
{# env配置:CSS Tab(无需 JS)+ 双列表单 #}
<div class="env-config-page">
<div class="env-config-head card">
<div class="env-config-head-row">
<div>
<h2>env 配置</h2>
<p class="settings-env-hint env-config-head-hint">按分类修改改完点保存含「需重启」的项请用「保存并重启」<strong>AI 配置</strong>请在中控 → 系统设置 → AI 配置统一维护</p>
<p class="settings-env-hint env-config-head-hint">按分类修改,改完点保存.含「需重启」的项请用「保存并重启」.<strong>AI 配置</strong>请在中控 → 系统设置 → AI 配置统一维护.</p>
</div>
<div class="env-config-toolbar">
<button type="button" class="btn-primary btn-sm" id="env-config-save">保存</button>
@@ -29,7 +29,7 @@
{% for group in env_config_groups %}
<section class="env-panel env-panel--{{ loop.index0 }}" role="tabpanel">
{% if group.has_restart %}
<p class="env-panel-hint">本组含需重启项修改后请点「保存并重启」</p>
<p class="env-panel-hint">本组含需重启项,修改后请点「保存并重启」.</p>
{% endif %}
<div class="env-form-grid">
{% for field in group.fields %}
@@ -56,7 +56,7 @@
id="env-f-{{ field.key }}"
type="password"
data-env-key="{{ field.key }}"
placeholder="{% if field.has_value %}修改时填写新值留空不修改{% else %}请输入{% endif %}"
placeholder="{% if field.has_value %}修改时填写新值,留空不修改{% else %}请输入{% endif %}"
autocomplete="off"
>
{% else %}
@@ -1,6 +1,6 @@
{% if force_close.enabled %}
<span class="force-close-badge" id="force-close-header-badge" role="status"
title="北京时间 {{ force_close.hour_label }} 整点未平仓将市价强制清仓result=强制清仓"
title="北京时间 {{ force_close.hour_label }} 整点未平仓将市价强制清仓(result=强制清仓)"
data-force-close-at-ms="{{ force_close.next_at_ms or '' }}"
data-force-close-active="{{ '1' if force_close.active else '0' }}">
{{ force_close.label }} 已开启 · <span class="force-close-header-cd">{{ force_close.countdown or '--:--:--' }}</span>
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,8 @@
{# 统一顶栏状态 + 筛选(上)· 统计条(下) #}
{# 统一顶栏:状态 + 筛选(上)· 统计条(下) #}
<div class="instance-header-panel card">
<div class="instance-header-toolbar">
<div class="instance-header-toolbar-filter">
<span class="list-window-label" title="列表按 UTC 时间筛选默认本月">UTC {{ list_window.label }}</span>
<span class="list-window-label" title="列表按 UTC 时间筛选,默认本月">UTC {{ list_window.label }}</span>
<label class="list-window-preset">预设
<select id="win-preset-select" onchange="toggleListWindowCustom()">
<option value="utc_this_month" {% if list_window.preset == 'utc_this_month' %}selected{% endif %}>本月</option>
@@ -26,7 +26,7 @@
<div class="instance-toolbar-status">
<div class="exchange-tag">{{ exchange_display }}</div>
{% if trade_policy.badge_text %}
<span class="trade-policy-badge" title="账户交易限制.env">{{ trade_policy.badge_text }}</span>
<span class="trade-policy-badge" title="账户交易限制(.env)">{{ trade_policy.badge_text }}</span>
{% endif %}
{% include 'force_close_header_badge.html' %}
<span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span>
@@ -1,4 +1,4 @@
{# 资金与统计条顶栏 / 系统设置共用单行展示 #}
{# 资金与统计条(顶栏 / 系统设置共用,单行展示) #}
<div class="instance-header-stats{% if options_enabled %} instance-header-stats--options{% endif %}">
<div class="stat-strip-item stat-strip-item--primary">
<div class="label">交易所</div>
@@ -16,7 +16,7 @@
<div class="label">胜率</div>
<div class="value" id="stat-rate" data-funds-field="stat-rate">{{ rate }}%</div>
</div>
<div class="stat-strip-item" title="平均盈利 ÷ 平均亏损当前列表窗口">
<div class="stat-strip-item" title="平均盈利 ÷ 平均亏损(当前列表窗口)">
<div class="label">盈亏比</div>
<div class="value" id="stat-pl-ratio" data-funds-field="stat-pl-ratio">{% if profit_loss_ratio is not none %}{{ profit_loss_ratio }}{% else %}—{% endif %}</div>
</div>
+4 -4
View File
@@ -1,15 +1,15 @@
{# 三所统一顶栏实时价 + 可选整点前开仓开关划转已移至系统设置 #}
{# 三所统一顶栏:实时价 + 可选整点前开仓开关(划转已移至系统设置) #}
<div class="rule-tip instance-price-bar">
实时价格更新<span id="price-last-updated">--</span>北京时间 UTC+8
实时价格更新:<span id="price-last-updated">--</span>(北京时间 UTC+8)
</div>
{% if ui_open_guard_enabled %}
<div class="rule-tip" id="open-guard-bar" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#cfd3ef">
<input type="checkbox" id="allow-open-before-reset" {% if not open_guard_enabled %}checked{% endif %}>
允许北京时间 {{ reset_hour }}:00 前开仓斐波成交登记人工下单
允许北京时间 {{ reset_hour }}:00 前开仓(斐波成交登记,人工下单)
</label>
<span id="open-guard-status" style="color:#8892b0;font-size:.75rem">
{% if open_guard_enabled %}已限制{{ reset_hour }}:00 前不可开仓{% else %}已放开{{ reset_hour }}:00 前允许开仓{% endif %}
{% if open_guard_enabled %}已限制:{{ reset_hour }}:00 前不可开仓{% else %}已放开:{{ reset_hour }}:00 前允许开仓{% endif %}
</span>
</div>
{% endif %}
@@ -1,12 +1,12 @@
{# 系统设置 · 资金划转三所共用 #}
{# 系统设置 · 资金划转(三所共用) #}
<div class="settings-transfer-panel">
<p class="rule-tip settings-transfer-auto">
自动划转 <strong>{{ '开启' if auto_transfer_enabled else '关闭' }}</strong>
每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong> 起该整点小时内尝试
账簿按 <strong>UTC 自然日</strong> 去重
<code>{{ auto_transfer_to }}</code> 调整至 <strong>{{ transfer_amount_fmt|default(funds_fmt(auto_transfer_amount)) }}U</strong>
不足从 <code>{{ auto_transfer_from }}</code> 划入超出划回 <code>{{ auto_transfer_from }}</code>
<strong>持仓中不划转</strong>并微信通知
自动划转 <strong>{{ '开启' if auto_transfer_enabled else '关闭' }}</strong>:
每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong> 起该整点小时内尝试;
账簿按 <strong>UTC 自然日</strong> 去重;
<code>{{ auto_transfer_to }}</code> 调整至 <strong>{{ transfer_amount_fmt|default(funds_fmt(auto_transfer_amount)) }}U</strong>:
不足从 <code>{{ auto_transfer_from }}</code> 划入,超出划回 <code>{{ auto_transfer_from }}</code>;
<strong>持仓中不划转</strong>并微信通知.
</p>
<form action="/manual_transfer" method="post" class="form-row gate-transfer-form settings-transfer-form">
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额 U" required>
+1 -1
View File
@@ -114,7 +114,7 @@
<body>
<div class="login-box">
<h2>交易监控系统登录</h2>
<p class="exchange-line">交易所<strong>{{ exchange_display }}</strong></p>
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">{{ messages[0] }}</div>
@@ -1,36 +1,72 @@
{# 趋势户两级开仓类型 → 自动 trade_style日内户假破 / 结构突破 #}
{% macro order_entry_type_fields() -%}
{% if order_entry_profile == 'trend_div' %}
<div class="order-entry-model-row">
<select id="order-entry-category" class="order-entry-category" required title="反转 / 顺势 / 波段" aria-label="开仓性质">
<option value="">性质</option>
{% for cat in entry_model_categories %}
<option value="{{ cat.key }}">{{ cat.label }}</option>
{% endfor %}
</select>
<select name="entry_model" id="order-entry-model" class="order-entry-model-sub" required disabled title="启动A/B大分歧A/B小分歧" aria-label="开仓类型">
<option value="">类型</option>
{% for cat in entry_model_categories %}
{% for opt in cat.options %}
<option value="{{ opt.code }}" data-entry-category="{{ cat.key }}" data-trade-style="{{ opt.trade_style }}"{% if opt.help %} title="{{ opt.help }}"{% endif %} hidden disabled>{{ opt.label }}</option>
{% endfor %}
{% endfor %}
</select>
{# 趋势户:两级开仓类型 → 自动 trade_style;日内户:假破 / 结构突破 #}
{% macro order_entry_type_fields() -%}
{% if order_entry_profile == 'trend_div' %}
<div class="order-entry-model-row">
<select id="order-entry-category" class="order-entry-category" required title="反转 / 顺势 / 波段" aria-label="开仓性质">
<option value="">性质</option>
{% for cat in entry_model_categories %}
<option value="{{ cat.key }}">{{ cat.label }}</option>
{% endfor %}
</select>
<select name="entry_model" id="order-entry-model" class="order-entry-model-sub" required disabled title="启动A/B,大分歧A/B,小分歧" aria-label="开仓类型">
<option value="">类型</option>
{% for cat in entry_model_categories %}
{% for opt in cat.options %}
<option value="{{ opt.code }}" data-entry-category="{{ cat.key }}" data-trade-style="{{ opt.trade_style }}"{% if opt.help %} title="{{ opt.help }}"{% endif %} hidden disabled>{{ opt.label }}</option>
{% endfor %}
{% endfor %}
</select>
<input type="hidden" name="trade_style" id="order-trade-style-hidden" value="trend">
<span id="order-trade-style-hint" class="order-trade-style-hint" title="由开仓类型自动设定">趋势单</span>
</div>
{% elif order_entry_profile == 'intraday' %}
<select name="entry_model" id="order-entry-model" class="order-entry-intraday" required title="日内开仓类型" aria-label="开仓类型">
<option value="">开仓类型</option>
{% for opt in intraday_entry_model_options %}
<option value="{{ opt.code }}"{% if opt.help %} title="{{ opt.help }}"{% endif %}>{{ opt.label }}</option>
{% endfor %}
</select>
<input type="hidden" name="trade_style" value="trend">
{% else %}
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
{% endif %}
{%- endmacro %}
@@ -1,7 +1,7 @@
{# 以损定仓杠杆按币种默认BTC/ETH 10x其它 5x),不可选手输 #}
{# 以损定仓:杠杆按币种默认(BTC/ETH 10x,其它 5x),不可选手输 #}
{% macro order_leverage_fields() -%}
{% if position_sizing_mode != 'full_margin' %}
<input type="hidden" id="order-leverage" name="leverage" value="">
<span id="order-leverage-hint" class="order-leverage-hint" title="BTC/ETH 默认10x其它默认5x">杠杆 —</span>
<span id="order-leverage-hint" class="order-leverage-hint" title="BTC/ETH 默认10x,其它默认5x">杠杆 —</span>
{% endif %}
{%- endmacro %}
@@ -1,9 +1,9 @@
{# 系统设置 · 账户密码外层 card 由 settings_panel 提供 #}
{# 系统设置 · 账户密码(外层 card 由 settings_panel 提供) #}
<h2>账户密码修改</h2>
<p class="settings-subcard-desc">修改网页登录账号密码写入 <code>.env</code> 后需重启实例生效</p>
<p class="settings-subcard-desc">修改网页登录账号密码,写入 <code>.env</code> 后需重启实例生效.</p>
<div class="settings-password-form">
<label>当前密码 <input type="password" id="pwd-old" autocomplete="current-password"></label>
<label>新用户名可选 <input type="text" id="pwd-new-username" autocomplete="username"></label>
<label>新用户名(可选) <input type="text" id="pwd-new-username" autocomplete="username"></label>
<label>新密码 <input type="password" id="pwd-new" autocomplete="new-password"></label>
<label>确认新密码 <input type="password" id="pwd-confirm" autocomplete="new-password"></label>
</div>
@@ -1,9 +1,9 @@
{# 风控说明只读展示 .env 风控参数与当前账户状态 #}
{# 风控说明:只读展示 .env 风控参数与当前账户状态 #}
<div class="risk-policy-page">
<div class="card settings-card settings-card--risk">
<h2>风控说明</h2>
<p class="settings-live-status">
当前账户状态
当前账户状态:
<span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}">
{{ instance_settings.risk_status_label }}
</span>
@@ -12,9 +12,9 @@
{% endif %}
</p>
{% if instance_settings.trade_policy_note %}
<p class="settings-policy-note">账户限制{{ instance_settings.trade_policy_note }}</p>
<p class="settings-policy-note">账户限制:{{ instance_settings.trade_policy_note }}</p>
{% endif %}
<p class="settings-env-hint">以下参数读取自本实例 <code>.env</code>修改后需重启进程生效</p>
<p class="settings-env-hint">以下参数读取自本实例 <code>.env</code>,修改后需重启进程生效.</p>
<div class="settings-risk-sections">
{% for section in instance_settings.sections %}
<div class="card settings-subcard">
+4 -4
View File
@@ -1,8 +1,8 @@
{# 系统设置CSS Tab与 env 配置同方案 #}
{# 系统设置:CSS Tab(与 env 配置同方案) #}
<div class="settings-page">
<div class="env-config-head card">
<h2>系统设置</h2>
<p class="settings-env-hint env-config-head-hint">各区块说明见 <code>docs/系统设置说明.md</code></p>
<p class="settings-env-hint env-config-head-hint">各区块说明见 <code>docs/系统设置说明.md</code>.</p>
</div>
{% if settings_tabs %}
@@ -24,7 +24,7 @@
{% include 'password_settings_panel.html' %}
{% elif tab.key == 'transfer' %}
<h2>永续资金划转</h2>
<p class="settings-subcard-desc">子账户永续资金账户与交易账户之间划转 USDT</p>
<p class="settings-subcard-desc">子账户永续:资金账户与交易账户之间划转 USDT.</p>
{% include 'instance_transfer_panel.html' %}
{% elif tab.key == 'export' %}
<h2>数据导出</h2>
@@ -32,7 +32,7 @@
<div class="settings-export-links-inline settings-export-links-block">
<a href="/export/trade_records">交易记录</a>
<a href="/export/journal_entries">复盘记录</a>
<a href="/export/key_monitors">关键位当前</a>
<a href="/export/key_monitors">关键位(当前)</a>
<a href="/export/key_monitor_history">关键位历史</a>
</div>
{% elif tab.key == 'options_swap' %}