Coverage for django_napse/core/models/bots/controller.py: 78%

171 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-12 13:49 +0000

1import math 

2from datetime import datetime, timedelta, timezone 

3from typing import Optional 

4 

5from django.db import models 

6from requests.exceptions import ConnectionError, ReadTimeout, SSLError 

7 

8from django_napse.core.models.bots.managers.controller import ControllerManager 

9from django_napse.core.models.orders.order import Order, OrderBatch 

10from django_napse.utils.constants import EXCHANGE_INTERVALS, EXCHANGE_PAIRS, ORDER_STATUS, SIDES, STABLECOINS 

11from django_napse.utils.errors import ControllerError 

12from django_napse.utils.trading.binance_controller import BinanceController 

13 

14 

15class Controller(models.Model): 

16 exchange_account = models.ForeignKey("ExchangeAccount", on_delete=models.CASCADE, related_name="controller") 

17 

18 pair = models.CharField(max_length=10) 

19 base = models.CharField(max_length=10) 

20 quote = models.CharField(max_length=10) 

21 interval = models.CharField(max_length=10) 

22 

23 min_notional = models.FloatField(null=True) 

24 min_trade = models.FloatField(null=True) 

25 lot_size = models.IntegerField(null=True) 

26 price = models.FloatField(null=True) 

27 last_price_update = models.DateTimeField(null=True) 

28 last_settings_update = models.DateTimeField(null=True) 

29 

30 objects = ControllerManager() 

31 

32 class Meta: 

33 unique_together = ("pair", "interval", "exchange_account") 

34 

35 def __str__(self): 

36 return f"Controller {self.pk=}" 

37 

38 def save(self, *args, **kwargs): 

39 if not self.pk: 

40 self.base = self.base.upper() 

41 self.quote = self.quote.upper() 

42 if self.base == self.quote: 

43 error_msg = f"Base and quote cannot be the same: {self.base} - {self.quote}" 

44 raise ControllerError.InvalidSetting(error_msg) 

45 self.pair = self.base + self.quote 

46 self._update_variables() 

47 if self.interval not in EXCHANGE_INTERVALS[self.exchange.name]: 

48 error_msg = f"Invalid interval: {self.interval}" 

49 raise ControllerError.InvalidSetting(error_msg) 

50 return super().save(*args, **kwargs) 

51 

52 def info(self, verbose=True, beacon=""): 

53 string = "" 

54 string += f"{beacon}Controller {self.pk}:\n" 

55 string += f"{beacon}Args:\n" 

56 string += f"{beacon}\t{self.pair=}\n" 

57 string += f"{beacon}\t{self.base=}\n" 

58 string += f"{beacon}\t{self.quote=}\n" 

59 string += f"{beacon}\t{self.interval=}\n" 

60 string += f"{beacon}\t{self.min_notional=}\n" 

61 string += f"{beacon}\t{self.min_trade=}\n" 

62 string += f"{beacon}\t{self.lot_size=}\n" 

63 string += f"{beacon}\t{self.price=}\n" 

64 string += f"{beacon}\t{self.last_price_update=}\n" 

65 string += f"{beacon}\t{self.last_settings_update=}\n" 

66 

67 if verbose: # pragma: no cover 

68 print(string) 

69 return string 

70 

71 @staticmethod 

72 def get(exchange_account, base: str, quote: str, interval: str = "1m") -> "Controller": 

73 """Return a controller object from the database.""" 

74 try: 

75 controller = Controller.objects.get( 

76 exchange_account=exchange_account, 

77 base=base, 

78 quote=quote, 

79 interval=interval, 

80 ) 

81 except Controller.DoesNotExist: 

82 controller = Controller.objects.create( 

83 exchange_account=exchange_account, 

84 base=base, 

85 quote=quote, 

86 interval=interval, 

87 bypass=True, 

88 ) 

89 controller.update_variables() 

90 return controller 

91 

92 @property 

93 def exchange_controller(self): 

94 return self.exchange_account.find().exchange_controller() 

95 

96 @property 

97 def exchange(self): 

98 return self.exchange_account.exchange 

99 

100 def update_variables(self) -> None: 

