Coverage for src/cc_liquid/callbacks.py: 52%
97 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-10-13 20:16 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-10-13 20:16 +0000
1"""Callbacks for cc-liquid trader.py UI/UX abstraction.
3This module defines the protocol and non-Rich implementations only, to keep the
4core package free of UI dependencies. Rich-based callbacks live in
5`cli_callbacks.py`.
6"""
8from typing import Any, Protocol, TYPE_CHECKING
10if TYPE_CHECKING:
11 from .order import Order, OrderHistory
14class CCLiquidCallbacks(Protocol):
15 """Protocol for trader callbacks to abstract UI/UX concerns."""
17 # High-level lifecycle methods
18 def ask_confirmation(self, message: str) -> bool:
19 """Ask user for confirmation."""
20 ...
22 def info(self, message: str) -> None:
23 """Display info message."""
24 ...
26 def warn(self, message: str) -> None:
27 """Display warning message."""
28 ...
30 def error(self, message: str) -> None:
31 """Display error message."""
32 ...
34 def on_config_override(self, overrides: list[str]) -> None:
35 """Display applied configuration overrides."""
36 ...
38 # Trade execution progress hooks
39 def on_trade_start(self, idx: int, total: int, trade: dict[str, Any]) -> None:
40 """Called when a trade execution starts."""
41 ...
43 def on_trade_fill(
44 self, trade: dict[str, Any], fill_data: dict[str, Any], slippage_pct: float
45 ) -> None:
46 """Called when a trade is filled."""
47 ...
49 def on_trade_fail(self, trade: dict[str, Any], reason: str) -> None:
50 """Called when a trade fails."""
51 ...
53 def on_batch_complete(self, success: list[dict], failed: list[dict]) -> None:
54 """Called when a batch of trades completes."""
55 ...
57 def show_trade_plan(
58 self,
59 target_positions: dict,
60 trades: list,
61 account_value: float,
62 leverage: float,
63 ) -> None:
64 """Display the trade plan before execution."""
65 ...
67 def show_execution_summary(
68 self,
69 successful_trades: list[dict],
70 all_trades: list[dict],
71 target_positions: dict,
72 account_value: float,
73 ) -> None:
74 """Display execution summary after trades complete."""
75 ...
77 def on_order_submitted(self, order: "Order") -> None:
78 """Called when order is submitted to exchange."""
79 ...
81 def on_order_update(self, order: "Order", fill_pct: float) -> None:
82 """Called when order status changes (fill progress)."""
83 ...
85 def show_open_orders(self, orders: list["Order"]) -> None:
86 """Display table of open orders."""
87 ...
89 def show_order_history(self, history: "OrderHistory", days: int) -> None:
90 """Display order history and performance stats."""
91 ...
94class NoOpCallbacks:
95 """No-op implementation for silent operation (e.g., notebooks, tests)."""
97 def ask_confirmation(self, message: str) -> bool: # noqa: D401
98 return True # auto-confirm in silent mode
100 def info(self, message: str) -> None: # noqa: D401
101 pass
103 def warn(self, message: str) -> None: # noqa: D401
104 pass
106 def error(self, message: str) -> None: # noqa: D401
107 pass
109 def on_config_override(self, overrides: list[str]) -> None: # noqa: D401
110 pass
112 def on_trade_start(self, idx: int, total: int, trade: dict[str, Any]) -> None:
113 pass
115 def on_trade_fill(
116 self, trade: dict[str, Any], fill_data: dict[str, Any], slippage_pct: float
117 ) -> None:
118 pass
120 def on_trade_fail(self, trade: dict[str, Any], reason: str) -> None:
121 pass
123 def on_batch_complete(self, success: list[dict], failed: list[dict]) -> None:
124 pass
126 def show_trade_plan(
127 self,
128 target_positions: dict,
129 trades: list,
130 account_value: float,
131 leverage: float,
132 ) -> None:
133 pass
135 def show_execution_summary(
136 self,
137 successful_trades: list[dict],
138 all_trades: list[dict],
139 target_positions: dict,
140 account_value: float,
141 ) -> None:
142 pass
144 def on_order_submitted(self, order) -> None:
145 pass
147 def on_order_update(self, order, fill_pct: float) -> None:
148 pass
150 def show_open_orders(self, orders: list) -> None:
151 pass
153 def show_order_history(self, history, days: int) -> None:
154 pass
157class PrintCallbacks:
158 """Lightweight `print`-based callbacks for scripts & notebooks.
160 Provides human-readable stdout messages without any Rich dependency.
161 Useful when running in Jupyter or small automation scripts where you still
162 want basic visibility but not full colour UI.
163 """
165 def __init__(self, auto_confirm: bool = False) -> None:
166 self.auto_confirm = auto_confirm
168 def ask_confirmation(self, message: str) -> bool: # noqa: D401
169 if self.auto_confirm:
170 print(f"AUTO-CONFIRM: {message}")
171 return True
172 return input(f"{message} (y/n): ").strip().lower() in {"y", "yes"}
174 def info(self, message: str) -> None: # noqa: D401
175 print(f"INFO: {message}")
177 def warn(self, message: str) -> None: # noqa: D401
178 print(f"WARNING: {message}")
180 def error(self, message: str) -> None: # noqa: D401
181 print(f"ERROR: {message}")
183 def on_config_override(self, overrides: list[str]) -> None: # noqa: D401
184 if overrides:
185 print(f"Applied CLI overrides: {', '.join(overrides)}")
187 def on_trade_start(self, idx: int, total: int, trade: dict[str, Any]) -> None:
188 side = "BUY" if trade["is_buy"] else "SELL"
189 print(
190 f"[{idx}/{total}] {trade['coin']} {side} {trade['sz']:.4f} @ ${trade['price']:,.4f} …",
191 end=" ",
192 )
194 def on_trade_fill(
195 self, trade: dict[str, Any], fill_data: dict[str, Any], slippage_pct: float
196 ) -> None:
197 avg_px = float(fill_data.get("avgPx", trade["price"]))
198 print(f"✓ filled @ ${avg_px:,.4f} (slip {slippage_pct:+.3f}%)")
200 def on_trade_fail(self, trade: dict[str, Any], reason: str) -> None:
201 print(f"✗ failed: {reason}")
203 def on_batch_complete(self, success: list[dict], failed: list[dict]) -> None:
204 print(f"\nExecution complete › success {len(success)} | failed {len(failed)}")
206 def show_trade_plan(
207 self,
208 target_positions: dict,
209 trades: list,
210 account_value: float,
211 leverage: float,
212 ) -> None:
213 print("\n=== Portfolio Rebalancing Plan ===")
214 print(f"Account value: ${account_value:,.2f} | leverage {leverage}x")
215 if trades:
216 tot = sum(abs(t.get("delta_value", 0)) for t in trades)
217 print(f"Planned trades: {len(trades)} | volume ${tot:,.2f}")
219 def show_execution_summary(
220 self,
221 successful_trades: list[dict],
222 all_trades: list[dict],
223 target_positions: dict,
224 account_value: float,
225 ) -> None:
226 succ = len(successful_trades)
227 print(f"\nSummary: {succ}/{len(all_trades)} trades succeeded")
229 def on_order_submitted(self, order) -> None:
230 print(f"Order submitted: {order.coin} {order.side} (ID: {order.order_id[:12]}...)")
232 def on_order_update(self, order, fill_pct: float) -> None:
233 print(f" {order.coin}: {fill_pct:.0f}% filled ({order.filled_size:.2f}/{order.size:.2f})")
235 def show_open_orders(self, orders: list) -> None:
236 print(f"\n=== Open Orders ({len(orders)}) ===")
237 for order in orders:
238 print(f"{order.coin} {order.side} {order.status} - {order.filled_size:.2f}/{order.size:.2f}")
240 def show_order_history(self, history, days: int) -> None:
241 print(f"\n=== Order History ({days} days) ===")
242 print(f"Total orders: {len(history.orders)}")
243 print(f"Fill rate: {history.fill_rate:.1%}")
244 print(f"Avg slippage: {history.total_slippage_bps:.1f}bps")