Label dashboard positions from monitor sources with priority.
Match hedge/roll/trend/order/key monitors for source badges, show option target monitors in green, and refresh the dashboard with the board 5s cycle. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -240,6 +240,7 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
|
||||
"underlying": row.get("underlying"),
|
||||
"opt_type": opt_type,
|
||||
"target_index": target_f,
|
||||
"plan_type": "options_options",
|
||||
"managed_by": "hedge_plan",
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -175,6 +175,7 @@ def build_hub_monitor_payload(
|
||||
orders,
|
||||
trends,
|
||||
rolls,
|
||||
hedges=None,
|
||||
enrich=None,
|
||||
risk_status=None,
|
||||
) -> dict:
|
||||
@@ -185,6 +186,7 @@ def build_hub_monitor_payload(
|
||||
"orders": orders,
|
||||
"trends": trends,
|
||||
"rolls": rolls,
|
||||
"hedges": hedges if isinstance(hedges, list) else [],
|
||||
"key_prices": [],
|
||||
}
|
||||
if isinstance(risk_status, dict):
|
||||
@@ -193,6 +195,9 @@ def build_hub_monitor_payload(
|
||||
extra = enrich(keys=keys, orders=orders, trends=trends, rolls=rolls)
|
||||
if isinstance(extra, dict):
|
||||
payload.update(extra)
|
||||
# enrich 可能不返回 hedges,保留本地组装的对冲列表.
|
||||
if "hedges" not in extra:
|
||||
payload["hedges"] = hedges if isinstance(hedges, list) else []
|
||||
return payload
|
||||
|
||||
|
||||
@@ -572,6 +577,17 @@ def register_hub_routes(app):
|
||||
rolls.append(_row_to_dict(row))
|
||||
except Exception:
|
||||
pass
|
||||
hedges = []
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans
|
||||
|
||||
hedge_rows: list = []
|
||||
for st in ("opening", "active", "partial"):
|
||||
hedge_rows.extend(list_plans(conn, status=st, limit=80))
|
||||
hedge_rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
|
||||
hedges = attach_legs_to_plans(conn, hedge_rows)
|
||||
except Exception:
|
||||
hedges = []
|
||||
risk_status = None
|
||||
risk_fn = c.get("risk_status_fn")
|
||||
if callable(risk_fn):
|
||||
@@ -589,6 +605,7 @@ def register_hub_routes(app):
|
||||
orders=orders,
|
||||
trends=trends,
|
||||
rolls=rolls,
|
||||
hedges=hedges,
|
||||
enrich=enrich,
|
||||
risk_status=risk_status,
|
||||
)
|
||||
@@ -601,6 +618,7 @@ def register_hub_routes(app):
|
||||
orders=orders,
|
||||
trends=trends,
|
||||
rolls=rolls,
|
||||
hedges=hedges,
|
||||
risk_status=risk_status,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -53,6 +53,20 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
if not mon:
|
||||
# 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。
|
||||
p["target_index"] = hedge_target.get("target_index")
|
||||
try:
|
||||
from lib.instance.instance_dashboard_lib import (
|
||||
_format_options_target,
|
||||
_resolve_options_source,
|
||||
)
|
||||
|
||||
inst = str(p.get("inst_id") or "")
|
||||
source_key, source_label = _resolve_options_source(conn, inst)
|
||||
p["source"] = source_key
|
||||
p["source_label"] = source_label
|
||||
p["target_monitor_text"] = _format_options_target(p)
|
||||
except Exception:
|
||||
p.setdefault("source_label", "—")
|
||||
p.setdefault("target_monitor_text", "—")
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
|
||||
@@ -330,6 +330,8 @@ async def _run_board_aggregate() -> dict:
|
||||
await asyncio.to_thread(record_fund_snapshot_from_board, body.get("rows") or [])
|
||||
except Exception:
|
||||
pass
|
||||
# 监控聚合完成即唤醒数据看板,持仓来源与监控 5s 同步.
|
||||
dashboard_store.request_refresh()
|
||||
return {"ok": True, **body}
|
||||
except asyncio.TimeoutError:
|
||||
return {
|
||||
|
||||
@@ -973,9 +973,119 @@ def format_account_remark(ac: dict) -> str:
|
||||
return ";".join(parts)
|
||||
|
||||
|
||||
def _monitor_item_matches_position(item: dict, symbol: str, side: str) -> bool:
|
||||
o_sym = item.get("exchange_symbol") or item.get("symbol") or ""
|
||||
if not _symbols_match(symbol, o_sym):
|
||||
return False
|
||||
return (str(item.get("direction") or "").lower() == str(side or "").lower())
|
||||
|
||||
|
||||
def _order_monitor_source_label(order: dict) -> tuple[int, str]:
|
||||
"""返回 (优先级, 来源标签). 对冲=1 … 关键位=5."""
|
||||
mt = str(
|
||||
order.get("monitor_type_display")
|
||||
or order.get("monitor_type_label")
|
||||
or order.get("monitor_type")
|
||||
or ""
|
||||
).strip()
|
||||
if "顺势" in mt:
|
||||
return 2, "顺势加仓"
|
||||
if "趋势" in mt:
|
||||
return 3, "趋势回调"
|
||||
if "关键位" in mt:
|
||||
return 5, "关键位"
|
||||
return 4, "下单监控"
|
||||
|
||||
|
||||
def _hedge_matches_position(plan: dict, symbol: str, side: str) -> bool:
|
||||
"""进行中对冲计划是否覆盖该永续仓(方向 + 永续腿/标的)."""
|
||||
direction = str(plan.get("direction") or "").lower()
|
||||
if direction and direction != str(side or "").lower():
|
||||
return False
|
||||
for leg in plan.get("legs") or []:
|
||||
if not isinstance(leg, dict):
|
||||
continue
|
||||
if str(leg.get("leg_role") or "") != "perp":
|
||||
continue
|
||||
if str(leg.get("status") or "open") not in ("", "open"):
|
||||
continue
|
||||
if _symbols_match(symbol, str(leg.get("symbol") or "")):
|
||||
return True
|
||||
und = str(plan.get("underlying") or "").strip()
|
||||
if und and _symbols_match(symbol, und):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _hedge_source_label(plan: dict) -> str:
|
||||
pt = str(plan.get("plan_type") or "").strip()
|
||||
if pt == "perp_options" or str(plan.get("plan_type_label") or "") == "永期对冲":
|
||||
return "永期对冲"
|
||||
if pt == "options_options" or str(plan.get("plan_type_label") or "") == "期期对冲":
|
||||
return "期期对冲"
|
||||
return "对冲"
|
||||
|
||||
|
||||
def resolve_position_monitor_source(pos: dict, hub_mon: Optional[dict]) -> str:
|
||||
"""仓位来源:对冲 > 顺势加仓 > 趋势回调 > 下单监控 > 关键位;对不上为 —."""
|
||||
if not isinstance(hub_mon, dict) or hub_mon.get("ok") is False:
|
||||
return "—"
|
||||
sym = str(pos.get("symbol") or "")
|
||||
side = str(pos.get("side") or "")
|
||||
if not sym:
|
||||
return "—"
|
||||
candidates: list[tuple[int, str]] = []
|
||||
for h in hub_mon.get("hedges") or []:
|
||||
if isinstance(h, dict) and _hedge_matches_position(h, sym, side):
|
||||
candidates.append((1, _hedge_source_label(h)))
|
||||
for r in hub_mon.get("rolls") or []:
|
||||
if isinstance(r, dict) and _monitor_item_matches_position(r, sym, side):
|
||||
candidates.append((2, "顺势加仓"))
|
||||
for t in hub_mon.get("trends") or []:
|
||||
if isinstance(t, dict) and _monitor_item_matches_position(t, sym, side):
|
||||
candidates.append((3, "趋势回调"))
|
||||
for o in hub_mon.get("orders") or []:
|
||||
if isinstance(o, dict) and _monitor_item_matches_position(o, sym, side):
|
||||
candidates.append(_order_monitor_source_label(o))
|
||||
for k in hub_mon.get("keys") or []:
|
||||
if isinstance(k, dict) and _monitor_item_matches_position(k, sym, side):
|
||||
candidates.append((5, "关键位"))
|
||||
if not candidates:
|
||||
return "—"
|
||||
candidates.sort(key=lambda x: x[0])
|
||||
return candidates[0][1]
|
||||
|
||||
|
||||
def _options_source_label(p: dict) -> str:
|
||||
"""看板期权来源:仅对冲标期期/永期;纯期权或对不上监控显示 —."""
|
||||
source = str(p.get("source") or "").strip()
|
||||
label = str(p.get("source_label") or "").strip()
|
||||
if source == "perp_options" or label == "永期对冲":
|
||||
return "永期对冲"
|
||||
if source == "options_options" or label == "期期对冲":
|
||||
return "期期对冲"
|
||||
hedge = p.get("hedge_plan_target") if isinstance(p.get("hedge_plan_target"), dict) else None
|
||||
if hedge:
|
||||
return _hedge_source_label(hedge)
|
||||
return "—"
|
||||
|
||||
|
||||
def _options_target_monitor_text(p: dict) -> str:
|
||||
raw = p.get("target_monitor_text")
|
||||
if raw not in (None, ""):
|
||||
return str(raw)
|
||||
try:
|
||||
from lib.instance.instance_dashboard_lib import _format_options_target
|
||||
|
||||
return _format_options_target(p)
|
||||
except Exception:
|
||||
return "—"
|
||||
|
||||
|
||||
def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
||||
"""数据看板分户卡片:监控仅数量,持仓逐行(含浮盈亏与来源)."""
|
||||
"""数据看板分户卡片:监控数量 + 持仓表(来源=监控匹配)."""
|
||||
mon = ac.get("monitor_lines") or {}
|
||||
hub_mon = ac.get("hub_monitor") if isinstance(ac.get("hub_monitor"), dict) else None
|
||||
position_lines: list[dict[str, Any]] = []
|
||||
for p in _filter_open_positions(ac.get("positions") or []):
|
||||
sym = p.get("symbol") or "?"
|
||||
@@ -984,10 +1094,11 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
||||
if contracts is None:
|
||||
contracts = p.get("size")
|
||||
upnl = _position_float_pnl(p)
|
||||
source = resolve_position_monitor_source(p, hub_mon)
|
||||
position_lines.append(
|
||||
{
|
||||
"kind": "position",
|
||||
"source": "永续",
|
||||
"source": source,
|
||||
"symbol": sym,
|
||||
"side": side,
|
||||
"contracts": contracts,
|
||||
@@ -998,28 +1109,21 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
||||
opt_snap = ac.get("options_snapshot") if isinstance(ac.get("options_snapshot"), dict) else {}
|
||||
options_positions: list[dict[str, Any]] = []
|
||||
if opt_snap.get("ok") is not False and opt_snap.get("enabled") is not False:
|
||||
from lib.options.options_pricing_lib import format_options_breakeven_line
|
||||
|
||||
for p in opt_snap.get("positions") or []:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
options_positions.append(p)
|
||||
inst = p.get("inst_id") or "?"
|
||||
opt_type = (p.get("opt_type") or "").upper()
|
||||
row = dict(p)
|
||||
row["source_label"] = _options_source_label(p)
|
||||
row["target_monitor_text"] = _options_target_monitor_text(p)
|
||||
options_positions.append(row)
|
||||
inst = row.get("inst_id") or "?"
|
||||
opt_type = (row.get("opt_type") or "").upper()
|
||||
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else opt_type or "OPT"
|
||||
upl = p.get("upl")
|
||||
be_line = format_options_breakeven_line(
|
||||
expiry_be_px=p.get("expiry_be_px"),
|
||||
close_be_px=p.get("close_be_px"),
|
||||
idx_px=p.get("idx_px"),
|
||||
)
|
||||
text = f"期权 {inst} {label}"
|
||||
if be_line:
|
||||
text = f"{text} {be_line}"
|
||||
upl = row.get("upl")
|
||||
line: dict[str, Any] = {
|
||||
"kind": "options",
|
||||
"source": "期权",
|
||||
"text": text,
|
||||
"source": row.get("source_label") or "—",
|
||||
"text": f"期权 {inst} {label}",
|
||||
}
|
||||
if upl is not None:
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""中控数据看板:三户当日总览(无 AI,纯数据聚合)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Optional
|
||||
|
||||
@@ -14,7 +15,8 @@ from hub_ai.config import trading_day_reset_hour
|
||||
from lib.hub.hub_trades_lib import current_trading_day
|
||||
|
||||
LOSS_ALERT_PCT = 5.0
|
||||
DASHBOARD_POLL_INTERVAL_SEC = 60
|
||||
# 与监控区 board 默认 5s 对齐,看板持仓来源跟监控同步.
|
||||
DASHBOARD_POLL_INTERVAL_SEC = float(os.getenv("DASHBOARD_POLL_INTERVAL_SEC", "5"))
|
||||
|
||||
|
||||
def _safe_float(v: Any) -> Optional[float]:
|
||||
|
||||
@@ -373,6 +373,51 @@ body.hub-page-dashboard .page#page-dashboard {
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.dash-pos-source.is-hedge {
|
||||
color: #fbbf24;
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.dash-pos-source.is-roll {
|
||||
color: #6ee7b7;
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.dash-pos-source.is-trend {
|
||||
color: #93c5fd;
|
||||
background: rgba(59, 130, 246, 0.18);
|
||||
border: 1px solid rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
|
||||
.dash-pos-source.is-order {
|
||||
color: #c4b5fd;
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
border: 1px solid rgba(139, 92, 246, 0.35);
|
||||
}
|
||||
|
||||
.dash-pos-source.is-key {
|
||||
color: #fdba74;
|
||||
background: rgba(249, 115, 22, 0.16);
|
||||
border: 1px solid rgba(249, 115, 22, 0.4);
|
||||
}
|
||||
|
||||
.dash-pos-source.is-none {
|
||||
color: var(--dash-muted);
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
}
|
||||
|
||||
.dash-target-monitor {
|
||||
color: var(--dash-muted);
|
||||
}
|
||||
|
||||
.dash-target-monitor.is-on {
|
||||
color: #4ade80;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dash-pos-source.is-perp {
|
||||
color: #93c5fd;
|
||||
background: rgba(59, 130, 246, 0.18);
|
||||
|
||||
@@ -144,19 +144,29 @@
|
||||
return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0);
|
||||
}
|
||||
|
||||
function sourceBadgeClass(source) {
|
||||
const s = String(source || "");
|
||||
if (s.indexOf("对冲") >= 0) return "is-hedge";
|
||||
if (s.indexOf("顺势") >= 0) return "is-roll";
|
||||
if (s.indexOf("趋势") >= 0) return "is-trend";
|
||||
if (s.indexOf("关键位") >= 0) return "is-key";
|
||||
if (s.indexOf("下单") >= 0) return "is-order";
|
||||
return "is-none";
|
||||
}
|
||||
|
||||
function renderDashboardPerpTable(lines) {
|
||||
const rows = Array.isArray(lines) ? lines : [];
|
||||
if (!rows.length) return "";
|
||||
const body = rows
|
||||
.map((ln) => {
|
||||
const source = esc((ln && ln.source) || "永续");
|
||||
const source = String((ln && ln.source) || "—");
|
||||
const symbol = esc((ln && (ln.symbol || ln.text)) || "—");
|
||||
const side = esc((ln && ln.side) || "—");
|
||||
const contracts =
|
||||
ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—";
|
||||
const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN;
|
||||
return `<tr>
|
||||
<td><span class="dash-pos-source is-perp">${source}</span></td>
|
||||
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
|
||||
<td>${symbol}</td>
|
||||
<td>${side}</td>
|
||||
<td>${contracts}</td>
|
||||
@@ -188,14 +198,16 @@
|
||||
: (p.opt_type || "").toUpperCase() === "P"
|
||||
? "Put"
|
||||
: p.opt_type || "—";
|
||||
const source = String(p.source_label || p.source || "—");
|
||||
const target = String(p.target_monitor_text || "—");
|
||||
const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor";
|
||||
return `<tr>
|
||||
<td><span class="dash-pos-source is-opt">期权</span></td>
|
||||
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
|
||||
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
||||
<td>${esc(optType)}</td>
|
||||
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
||||
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
||||
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
|
||||
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
|
||||
<td><span class="${targetCls}">${esc(target)}</span></td>
|
||||
<td class="${pnlClass(p.upl)}">${p.upl != null ? pnlSigned(p.upl, 2) : "—"}</td>
|
||||
</tr>`;
|
||||
})
|
||||
@@ -205,7 +217,7 @@
|
||||
<div class="dash-table-wrap dash-options-table-wrap">
|
||||
<table class="dash-table dash-options-table">
|
||||
<thead><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>指数</th><th>目标监控</th><th>浮盈</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
@@ -360,7 +372,7 @@
|
||||
const ver = Number(data.dashboard_version) || 0;
|
||||
if (ver) localDashVersion = ver;
|
||||
renderPayload(data);
|
||||
const sec = Number(data.poll_interval_sec) || 60;
|
||||
const sec = Number(data.poll_interval_sec) || 5;
|
||||
setStatus(options.silent ? `SSE 已连接 · 后台每 ${sec}s 聚合` : `已更新 · 后台每 ${sec}s 聚合`);
|
||||
} catch (e) {
|
||||
setStatus(String(e.message || e), true);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260717-dash-pos-only" />
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260717-dash-source" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -1373,7 +1373,7 @@
|
||||
<script src="/assets/archive.js?v=20260717-archive-cal-chart"></script>
|
||||
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||
<script src="/assets/dashboard.js?v=20260717-dash-pos-only"></script>
|
||||
<script src="/assets/dashboard.js?v=20260717-dash-source"></script>
|
||||
<script src="/assets/strategy.js?v=3"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
<script src="/assets/logs.js?v=1"></script>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""数据看板仓位来源:监控匹配优先级."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "manual_trading_hub"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from hub_ai.context import ( # noqa: E402
|
||||
_options_source_label,
|
||||
resolve_position_monitor_source,
|
||||
)
|
||||
|
||||
|
||||
class TestDashboardPositionSource(unittest.TestCase):
|
||||
def test_priority_hedge_over_roll(self):
|
||||
hub = {
|
||||
"ok": True,
|
||||
"hedges": [
|
||||
{
|
||||
"plan_type": "perp_options",
|
||||
"direction": "long",
|
||||
"legs": [{"leg_role": "perp", "symbol": "ETH/USDT:USDT", "status": "open"}],
|
||||
}
|
||||
],
|
||||
"rolls": [{"symbol": "ETH/USDT:USDT", "direction": "long"}],
|
||||
}
|
||||
self.assertEqual(
|
||||
resolve_position_monitor_source({"symbol": "ETH/USDT:USDT", "side": "long"}, hub),
|
||||
"永期对冲",
|
||||
)
|
||||
|
||||
def test_unmatched_is_dash(self):
|
||||
hub = {"ok": True, "orders": [], "trends": [], "rolls": [], "keys": [], "hedges": []}
|
||||
self.assertEqual(
|
||||
resolve_position_monitor_source({"symbol": "BTC/USDT:USDT", "side": "short"}, hub),
|
||||
"—",
|
||||
)
|
||||
|
||||
def test_roll_beats_order(self):
|
||||
hub = {
|
||||
"ok": True,
|
||||
"rolls": [{"symbol": "BTC/USDT:USDT", "direction": "short"}],
|
||||
"orders": [{"symbol": "BTC/USDT:USDT", "direction": "short", "monitor_type": "下单监控"}],
|
||||
}
|
||||
self.assertEqual(
|
||||
resolve_position_monitor_source({"symbol": "BTC/USDT:USDT", "side": "short"}, hub),
|
||||
"顺势加仓",
|
||||
)
|
||||
|
||||
def test_options_plain_is_dash(self):
|
||||
self.assertEqual(_options_source_label({"source": "option", "source_label": "纯期权"}), "—")
|
||||
self.assertEqual(
|
||||
_options_source_label({"source": "options_options", "source_label": "期期对冲"}),
|
||||
"期期对冲",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -32,6 +32,7 @@ class TestHubMonitorPayload(unittest.TestCase):
|
||||
self.assertEqual(out["keys"], keys)
|
||||
self.assertEqual(out["orders"], orders)
|
||||
self.assertEqual(out["rolls"], rolls)
|
||||
self.assertEqual(out["hedges"], [])
|
||||
self.assertEqual(out["trends"][0]["add_count"], 2)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user