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

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

2 

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

7 

8from typing import Any, Protocol, TYPE_CHECKING 

9 

10if TYPE_CHECKING: 

11 from .order import Order, OrderHistory 

12 

13 

14class 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 

94class 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 

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