Coverage for django_napse/core/models/orders/order.py: 76%
172 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-12 13:49 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-12 13:49 +0000
1from typing import Optional
3from django.db import models
5from django_napse.core.models.orders.managers import OrderManager
6from django_napse.core.models.transactions.credit import Credit
7from django_napse.core.models.transactions.debit import Debit
8from django_napse.core.models.transactions.transaction import Transaction
9from django_napse.utils.constants import EXCHANGE_PAIRS, MODIFICATION_STATUS, ORDER_STATUS, SIDES, TRANSACTION_TYPES
10from django_napse.utils.errors import OrderError
13class OrderBatch(models.Model):
14 status = models.CharField(default=ORDER_STATUS.PENDING, max_length=15)
15 controller = models.ForeignKey("Controller", on_delete=models.CASCADE, related_name="order_batches")
17 def __str__(self) -> str:
18 return f"ORDER BATCH {self.pk}"
20 def set_status_ready(self):
21 if self.status == ORDER_STATUS.PENDING:
22 self.status = ORDER_STATUS.READY
23 self.save()
24 else:
25 error_msg = f"Order {self.pk} is not pending."
26 raise OrderError.StatusError(error_msg)
28 def _set_status_post_process(self, receipt: dict) -> None:
29 if self.status != ORDER_STATUS.READY:
30 error_msg = f"Order {self.pk} is not ready."
31 raise OrderError.StatusError(error_msg)
32 buy_failed = False
33 sell_failed = False
34 if "error" in receipt[SIDES.BUY]:
35 buy_failed = True
36 if "error" in receipt[SIDES.SELL]:
37 sell_failed = True
38 if buy_failed and sell_failed:
39 self.status = ORDER_STATUS.FAILED
40 elif buy_failed:
41 self.status = ORDER_STATUS.ONLY_SELL_PASSED
42 elif sell_failed:
43 self.status = ORDER_STATUS.ONLY_BUY_PASSED
44 else:
45 self.status = ORDER_STATUS.PASSED
48class Order(models.Model):
49 batch = models.ForeignKey("OrderBatch", on_delete=models.CASCADE, related_name="orders")
50 connection = models.ForeignKey("Connection", on_delete=models.CASCADE, related_name="orders")
51 price = models.FloatField()
52 pair = models.CharField(max_length=10)
53 side = models.CharField(max_length=10)
54 completed = models.BooleanField(default=False)
56 asked_for_amount = models.FloatField()
57 asked_for_ticker = models.CharField(max_length=10)
59 debited_amount = models.FloatField(default=0)
61 batch_share = models.FloatField(default=0)
62 exit_amount_base = models.FloatField(default=0)
63 exit_amount_quote = models.FloatField(default=0)
64 fees = models.FloatField(default=0)
65 fee_ticker = models.CharField(max_length=10, blank=True)
67 created_at = models.DateTimeField(auto_now_add=True)
69 objects = OrderManager()
71 def __str__(self):
72 return f"ORDER: {self.pk=}"
74 def info(self, verbose=True, beacon=""):
75 string = ""
76 string += f"{beacon}Order ({self.pk=}):\n"
77 string += f"{beacon}Args:\n"
78 string += f"{beacon}\t{self.connection=}\n"
79 string += f"{beacon}\t{self.pair=}\n"
80 string += f"{beacon}\t{self.asked_for_amount=}\n"
81 string += f"{beacon}\t{self.asked_for_ticker=}\n"
82 string += f"{beacon}\t{self.debited_amount=}\n"
83 string += f"{beacon}\t{self.batch_share=}\n"
84 string += f"{beacon}\t{self.exit_amount_base=}\n"
85 string += f"{beacon}\t{self.exit_amount_quote=}\n"
86 string += f"{beacon}\t{self.fees=}\n"
87 string += f"{beacon}\t{self.fee_ticker=}\n"
88 string += f"{beacon}\t{self.side=}\n"
89 string += f"{beacon}\t{self.price=}\n"
90 string += f"{beacon}\t{self.completed=}\n"
92 new_beacon = beacon + "\t"
93 string += f"{beacon}Wallet:\n"
94 string += f"{beacon}{self.wallet.info(verbose=False, beacon=new_beacon)}\n"
95 string += f"{beacon}Transactions in:\n"
96 if self.wallet.transactions_to.all().count() == 0:
97 string += f"{beacon}\tNone.\n"
98 else:
99 for transaction in self.wallet.transactions_to.all():
100 trans_str = transaction.info(verbose=False, beacon=new_beacon)
101 string += f"{trans_str}\n"
102 string += f"{beacon}Transactions out:\n"
103 if self.wallet.transactions_from.all().count() == 0:
104 string += f"{beacon}\tNone.\n"
105 else:
106 for transaction in self.wallet.transactions_from.all():
107 trans_str = transaction.info(verbose=False, beacon=new_beacon)
108 string += f"{trans_str}\n"
109 if verbose: # pragma: no cover
110 print(string)
111 return string
113 @property
114 def testing(self):
115 return self.connection.testing
117 @property
118 def exchange_account(self):
119 return self.connection.wallet.exchange_account
121 def _calculate_exit_amounts(self, controller, executed_amounts: dict, fees: dict) -> None:
122 if self.batch_share != 0:
123 if self.side == SIDES.BUY:
124 if executed_amounts == {}:
125 self.exit_amount_base = 0
126 self.exit_amount_quote = self.debited_amount
127 self.fees = 0
128 self.fee_ticker = controller.base
129 else:
130 self.exit_amount_base = executed_amounts[controller.base] * self.batch_share
131 self.exit_amount_quote = self.debited_amount + executed_amounts[controller.quote] * self.batch_share
132 self.fees = fees[controller.base] * self.batch_share
133 self.fee_ticker = controller.base
134 elif self.side == SIDES.SELL:
135 if executed_amounts == {}:
136 self.exit_amount_base = self.debited_amount
137 self.exit_amount_quote = 0
138 self.fees = 0
139 self.fee_ticker = controller.quote
140 else:
141 self.exit_amount_base = self.debited_amount + executed_amounts[controller.base] * self.batch_share
142 self.exit_amount_quote = executed_amounts[controller.quote] * self.batch_share
143 self.fees = fees[controller.quote] * self.batch_share
144 self.fee_ticker = controller.quote
145 else:
146 error_msg = f"Souldn't be calculating exit amount for order {self.pk} with side {self.side}."
147 raise OrderError.ProcessError(error_msg)
148 else:
149 self.exit_amount_base = 0
150 self.exit_amount_quote = 0
151 self.fees = 0
152 self.fee_ticker = controller.base
154 def _calculate_batch_share(self, total: float):
155 self.batch_share = self.asked_for_amount / total
157 def passed(self, batch: Optional[OrderBatch] = None):
158 batch = batch or self.batch
159 if (self.side == SIDES.BUY and (batch.status == ORDER_STATUS.PASSED or batch.status == ORDER_STATUS.ONLY_BUY_PASSED)) or (
160 self.side == SIDES.SELL and (batch.status == ORDER_STATUS.PASSED or batch.status == ORDER_STATUS.ONLY_SELL_PASSED)
161 ):
162 return True
163 return False
165 def _apply_modifications(self, batch, modifications, **kwargs):
166 all_modifications = []
167 all_modified_objects = []
168 if self.passed(batch):
169 for modification in modifications:
170 modified_object, modification_object = modification._apply(order=self, **kwargs)
171 all_modifications.append(modification_object)
172 all_modified_objects.append(modified_object)
173 else:
174 for modification in [modification for modification in modifications if modification.ignore_failed_order]:
175 modified_object, modification_object = modification._apply(order=self, **kwargs)
176 all_modifications.append(modification_object)
177 all_modified_objects.append(modified_object)
178 for modification in [modification for modification in modifications if not modification.ignore_failed_order]:
179 modification.status = MODIFICATION_STATUS.REJECTED
180 all_modifications.append(modification)
181 return all_modifications, all_modified_objects
183 def apply_modifications(self):
184 modifications, modified_objects = self._apply_modifications(
185 batch=self.batch,
186 modifications=[modification.find() for modification in self.modifications.all()],
187 strategy=self.connection.bot.strategy.find(),
188 architecture=self.connection.bot.architecture.find(),
189 currencies=self.connection.wallet.to_dict()["currencies"],
190 )
191 for modification in modifications:
192 modification.save()
193 for modified_object in modified_objects:
194 modified_object.save()
195 return modifications
197 def apply_swap(self):
198 if self.side == SIDES.BUY:
199 Debit.objects.create(
200 wallet=self.wallet,
201 amount=self.debited_amount - self.exit_amount_quote,
202 ticker=self.batch.controller.quote,
203 )
204 Credit.objects.create(
205 wallet=self.wallet,
206 amount=self.exit_amount_base,
207 ticker=self.batch.controller.base,
208 )
209 elif self.side == SIDES.SELL:
210 Debit.objects.create(
211 wallet=self.wallet,
212 amount=self.debited_amount - self.exit_amount_base,
213 ticker=self.batch.controller.base,
214 )
215 Credit.objects.create(
216 wallet=self.wallet,
217 amount=self.exit_amount_quote,
218 ticker=self.batch.controller.quote,
219 )
221 def process_payout(self):
222 if self.side == SIDES.KEEP:
223 return
224 if self.passed():
225 Transaction.objects.create(
226 from_wallet=self.wallet,
227 to_wallet=self.connection.wallet,
228 amount=self.exit_amount_base,
229 ticker=self.batch.controller.base,
230 transaction_type=TRANSACTION_TYPES.ORDER_PAYOUT,
231 )
232 Transaction.objects.create(
233 from_wallet=self.wallet,
234 to_wallet=self.connection.wallet,
235 amount=self.exit_amount_quote,
236 ticker=self.batch.controller.quote,
237 transaction_type=TRANSACTION_TYPES.ORDER_PAYOUT,
238 )
239 else:
240 Transaction.objects.create(
241 from_wallet=self.wallet,
242 to_wallet=self.connection.wallet,
243 amount=self.exit_amount_base,
244 ticker=self.batch.controller.base,
245 transaction_type=TRANSACTION_TYPES.ORDER_REFUND,
246 )
247 Transaction.objects.create(
248 from_wallet=self.wallet,
249 to_wallet=self.connection.wallet,
250 amount=self.exit_amount_quote,
251 ticker=self.batch.controller.quote,
252 transaction_type=TRANSACTION_TYPES.ORDER_REFUND,
253 )
255 def tickers_info(self) -> dict[str, str]:
256 """Give informations about received, spent & fee tickers."""
257 spent_ticker = EXCHANGE_PAIRS[self.connection.space.exchange_account.exchange.name][self.pair]["base" if self.side == SIDES.SELL else "quote"]
258 received_ticker = EXCHANGE_PAIRS[self.connection.space.exchange_account.exchange.name][self.pair][
259 "quote" if self.side == SIDES.SELL else "base"
260 ]
261 fee_ticker = self.fee_ticker
263 return {
264 "spent_ticker": spent_ticker,
265 "received_ticker": received_ticker,
266 "fee_ticker": fee_ticker,
267 }