Coverage for src/cc_liquid/callbacks.py: 52%

1"""Callbacks for cc-liquid trader.py UI/UX abstraction.

2

3 This module defines the protocol and non-Rich implementations only, to keep the

4 core package free of UI dependencies. Rich-based callbacks live in

5`cli_callbacks.py`.

6"""

7

8 from typing import Any, Protocol, TYPE_CHECKING

9

10 if TYPE_CHECKING:

11 from .order import Order, OrderHistory

12

13

14 class CCLiquidCallbacks(Protocol):

15 """Protocol for trader callbacks to abstract UI/UX concerns."""

16

17 # High-level lifecycle methods

18 def ask_confirmation(self, message: str) -> bool:

19 """Ask user for confirmation."""

20 ...

21

22 def info(self, message: str) -> None:

23 """Display info message."""

24 ...

25

26 def warn(self, message: str) -> None:

27 """Display warning message."""

28 ...

29

30 def error(self, message: str) -> None:

31 """Display error message."""

32 ...

33

34 def on_config_override(self, overrides: list[str]) -> None:

35 """Display applied configuration overrides."""

36 ...

37

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 ...

42

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 ...

48

49 def on_trade_fail(self, trade: dict[str, Any], reason: str) -> None:

50 """Called when a trade fails."""

51 ...

52

53 def on_batch_complete(self, success: list[dict], failed: list[dict]) -> None:

54 """Called when a batch of trades completes."""

55 ...

56

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 ...

66

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 ...

76

77 def on_order_submitted(self, order: "Order") -> None:

78 """Called when order is submitted to exchange."""

79 ...

80

81 def on_order_update(self, order: "Order", fill_pct: float) -> None:

82 """Called when order status changes (fill progress)."""

83 ...

84

85 def show_open_orders(self, orders: list["Order"]) -> None:

86 """Display table of open orders."""

87 ...

88

89 def show_order_history(self, history: "OrderHistory", days: int) -> None:

90 """Display order history and performance stats."""

91 ...

92

93

94 class NoOpCallbacks:

95 """No-op implementation for silent operation (e.g., notebooks, tests)."""

96

97 def ask_confirmation(self, message: str) -> bool: # noqa: D401

98 return True # auto-confirm in silent mode

99

100 def info(self, message: str) -> None: # noqa: D401

101 pass

102

103 def warn(self, message: str) -> None: # noqa: D401

104 pass

105

106 def error(self, message: str) -> None: # noqa: D401

107 pass

108

109 def on_config_override(self, overrides: list[str]) -> None: # noqa: D401

110 pass

111

112 def on_trade_start(self, idx: int, total: int, trade: dict[str, Any]) -> None:

113 pass

114

115 def on_trade_fill(

116 self, trade: dict[str, Any], fill_data: dict[str, Any], slippage_pct: float

117 ) -> None:

118 pass

119

120 def on_trade_fail(self, trade: dict[str, Any], reason: str) -> None:

121 pass

122

123 def on_batch_complete(self, success: list[dict], failed: list[dict]) -> None:

124 pass

125

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

134

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

143

144 def on_order_submitted(self, order) -> None:

145 pass

146

147 def on_order_update(self, order, fill_pct: float) -> None:

148 pass

149

150 def show_open_orders(self, orders: list) -> None:

151 pass

152

153 def show_order_history(self, history, days: int) -> None:

154 pass

155

156

157 class PrintCallbacks:

158 """Lightweight `print`-based callbacks for scripts & notebooks.

159

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 """

164

165 def __init__(self, auto_confirm: bool = False) -> None:

166 self.auto_confirm = auto_confirm

167

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"}

173

174 def info(self, message: str) -> None: # noqa: D401

175 print(f"INFO: {message}")

176

177 def warn(self, message: str) -> None: # noqa: D401

178 print(f"WARNING: {message}")

179

180 def error(self, message: str) -> None: # noqa: D401

181 print(f"ERROR: {message}")

182

183 def on_config_override(self, overrides: list[str]) -> None: # noqa: D401

184 if overrides:

185 print(f"Applied CLI overrides: {', '.join(overrides)}")

186

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 )

193

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}%)")

199

200 def on_trade_fail(self, trade: dict[str, Any], reason: str) -> None:

201 print(f"✗ failed: {reason}")

202

203 def on_batch_complete(self, success: list[dict], failed: list[dict]) -> None:

204 print(f"\nExecution complete › success {len(success)} | failed {len(failed)}")

205

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}")

218

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")

228

229 def on_order_submitted(self, order) -> None:

230 print(f"Order submitted: {order.coin} {order.side} (ID: {order.order_id[:12]}...)")

231

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})")

234

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}")

239

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")