Skip to content

Commit 984ec3d

Browse files
committed
Expose quote snapshots in LongBridge dry-run reports
1 parent 65103cf commit 984ec3d

4 files changed

Lines changed: 42 additions & 2 deletions

File tree

application/execution_service.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ class ExecutionCycleResult:
248248
note_logs: tuple[str, ...]
249249
action_done: bool
250250
dry_run_orders: tuple[dict, ...] = ()
251+
quote_snapshots: tuple[dict, ...] = ()
251252

252253

253254
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
@@ -265,6 +266,17 @@ def _coerce_order_quantity(value):
265266
return value
266267

267268

269+
def _serialize_quote_snapshot(snapshot) -> dict:
270+
return {
271+
"symbol": str(getattr(snapshot, "symbol", "") or "").strip().upper(),
272+
"as_of": str(getattr(snapshot, "as_of", "") or ""),
273+
"last_price": float(getattr(snapshot, "last_price", 0.0) or 0.0),
274+
"bid_price": getattr(snapshot, "bid_price", None),
275+
"ask_price": getattr(snapshot, "ask_price", None),
276+
"currency": str(getattr(snapshot, "currency", "") or "").strip(),
277+
}
278+
279+
268280
def _safe_haven_cash_symbols(*, portfolio: dict, allocation: dict) -> tuple[str, ...]:
269281
symbols: list[str] = []
270282
for symbol in allocation.get("safe_haven_symbols", ()):
@@ -422,9 +434,12 @@ def _sell_order_quantity(
422434
)
423435

424436

425-
def safe_quote_last_price(symbol, *, market_data_port, notify_issue):
437+
def safe_quote_last_price(symbol, *, market_data_port, notify_issue, quote_recorder=None):
426438
try:
427-
return float(market_data_port.get_quote(symbol).last_price)
439+
snapshot = market_data_port.get_quote(symbol)
440+
if quote_recorder is not None:
441+
quote_recorder(snapshot)
442+
return float(snapshot.last_price)
428443
except Exception as exc:
429444
notify_issue("Quote failed", f"Symbol: {symbol}\n{exc}")
430445
return None
@@ -574,6 +589,7 @@ def execute_rebalance_cycle(
574589
skip_logs: list[str] = []
575590
note_logs: list[str] = []
576591
dry_run_orders: list[dict] = []
592+
quote_snapshots_by_symbol: dict[str, dict] = {}
577593
small_account_cash_note_keys: set[str] = set()
578594
action_done = False
579595
sell_submitted = False
@@ -585,6 +601,12 @@ def execute_rebalance_cycle(
585601
def market_symbol(symbol):
586602
return _market_symbol(symbol, symbol_suffix=symbol_suffix)
587603

604+
def record_quote_snapshot(snapshot) -> None:
605+
payload = _serialize_quote_snapshot(snapshot)
606+
symbol = payload.get("symbol")
607+
if symbol:
608+
quote_snapshots_by_symbol[symbol] = payload
609+
588610
strategy_assets = tuple(allocation["strategy_symbols"])
589611
market_values = dict(portfolio["market_values"])
590612
quantities = dict(portfolio["quantities"])
@@ -718,6 +740,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
718740
market_symbol(symbol),
719741
market_data_port=market_data_port,
720742
notify_issue=notify_issue,
743+
quote_recorder=record_quote_snapshot,
721744
)
722745
if price is None:
723746
continue
@@ -807,6 +830,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
807830
market_symbol(cash_sweep_symbol),
808831
market_data_port=market_data_port,
809832
notify_issue=notify_issue,
833+
quote_recorder=record_quote_snapshot,
810834
)
811835
if sweep_price is not None and sweep_price > 0.0:
812836
funding_needs = []
@@ -815,6 +839,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
815839
market_symbol(buy_symbol),
816840
market_data_port=market_data_port,
817841
notify_issue=notify_issue,
842+
quote_recorder=record_quote_snapshot,
818843
)
819844
if buy_price is None:
820845
continue
@@ -958,6 +983,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
958983
market_symbol(symbol),
959984
market_data_port=market_data_port,
960985
notify_issue=notify_issue,
986+
quote_recorder=record_quote_snapshot,
961987
)
962988
if price is None:
963989
continue
@@ -1078,6 +1104,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
10781104
market_symbol(cash_sweep_symbol),
10791105
market_data_port=market_data_port,
10801106
notify_issue=notify_issue,
1107+
quote_recorder=record_quote_snapshot,
10811108
)
10821109
if cash_sweep_price is not None and cash_sweep_price > 0.0 and investable_cash > cash_sweep_price * 2:
10831110
substitution_threshold = max(
@@ -1132,4 +1159,5 @@ def record_dry_run(symbol, side, quantity, price, *, order_type):
11321159
note_logs=tuple(note_logs),
11331160
action_done=action_done,
11341161
dry_run_orders=tuple(dry_run_orders),
1162+
quote_snapshots=tuple(quote_snapshots_by_symbol.values()),
11351163
)

