dijkies 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dijkies/__init__.py +4 -0
- dijkies/backtest.py +98 -0
- dijkies/credentials.py +6 -0
- dijkies/data_pipeline.py +15 -0
- dijkies/deployment.py +127 -0
- dijkies/evaluate.py +235 -0
- dijkies/exceptions.py +60 -0
- dijkies/exchange_market_api.py +235 -0
- dijkies/executors.py +665 -0
- dijkies/logger.py +16 -0
- dijkies/performance.py +166 -0
- dijkies/strategy.py +68 -0
- dijkies-0.0.3.dist-info/METADATA +191 -0
- dijkies-0.0.3.dist-info/RECORD +15 -0
- dijkies-0.0.3.dist-info/WHEEL +4 -0
dijkies/executors.py
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Literal, Optional, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
import uuid
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from pandas.core.series import Series
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from decimal import Decimal, getcontext, ROUND_DOWN
|
|
17
|
+
from tenacity import retry, wait_fixed, stop_after_attempt
|
|
18
|
+
|
|
19
|
+
from python_bitvavo_api.bitvavo import Bitvavo
|
|
20
|
+
|
|
21
|
+
from dijkies.exceptions import (
|
|
22
|
+
NoOrderFoundError,
|
|
23
|
+
MultipleOrdersFoundError,
|
|
24
|
+
PlaceOrderError,
|
|
25
|
+
GetOrderInfoError,
|
|
26
|
+
CancelOrderError,
|
|
27
|
+
MethodNotDefinedError
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from dijkies.credentials import Credentials
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Order(BaseModel):
|
|
34
|
+
order_id: str
|
|
35
|
+
exchange: Literal["bitvavo"]
|
|
36
|
+
market: str
|
|
37
|
+
time_created: int
|
|
38
|
+
time_canceled: Union[int, None] = None
|
|
39
|
+
time_filled: Union[int, None] = None
|
|
40
|
+
on_hold: float = 0
|
|
41
|
+
side: Literal["buy", "sell"]
|
|
42
|
+
limit_price: Optional[float] = None
|
|
43
|
+
actual_price: Optional[float] = None
|
|
44
|
+
filled: float = 0
|
|
45
|
+
filled_quote: float = 0
|
|
46
|
+
fee: float = 0
|
|
47
|
+
is_taker: bool
|
|
48
|
+
status: Literal["open", "filled", "cancelled"]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_filled(self) -> bool:
|
|
52
|
+
return self.status == "filled"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_open(self) -> bool:
|
|
56
|
+
return self.status == "open"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_cancelled(self) -> bool:
|
|
60
|
+
return self.status == "cancelled"
|
|
61
|
+
|
|
62
|
+
def is_equal(self, order: "Order") -> bool:
|
|
63
|
+
return self.status == order.status
|
|
64
|
+
|
|
65
|
+
def is_not_equal(self, order: "Order") -> bool:
|
|
66
|
+
return not self.is_equal(order)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class State(BaseModel):
|
|
70
|
+
base: str
|
|
71
|
+
total_base: float
|
|
72
|
+
total_quote: float
|
|
73
|
+
orders: list[Order] = []
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def number_of_transactions(self) -> int:
|
|
77
|
+
return len(self.filled_orders)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def total_fee_paid(self) -> float:
|
|
81
|
+
return sum([o.fee for o in self.filled_orders])
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def filled_orders(self) -> list[Order]:
|
|
85
|
+
return [o for o in self.orders if o.is_filled]
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def open_orders(self) -> list[Order]:
|
|
89
|
+
return [o for o in self.orders if o.is_open]
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def cancelled_orders(self) -> list[Order]:
|
|
93
|
+
return [o for o in self.orders if o.is_cancelled]
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def base_on_hold(self) -> float:
|
|
97
|
+
return sum([order.on_hold for order in self.sell_orders])
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def quote_on_hold(self) -> float:
|
|
101
|
+
return sum([order.on_hold for order in self.buy_orders])
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def base_available(self) -> float:
|
|
105
|
+
return self.total_base - self.base_on_hold
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def quote_available(self) -> float:
|
|
109
|
+
return self.total_quote - self.quote_on_hold
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def buy_orders(self) -> list[Order]:
|
|
113
|
+
return [o for o in self.open_orders if o.side == "buy"]
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def sell_orders(self) -> list[Order]:
|
|
117
|
+
return [o for o in self.open_orders if o.side == "sell"]
|
|
118
|
+
|
|
119
|
+
def add_order(self, order: Order) -> None:
|
|
120
|
+
self.orders.append(order)
|
|
121
|
+
|
|
122
|
+
def get_order(self, order_id: str) -> Order:
|
|
123
|
+
list_found_order = [o for o in self.orders if o.order_id == order_id]
|
|
124
|
+
if len(list_found_order) == 0:
|
|
125
|
+
raise NoOrderFoundError(order_id)
|
|
126
|
+
elif len(list_found_order) > 1:
|
|
127
|
+
raise MultipleOrdersFoundError(order_id)
|
|
128
|
+
return list_found_order[0]
|
|
129
|
+
|
|
130
|
+
def cancel_order(self, order: Order) -> None:
|
|
131
|
+
found_order = self.get_order(order.order_id)
|
|
132
|
+
found_order.status = "cancelled"
|
|
133
|
+
|
|
134
|
+
def process_filled_order(self, filled_order: Order) -> None:
|
|
135
|
+
if filled_order.side == "buy":
|
|
136
|
+
quote_mutation = -(filled_order.filled_quote + filled_order.fee)
|
|
137
|
+
base_mutation = filled_order.filled
|
|
138
|
+
else:
|
|
139
|
+
quote_mutation = filled_order.filled_quote - filled_order.fee
|
|
140
|
+
base_mutation = -filled_order.filled
|
|
141
|
+
|
|
142
|
+
self.total_quote += quote_mutation
|
|
143
|
+
self.total_base += base_mutation
|
|
144
|
+
|
|
145
|
+
if filled_order.is_taker:
|
|
146
|
+
self.add_order(filled_order)
|
|
147
|
+
else:
|
|
148
|
+
found_order = self.get_order(filled_order.order_id)
|
|
149
|
+
found_order.status = "filled"
|
|
150
|
+
|
|
151
|
+
self._check_non_negative()
|
|
152
|
+
|
|
153
|
+
def _check_non_negative(self) -> None:
|
|
154
|
+
if self.base_available < -1e-9:
|
|
155
|
+
raise ValueError(f"Negative base balance: {self.base_available}")
|
|
156
|
+
if self.quote_available < -1e-9:
|
|
157
|
+
raise ValueError(f"Negative quote balance: {self.quote_available}")
|
|
158
|
+
|
|
159
|
+
def total_value_in_base(self, price: float) -> float:
|
|
160
|
+
return self.total_base + self.total_quote / price
|
|
161
|
+
|
|
162
|
+
def total_value_in_quote(self, price: float) -> float:
|
|
163
|
+
return self.total_quote + self.total_base * price
|
|
164
|
+
|
|
165
|
+
def fraction_value_in_quote(self, price: float) -> float:
|
|
166
|
+
return self.total_quote / max(self.total_value_in_quote(price), 0.00000001)
|
|
167
|
+
|
|
168
|
+
def fraction_value_in_base(self, price: float) -> float:
|
|
169
|
+
return 1 - self.fraction_value_in_quote(price)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ExchangeAssetClient(ABC):
|
|
173
|
+
def __init__(self, state: State) -> None:
|
|
174
|
+
self.state = state
|
|
175
|
+
|
|
176
|
+
def clear_credentials(self) -> None:
|
|
177
|
+
raise MethodNotDefinedError()
|
|
178
|
+
|
|
179
|
+
def set_credentials(self, credentials: Credentials) -> None:
|
|
180
|
+
raise MethodNotDefinedError()
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def place_limit_buy_order(
|
|
184
|
+
self, base: str, limit_price: float, amount_in_quote: float
|
|
185
|
+
) -> Order:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def place_limit_sell_order(
|
|
190
|
+
self, base: str, limit_price: float, amount_in_base: float
|
|
191
|
+
) -> Order:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def place_market_buy_order(self, base: str, amount_in_quote: float) -> Order:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
def place_market_sell_order(self, base: str, amount_in_base: float) -> Order:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
@abstractmethod
|
|
203
|
+
def get_order_info(self, order: Order) -> Order:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
@abstractmethod
|
|
207
|
+
def cancel_order(self, order: Order) -> Order:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
def update_state(self) -> None:
|
|
211
|
+
for order in self.state.open_orders:
|
|
212
|
+
newest_info_order = self.get_order_info(order)
|
|
213
|
+
if order.is_not_equal(newest_info_order):
|
|
214
|
+
self.state.process_filled_order(newest_info_order)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class BacktestExchangeAssetClient(ExchangeAssetClient):
|
|
218
|
+
def __init__(
|
|
219
|
+
self, state: State, fee_market_order: float, fee_limit_order: float
|
|
220
|
+
) -> None:
|
|
221
|
+
super().__init__(state)
|
|
222
|
+
self.fee_market_order = fee_market_order
|
|
223
|
+
self.fee_limit_order = fee_limit_order
|
|
224
|
+
self.current_candle = pd.Series({"high": 0, "low": 0})
|
|
225
|
+
|
|
226
|
+
def update_current_candle(self, current_candle: Series) -> None:
|
|
227
|
+
self.current_candle = current_candle
|
|
228
|
+
|
|
229
|
+
def place_limit_buy_order(
|
|
230
|
+
self, base: str, limit_price: float, amount_in_quote: float
|
|
231
|
+
) -> Order:
|
|
232
|
+
order = Order(
|
|
233
|
+
order_id=str(uuid.uuid4()),
|
|
234
|
+
exchange="bitvavo",
|
|
235
|
+
time_created=int(time.time()),
|
|
236
|
+
market=base,
|
|
237
|
+
side="buy",
|
|
238
|
+
limit_price=limit_price,
|
|
239
|
+
on_hold=amount_in_quote,
|
|
240
|
+
status="open",
|
|
241
|
+
is_taker=False,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self.state.add_order(order)
|
|
245
|
+
|
|
246
|
+
return order
|
|
247
|
+
|
|
248
|
+
def place_limit_sell_order(
|
|
249
|
+
self, base: str, limit_price: float, amount_in_base: float
|
|
250
|
+
) -> Order:
|
|
251
|
+
order = Order(
|
|
252
|
+
order_id=str(uuid.uuid4()),
|
|
253
|
+
exchange="bitvavo",
|
|
254
|
+
time_created=int(time.time()),
|
|
255
|
+
market=base,
|
|
256
|
+
side="sell",
|
|
257
|
+
limit_price=limit_price,
|
|
258
|
+
on_hold=amount_in_base,
|
|
259
|
+
status="open",
|
|
260
|
+
is_taker=False,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.state.add_order(order)
|
|
264
|
+
|
|
265
|
+
return order
|
|
266
|
+
|
|
267
|
+
def place_market_buy_order(self, base: str, amount_in_quote: float) -> Order:
|
|
268
|
+
fee = amount_in_quote * self.fee_market_order / (1 + self.fee_market_order)
|
|
269
|
+
amount_in_base = (amount_in_quote - fee) / self.current_candle.close
|
|
270
|
+
|
|
271
|
+
order = Order(
|
|
272
|
+
order_id=str(uuid.uuid4()),
|
|
273
|
+
exchange="bitvavo",
|
|
274
|
+
time_created=int(time.time()),
|
|
275
|
+
market=base,
|
|
276
|
+
side="buy",
|
|
277
|
+
filled=amount_in_base,
|
|
278
|
+
filled_quote=amount_in_quote - fee,
|
|
279
|
+
status="filled",
|
|
280
|
+
fee=fee,
|
|
281
|
+
is_taker=True,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
self.state.process_filled_order(order)
|
|
285
|
+
|
|
286
|
+
return order
|
|
287
|
+
|
|
288
|
+
def place_market_sell_order(self, base: str, amount_in_base: float) -> Order:
|
|
289
|
+
amount_in_quote = amount_in_base * self.current_candle.close
|
|
290
|
+
fee = amount_in_quote * self.fee_market_order
|
|
291
|
+
|
|
292
|
+
order = Order(
|
|
293
|
+
order_id=str(uuid.uuid4()),
|
|
294
|
+
exchange="bitvavo",
|
|
295
|
+
time_created=int(time.time()),
|
|
296
|
+
market=base,
|
|
297
|
+
side="sell",
|
|
298
|
+
filled=amount_in_base,
|
|
299
|
+
filled_quote=amount_in_quote,
|
|
300
|
+
status="filled",
|
|
301
|
+
fee=fee,
|
|
302
|
+
is_taker=True,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
self.state.process_filled_order(order)
|
|
306
|
+
|
|
307
|
+
return order
|
|
308
|
+
|
|
309
|
+
def get_order_info(self, order: Order) -> Order:
|
|
310
|
+
found_order = self.state.get_order(order.order_id)
|
|
311
|
+
if found_order.status == "open":
|
|
312
|
+
is_filled = (
|
|
313
|
+
found_order.side == "buy"
|
|
314
|
+
and found_order.limit_price >= self.current_candle.low
|
|
315
|
+
) or (
|
|
316
|
+
found_order.side == "sell"
|
|
317
|
+
and found_order.limit_price <= self.current_candle.high
|
|
318
|
+
)
|
|
319
|
+
if is_filled:
|
|
320
|
+
return self.fill_open_order(found_order)
|
|
321
|
+
return found_order
|
|
322
|
+
|
|
323
|
+
def cancel_order(self, order: Order) -> Order:
|
|
324
|
+
self.state.cancel_order(order)
|
|
325
|
+
return order
|
|
326
|
+
|
|
327
|
+
def fill_open_order(self, order: Order) -> Order:
|
|
328
|
+
fee_limit_order = self.fee_limit_order
|
|
329
|
+
if order.status != "open":
|
|
330
|
+
raise ValueError("only open orders can be filled")
|
|
331
|
+
if order.side == "buy":
|
|
332
|
+
fee = order.on_hold * fee_limit_order / (1 + fee_limit_order)
|
|
333
|
+
filled_quote = order.on_hold - fee
|
|
334
|
+
filled = filled_quote / order.limit_price # type: ignore
|
|
335
|
+
else:
|
|
336
|
+
filled = order.on_hold
|
|
337
|
+
filled_quote = order.on_hold * order.limit_price # type: ignore
|
|
338
|
+
fee = filled_quote * fee_limit_order
|
|
339
|
+
return Order(
|
|
340
|
+
order_id=order.order_id,
|
|
341
|
+
exchange=order.exchange,
|
|
342
|
+
time_created=order.time_created,
|
|
343
|
+
market=order.market,
|
|
344
|
+
side=order.side,
|
|
345
|
+
limit_price=order.limit_price,
|
|
346
|
+
on_hold=0,
|
|
347
|
+
status="filled",
|
|
348
|
+
is_taker=False,
|
|
349
|
+
fee=fee,
|
|
350
|
+
filled_quote=filled_quote,
|
|
351
|
+
filled=filled,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def order_from_bitvavo_response(response: dict) -> Order:
|
|
356
|
+
return Order(
|
|
357
|
+
exchange="bitvavo",
|
|
358
|
+
order_id=response["orderId"],
|
|
359
|
+
market=response["market"],
|
|
360
|
+
time_created=int(response["created"]),
|
|
361
|
+
time_canceled=None,
|
|
362
|
+
time_filled=(
|
|
363
|
+
max([int(fill["timestamp"]) for fill in response["fills"]])
|
|
364
|
+
if len(response["fills"]) > 0
|
|
365
|
+
else None
|
|
366
|
+
),
|
|
367
|
+
on_hold=float(response["onHold"]),
|
|
368
|
+
side=response["side"],
|
|
369
|
+
limit_price=float(response["price"]) if "price" in response else None,
|
|
370
|
+
actual_price=(
|
|
371
|
+
float(response["filledAmountQuote"]) / float(response["filledAmount"])
|
|
372
|
+
if float(response["filledAmount"]) > 0
|
|
373
|
+
else None
|
|
374
|
+
),
|
|
375
|
+
filled=float(response["filledAmount"]),
|
|
376
|
+
filled_quote=float(response["filledAmountQuote"]),
|
|
377
|
+
fee=float(response["feePaid"]),
|
|
378
|
+
is_taker=response["fills"][0]["taker"] if response["fills"] else False,
|
|
379
|
+
status=(
|
|
380
|
+
response["status"]
|
|
381
|
+
if response["status"] in ["filled", "cancelled"]
|
|
382
|
+
else "open"
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
388
|
+
max_fee = 0.0025
|
|
389
|
+
|
|
390
|
+
def __init__(
|
|
391
|
+
self,
|
|
392
|
+
state: State,
|
|
393
|
+
bitvavo_api_key: str,
|
|
394
|
+
bitvavo_api_secret_key: str,
|
|
395
|
+
operator_id: int,
|
|
396
|
+
logger: logging.Logger
|
|
397
|
+
) -> None:
|
|
398
|
+
super().__init__(state)
|
|
399
|
+
self.operator_id = operator_id
|
|
400
|
+
self.bitvavo = Bitvavo(
|
|
401
|
+
{
|
|
402
|
+
"APIKEY": bitvavo_api_key,
|
|
403
|
+
"APISECRET": bitvavo_api_secret_key,
|
|
404
|
+
"RESTURL": "https://api.bitvavo.com/v2",
|
|
405
|
+
"WSURL": "wss://ws.bitvavo.com/v2/",
|
|
406
|
+
"ACCESSWINDOW": 10000,
|
|
407
|
+
"DEBUGGING": False,
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
self.logger = logger
|
|
411
|
+
|
|
412
|
+
def set_credentials(self, api_key: str, api_secret_key: str) -> None:
|
|
413
|
+
self.bitvavo.APIKEY = api_key
|
|
414
|
+
self.bitvavo.APISECRET = api_secret_key
|
|
415
|
+
|
|
416
|
+
def clear_credentials(self) -> None:
|
|
417
|
+
self.bitvavo.APIKEY = ""
|
|
418
|
+
self.bitvavo.APISECRET = ""
|
|
419
|
+
|
|
420
|
+
def quantity_decimals(self, base: str) -> int:
|
|
421
|
+
trading_pair = base + "-EUR"
|
|
422
|
+
return self.bitvavo.markets(
|
|
423
|
+
{'market': trading_pair}
|
|
424
|
+
)['quantityDecimals']
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def __closest_valid_price(price: float) -> float:
|
|
428
|
+
getcontext().prec = 20
|
|
429
|
+
price = Decimal(str(price))
|
|
430
|
+
x = 0
|
|
431
|
+
|
|
432
|
+
ten = Decimal("10")
|
|
433
|
+
|
|
434
|
+
if price > 1:
|
|
435
|
+
while price / (ten**x) > 1:
|
|
436
|
+
x += 1
|
|
437
|
+
else:
|
|
438
|
+
while price / (ten**x) < 1:
|
|
439
|
+
x -= 1
|
|
440
|
+
x += 1
|
|
441
|
+
|
|
442
|
+
shifted = price / (ten**x)
|
|
443
|
+
rounded = shifted.quantize(Decimal("1.00000"), rounding=ROUND_DOWN)
|
|
444
|
+
corrected = rounded * (ten**x)
|
|
445
|
+
|
|
446
|
+
return float(corrected)
|
|
447
|
+
|
|
448
|
+
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
449
|
+
def place_limit_buy_order(
|
|
450
|
+
self, base: str, limit_price: float, amount_in_quote: float
|
|
451
|
+
) -> Order:
|
|
452
|
+
trading_pair = base + "-EUR"
|
|
453
|
+
limit_price = self.__closest_valid_price(price=float(limit_price))
|
|
454
|
+
|
|
455
|
+
amount_in_base = round(
|
|
456
|
+
(float(amount_in_quote) - 0.01) / (limit_price * (1 + self.max_fee)),
|
|
457
|
+
self.quantity_decimals(base),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
response = self.bitvavo.placeOrder(
|
|
461
|
+
market=trading_pair,
|
|
462
|
+
side="buy",
|
|
463
|
+
orderType="limit",
|
|
464
|
+
body={
|
|
465
|
+
"amount": str(amount_in_base),
|
|
466
|
+
"price": str(limit_price),
|
|
467
|
+
"operatorId": self.operator_id,
|
|
468
|
+
},
|
|
469
|
+
)
|
|
470
|
+
if "errorCode" in response:
|
|
471
|
+
true_balance = self.bitvavo.balance({"symbol": "EUR"})[0]
|
|
472
|
+
error_message = \
|
|
473
|
+
f"""
|
|
474
|
+
failed to place limit buy-order\n\n
|
|
475
|
+
|
|
476
|
+
trading_pair: {trading_pair}\n
|
|
477
|
+
limit_price: {limit_price}\n
|
|
478
|
+
amount_in_quote: {amount_in_quote}\n\n
|
|
479
|
+
available: {true_balance['available']}\n\n
|
|
480
|
+
in orders: {true_balance['inOrder']}\n\n
|
|
481
|
+
|
|
482
|
+
error: \n\n
|
|
483
|
+
|
|
484
|
+
{response}
|
|
485
|
+
"""
|
|
486
|
+
self.logger.error(error_message)
|
|
487
|
+
raise PlaceOrderError(error_message)
|
|
488
|
+
|
|
489
|
+
self.logger.info(f"limit order placed: {json.dumps(response)}")
|
|
490
|
+
order = order_from_bitvavo_response(response)
|
|
491
|
+
self.state.add_order(order)
|
|
492
|
+
return order
|
|
493
|
+
|
|
494
|
+
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
495
|
+
def place_limit_sell_order(
|
|
496
|
+
self, base: str, limit_price: float, amount_in_base: float
|
|
497
|
+
) -> Order:
|
|
498
|
+
trading_pair = base + "-EUR"
|
|
499
|
+
|
|
500
|
+
quantity_decimals = self.quantity_decimals(base)
|
|
501
|
+
|
|
502
|
+
factor = 1 / (10 ** quantity_decimals)
|
|
503
|
+
amount_in_base = round(
|
|
504
|
+
(float(amount_in_base) // factor) * factor,
|
|
505
|
+
quantity_decimals,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
limit_price = self.__closest_valid_price(price=float(limit_price))
|
|
509
|
+
|
|
510
|
+
response = self.bitvavo.placeOrder(
|
|
511
|
+
market=trading_pair,
|
|
512
|
+
side="sell",
|
|
513
|
+
orderType="limit",
|
|
514
|
+
body={
|
|
515
|
+
"amount": str(amount_in_base),
|
|
516
|
+
"price": str(limit_price),
|
|
517
|
+
"operatorId": self.operator_id,
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
if "errorCode" in response:
|
|
521
|
+
true_balance = self.bitvavo.balance({"symbol": base})[0]
|
|
522
|
+
error_message = \
|
|
523
|
+
f"""
|
|
524
|
+
failed to place limit buy-order\n\n
|
|
525
|
+
|
|
526
|
+
trading_pair: {trading_pair}\n
|
|
527
|
+
limit_price: {limit_price}\n
|
|
528
|
+
amount_in_base: {amount_in_base}\n\n
|
|
529
|
+
available: {true_balance['available']}\n\n
|
|
530
|
+
in orders: {true_balance['inOrder']}\n\n
|
|
531
|
+
|
|
532
|
+
error: \n\n
|
|
533
|
+
|
|
534
|
+
{response}
|
|
535
|
+
"""
|
|
536
|
+
self.logger.error(error_message)
|
|
537
|
+
raise PlaceOrderError(error_message)
|
|
538
|
+
|
|
539
|
+
self.logger.info(f"limit order placed: {json.dumps(response)}")
|
|
540
|
+
order = order_from_bitvavo_response(response)
|
|
541
|
+
self.state.add_order(order)
|
|
542
|
+
return order
|
|
543
|
+
|
|
544
|
+
# @retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
545
|
+
def place_market_buy_order(
|
|
546
|
+
self, base: str, amount_in_quote: float
|
|
547
|
+
) -> Order:
|
|
548
|
+
trading_pair = base + "-EUR"
|
|
549
|
+
|
|
550
|
+
amount_in_quote = str(round(float(amount_in_quote), 2))
|
|
551
|
+
|
|
552
|
+
response = self.bitvavo.placeOrder(
|
|
553
|
+
market=trading_pair,
|
|
554
|
+
side="buy",
|
|
555
|
+
orderType="market",
|
|
556
|
+
body={"amountQuote": amount_in_quote, "operatorId": self.operator_id},
|
|
557
|
+
)
|
|
558
|
+
if "errorCode" in response:
|
|
559
|
+
|
|
560
|
+
error_message = \
|
|
561
|
+
f"""
|
|
562
|
+
failed to place market buy-order\n\n
|
|
563
|
+
trading_pair: {trading_pair}\n
|
|
564
|
+
amount_in_quote: {amount_in_quote}\n\n
|
|
565
|
+
error:\n\n
|
|
566
|
+
{response}
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
self.logger.error(error_message)
|
|
570
|
+
raise PlaceOrderError(error_message)
|
|
571
|
+
|
|
572
|
+
self.logger.info(f"market buy order placed: {json.dumps(response)}")
|
|
573
|
+
order = order_from_bitvavo_response(response)
|
|
574
|
+
time.sleep(3)
|
|
575
|
+
order = self.get_order_info(order)
|
|
576
|
+
self.state.process_filled_order(order)
|
|
577
|
+
return order
|
|
578
|
+
|
|
579
|
+
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
580
|
+
def place_market_sell_order(
|
|
581
|
+
self, base: str, amount_in_base: float
|
|
582
|
+
) -> Order:
|
|
583
|
+
trading_pair = base + "-EUR"
|
|
584
|
+
quantity_decimals = self.quantity_decimals(base)
|
|
585
|
+
|
|
586
|
+
factor = 1 / (10 ** quantity_decimals)
|
|
587
|
+
amount_in_base = round(
|
|
588
|
+
(float(amount_in_base) // factor) * factor,
|
|
589
|
+
quantity_decimals,
|
|
590
|
+
)
|
|
591
|
+
response = self.bitvavo.placeOrder(
|
|
592
|
+
market=trading_pair,
|
|
593
|
+
side="sell",
|
|
594
|
+
orderType="market",
|
|
595
|
+
body={"amount": str(amount_in_base), "operatorId": self.operator_id},
|
|
596
|
+
)
|
|
597
|
+
if "errorCode" in response:
|
|
598
|
+
error_message = \
|
|
599
|
+
f"""
|
|
600
|
+
failed to place market sell-order\n\n
|
|
601
|
+
trading_pair: {trading_pair}\n
|
|
602
|
+
amount_in_base: {amount_in_base}\n\n
|
|
603
|
+
error:\n\n
|
|
604
|
+
{response}
|
|
605
|
+
"""
|
|
606
|
+
self.logger.error(error_message)
|
|
607
|
+
raise PlaceOrderError(error_message)
|
|
608
|
+
|
|
609
|
+
self.logger.info(f"market sell order placed: {json.dumps(response)}")
|
|
610
|
+
order = order_from_bitvavo_response(response)
|
|
611
|
+
time.sleep(3)
|
|
612
|
+
order = self.get_order_info(order)
|
|
613
|
+
self.state.process_filled_order(order)
|
|
614
|
+
return order
|
|
615
|
+
|
|
616
|
+
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
617
|
+
def get_order_info(
|
|
618
|
+
self, order: Order
|
|
619
|
+
) -> Order:
|
|
620
|
+
|
|
621
|
+
response = self.bitvavo.getOrder(
|
|
622
|
+
market=order.market, orderId=order.order_id
|
|
623
|
+
)
|
|
624
|
+
if "errorCode" in response:
|
|
625
|
+
error_message = \
|
|
626
|
+
f"""
|
|
627
|
+
failed to retrieve order information\n\n
|
|
628
|
+
order: {order.order_id}\n
|
|
629
|
+
trading_pair: {order.market}\n\n
|
|
630
|
+
error: \n\n
|
|
631
|
+
|
|
632
|
+
{response}
|
|
633
|
+
"""
|
|
634
|
+
self.logger.exception(error_message)
|
|
635
|
+
raise GetOrderInfoError(response)
|
|
636
|
+
return order_from_bitvavo_response(response)
|
|
637
|
+
|
|
638
|
+
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
639
|
+
def cancel_order(self, order: Order) -> Order:
|
|
640
|
+
response = self.bitvavo.cancelOrder(
|
|
641
|
+
market=order.market,
|
|
642
|
+
orderId=order.order_id,
|
|
643
|
+
operatorId=self.operator_id,
|
|
644
|
+
)
|
|
645
|
+
if "errorCode" in response:
|
|
646
|
+
error_message = \
|
|
647
|
+
f"""
|
|
648
|
+
failed to cancel order\n\n
|
|
649
|
+
trading_pair: {order.market}\n
|
|
650
|
+
order_id: {order.order_id}\n\n
|
|
651
|
+
error:\n\n
|
|
652
|
+
|
|
653
|
+
{response}
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
self.logger.exception(error_message)
|
|
657
|
+
|
|
658
|
+
if not (
|
|
659
|
+
response["errorCode"] == 240 and
|
|
660
|
+
"No active order found." in response["error"]
|
|
661
|
+
):
|
|
662
|
+
raise CancelOrderError(response)
|
|
663
|
+
|
|
664
|
+
self.logger.info(f"order cancelled: {order.order_id}")
|
|
665
|
+
self.state.cancel_order(order)
|
dijkies/logger.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_logger() -> logging.Logger:
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
if not logger.handlers: # Prevent adding multiple handlers in interactive use
|
|
9
|
+
handler = logging.StreamHandler()
|
|
10
|
+
handler.setFormatter(
|
|
11
|
+
logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
12
|
+
)
|
|
13
|
+
logger.addHandler(handler)
|
|
14
|
+
|
|
15
|
+
logger.setLevel(logging.INFO)
|
|
16
|
+
return logger
|