Fix stats dirty data: today filter, exclude synthetic closes, scrub frozen float PnL.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,10 +6,15 @@ import secrets
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Iterator, Optional
|
from typing import Any, Iterator, Optional
|
||||||
|
|
||||||
from app.config import Config
|
from app.config import Config
|
||||||
|
|
||||||
|
BJT = timezone(timedelta(hours=8))
|
||||||
|
# 对账/去重产生的假平仓,不计入盈亏统计
|
||||||
|
SYNTHETIC_CLOSE_EVENTS = frozenset({"reconcile_closed", "dedupe_closed"})
|
||||||
|
|
||||||
|
|
||||||
def _connect() -> sqlite3.Connection:
|
def _connect() -> sqlite3.Connection:
|
||||||
Config.DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
Config.DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -125,6 +130,50 @@ def now() -> float:
|
|||||||
return time.time()
|
return time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def day_bounds_bjt(day: Optional[str] = None) -> tuple[float, float, str]:
|
||||||
|
"""北京时间某日 [start, end);day=YYYY-MM-DD,默认今天。"""
|
||||||
|
if day:
|
||||||
|
d0 = datetime.strptime(day.strip(), "%Y-%m-%d").replace(
|
||||||
|
hour=0, minute=0, second=0, microsecond=0, tzinfo=BJT
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
d0 = datetime.now(BJT).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
d1 = d0 + timedelta(days=1)
|
||||||
|
return d0.timestamp(), d1.timestamp(), d0.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_event(snapshot_json: Any) -> str:
|
||||||
|
try:
|
||||||
|
snap = json.loads(snapshot_json or "{}")
|
||||||
|
except (TypeError, json.JSONDecodeError):
|
||||||
|
return ""
|
||||||
|
if not isinstance(snap, dict):
|
||||||
|
return ""
|
||||||
|
return str(snap.get("event") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def is_synthetic_close(snapshot_json: Any) -> bool:
|
||||||
|
return _snapshot_event(snapshot_json) in SYNTHETIC_CLOSE_EVENTS
|
||||||
|
|
||||||
|
|
||||||
|
def scrub_synthetic_trade_profits(conn: sqlite3.Connection) -> int:
|
||||||
|
"""把历史假平仓上的「冻结浮盈」清掉,避免脏数据继续进统计。"""
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, snapshot_json, profit FROM trades
|
||||||
|
WHERE close_time IS NOT NULL AND close_time != ''
|
||||||
|
AND profit IS NOT NULL
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
n = 0
|
||||||
|
for r in rows:
|
||||||
|
if not is_synthetic_close(r["snapshot_json"]):
|
||||||
|
continue
|
||||||
|
conn.execute("UPDATE trades SET profit=NULL WHERE id=?", (r["id"],))
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
def row_to_dict(row: Optional[sqlite3.Row]) -> Optional[dict]:
|
def row_to_dict(row: Optional[sqlite3.Row]) -> Optional[dict]:
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
@@ -449,19 +498,27 @@ def trade_stats(
|
|||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
account_id: Optional[int] = None,
|
account_id: Optional[int] = None,
|
||||||
machine_id: Optional[int] = None,
|
machine_id: Optional[int] = None,
|
||||||
|
day: Optional[str] = None,
|
||||||
|
period: str = "today",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""程序统计看板:盈亏/胜率/持仓时长分段/按机器与方向。"""
|
"""程序统计看板:默认北京时间「今日」真实平仓;排除对账/去重假平仓。"""
|
||||||
clause, params = _trades_where(account_id, machine_id)
|
clause, params = _trades_where(account_id, machine_id)
|
||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT t.id, t.side, t.symbol, t.profit, t.open_time, t.close_time,
|
SELECT t.id, t.side, t.symbol, t.profit, t.open_time, t.close_time,
|
||||||
t.volume, t.machine_id, t.entry_style, m.name AS machine_name
|
t.volume, t.machine_id, t.entry_style, t.snapshot_json,
|
||||||
|
m.name AS machine_name
|
||||||
FROM trades t
|
FROM trades t
|
||||||
LEFT JOIN machines m ON m.id = t.machine_id
|
LEFT JOIN machines m ON m.id = t.machine_id
|
||||||
{clause}
|
{clause}
|
||||||
ORDER BY COALESCE(t.close_time, t.open_time, t.created_at) DESC
|
ORDER BY COALESCE(t.close_time, t.open_time, t.created_at) DESC
|
||||||
"""
|
"""
|
||||||
rows = [row_to_dict(r) for r in conn.execute(sql, tuple(params)).fetchall()]
|
raw_rows = [row_to_dict(r) for r in conn.execute(sql, tuple(params)).fetchall()]
|
||||||
now_ts = now()
|
now_ts = now()
|
||||||
|
period = (period or "today").strip().lower()
|
||||||
|
day_label = ""
|
||||||
|
start_ts = end_ts = None
|
||||||
|
if period != "all":
|
||||||
|
start_ts, end_ts, day_label = day_bounds_bjt(day)
|
||||||
|
|
||||||
bucket_labels = {
|
bucket_labels = {
|
||||||
"lt_5m": "< 5 分钟",
|
"lt_5m": "< 5 分钟",
|
||||||
@@ -478,6 +535,33 @@ def trade_stats(
|
|||||||
}
|
}
|
||||||
by_style: dict[str, dict[str, Any]] = {s: _empty_group(s) for s in ENTRY_STYLES}
|
by_style: dict[str, dict[str, Any]] = {s: _empty_group(s) for s in ENTRY_STYLES}
|
||||||
|
|
||||||
|
rows: list[dict] = []
|
||||||
|
skipped_synthetic = 0
|
||||||
|
for t in raw_rows:
|
||||||
|
closed_flag = t.get("close_time") not in (None, "")
|
||||||
|
if closed_flag and is_synthetic_close(t.get("snapshot_json")):
|
||||||
|
skipped_synthetic += 1
|
||||||
|
continue
|
||||||
|
if period != "all" and start_ts is not None and end_ts is not None:
|
||||||
|
# 今日:按平仓时间;仍持仓的按开仓时间计入「持仓中」但不进合计盈亏
|
||||||
|
if closed_flag:
|
||||||
|
ct = t.get("close_time")
|
||||||
|
try:
|
||||||
|
cts = float(ct)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if cts < start_ts or cts >= end_ts:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
ot = t.get("open_time") or t.get("created_at")
|
||||||
|
try:
|
||||||
|
ots = float(ot)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if ots < start_ts or ots >= end_ts:
|
||||||
|
continue
|
||||||
|
rows.append(t)
|
||||||
|
|
||||||
total = len(rows)
|
total = len(rows)
|
||||||
closed = 0
|
closed = 0
|
||||||
open_pos = 0
|
open_pos = 0
|
||||||
@@ -488,19 +572,23 @@ def trade_stats(
|
|||||||
hold_sum = 0.0
|
hold_sum = 0.0
|
||||||
hold_n = 0
|
hold_n = 0
|
||||||
annotated = 0
|
annotated = 0
|
||||||
|
scored = 0 # 计入盈亏的已平仓笔数
|
||||||
|
|
||||||
for t in rows:
|
for t in rows:
|
||||||
profit = t.get("profit")
|
|
||||||
hold = _hold_seconds(t.get("open_time"), t.get("close_time"), now_ts)
|
|
||||||
closed_flag = t.get("close_time") not in (None, "")
|
closed_flag = t.get("close_time") not in (None, "")
|
||||||
|
hold = _hold_seconds(t.get("open_time"), t.get("close_time"), now_ts)
|
||||||
|
# 合计盈亏/胜率只统计真实已平仓;持仓浮盈不进合计
|
||||||
|
profit = t.get("profit") if closed_flag else None
|
||||||
|
|
||||||
if closed_flag:
|
if closed_flag:
|
||||||
closed += 1
|
closed += 1
|
||||||
else:
|
else:
|
||||||
open_pos += 1
|
open_pos += 1
|
||||||
|
|
||||||
if profit is not None:
|
if closed_flag and profit is not None:
|
||||||
p = float(profit)
|
p = float(profit)
|
||||||
profit_sum += p
|
profit_sum += p
|
||||||
|
scored += 1
|
||||||
if p > 1e-9:
|
if p > 1e-9:
|
||||||
wins += 1
|
wins += 1
|
||||||
gross_win += p
|
gross_win += p
|
||||||
@@ -510,7 +598,7 @@ def trade_stats(
|
|||||||
else:
|
else:
|
||||||
breakeven += 1
|
breakeven += 1
|
||||||
|
|
||||||
if hold is not None:
|
if closed_flag and hold is not None:
|
||||||
hold_sum += hold
|
hold_sum += hold
|
||||||
hold_n += 1
|
hold_n += 1
|
||||||
|
|
||||||
@@ -518,30 +606,34 @@ def trade_stats(
|
|||||||
if style:
|
if style:
|
||||||
annotated += 1
|
annotated += 1
|
||||||
|
|
||||||
|
# 分组也只累加已平仓盈亏,避免浮盈干扰分类
|
||||||
cat = entry_category(style)
|
cat = entry_category(style)
|
||||||
_accumulate(by_category[cat], profit, hold)
|
_accumulate(by_category[cat], profit, hold if closed_flag else None)
|
||||||
if style in by_style:
|
if style in by_style:
|
||||||
_accumulate(by_style[style], profit, hold)
|
_accumulate(by_style[style], profit, hold if closed_flag else None)
|
||||||
elif style:
|
elif style:
|
||||||
key = f"旧 · {style}"
|
key = f"旧 · {style}"
|
||||||
if key not in by_style:
|
if key not in by_style:
|
||||||
by_style[key] = _empty_group(key)
|
by_style[key] = _empty_group(key)
|
||||||
_accumulate(by_style[key], profit, hold)
|
_accumulate(by_style[key], profit, hold if closed_flag else None)
|
||||||
|
|
||||||
bk = _bucket_key(hold)
|
if closed_flag:
|
||||||
_accumulate(by_hold[bk], profit, hold)
|
bk = _bucket_key(hold)
|
||||||
|
_accumulate(by_hold[bk], profit, hold)
|
||||||
|
|
||||||
mid = t.get("machine_id")
|
mid = t.get("machine_id")
|
||||||
mkey = str(mid) if mid is not None else "none"
|
mkey = str(mid) if mid is not None else "none"
|
||||||
if mkey not in by_machine:
|
if mkey not in by_machine:
|
||||||
name = t.get("machine_name") or ("未绑定" if mid is None else f"#{mid}")
|
name = t.get("machine_name") or ("未绑定" if mid is None else f"#{mid}")
|
||||||
by_machine[mkey] = _empty_group(name)
|
by_machine[mkey] = _empty_group(name)
|
||||||
_accumulate(by_machine[mkey], profit, hold)
|
_accumulate(by_machine[mkey], profit, hold)
|
||||||
|
|
||||||
side = (t.get("side") or "—").lower()
|
side = (t.get("side") or "—").lower()
|
||||||
if side not in by_side:
|
if side not in by_side:
|
||||||
by_side[side] = _empty_group("买入" if side == "buy" else ("卖出" if side == "sell" else side))
|
by_side[side] = _empty_group(
|
||||||
_accumulate(by_side[side], profit, hold)
|
"买入" if side == "buy" else ("卖出" if side == "sell" else side)
|
||||||
|
)
|
||||||
|
_accumulate(by_side[side], profit, hold)
|
||||||
|
|
||||||
decided = wins + losses
|
decided = wins + losses
|
||||||
avg_hold = (hold_sum / hold_n) if hold_n else None
|
avg_hold = (hold_sum / hold_n) if hold_n else None
|
||||||
@@ -555,7 +647,7 @@ def trade_stats(
|
|||||||
"breakeven": breakeven,
|
"breakeven": breakeven,
|
||||||
"win_rate": (wins / decided * 100.0) if decided else 0.0,
|
"win_rate": (wins / decided * 100.0) if decided else 0.0,
|
||||||
"profit": profit_sum,
|
"profit": profit_sum,
|
||||||
"avg_profit": (profit_sum / total) if total else 0.0,
|
"avg_profit": (profit_sum / scored) if scored else 0.0,
|
||||||
"gross_win": gross_win,
|
"gross_win": gross_win,
|
||||||
"gross_loss": gross_loss,
|
"gross_loss": gross_loss,
|
||||||
"profit_factor": (gross_win / gross_loss) if gross_loss > 1e-9 else (None if gross_win <= 0 else 999.0),
|
"profit_factor": (gross_win / gross_loss) if gross_loss > 1e-9 else (None if gross_win <= 0 else 999.0),
|
||||||
@@ -563,6 +655,9 @@ def trade_stats(
|
|||||||
"avg_hold": _fmt_hold_secs(avg_hold),
|
"avg_hold": _fmt_hold_secs(avg_hold),
|
||||||
"annotated": annotated,
|
"annotated": annotated,
|
||||||
"unannotated": total - annotated,
|
"unannotated": total - annotated,
|
||||||
|
"skipped_synthetic": skipped_synthetic,
|
||||||
|
"period": period,
|
||||||
|
"day": day_label,
|
||||||
"by_hold": [_finalize_group(by_hold[k]) for k in ("lt_5m", "m5_30", "m30_2h", "ge_2h", "unknown") if by_hold[k]["count"] or k != "unknown"],
|
"by_hold": [_finalize_group(by_hold[k]) for k in ("lt_5m", "m5_30", "m30_2h", "ge_2h", "unknown") if by_hold[k]["count"] or k != "unknown"],
|
||||||
"by_machine": sorted(
|
"by_machine": sorted(
|
||||||
[_finalize_group(g) for g in by_machine.values()],
|
[_finalize_group(g) for g in by_machine.values()],
|
||||||
|
|||||||
+20
-8
@@ -782,7 +782,7 @@ def register_routes(app: Flask) -> None:
|
|||||||
)
|
)
|
||||||
tid = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
|
tid = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
|
||||||
|
|
||||||
# 同一账户+单号只保留一条「持仓中」,其余重复未平仓直接关掉
|
# 同一账户+单号只保留一条「持仓中」,其余重复未平仓直接关掉(不计盈亏)
|
||||||
if ticket and close_time in (None, ""):
|
if ticket and close_time in (None, ""):
|
||||||
dup_snap = json.dumps(
|
dup_snap = json.dumps(
|
||||||
{"event": "dedupe_closed", "kept_id": tid}, ensure_ascii=False
|
{"event": "dedupe_closed", "kept_id": tid}, ensure_ascii=False
|
||||||
@@ -790,7 +790,7 @@ def register_routes(app: Flask) -> None:
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE trades
|
UPDATE trades
|
||||||
SET close_time=?, snapshot_json=?
|
SET close_time=?, profit=NULL, snapshot_json=?
|
||||||
WHERE account_id=? AND ticket=? AND id!=?
|
WHERE account_id=? AND ticket=? AND id!=?
|
||||||
AND (close_time IS NULL OR close_time='')
|
AND (close_time IS NULL OR close_time='')
|
||||||
""",
|
""",
|
||||||
@@ -839,16 +839,14 @@ def register_routes(app: Flask) -> None:
|
|||||||
snap = {}
|
snap = {}
|
||||||
snap["event"] = "reconcile_closed"
|
snap["event"] = "reconcile_closed"
|
||||||
snap["reconciled_at"] = now_ts
|
snap["reconciled_at"] = now_ts
|
||||||
profit = r["profit"]
|
# 假平仓不写入冻结浮盈,避免脏数据进统计
|
||||||
if profit is None:
|
|
||||||
profit = 0.0
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE trades
|
UPDATE trades
|
||||||
SET close_time=?, profit=?, snapshot_json=?
|
SET close_time=?, profit=NULL, snapshot_json=?
|
||||||
WHERE id=?
|
WHERE id=?
|
||||||
""",
|
""",
|
||||||
(now_ts, profit, json.dumps(snap, ensure_ascii=False), r["id"]),
|
(now_ts, json.dumps(snap, ensure_ascii=False), r["id"]),
|
||||||
)
|
)
|
||||||
closed_n += 1
|
closed_n += 1
|
||||||
return jsonify({"ok": True, "closed": closed_n})
|
return jsonify({"ok": True, "closed": closed_n})
|
||||||
@@ -927,15 +925,29 @@ def register_routes(app: Flask) -> None:
|
|||||||
def records_stats_page():
|
def records_stats_page():
|
||||||
u = current_user()
|
u = current_user()
|
||||||
machine_raw = (request.args.get("machine_id") or "").strip()
|
machine_raw = (request.args.get("machine_id") or "").strip()
|
||||||
|
period = (request.args.get("period") or "today").strip().lower()
|
||||||
|
if period not in ("today", "all"):
|
||||||
|
period = "today"
|
||||||
|
day = (request.args.get("day") or "").strip() or None
|
||||||
account_id = None if u["role"] == "admin" else u.get("account_id")
|
account_id = None if u["role"] == "admin" else u.get("account_id")
|
||||||
machines, machine_id = _resolve_machine_filter(u, machine_raw)
|
machines, machine_id = _resolve_machine_filter(u, machine_raw)
|
||||||
with database.get_db() as conn:
|
with database.get_db() as conn:
|
||||||
stats = database.trade_stats(conn, account_id=account_id, machine_id=machine_id)
|
# 清掉历史假平仓上误写的盈亏
|
||||||
|
database.scrub_synthetic_trade_profits(conn)
|
||||||
|
stats = database.trade_stats(
|
||||||
|
conn,
|
||||||
|
account_id=account_id,
|
||||||
|
machine_id=machine_id,
|
||||||
|
day=day,
|
||||||
|
period=period,
|
||||||
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"records_stats.html",
|
"records_stats.html",
|
||||||
stats=stats,
|
stats=stats,
|
||||||
machines=machines,
|
machines=machines,
|
||||||
selected_machine_id=machine_id,
|
selected_machine_id=machine_id,
|
||||||
|
selected_period=period,
|
||||||
|
selected_day=stats.get("day") or day or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/records/<int:tid>")
|
@app.get("/records/<int:tid>")
|
||||||
|
|||||||
@@ -2,15 +2,26 @@
|
|||||||
{% block title %}统计看板 · {{ app_name }}{% endblock %}
|
{% block title %}统计看板 · {{ app_name }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 style="margin:0 0 6px">统计看板</h2>
|
<h2 style="margin:0 0 6px">统计看板</h2>
|
||||||
<p class="muted" style="margin:0 0 10px">程序汇总:顺势/反转 × 三种结构手法,其他=乱作。</p>
|
<p class="muted" style="margin:0 0 10px">
|
||||||
|
默认按北京时间<strong>今日真实平仓</strong>统计;对账/去重产生的假平仓已排除。
|
||||||
|
{% if selected_period == 'today' and stats.day %}当前:{{ stats.day }}{% endif %}
|
||||||
|
{% if stats.skipped_synthetic %} · 已排除假平仓 {{ stats.skipped_synthetic }} 笔{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
||||||
<a class="btn secondary" href="{{ url_for('records_page', machine_id=selected_machine_id) if selected_machine_id else url_for('records_page') }}">交易列表</a>
|
<a class="btn secondary" href="{{ url_for('records_page', machine_id=selected_machine_id) if selected_machine_id else url_for('records_page') }}">交易列表</a>
|
||||||
<a class="btn" href="{{ url_for('records_stats_page', machine_id=selected_machine_id) if selected_machine_id else url_for('records_stats_page') }}">统计看板</a>
|
<a class="btn" href="{{ url_for('records_stats_page', machine_id=selected_machine_id, period=selected_period, day=selected_day) if selected_machine_id else url_for('records_stats_page', period=selected_period, day=selected_day) }}">统计看板</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:14px">
|
<div class="card" style="margin-bottom:14px">
|
||||||
<form method="get" action="{{ url_for('records_stats_page') }}" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
<form method="get" action="{{ url_for('records_stats_page') }}" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||||
|
<label class="muted" for="period" style="margin:0">范围</label>
|
||||||
|
<select name="period" id="period" style="min-width:120px;width:auto">
|
||||||
|
<option value="today" {% if selected_period=='today' %}selected{% endif %}>今日</option>
|
||||||
|
<option value="all" {% if selected_period=='all' %}selected{% endif %}>全部</option>
|
||||||
|
</select>
|
||||||
|
<label class="muted" for="day" style="margin:0">日期</label>
|
||||||
|
<input type="date" name="day" id="day" value="{{ selected_day or '' }}" style="width:auto">
|
||||||
<label class="muted" for="machine_id" style="margin:0">机器</label>
|
<label class="muted" for="machine_id" style="margin:0">机器</label>
|
||||||
<select name="machine_id" id="machine_id" style="min-width:200px;width:auto">
|
<select name="machine_id" id="machine_id" style="min-width:200px;width:auto">
|
||||||
<option value="">全部机器</option>
|
<option value="">全部机器</option>
|
||||||
@@ -21,9 +32,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="btn">筛选</button>
|
<button type="submit" class="btn">筛选</button>
|
||||||
{% if selected_machine_id %}
|
<a class="btn secondary" href="{{ url_for('records_stats_page') }}">今日重置</a>
|
||||||
<a class="btn secondary" href="{{ url_for('records_stats_page') }}">清除</a>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -39,7 +48,7 @@
|
|||||||
<div class="muted" style="margin-top:6px">盈 {{ stats.wins }} · 亏 {{ stats.losses }} · 平 {{ stats.breakeven }}</div>
|
<div class="muted" style="margin-top:6px">盈 {{ stats.wins }} · 亏 {{ stats.losses }} · 平 {{ stats.breakeven }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="muted">合计盈亏</div>
|
<div class="muted">合计盈亏{% if selected_period=='today' %}(今日已平){% endif %}</div>
|
||||||
<div class="stat" style="color:{% if stats.profit>0 %}var(--green){% elif stats.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
<div class="stat" style="color:{% if stats.profit>0 %}var(--green){% elif stats.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
||||||
{{ '%.2f'|format(stats.profit) }}
|
{{ '%.2f'|format(stats.profit) }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user