fix: Gate hub close sync and trade record open stop-loss snapshot
Sync order monitors from Gate position history after hub flat close. Store and display initial_stop_loss in trade records instead of post-amend exchange stops. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1993,13 +1993,12 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
|
item["display_open_stop_loss"] = open_stop
|
||||||
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
||||||
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
||||||
open_stop = item.get("initial_stop_loss")
|
|
||||||
if open_stop in (None, ""):
|
|
||||||
open_stop = base_stop
|
|
||||||
item["display_open_stop_loss"] = open_stop
|
|
||||||
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
||||||
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
||||||
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
||||||
@@ -2542,7 +2541,9 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
snap_sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
er = (
|
er = (
|
||||||
(entry_reason or "").strip()
|
(entry_reason or "").strip()
|
||||||
or entry_reason_from_key_signal(kst)
|
or entry_reason_from_key_signal(kst)
|
||||||
|
|||||||
+130
-7
@@ -1953,13 +1953,12 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
|
item["display_open_stop_loss"] = open_stop
|
||||||
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
||||||
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
||||||
open_stop = item.get("initial_stop_loss")
|
|
||||||
if open_stop in (None, ""):
|
|
||||||
open_stop = base_stop
|
|
||||||
item["display_open_stop_loss"] = open_stop
|
|
||||||
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
||||||
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
||||||
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
||||||
@@ -2259,7 +2258,9 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
snap_sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
er = (
|
er = (
|
||||||
(entry_reason or "").strip()
|
(entry_reason or "").strip()
|
||||||
or entry_reason_from_key_signal(kst)
|
or entry_reason_from_key_signal(kst)
|
||||||
@@ -3710,8 +3711,31 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
if hit is None and since_ms:
|
if hit is None and since_ms:
|
||||||
trades = exchange.fetch_my_trades(exchange_symbol, since=None, limit=100)
|
trades = exchange.fetch_my_trades(exchange_symbol, since=None, limit=100)
|
||||||
hit = pick_from_trades(trades)
|
hit = pick_from_trades(trades)
|
||||||
|
if hit is not None:
|
||||||
return hit
|
return hit
|
||||||
except Exception:
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from gate_position_history_lib import pick_gate_position_close
|
||||||
|
|
||||||
|
pos = pick_gate_position_close(
|
||||||
|
fetch_gate_positions_close_history(),
|
||||||
|
exchange_symbol,
|
||||||
|
direction,
|
||||||
|
opened_at_ms=since_ms,
|
||||||
|
)
|
||||||
|
if pos:
|
||||||
|
return {
|
||||||
|
"price": None,
|
||||||
|
"timestamp": pos["close_ms"],
|
||||||
|
"side": close_side,
|
||||||
|
"_from_position_history": True,
|
||||||
|
"_realized_pnl": pos.get("pnl"),
|
||||||
|
"_sync_key": pos.get("sync_key"),
|
||||||
|
"_open_ms": pos.get("open_ms"),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -3819,7 +3843,7 @@ def calc_weighted_exit_price(trades):
|
|||||||
return weighted_sum / total_amount
|
return weighted_sum / total_amount
|
||||||
|
|
||||||
|
|
||||||
def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_manual=False):
|
||||||
"""
|
"""
|
||||||
交易所已无仓、本地仍为 active 时,推断平仓类型/时间/盈亏。
|
交易所已无仓、本地仍为 active 时,推断平仓类型/时间/盈亏。
|
||||||
返回 (result, pnl_amount, closed_at_str, miss_reason)。
|
返回 (result, pnl_amount, closed_at_str, miss_reason)。
|
||||||
@@ -3844,6 +3868,12 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
|||||||
ts = trade.get("timestamp")
|
ts = trade.get("timestamp")
|
||||||
if ts:
|
if ts:
|
||||||
closed_at_str = ms_to_app_local_str(int(ts))
|
closed_at_str = ms_to_app_local_str(int(ts))
|
||||||
|
if trade.get("_from_position_history"):
|
||||||
|
pnl_hist = trade.get("_realized_pnl")
|
||||||
|
if pnl_hist is not None:
|
||||||
|
note = "中控平仓后按 Gate 平仓历史同步盈亏" if prefer_manual else "按 Gate 平仓历史同步盈亏"
|
||||||
|
res = "手动平仓" if prefer_manual else "外部平仓"
|
||||||
|
return (res, float(pnl_hist), closed_at_str, note)
|
||||||
|
|
||||||
if exit_px is None or exit_px <= 0:
|
if exit_px is None or exit_px <= 0:
|
||||||
p = get_price(sym)
|
p = get_price(sym)
|
||||||
@@ -3866,6 +3896,13 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
|||||||
|
|
||||||
result = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_px)
|
result = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_px)
|
||||||
pnl = calc_pnl(direction, trigger_price, exit_px, margin_capital, leverage)
|
pnl = calc_pnl(direction, trigger_price, exit_px, margin_capital, leverage)
|
||||||
|
if prefer_manual:
|
||||||
|
return (
|
||||||
|
"手动平仓",
|
||||||
|
pnl,
|
||||||
|
closed_at_str,
|
||||||
|
"中控平仓后按交易所成交记录同步",
|
||||||
|
)
|
||||||
if result:
|
if result:
|
||||||
return (
|
return (
|
||||||
result,
|
result,
|
||||||
@@ -3881,6 +3918,91 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_hub_external_close(conn, symbol, direction):
|
||||||
|
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
|
||||||
|
if not exchange_private_api_configured():
|
||||||
|
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
|
||||||
|
from gate_position_history_lib import unified_symbol_for_match
|
||||||
|
|
||||||
|
sym_u = unified_symbol_for_match(symbol)
|
||||||
|
dir_l = (direction or "").strip().lower()
|
||||||
|
if dir_l not in ("long", "short"):
|
||||||
|
return {"ok": False, "msg": "side 须为 long 或 short", "synced": 0}
|
||||||
|
synced = 0
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM order_monitors WHERE status IN ('active', 'error')"
|
||||||
|
).fetchall()
|
||||||
|
for r in rows:
|
||||||
|
if unified_symbol_for_match(r["symbol"]) != sym_u:
|
||||||
|
continue
|
||||||
|
if (r["direction"] or "").strip().lower() != dir_l:
|
||||||
|
continue
|
||||||
|
oid = int(r["id"])
|
||||||
|
if r["status"] == "error":
|
||||||
|
opened_at_chk = get_opened_at_value(r)
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM trade_records WHERE symbol=? AND opened_at=? AND monitor_type=? LIMIT 1",
|
||||||
|
(r["symbol"], opened_at_chk, order_row_monitor_type(r)),
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (oid,))
|
||||||
|
synced += 1
|
||||||
|
continue
|
||||||
|
exchange_symbol = resolve_monitor_exchange_symbol(r)
|
||||||
|
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
||||||
|
if live_contracts is None:
|
||||||
|
continue
|
||||||
|
if live_contracts > 0:
|
||||||
|
time.sleep(0.6)
|
||||||
|
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
||||||
|
if live_contracts is None or live_contracts > 0:
|
||||||
|
continue
|
||||||
|
global _RECONCILE_FLAT_STREAK
|
||||||
|
_RECONCILE_FLAT_STREAK.pop(oid, None)
|
||||||
|
cancel_gate_swap_trigger_orders(exchange_symbol)
|
||||||
|
opened_at = get_opened_at_value(r)
|
||||||
|
opened_at_ms = _to_ms_with_fallback(r["opened_at_ms"] if "opened_at_ms" in r.keys() else None, opened_at)
|
||||||
|
result, pnl_amount, closed_at, miss_reason = resolve_synced_flat_close(
|
||||||
|
r, opened_at, opened_at_ms=opened_at_ms, prefer_manual=True
|
||||||
|
)
|
||||||
|
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
|
||||||
|
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
|
||||||
|
session_date = r["session_date"] or get_trading_day(closed_at_dt)
|
||||||
|
update_session_capital(conn, session_date, pnl_amount)
|
||||||
|
insert_trade_record(
|
||||||
|
conn,
|
||||||
|
symbol=r["symbol"],
|
||||||
|
monitor_type=trade_record_monitor_type(conn, r),
|
||||||
|
trend_plan_id=trend_plan_id_from_monitor_row(r),
|
||||||
|
key_signal_type=order_row_key_signal_type(r),
|
||||||
|
direction=r["direction"],
|
||||||
|
trigger_price=r["trigger_price"],
|
||||||
|
stop_loss=r["stop_loss"],
|
||||||
|
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
|
||||||
|
take_profit=r["take_profit"],
|
||||||
|
margin_capital=margin_capital_for_trade_record(r),
|
||||||
|
leverage=r["leverage"],
|
||||||
|
pnl_amount=pnl_amount,
|
||||||
|
hold_seconds=hold_seconds,
|
||||||
|
trade_style=r["trade_style"],
|
||||||
|
risk_amount=r["risk_amount"],
|
||||||
|
planned_rr=calc_rr_ratio(r["direction"], r["trigger_price"], r["initial_stop_loss"] or r["stop_loss"], r["take_profit"]),
|
||||||
|
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
|
||||||
|
result=result,
|
||||||
|
miss_reason=handoff_trade_miss_reason(miss_reason, r),
|
||||||
|
opened_at=opened_at,
|
||||||
|
closed_at=closed_at,
|
||||||
|
)
|
||||||
|
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
|
||||||
|
clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day())
|
||||||
|
synced += 1
|
||||||
|
try:
|
||||||
|
sync_trade_records_from_exchange(conn, force=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True, "synced": synced}
|
||||||
|
|
||||||
|
|
||||||
def reconcile_external_closes(conn, days=None):
|
def reconcile_external_closes(conn, days=None):
|
||||||
global _RECONCILE_FLAT_STREAK
|
global _RECONCILE_FLAT_STREAK
|
||||||
if not exchange_private_api_configured():
|
if not exchange_private_api_configured():
|
||||||
@@ -8482,6 +8604,7 @@ try:
|
|||||||
views={"add_order": add_order, "add_key": add_key},
|
views={"add_order": add_order, "add_key": add_key},
|
||||||
ohlcv_fn=_hub_fetch_ohlcv,
|
ohlcv_fn=_hub_fetch_ohlcv,
|
||||||
volume_rank_fn=_hub_fetch_volume_rank,
|
volume_rank_fn=_hub_fetch_volume_rank,
|
||||||
|
reconcile_hub_flat_fn=reconcile_hub_external_close,
|
||||||
)
|
)
|
||||||
except Exception as _hub_err:
|
except Exception as _hub_err:
|
||||||
print(f"[hub_bridge] gate: {_hub_err}")
|
print(f"[hub_bridge] gate: {_hub_err}")
|
||||||
|
|||||||
@@ -1967,10 +1967,13 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
|
item["display_open_stop_loss"] = open_stop
|
||||||
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
||||||
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
||||||
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", base_stop)
|
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
||||||
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
||||||
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
||||||
item["effective_miss_reason"] = get_effective_trade_field(row, "reviewed_miss_reason", "miss_reason", item.get("miss_reason"))
|
item["effective_miss_reason"] = get_effective_trade_field(row, "reviewed_miss_reason", "miss_reason", item.get("miss_reason"))
|
||||||
@@ -2283,10 +2286,13 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
er = (entry_reason or "").strip() or None
|
er = (entry_reason or "").strip() or None
|
||||||
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO trade_records (symbol,monitor_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id,entry_reason,trend_plan_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
"INSERT INTO trade_records (symbol,monitor_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id,entry_reason,trend_plan_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss, take_profit,
|
symbol, monitor_type, direction, trigger_price, snap_sl, snap_sl, take_profit,
|
||||||
margin_capital, leverage, pnl_amount, hold_seconds,
|
margin_capital, leverage, pnl_amount, hold_seconds,
|
||||||
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
|
trade_style, risk_amount, planned_rr, actual_rr, hold_minutes,
|
||||||
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id, er,
|
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id, er,
|
||||||
@@ -4064,8 +4070,31 @@ def fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_
|
|||||||
if hit is None and since_ms:
|
if hit is None and since_ms:
|
||||||
trades = exchange.fetch_my_trades(exchange_symbol, since=None, limit=100)
|
trades = exchange.fetch_my_trades(exchange_symbol, since=None, limit=100)
|
||||||
hit = pick_from_trades(trades)
|
hit = pick_from_trades(trades)
|
||||||
|
if hit is not None:
|
||||||
return hit
|
return hit
|
||||||
except Exception:
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from gate_position_history_lib import pick_gate_position_close
|
||||||
|
|
||||||
|
pos = pick_gate_position_close(
|
||||||
|
fetch_gate_positions_close_history(),
|
||||||
|
exchange_symbol,
|
||||||
|
direction,
|
||||||
|
opened_at_ms=since_ms,
|
||||||
|
)
|
||||||
|
if pos:
|
||||||
|
return {
|
||||||
|
"price": None,
|
||||||
|
"timestamp": pos["close_ms"],
|
||||||
|
"side": close_side,
|
||||||
|
"_from_position_history": True,
|
||||||
|
"_realized_pnl": pos.get("pnl"),
|
||||||
|
"_sync_key": pos.get("sync_key"),
|
||||||
|
"_open_ms": pos.get("open_ms"),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -4173,7 +4202,7 @@ def calc_weighted_exit_price(trades):
|
|||||||
return weighted_sum / total_amount
|
return weighted_sum / total_amount
|
||||||
|
|
||||||
|
|
||||||
def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_manual=False):
|
||||||
"""
|
"""
|
||||||
交易所已无仓、本地仍为 active 时,推断平仓类型/时间/盈亏。
|
交易所已无仓、本地仍为 active 时,推断平仓类型/时间/盈亏。
|
||||||
返回 (result, pnl_amount, closed_at_str, miss_reason)。
|
返回 (result, pnl_amount, closed_at_str, miss_reason)。
|
||||||
@@ -4198,6 +4227,12 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
|||||||
ts = trade.get("timestamp")
|
ts = trade.get("timestamp")
|
||||||
if ts:
|
if ts:
|
||||||
closed_at_str = ms_to_app_local_str(int(ts))
|
closed_at_str = ms_to_app_local_str(int(ts))
|
||||||
|
if trade.get("_from_position_history"):
|
||||||
|
pnl_hist = trade.get("_realized_pnl")
|
||||||
|
if pnl_hist is not None:
|
||||||
|
note = "中控平仓后按 Gate 平仓历史同步盈亏" if prefer_manual else "按 Gate 平仓历史同步盈亏"
|
||||||
|
res = "手动平仓" if prefer_manual else "外部平仓"
|
||||||
|
return (res, float(pnl_hist), closed_at_str, note)
|
||||||
|
|
||||||
if exit_px is None or exit_px <= 0:
|
if exit_px is None or exit_px <= 0:
|
||||||
p = get_price(sym)
|
p = get_price(sym)
|
||||||
@@ -4220,6 +4255,13 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
|||||||
|
|
||||||
result = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_px)
|
result = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_px)
|
||||||
pnl = calc_pnl(direction, trigger_price, exit_px, margin_capital, leverage)
|
pnl = calc_pnl(direction, trigger_price, exit_px, margin_capital, leverage)
|
||||||
|
if prefer_manual:
|
||||||
|
return (
|
||||||
|
"手动平仓",
|
||||||
|
pnl,
|
||||||
|
closed_at_str,
|
||||||
|
"中控平仓后按交易所成交记录同步",
|
||||||
|
)
|
||||||
if result:
|
if result:
|
||||||
return (
|
return (
|
||||||
result,
|
result,
|
||||||
@@ -4235,6 +4277,87 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_hub_external_close(conn, symbol, direction):
|
||||||
|
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
|
||||||
|
if not exchange_private_api_configured():
|
||||||
|
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
|
||||||
|
from gate_position_history_lib import unified_symbol_for_match
|
||||||
|
|
||||||
|
sym_u = unified_symbol_for_match(symbol)
|
||||||
|
dir_l = (direction or "").strip().lower()
|
||||||
|
if dir_l not in ("long", "short"):
|
||||||
|
return {"ok": False, "msg": "side 须为 long 或 short", "synced": 0}
|
||||||
|
synced = 0
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM order_monitors WHERE status IN ('active', 'error')"
|
||||||
|
).fetchall()
|
||||||
|
for r in rows:
|
||||||
|
if unified_symbol_for_match(r["symbol"]) != sym_u:
|
||||||
|
continue
|
||||||
|
if (r["direction"] or "").strip().lower() != dir_l:
|
||||||
|
continue
|
||||||
|
oid = int(r["id"])
|
||||||
|
if r["status"] == "error":
|
||||||
|
opened_at_chk = get_opened_at_value(r)
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM trade_records WHERE symbol=? AND opened_at=? AND monitor_type='下单监控' LIMIT 1",
|
||||||
|
(r["symbol"], opened_at_chk),
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (oid,))
|
||||||
|
synced += 1
|
||||||
|
continue
|
||||||
|
exchange_symbol = r["exchange_symbol"] or normalize_exchange_symbol(r["symbol"])
|
||||||
|
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
||||||
|
if live_contracts is None:
|
||||||
|
continue
|
||||||
|
if live_contracts > 0:
|
||||||
|
time.sleep(0.6)
|
||||||
|
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
|
||||||
|
if live_contracts is None or live_contracts > 0:
|
||||||
|
continue
|
||||||
|
cancel_gate_swap_trigger_orders(exchange_symbol)
|
||||||
|
opened_at = get_opened_at_value(r)
|
||||||
|
opened_at_ms = _to_ms_with_fallback(r["opened_at_ms"] if "opened_at_ms" in r.keys() else None, opened_at)
|
||||||
|
result, pnl_amount, closed_at, miss_reason = resolve_synced_flat_close(
|
||||||
|
r, opened_at, opened_at_ms=opened_at_ms, prefer_manual=True
|
||||||
|
)
|
||||||
|
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
|
||||||
|
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
|
||||||
|
session_date = r["session_date"] or get_trading_day(closed_at_dt)
|
||||||
|
update_session_capital(conn, session_date, pnl_amount)
|
||||||
|
insert_trade_record(
|
||||||
|
conn,
|
||||||
|
symbol=r["symbol"],
|
||||||
|
monitor_type=trade_record_monitor_type(conn, r),
|
||||||
|
trend_plan_id=trend_plan_id_from_monitor_row(r),
|
||||||
|
direction=r["direction"],
|
||||||
|
trigger_price=r["trigger_price"],
|
||||||
|
stop_loss=r["stop_loss"],
|
||||||
|
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
|
||||||
|
take_profit=r["take_profit"],
|
||||||
|
margin_capital=r["margin_capital"],
|
||||||
|
leverage=r["leverage"],
|
||||||
|
pnl_amount=pnl_amount,
|
||||||
|
hold_seconds=hold_seconds,
|
||||||
|
trade_style=r["trade_style"],
|
||||||
|
risk_amount=r["risk_amount"],
|
||||||
|
planned_rr=calc_rr_ratio(r["direction"], r["trigger_price"], r["initial_stop_loss"] or r["stop_loss"], r["take_profit"]),
|
||||||
|
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
|
||||||
|
result=result,
|
||||||
|
miss_reason=handoff_trade_miss_reason(miss_reason, r),
|
||||||
|
opened_at=opened_at,
|
||||||
|
closed_at=closed_at,
|
||||||
|
)
|
||||||
|
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
|
||||||
|
synced += 1
|
||||||
|
try:
|
||||||
|
sync_trend_trade_records_from_exchange(conn)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True, "synced": synced}
|
||||||
|
|
||||||
|
|
||||||
def reconcile_external_closes(conn, days=None):
|
def reconcile_external_closes(conn, days=None):
|
||||||
synced_count = 0
|
synced_count = 0
|
||||||
cutoff_ms = None
|
cutoff_ms = None
|
||||||
@@ -7979,6 +8102,7 @@ try:
|
|||||||
},
|
},
|
||||||
ohlcv_fn=_hub_fetch_ohlcv,
|
ohlcv_fn=_hub_fetch_ohlcv,
|
||||||
volume_rank_fn=_hub_fetch_volume_rank,
|
volume_rank_fn=_hub_fetch_volume_rank,
|
||||||
|
reconcile_hub_flat_fn=reconcile_hub_external_close,
|
||||||
)
|
)
|
||||||
from strategy_trend_register import build_trend_config, patch_trend_hub_enrich
|
from strategy_trend_register import build_trend_config, patch_trend_hub_enrich
|
||||||
|
|
||||||
|
|||||||
@@ -568,14 +568,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<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>盈亏U(展示)</th><th>结果</th><th>操作</th></tr>
|
<tr><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 %}
|
{% for r in record %}
|
||||||
<tr id="trade-row-{{ r.id }}">
|
<tr id="trade-row-{{ r.id }}">
|
||||||
<td>{{ r.symbol }}</td>
|
<td>{{ r.symbol }}</td>
|
||||||
<td>{{ r.monitor_type }}</td>
|
<td>{{ r.monitor_type }}</td>
|
||||||
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></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>
|
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
|
||||||
{% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
|
{% 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 %}
|
{% set tp_show = r.effective_take_profit or r.take_profit %}
|
||||||
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
|
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
|
||||||
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
|
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
|
||||||
|
|||||||
@@ -1901,13 +1901,12 @@ def get_effective_trade_field(row, reviewed_key, base_key, default=None):
|
|||||||
|
|
||||||
def to_effective_trade_dict(row):
|
def to_effective_trade_dict(row):
|
||||||
item = row_to_dict(row)
|
item = row_to_dict(row)
|
||||||
base_stop = item.get("initial_stop_loss") if item.get("initial_stop_loss") not in (None, "") else item.get("stop_loss")
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
open_stop = snapshot_stop_loss(item.get("initial_stop_loss"), item.get("stop_loss"))
|
||||||
|
item["display_open_stop_loss"] = open_stop
|
||||||
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
item["effective_opened_at"] = get_effective_trade_field(row, "reviewed_opened_at", "opened_at", item.get("opened_at"))
|
||||||
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
item["effective_closed_at"] = get_effective_trade_field(row, "reviewed_closed_at", "closed_at", item.get("closed_at"))
|
||||||
open_stop = item.get("initial_stop_loss")
|
|
||||||
if open_stop in (None, ""):
|
|
||||||
open_stop = base_stop
|
|
||||||
item["display_open_stop_loss"] = open_stop
|
|
||||||
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
item["effective_stop_loss"] = get_effective_trade_field(row, "reviewed_stop_loss", "stop_loss", open_stop)
|
||||||
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
item["effective_take_profit"] = get_effective_trade_field(row, "reviewed_take_profit", "take_profit", item.get("take_profit"))
|
||||||
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
item["effective_result"] = get_effective_trade_field(row, "reviewed_result", "result", item.get("result"))
|
||||||
@@ -2154,7 +2153,9 @@ def insert_trade_record(
|
|||||||
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
open_ts_ms = _to_ms_with_fallback(opened_at_ms, open_ts)
|
||||||
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
close_ts_ms = _to_ms_with_fallback(closed_at_ms, close_ts)
|
||||||
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
|
||||||
snap_sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
|
from order_monitor_display_lib import snapshot_stop_loss
|
||||||
|
|
||||||
|
snap_sl = snapshot_stop_loss(initial_stop_loss, stop_loss)
|
||||||
er = (
|
er = (
|
||||||
(entry_reason or "").strip()
|
(entry_reason or "").strip()
|
||||||
or entry_reason_from_key_signal(kst)
|
or entry_reason_from_key_signal(kst)
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Gate 平仓历史匹配(fetch_positions_history),供 reconcile / 中控全平同步共用。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def unified_symbol_for_match(symbol_str: str) -> str:
|
||||||
|
x = (symbol_str or "").strip().upper()
|
||||||
|
if ":" in x:
|
||||||
|
x = x.split(":")[0]
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def pick_gate_position_close(
|
||||||
|
hist: list[dict],
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
*,
|
||||||
|
opened_at_ms: int | None = None,
|
||||||
|
closed_at_ms: int | None = None,
|
||||||
|
used_keys: set[str] | None = None,
|
||||||
|
max_close_delta_ms: int = 25 * 60 * 1000,
|
||||||
|
) -> dict | None:
|
||||||
|
"""
|
||||||
|
从 Gate 平仓历史列表中选取与 symbol/direction/开仓时间最匹配的一条。
|
||||||
|
返回 normalize 后的 dict(含 close_ms、pnl、sync_key 等),无匹配则 None。
|
||||||
|
"""
|
||||||
|
if not hist:
|
||||||
|
return None
|
||||||
|
sym_u = unified_symbol_for_match(symbol)
|
||||||
|
dir_l = (direction or "long").strip().lower()
|
||||||
|
if dir_l not in ("long", "short"):
|
||||||
|
return None
|
||||||
|
used = used_keys or set()
|
||||||
|
ref_ms = closed_at_ms or opened_at_ms
|
||||||
|
best = None
|
||||||
|
best_d = None
|
||||||
|
for h in hist:
|
||||||
|
if not isinstance(h, dict):
|
||||||
|
continue
|
||||||
|
sk = h.get("sync_key")
|
||||||
|
if not sk or sk in used:
|
||||||
|
continue
|
||||||
|
if h.get("symbol_u") != sym_u:
|
||||||
|
continue
|
||||||
|
if (h.get("side") or "").strip().lower() != dir_l:
|
||||||
|
continue
|
||||||
|
cm = h.get("close_ms")
|
||||||
|
if cm is None:
|
||||||
|
continue
|
||||||
|
if opened_at_ms is not None:
|
||||||
|
if cm < opened_at_ms - 15 * 60 * 1000:
|
||||||
|
continue
|
||||||
|
if cm > opened_at_ms + 15 * 86400 * 1000:
|
||||||
|
continue
|
||||||
|
if ref_ms is not None:
|
||||||
|
d = abs(int(cm) - int(ref_ms))
|
||||||
|
else:
|
||||||
|
d = 0
|
||||||
|
if best_d is None or d < best_d:
|
||||||
|
best_d = d
|
||||||
|
best = h
|
||||||
|
if best is None or best_d is None:
|
||||||
|
return None
|
||||||
|
if ref_ms is not None and best_d > max_close_delta_ms:
|
||||||
|
return None
|
||||||
|
return best
|
||||||
@@ -207,6 +207,7 @@ def install_on_app(
|
|||||||
ohlcv_fn=None,
|
ohlcv_fn=None,
|
||||||
account_fn=None,
|
account_fn=None,
|
||||||
volume_rank_fn=None,
|
volume_rank_fn=None,
|
||||||
|
reconcile_hub_flat_fn=None,
|
||||||
):
|
):
|
||||||
app.config["HUB_CTX"] = {
|
app.config["HUB_CTX"] = {
|
||||||
"exchange": exchange,
|
"exchange": exchange,
|
||||||
@@ -219,6 +220,7 @@ def install_on_app(
|
|||||||
"views": views,
|
"views": views,
|
||||||
"ohlcv_fn": ohlcv_fn,
|
"ohlcv_fn": ohlcv_fn,
|
||||||
"volume_rank_fn": volume_rank_fn,
|
"volume_rank_fn": volume_rank_fn,
|
||||||
|
"reconcile_hub_flat_fn": reconcile_hub_flat_fn,
|
||||||
}
|
}
|
||||||
install_hub_embed_headers(app)
|
install_hub_embed_headers(app)
|
||||||
configure_hub_embed_session(app)
|
configure_hub_embed_session(app)
|
||||||
@@ -607,6 +609,40 @@ def register_hub_routes(app):
|
|||||||
return jsonify({"ok": False, "msg": "该实例无趋势回调"}), 400
|
return jsonify({"ok": False, "msg": "该实例无趋势回调"}), 400
|
||||||
return jsonify(_invoke_view_get("stop_trend_pullback", f"/stop_trend_pullback/{pid}"))
|
return jsonify(_invoke_view_get("stop_trend_pullback", f"/stop_trend_pullback/{pid}"))
|
||||||
|
|
||||||
|
@app.route("/api/hub/order/sync-flat", methods=["POST"])
|
||||||
|
@_hub_auth_required
|
||||||
|
def api_hub_order_sync_flat():
|
||||||
|
"""中控市价全平后:同步 order_monitors 并读 Gate 平仓历史写交易记录。"""
|
||||||
|
fn = _ctx().get("reconcile_hub_flat_fn")
|
||||||
|
if not callable(fn):
|
||||||
|
return jsonify({"ok": False, "msg": "该实例未配置 order sync-flat"}), 400
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
symbol = (body.get("symbol") or request.form.get("symbol") or "").strip()
|
||||||
|
side = (
|
||||||
|
body.get("side")
|
||||||
|
or body.get("direction")
|
||||||
|
or request.form.get("side")
|
||||||
|
or ""
|
||||||
|
).strip().lower()
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({"ok": False, "msg": "symbol 不能为空"}), 400
|
||||||
|
if side not in ("long", "short"):
|
||||||
|
return jsonify({"ok": False, "msg": "side 须为 long 或 short"}), 400
|
||||||
|
get_db = _ctx().get("get_db")
|
||||||
|
if not callable(get_db):
|
||||||
|
return jsonify({"ok": False, "msg": "HUB_CTX 缺少 get_db"}), 500
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
out = fn(conn, symbol, side)
|
||||||
|
if not isinstance(out, dict):
|
||||||
|
out = {"ok": True, "synced": int(out or 0)}
|
||||||
|
conn.commit()
|
||||||
|
return jsonify(out)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "msg": str(e)}), 500
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
@app.route("/api/hub/trend/sync-flat", methods=["POST"])
|
@app.route("/api/hub/trend/sync-flat", methods=["POST"])
|
||||||
@_hub_auth_required
|
@_hub_auth_required
|
||||||
def api_hub_trend_sync_flat():
|
def api_hub_trend_sync_flat():
|
||||||
|
|||||||
@@ -1802,8 +1802,20 @@ async def api_close_position(exchange_id: str, body: ClosePositionBody):
|
|||||||
"payload": payload,
|
"payload": payload,
|
||||||
"ok": bool(isinstance(payload, dict) and payload.get("ok")),
|
"ok": bool(isinstance(payload, dict) and payload.get("ok")),
|
||||||
}
|
}
|
||||||
if out.get("ok") and "trend" in (ex.get("capabilities") or []):
|
if out.get("ok"):
|
||||||
|
ex_key = (ex.get("key") or "").strip().lower()
|
||||||
async with httpx.AsyncClient() as flask_client:
|
async with httpx.AsyncClient() as flask_client:
|
||||||
|
if ex_key in ("gate", "gate_bot"):
|
||||||
|
order_sync = await _fetch_flask_json(
|
||||||
|
flask_client,
|
||||||
|
ex,
|
||||||
|
"/api/hub/order/sync-flat",
|
||||||
|
method="POST",
|
||||||
|
json_body={"symbol": sym, "side": side},
|
||||||
|
)
|
||||||
|
if isinstance(order_sync, dict):
|
||||||
|
out["order_sync"] = order_sync
|
||||||
|
if "trend" in (ex.get("capabilities") or []):
|
||||||
sync_parsed = await _fetch_flask_json(
|
sync_parsed = await _fetch_flask_json(
|
||||||
flask_client,
|
flask_client,
|
||||||
ex,
|
ex,
|
||||||
|
|||||||
@@ -13,13 +13,27 @@ def _positive_float(value: Any) -> Optional[float]:
|
|||||||
|
|
||||||
|
|
||||||
def snapshot_stop_loss(initial_stop_loss: Any, stop_loss: Any) -> Optional[float]:
|
def snapshot_stop_loss(initial_stop_loss: Any, stop_loss: Any) -> Optional[float]:
|
||||||
"""展示盈亏比时优先用开仓时止损快照。"""
|
"""展示盈亏比 / 交易记录时优先用开仓时止损快照,不用后续改单后的止损。"""
|
||||||
sl = _positive_float(initial_stop_loss)
|
sl = _positive_float(initial_stop_loss)
|
||||||
if sl is not None:
|
if sl is not None:
|
||||||
return sl
|
return sl
|
||||||
return _positive_float(stop_loss)
|
return _positive_float(stop_loss)
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_open_stop_loss(row: Any) -> Optional[float]:
|
||||||
|
"""从 order_monitors 行取开仓止损快照。"""
|
||||||
|
try:
|
||||||
|
keys = row.keys() if hasattr(row, "keys") else ()
|
||||||
|
except Exception:
|
||||||
|
keys = ()
|
||||||
|
init = row["initial_stop_loss"] if "initial_stop_loss" in keys else None
|
||||||
|
cur = row["stop_loss"] if "stop_loss" in keys else None
|
||||||
|
if init is None and isinstance(row, dict):
|
||||||
|
init = row.get("initial_stop_loss")
|
||||||
|
cur = row.get("stop_loss")
|
||||||
|
return snapshot_stop_loss(init, cur)
|
||||||
|
|
||||||
|
|
||||||
def snapshot_rr(
|
def snapshot_rr(
|
||||||
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
||||||
direction: str,
|
direction: str,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from gate_position_history_lib import pick_gate_position_close, unified_symbol_for_match
|
||||||
|
|
||||||
|
|
||||||
|
def test_unified_symbol_strips_settle_suffix():
|
||||||
|
assert unified_symbol_for_match("BTC/USDT:USDT") == "BTC/USDT"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_gate_position_close_matches_symbol_side_and_time():
|
||||||
|
hist = [
|
||||||
|
{
|
||||||
|
"symbol_u": "SOL/USDT",
|
||||||
|
"side": "short",
|
||||||
|
"close_ms": 1_700_000_000_000,
|
||||||
|
"open_ms": 1_699_999_000_000,
|
||||||
|
"pnl": -1.25,
|
||||||
|
"sync_key": "SOL_USDT|1|short",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
hit = pick_gate_position_close(
|
||||||
|
hist,
|
||||||
|
"SOL/USDT:USDT",
|
||||||
|
"short",
|
||||||
|
opened_at_ms=1_699_999_500_000,
|
||||||
|
)
|
||||||
|
assert hit is not None
|
||||||
|
assert hit["pnl"] == -1.25
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from order_monitor_display_lib import (
|
from order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
is_sl_breakeven_secured,
|
is_sl_breakeven_secured,
|
||||||
|
monitor_open_stop_loss,
|
||||||
order_monitor_tpsl_needs_sync,
|
order_monitor_tpsl_needs_sync,
|
||||||
resolve_live_tpsl_prices,
|
resolve_live_tpsl_prices,
|
||||||
sl_breakeven_from_exchange_tpsl,
|
sl_breakeven_from_exchange_tpsl,
|
||||||
@@ -26,6 +27,11 @@ def test_snapshot_stop_loss_prefers_initial():
|
|||||||
assert snapshot_stop_loss(None, 2.6) == 2.6
|
assert snapshot_stop_loss(None, 2.6) == 2.6
|
||||||
|
|
||||||
|
|
||||||
|
def test_monitor_open_stop_loss_prefers_initial_snapshot():
|
||||||
|
row = {"initial_stop_loss": 64000, "stop_loss": 63200}
|
||||||
|
assert monitor_open_stop_loss(row) == 64000
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_rr_ignores_current_stop_after_manual_move():
|
def test_snapshot_rr_ignores_current_stop_after_manual_move():
|
||||||
rr = snapshot_rr(_calc_rr, "long", 2.726, 2.45, 2.65, 3.3)
|
rr = snapshot_rr(_calc_rr, "long", 2.726, 2.45, 2.65, 3.3)
|
||||||
assert rr is not None
|
assert rr is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user