main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def _summarize_cycle_result_for_report(cycle_result, *, dry_run: bool) -> dict:
133133
skip_logs = tuple(getattr(cycle_result, "skip_logs", ()) or ())
134134
note_logs = tuple(getattr(cycle_result, "note_logs", ()) or ())
135135
dry_run_orders = tuple(getattr(cycle_result, "dry_run_orders", ()) or ())
136+
quote_snapshots = tuple(getattr(cycle_result, "quote_snapshots", ()) or ())
136137
order_events_count = len(logs)
137138
orders_previewed_count = len(dry_run_orders) if dry_run_orders else (order_events_count if dry_run else 0)
138139
summary = {
@@ -145,6 +146,10 @@ def _summarize_cycle_result_for_report(cycle_result, *, dry_run: bool) -> dict:
145146
}
146147
if dry_run_orders:
147148
summary["orders_previewed"] = [dict(order) for order in dry_run_orders]
149+
if quote_snapshots:
150+
summary["quote_snapshot"] = {
151+
"quotes": [dict(snapshot) for snapshot in quote_snapshots],
152+
}
148153
return summary
149154

150155

tests/test_rebalance_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@ def estimate_max_purchase_quantity(_trade_context, symbol, **_kwargs):
383383
self.assertGreaterEqual(len(result.dry_run_orders), 1)
384384
self.assertTrue(all(order["status"] == "dry_run" for order in result.dry_run_orders))
385385
self.assertTrue(all(order["symbol"].endswith(".HK") for order in result.dry_run_orders))
386+
self.assertTrue(result.quote_snapshots)
387+
self.assertTrue(all(snapshot["symbol"].endswith(".HK") for snapshot in result.quote_snapshots))
386388

387389
def test_run_strategy_prefers_portfolio_port_runtime_path(self):
388390
sent_messages = []

tests/test_request_handling.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,10 @@ def test_cycle_result_summary_counts_dry_run_order_previews(self):
586586
{"symbol": "02800.HK", "side": "buy", "quantity": 100, "status": "dry_run"},
587587
{"symbol": "03033.HK", "side": "buy", "quantity": 200, "status": "dry_run"},
588588
),
589+
quote_snapshots=(
590+
{"symbol": "02800.HK", "last_price": 30.0, "currency": "HKD"},
591+
{"symbol": "03033.HK", "last_price": 20.0, "currency": "HKD"},
592+
),
589593
)
590594

591595
summary = module._summarize_cycle_result_for_report(cycle_result, dry_run=True)
@@ -597,6 +601,7 @@ def test_cycle_result_summary_counts_dry_run_order_previews(self):
597601
self.assertEqual(summary["notes_count"], 1)
598602
self.assertTrue(summary["dry_run_order_preview_available"])
599603
self.assertEqual(summary["orders_previewed"][0]["symbol"], "02800.HK")
604+
self.assertEqual(summary["quote_snapshot"]["quotes"][0]["symbol"], "02800.HK")
600605

601606

602607
if __name__ == "__main__":

0 commit comments

Comments
 (0)