101 """If the variables are older than 1 minute, update them.""" 

102 if self.last_settings_update is None or self.last_settings_update < datetime.now(tz=timezone.utc) - timedelta(minutes=1): 

103 self._update_variables() 

104 

105 def _update_variables(self) -> None: 

106 """Update the variables of the controller.""" 

107 exchange_controller = self.exchange_controller 

108 

109 if exchange_controller == "DEFAULT": 

110 pass 

111 elif exchange_controller.__class__ == BinanceController: 

112 try: 

113 filters = exchange_controller.client.get_symbol_info(self.pair)["filters"] 

114 last_price = exchange_controller.client.get_ticker(symbol=self.pair)["lastPrice"] 

115 for specific_filter in filters: 

116 if specific_filter["filterType"] == "LOT_SIZE": 

117 self.lot_size = -int(math.log10(float(specific_filter["stepSize"]))) 

118 if specific_filter["filterType"] in ["MIN_NOTIONAL", "NOTIONAL"]: 

119 self.min_notional = float(specific_filter["minNotional"]) 

120 self.min_trade = self.min_notional / float(last_price) 

121 self.last_settings_update = datetime.now(tz=timezone.utc) 

122 except ReadTimeout: 

123 print("ReadTimeout in _update_variables") 

124 except SSLError: 

125 print("SSLError in _update_variables") 

126 except ConnectionError: 

127 print("ConnectionError in _update_variables") 

128 except Exception as e: 

129 print(f"Exception in _update_variables: {e}, {type(e)}") 

130 else: 

131 error_msg = f"Exchange controller not supported: {exchange_controller.__class__}" 

132 raise NotImplementedError(error_msg) 

133 

134 if self.pk: 

135 self.save() 

136 

137 def process_orders(self, testing: bool, no_db_data: Optional[dict] = None) -> list[Order]: 

138 in_simulation = no_db_data is not None 

139 no_db_data = no_db_data or { 

140 "buy_orders": Order.objects.filter( 

141 order__batch__status=ORDER_STATUS.READY, 

142 order__side=SIDES.BUY, 

143 order__batch__controller=self, 

144 order__testing=testing, 

145 ), 

146 "sell_orders": Order.objects.filter( 

147 order__batch__status=ORDER_STATUS.READY, 

148 order__side=SIDES.SELL, 

149 order__batch__controller=self, 

150 order__testing=testing, 

151 ), 

152 "keep_orders": Order.objects.filter( 

153 order__batch__status=ORDER_STATUS.READY, 

154 order__side=SIDES.KEEP, 

155 order__batch__controller=self, 

156 order__testing=testing, 

157 ), 

158 "batches": OrderBatch.objects.filter(status=ORDER_STATUS.READY, batch__controller=self), 

159 "exchange_controller": self.exchange_controller, 

160 "min_trade": self.min_trade, 

161 "price": self.get_price(), 

162 } 

163 

164 aggregated_order = { 

165 "buy_amount": 0, 

166 "sell_amount": 0, 

167 "min_trade": no_db_data["min_trade"], 

168 "price": no_db_data["price"], 

169 "min_notional": self.min_notional, 

170 "pair": self.pair, 

171 } 

172 

173 for order in no_db_data["buy_orders"]: 

174 aggregated_order["buy_amount"] += order.asked_for_amount 

175 for order in no_db_data["sell_orders"]: 

176 aggregated_order["sell_amount"] += order.asked_for_amount 

177 

178 if aggregated_order["buy_amount"] > 0: 

179 for order in no_db_data["buy_orders"]: 

180 order._calculate_batch_share(total=aggregated_order["buy_amount"]) 

181 if aggregated_order["sell_amount"] > 0: 

182 for order in no_db_data["sell_orders"]: 

183 order._calculate_batch_share(total=aggregated_order["sell_amount"]) 

184 for order in no_db_data["keep_orders"]: 

185 order.batch_share = 0 

186 

187 receipt, executed_amounts_buy, executed_amounts_sell, fees_buy, fees_sell = no_db_data["exchange_controller"].submit_order( 

188 controller=self, 

189 aggregated_order=aggregated_order, 

190 testing=in_simulation or testing, 

191 ) 

