Add key-level auto trade, AI analysis, and trading UX improvements.
Key monitors use 5m close triggers with WeChat alerts and box/convergence auto orders; add pending-order worker, structured WeChat notify, AI settings/messages, session clock, CTP margin sizing, and dual-layer position limits. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+305
-13
@@ -17,7 +17,7 @@ from flask import flash, jsonify, redirect, render_template, request, url_for, R
|
||||
from contract_specs import calc_position_metrics, get_contract_spec
|
||||
from fee_specs import calc_fee_breakdown
|
||||
from kline_stream import sse_format
|
||||
from market_sessions import is_night_trading_session, is_trading_session
|
||||
from market_sessions import is_night_trading_session, is_trading_session, trading_session_clock
|
||||
from position_sizing import (
|
||||
MODE_AMOUNT,
|
||||
MODE_FIXED,
|
||||
@@ -25,12 +25,14 @@ from position_sizing import (
|
||||
calc_lots_by_amount,
|
||||
calc_lots_by_risk,
|
||||
calc_margin_usage_pct,
|
||||
cap_lots_for_margin_budget,
|
||||
calc_order_tick_metrics,
|
||||
normalize_sizing_mode,
|
||||
)
|
||||
from product_recommend import (
|
||||
assert_product_allowed_for_capital,
|
||||
should_apply_small_account_scope,
|
||||
small_account_margin_recommendations,
|
||||
small_account_scope_hint,
|
||||
SMALL_ACCOUNT_SCOPE_LABEL,
|
||||
)
|
||||
@@ -44,6 +46,7 @@ from ctp_settings import is_ctp_auto_connect_enabled
|
||||
from ctp_reconnect import start_ctp_reconnect_worker
|
||||
from ctp_premarket_connect import start_ctp_premarket_connect_worker
|
||||
from ctp_fee_worker import start_ctp_fee_worker
|
||||
from pending_order_worker import start_pending_order_worker
|
||||
from order_pending import (
|
||||
cancel_pending_monitor,
|
||||
pending_auto_cancel_remaining,
|
||||
@@ -71,7 +74,7 @@ from risk.account_risk_lib import (
|
||||
trading_day_label,
|
||||
)
|
||||
from strategy.strategy_db import init_strategy_tables
|
||||
from strategy.strategy_roll_lib import preview_roll
|
||||
from strategy.strategy_roll_lib import avg_entry_after_add, preview_roll
|
||||
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
|
||||
from strategy.strategy_trend_lib import (
|
||||
compute_trend_plan_futures,
|
||||
@@ -93,6 +96,7 @@ from trading_context import (
|
||||
get_pending_order_timeout_min,
|
||||
get_pending_order_timeout_sec,
|
||||
get_recommend_capital,
|
||||
get_roll_max_margin_pct,
|
||||
get_risk_percent,
|
||||
get_sizing_mode,
|
||||
get_trailing_be_tick_buffer,
|
||||
@@ -1015,7 +1019,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
margin = ctp_margin
|
||||
|
||||
codes = ths_to_codes(sym)
|
||||
tick = calc_order_tick_metrics(sym, lots, entry)
|
||||
tick = calc_order_tick_metrics(sym, lots, entry, trading_mode=mode)
|
||||
sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None
|
||||
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
|
||||
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
||||
@@ -1282,8 +1286,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
"trailing_r_locked": 0,
|
||||
}
|
||||
|
||||
def _reconcile_pending(conn, mode: str, *, capital: float = 0.0) -> None:
|
||||
reconcile_pending_orders(
|
||||
def _reconcile_pending(conn, mode: str, *, capital: float = 0.0) -> dict[str, int]:
|
||||
return reconcile_pending_orders(
|
||||
conn,
|
||||
mode,
|
||||
match_symbol_fn=_match_ctp_symbol,
|
||||
@@ -1512,6 +1516,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
"risk_status": risk,
|
||||
"trading_session": is_trading_session(),
|
||||
"night_session": is_night_trading_session(),
|
||||
"session_clock": trading_session_clock(),
|
||||
"pending_order_timeout_min": get_pending_order_timeout_min(get_setting),
|
||||
"sync_state": trading_state.sync_state,
|
||||
"sync_label": trading_state.sync_label(),
|
||||
@@ -1636,6 +1641,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
if rec_cache.get("needs_refresh"):
|
||||
_schedule_recommend_refresh()
|
||||
ctp_connected = is_ctp_connected(get_setting)
|
||||
margin_rec = small_account_margin_recommendations()
|
||||
return render_template(
|
||||
"trade.html",
|
||||
trading_mode=mode,
|
||||
@@ -1663,6 +1669,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
capital, ctp_connected=ctp_connected,
|
||||
),
|
||||
small_account_scope_hint=small_account_scope_hint(ctp_connected=ctp_connected),
|
||||
small_account_margin_rec=margin_rec if should_apply_small_account_scope(
|
||||
capital, ctp_connected=ctp_connected,
|
||||
) else None,
|
||||
session_clock=trading_session_clock(),
|
||||
roll_max_margin_pct=get_roll_max_margin_pct(get_setting),
|
||||
product_categories=PRODUCT_CATEGORIES,
|
||||
)
|
||||
finally:
|
||||
@@ -2060,11 +2071,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
lots_f = max(1, int(float(lots)))
|
||||
except (TypeError, ValueError):
|
||||
lots_f = 1
|
||||
metrics = calc_order_tick_metrics(sym, lots_f, price)
|
||||
mode = get_trading_mode(get_setting)
|
||||
metrics = calc_order_tick_metrics(sym, lots_f, price, trading_mode=mode)
|
||||
spec = get_contract_spec(sym)
|
||||
name = codes.get("name", sym) if codes else sym
|
||||
pos_long = pos_short = 0
|
||||
mode = get_trading_mode(get_setting)
|
||||
ctp_st = ctp_status(mode)
|
||||
if ctp_st.get("connected"):
|
||||
for p in _ctp_positions(mode):
|
||||
@@ -2111,10 +2122,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
conn.close()
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
margin_pct = get_max_margin_pct(get_setting)
|
||||
sizing_info = {}
|
||||
if sizing == MODE_AMOUNT:
|
||||
lots, err = calc_lots_by_amount(
|
||||
lots, err, sizing_info = calc_lots_by_amount(
|
||||
entry, sl, direction, get_fixed_amount(get_setting), sym,
|
||||
capital=capital, max_margin_pct=margin_pct,
|
||||
trading_mode=get_trading_mode(get_setting),
|
||||
)
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
@@ -2126,8 +2139,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
except (TypeError, ValueError):
|
||||
lots = 1
|
||||
metrics = calc_position_metrics(direction, entry, sl, tp, lots, entry, capital, sym)
|
||||
tick = calc_order_tick_metrics(sym, lots, entry)
|
||||
return jsonify({"ok": True, "lots": lots, "sizing_mode": sizing, "metrics": metrics, "tick": tick, "capital": capital})
|
||||
tick = calc_order_tick_metrics(
|
||||
sym, lots, entry, direction=direction, trading_mode=get_trading_mode(get_setting),
|
||||
)
|
||||
return jsonify({
|
||||
"ok": True, "lots": lots, "sizing_mode": sizing,
|
||||
"metrics": metrics, "tick": tick, "capital": capital,
|
||||
"sizing_info": sizing_info,
|
||||
})
|
||||
|
||||
@app.route("/api/trade/order", methods=["POST"])
|
||||
@login_required
|
||||
@@ -2184,9 +2203,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
if sl <= 0:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": "固定金额模式须填写止损价"}), 400
|
||||
lots_calc, err = calc_lots_by_amount(
|
||||
lots_calc, err, _sizing_info = calc_lots_by_amount(
|
||||
price, sl, direction, get_fixed_amount(get_setting), sym,
|
||||
capital=_capital(conn), max_margin_pct=get_max_margin_pct(get_setting),
|
||||
trading_mode=mode,
|
||||
)
|
||||
if err:
|
||||
conn.close()
|
||||
@@ -2201,6 +2221,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
extra_symbol=sym if offset.startswith("open") else "",
|
||||
extra_lots=lots if offset.startswith("open") else 0,
|
||||
extra_price=price if offset.startswith("open") else 0,
|
||||
extra_direction=direction if offset.startswith("open") else "long",
|
||||
trading_mode=mode,
|
||||
)
|
||||
if offset.startswith("open") and usage > margin_pct:
|
||||
conn.close()
|
||||
@@ -2313,7 +2335,52 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
|
||||
if offset.startswith("open"):
|
||||
from db_conn import DB_PATH
|
||||
from ai_worker import schedule_ai_event_analysis
|
||||
from trade_notify import notify_manual_open_filled
|
||||
|
||||
if filled:
|
||||
open_sl = float(d.get("stop_loss") or 0) if d.get("stop_loss") else None
|
||||
open_tp = None if d.get("trailing_be") else d.get("take_profit")
|
||||
if open_tp is not None:
|
||||
try:
|
||||
open_tp = float(open_tp)
|
||||
except (TypeError, ValueError):
|
||||
open_tp = None
|
||||
codes = ths_to_codes(sym) or {}
|
||||
if open_sl and open_sl > 0:
|
||||
notify_manual_open_filled(
|
||||
send_wechat=send_wechat_msg,
|
||||
get_setting=get_setting,
|
||||
mode_label=trading_mode_label(get_setting),
|
||||
sym=sym,
|
||||
symbol_name=codes.get("name") or sym,
|
||||
direction=direction,
|
||||
entry=price,
|
||||
sl=open_sl,
|
||||
tp=open_tp,
|
||||
lots=lots,
|
||||
capital=_capital(conn),
|
||||
order_id=str(result.get("order_id") or ""),
|
||||
trailing_be=bool(d.get("trailing_be")),
|
||||
be_tick_buffer=get_trailing_be_tick_buffer(get_setting),
|
||||
schedule_ai_fn=schedule_ai_event_analysis,
|
||||
db_path=DB_PATH,
|
||||
)
|
||||
else:
|
||||
send_wechat_msg(
|
||||
f"{trading_mode_label(get_setting)} 开仓 {sym} {direction} {lots}手 @{price}"
|
||||
)
|
||||
elif not filled:
|
||||
send_wechat_msg(
|
||||
f"委托已提交 · {sym} {direction} {lots}手挂单中"
|
||||
f"({get_pending_order_timeout_sec(get_setting) // 60} 分钟未成交自动撤单)"
|
||||
)
|
||||
elif not offset.startswith("open"):
|
||||
send_wechat_msg(
|
||||
f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}"
|
||||
)
|
||||
conn.close()
|
||||
_push_position_snapshot_async()
|
||||
return jsonify({
|
||||
@@ -2587,6 +2654,57 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
send_wechat_msg(f"趋势回调首仓 {sym} {plan['first_lots']}手")
|
||||
return jsonify({"ok": True, "plan_id": plan_id, "plan": plan})
|
||||
|
||||
def _apply_roll_margin_cap(
|
||||
preview: dict,
|
||||
*,
|
||||
conn,
|
||||
mode: str,
|
||||
mon: dict,
|
||||
capital: float,
|
||||
) -> tuple[dict, Optional[str]]:
|
||||
"""滚仓:风险算手数后再按滚仓保证金上限收紧。"""
|
||||
if not preview:
|
||||
return preview, "预览无效"
|
||||
sym = mon["symbol"]
|
||||
direction = (mon.get("direction") or "long").strip().lower()
|
||||
price = float(preview.get("add_price") or 0)
|
||||
qty_existing = float(mon.get("lots") or 0)
|
||||
entry_existing = float(mon.get("entry_price") or 0)
|
||||
mult = int(get_contract_spec(sym).get("mult") or 1)
|
||||
roll_pct = get_roll_max_margin_pct(get_setting)
|
||||
add_lots = int(preview.get("add_lots") or 0)
|
||||
positions = _ctp_positions(mode, refresh_if_empty=False)
|
||||
capped, usage = cap_lots_for_margin_budget(
|
||||
positions, capital, sym, direction, price, add_lots, roll_pct, trading_mode=mode,
|
||||
)
|
||||
if capped < 1:
|
||||
return preview, f"滚仓后保证金占用将超过上限 {roll_pct:g}%"
|
||||
out = dict(preview)
|
||||
if capped < add_lots:
|
||||
out["add_lots"] = capped
|
||||
out["qty_after"] = int(qty_existing + capped)
|
||||
out["avg_entry_after"] = round(
|
||||
avg_entry_after_add(qty_existing, entry_existing, capped, price), 4,
|
||||
)
|
||||
sl = float(out.get("new_stop_loss") or 0)
|
||||
tp = float(out.get("initial_take_profit") or 0)
|
||||
new_avg = float(out["avg_entry_after"])
|
||||
new_qty = float(out["qty_after"])
|
||||
if direction == "long":
|
||||
out["loss_at_sl"] = round((new_avg - sl) * new_qty * mult, 2)
|
||||
out["reward_at_tp"] = round((tp - new_avg) * new_qty * mult, 2)
|
||||
else:
|
||||
out["loss_at_sl"] = round((sl - new_avg) * new_qty * mult, 2)
|
||||
out["reward_at_tp"] = round((new_avg - tp) * new_qty * mult, 2)
|
||||
out["margin_capped"] = True
|
||||
out["margin_cap_note"] = (
|
||||
f"按滚仓保证金上限 {roll_pct:g}% 收紧:"
|
||||
f"风险算 {add_lots} 手 → 实际 {capped} 手"
|
||||
)
|
||||
out["margin_usage_pct"] = round(usage, 2)
|
||||
out["roll_max_margin_pct"] = roll_pct
|
||||
return out, None
|
||||
|
||||
@app.route("/api/strategy/roll/preview", methods=["POST"])
|
||||
@login_required
|
||||
def api_roll_preview():
|
||||
@@ -2618,6 +2736,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
)
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
preview, merr = _apply_roll_margin_cap(
|
||||
preview, conn=conn, mode=get_trading_mode(get_setting), mon=dict(mon), capital=capital,
|
||||
)
|
||||
if merr:
|
||||
return jsonify({"ok": False, "error": merr}), 400
|
||||
return jsonify({"ok": True, "preview": preview})
|
||||
|
||||
@app.route("/api/strategy/roll/execute", methods=["POST"])
|
||||
@@ -2637,6 +2760,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
sym = mon["symbol"]
|
||||
spec = get_contract_spec(sym)
|
||||
capital = _capital(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
prev, err = preview_roll(
|
||||
direction=mon["direction"],
|
||||
symbol=sym,
|
||||
@@ -2653,8 +2777,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 400
|
||||
prev, merr = _apply_roll_margin_cap(
|
||||
prev, conn=conn, mode=mode, mon=dict(mon), capital=capital,
|
||||
)
|
||||
if merr:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": merr}), 400
|
||||
price = float(prev["add_price"])
|
||||
mode = get_trading_mode(get_setting)
|
||||
try:
|
||||
execute_order(
|
||||
conn, mode=mode, offset="open", symbol=sym,
|
||||
@@ -2801,6 +2930,153 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
|
||||
app._check_trend_plans = check_trend_plans
|
||||
|
||||
def _execute_key_breakout(conn, row, bar, break_side):
|
||||
"""关键位箱体/收敛:5m 收盘突破后自动市价开仓。"""
|
||||
from key_monitor_lib import (
|
||||
calc_breakout_sl_tp,
|
||||
format_auto_breakout_msg,
|
||||
normalize_monitor_type,
|
||||
resolve_order_direction,
|
||||
)
|
||||
|
||||
sym = (row.get("symbol") or "").strip()
|
||||
bar_time = str(bar.get("time") or "")[:19]
|
||||
monitor_type = normalize_monitor_type(row.get("monitor_type") or "")
|
||||
trade_mode = row.get("trade_mode") or "顺势"
|
||||
direction = resolve_order_direction(break_side, trade_mode)
|
||||
trailing_be = int(row.get("trailing_be") or 0)
|
||||
try:
|
||||
rr = float(row.get("risk_reward") or (3 if trailing_be else 2))
|
||||
except (TypeError, ValueError):
|
||||
rr = 3.0 if trailing_be else 2.0
|
||||
if trailing_be and rr < 3:
|
||||
rr = 3.0
|
||||
|
||||
def _notify(ok: bool, detail: str, **kw):
|
||||
send_wechat_msg(format_auto_breakout_msg(
|
||||
row,
|
||||
break_side=break_side,
|
||||
direction=direction,
|
||||
entry=kw.get("entry", 0),
|
||||
sl=kw.get("sl", 0),
|
||||
tp=kw.get("tp", 0),
|
||||
lots=kw.get("lots", 0),
|
||||
bar_time=bar_time,
|
||||
ok=ok,
|
||||
detail=detail,
|
||||
))
|
||||
|
||||
try:
|
||||
init_strategy_tables(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
if not ctp_status(mode).get("connected"):
|
||||
_notify(False, "CTP 未连接")
|
||||
return False, "CTP 未连接"
|
||||
if not is_trading_session():
|
||||
_notify(False, "非交易时段")
|
||||
return False, "非交易时段"
|
||||
|
||||
try:
|
||||
entry = float(bar.get("close") or 0)
|
||||
except (TypeError, ValueError):
|
||||
_notify(False, "K 线收盘价无效")
|
||||
return False, "K 线收盘价无效"
|
||||
if entry <= 0:
|
||||
_notify(False, "K 线收盘价无效")
|
||||
return False, "K 线收盘价无效"
|
||||
|
||||
sl, tp = calc_breakout_sl_tp(
|
||||
sym=sym, direction=direction, entry=entry, bar=bar, risk_reward=rr,
|
||||
)
|
||||
err = assert_can_open(conn, active_count=_effective_active_position_count(conn, mode))
|
||||
if err:
|
||||
_notify(False, err, entry=entry, sl=sl, tp=tp, lots=0)
|
||||
return False, err
|
||||
|
||||
capital = _capital(conn)
|
||||
lots, lot_err = calc_lots_by_risk(
|
||||
entry, sl, direction, capital, get_risk_percent(get_setting), sym,
|
||||
max_margin_pct=get_max_margin_pct(get_setting), trading_mode=mode,
|
||||
)
|
||||
if lot_err or not lots:
|
||||
msg = lot_err or "手数计算失败"
|
||||
_notify(False, msg, entry=entry, sl=sl, tp=tp, lots=0)
|
||||
return False, msg
|
||||
|
||||
result = execute_order(
|
||||
conn,
|
||||
mode=mode,
|
||||
offset="open",
|
||||
symbol=sym,
|
||||
direction=direction,
|
||||
lots=lots,
|
||||
price=entry,
|
||||
settings=_settings_dict(),
|
||||
order_type="market",
|
||||
)
|
||||
open_ts = bar_time.replace("T", " ") if bar_time else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
vt_order_id = str(result.get("order_id") or "")
|
||||
mid = _upsert_open_monitor(
|
||||
conn,
|
||||
sym=sym,
|
||||
direction=direction,
|
||||
lots=lots,
|
||||
price=entry,
|
||||
sl=sl,
|
||||
tp=tp,
|
||||
trailing_be=trailing_be,
|
||||
open_time=open_ts,
|
||||
monitor_type=monitor_type,
|
||||
status="pending",
|
||||
vt_order_id=vt_order_id or None,
|
||||
order_price=entry,
|
||||
)
|
||||
_reconcile_pending(conn, mode, capital=capital)
|
||||
st_row = conn.execute(
|
||||
"SELECT status FROM trade_order_monitors WHERE id=?", (mid,),
|
||||
).fetchone()
|
||||
filled = st_row and (st_row["status"] or "").strip().lower() == "active"
|
||||
rejected = st_row and (st_row["status"] or "").strip().lower() == "closed"
|
||||
if rejected:
|
||||
conn.commit()
|
||||
_notify(False, "委托被柜台拒绝或撤销", entry=entry, sl=sl, tp=tp, lots=lots)
|
||||
return False, "委托被拒绝"
|
||||
if filled:
|
||||
_sync_monitor_from_ctp(
|
||||
conn, mid, sym, direction, mode, capital=capital,
|
||||
)
|
||||
conn.commit()
|
||||
if filled:
|
||||
from db_conn import DB_PATH
|
||||
from ai_worker import schedule_ai_event_analysis
|
||||
from trade_notify import notify_key_breakout_open
|
||||
|
||||
notify_key_breakout_open(
|
||||
send_wechat=send_wechat_msg,
|
||||
get_setting=get_setting,
|
||||
mode_label=trading_mode_label(get_setting),
|
||||
row=row,
|
||||
break_side=break_side,
|
||||
bar_time=bar_time,
|
||||
direction=direction,
|
||||
entry=entry,
|
||||
sl=sl,
|
||||
tp=tp,
|
||||
lots=lots,
|
||||
capital=capital,
|
||||
order_id=vt_order_id,
|
||||
schedule_ai_fn=schedule_ai_event_analysis,
|
||||
db_path=DB_PATH,
|
||||
)
|
||||
_push_position_snapshot_async(fast=False)
|
||||
return True, "已下单" if filled else "委托已提交"
|
||||
except Exception as exc:
|
||||
logger.warning("key breakout auto order: %s", exc)
|
||||
_notify(False, str(exc))
|
||||
return False, str(exc)
|
||||
|
||||
app._execute_key_breakout = _execute_key_breakout
|
||||
|
||||
@app.route("/settings/trading", methods=["POST"])
|
||||
@login_required
|
||||
def settings_trading_post():
|
||||
@@ -2882,3 +3158,19 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
get_setting_fn=get_setting,
|
||||
set_setting_fn=set_setting,
|
||||
)
|
||||
from ai_worker import start_ai_worker
|
||||
|
||||
start_ai_worker(
|
||||
db_path=DB_PATH,
|
||||
get_setting_fn=get_setting,
|
||||
set_setting_fn=set_setting,
|
||||
send_wechat_fn=send_wechat_msg,
|
||||
)
|
||||
start_pending_order_worker(
|
||||
db_path=DB_PATH,
|
||||
get_mode_fn=lambda: get_trading_mode(get_setting),
|
||||
init_tables_fn=_init_tables,
|
||||
get_capital_fn=_capital,
|
||||
reconcile_fn=_reconcile_pending,
|
||||
on_changed_fn=lambda: _push_position_snapshot_async(fast=False),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user