192 all_orders = [] 

193 for order in no_db_data["buy_orders"]: 

194 order._calculate_exit_amounts( 

195 controller=self, 

196 executed_amounts=executed_amounts_buy, 

197 fees=fees_buy, 

198 ) 

199 all_orders.append(order) 

200 for order in no_db_data["sell_orders"]: 

201 order._calculate_exit_amounts( 

202 controller=self, 

203 executed_amounts=executed_amounts_sell, 

204 fees=fees_sell, 

205 ) 

206 all_orders.append(order) 

207 for order in no_db_data["keep_orders"]: 

208 order._calculate_exit_amounts( 

209 controller=self, 

210 executed_amounts={}, 

211 fees={}, 

212 ) 

213 all_orders.append(order) 

214 

215 for batch in no_db_data["batches"]: 

216 batch._set_status_post_process(receipt=receipt) 

217 

218 return all_orders 

219 

220 def apply_orders(self, orders): 

221 for order in orders: 

222 order.save() 

223 order.apply_swap() 

224 

225 def send_candles_to_bots(self, closed_candle, current_candle) -> list: 

226 """Scan all bots (that are allowed to trade) and get their orders. 

227 

228 Args: 

229 ---- 

230 closed_candle : The candle that just closed. 

231 current_candle : The candle that is currently open. 

232 

233 Returns: 

234 ------- 

235 list: A list of orders. 

236 """ 

237 orders = [] 

238 for bot in self.bots.all().filter(is_simulation=False, fleet__running=True, can_trade=True): 

239 bot = bot.find() 

240 orders.append(bot.give_order(closed_candle, current_candle)) 

241 return orders 

242 

243 @staticmethod 

244 def get_asset_price(exchange_account, base: str, quote: str = "USDT") -> float: 

245 """Get the price of an asset.""" 

246 if base in STABLECOINS[exchange_account.exchange.name]: 

247 return 1 

248 if base + quote not in EXCHANGE_PAIRS[exchange_account.exchange.name]: 

249 error_msg = f"Invalid pair: {base+quote} on {exchange_account.exchange.name}" 

250 raise ControllerError.InvalidPair(error_msg) 

251 controller = Controller.get(exchange_account=exchange_account, base=base, quote=quote, interval="1m") 

252 

253 return float(controller.get_price()) 

254 

255 def _get_price(self) -> float: 

256 """Get the price of the pair. 

257 

258 Always calls the exchange API. (Can be costly) 

259 

260 Returns: 

261 float: The price of the pair. 

262 """ 

263 exchange_controller = self.exchange_controller 

264 if exchange_controller.__class__ == BinanceController: 

265 try: 

266 self.price = float(exchange_controller.client.get_ticker(symbol=self.pair)["lastPrice"]) 

267 self.last_price_update = datetime.now(tz=timezone.utc) 

268 except ReadTimeout: 

269 print("ReadTimeout in _get_price") 

270 except SSLError: 

271 print("SSLError in _get_price") 

272 except ConnectionError: 

273 print("ConnectionError in _get_price") 

274 except Exception as e: 

275 print(f"Exception in _get_price: {e}, {type(e)}") 

276 else: 

277 error_msg = f"Exchange controller not supported: {exchange_controller.__class__}" 

278 raise NotImplementedError(error_msg) 

279 self.save() 

280 

281 def get_price(self) -> float: 

282 """Retreive the price of the pair. 

283 

284 Only updates the price if it is older than 1 minute. 

285 

286 Returns: 

287 price: The price of the pair. 

288 """ 

289 if self.last_price_update is None or self.last_price_update < datetime.now(tz=timezone.utc) - timedelta(minutes=1): 

290 self._get_price() 

291 return self.price 

292 

293 def download( 

294 self, 

295 start_date: datetime, 

296 end_date: datetime, 

297 squash: bool = False, 

298 verbose: int = 0, 

299 ): 

300 return self.exchange_controller.download( 

301 controller=self, 

302 start_date=start_date, 

303 end_date=end_date, 

304 squash=squash, 

305 verbose=verbose, 

306 ) 

307 

308 return f"CANDLE {self.pk}"