dijkies 0.0.3__py3-none-any.whl → 0.1.1__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/executors.py CHANGED
@@ -1,38 +1,29 @@
1
+ import logging
2
+ import time
3
+ import uuid
1
4
  from abc import ABC, abstractmethod
5
+ from decimal import ROUND_DOWN, Decimal, getcontext
2
6
  from typing import Literal, Optional, Union
3
7
 
4
- from pydantic import BaseModel
5
-
6
- import uuid
7
- import time
8
-
9
8
  import pandas as pd
10
9
  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
-
10
+ from pydantic import BaseModel
19
11
  from python_bitvavo_api.bitvavo import Bitvavo
20
12
 
21
13
  from dijkies.exceptions import (
22
- NoOrderFoundError,
23
- MultipleOrdersFoundError,
24
- PlaceOrderError,
25
14
  GetOrderInfoError,
26
- CancelOrderError,
27
- MethodNotDefinedError
15
+ InsufficientBalanceError,
16
+ InsufficientOrderValueError,
17
+ MultipleOrdersFoundError,
18
+ NoOrderFoundError,
28
19
  )
29
20
 
30
- from dijkies.credentials import Credentials
21
+ SUPPORTED_EXCHANGES = Literal["bitvavo", "backtest"]
31
22
 
32
23
 
33
24
  class Order(BaseModel):
34
25
  order_id: str
35
- exchange: Literal["bitvavo"]
26
+ exchange: SUPPORTED_EXCHANGES
36
27
  market: str
37
28
  time_created: int
38
29
  time_canceled: Union[int, None] = None
@@ -173,12 +164,6 @@ class ExchangeAssetClient(ABC):
173
164
  def __init__(self, state: State) -> None:
174
165
  self.state = state
175
166
 
176
- def clear_credentials(self) -> None:
177
- raise MethodNotDefinedError()
178
-
179
- def set_credentials(self, credentials: Credentials) -> None:
180
- raise MethodNotDefinedError()
181
-
182
167
  @abstractmethod
183
168
  def place_limit_buy_order(
184
169
  self, base: str, limit_price: float, amount_in_quote: float
@@ -221,7 +206,7 @@ class BacktestExchangeAssetClient(ExchangeAssetClient):
221
206
  super().__init__(state)
222
207
  self.fee_market_order = fee_market_order
223
208
  self.fee_limit_order = fee_limit_order
224
- self.current_candle = pd.Series({"high": 0, "low": 0})
209
+ self.current_candle = pd.Series({"high": 80000, "low": 78000, "close": 79000})
225
210
 
226
211
  def update_current_candle(self, current_candle: Series) -> None:
227
212
  self.current_candle = current_candle
@@ -336,6 +321,7 @@ class BacktestExchangeAssetClient(ExchangeAssetClient):
336
321
  filled = order.on_hold
337
322
  filled_quote = order.on_hold * order.limit_price # type: ignore
338
323
  fee = filled_quote * fee_limit_order
324
+ order.fee = fee
339
325
  return Order(
340
326
  order_id=order.order_id,
341
327
  exchange=order.exchange,
@@ -393,7 +379,7 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
393
379
  bitvavo_api_key: str,
394
380
  bitvavo_api_secret_key: str,
395
381
  operator_id: int,
396
- logger: logging.Logger
382
+ logger: logging.Logger,
397
383
  ) -> None:
398
384
  super().__init__(state)
399
385
  self.operator_id = operator_id
@@ -409,19 +395,9 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
409
395
  )
410
396
  self.logger = logger
411
397
 
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
398
  def quantity_decimals(self, base: str) -> int:
421
399
  trading_pair = base + "-EUR"
422
- return self.bitvavo.markets(
423
- {'market': trading_pair}
424
- )['quantityDecimals']
400
+ return self.bitvavo.markets({"market": trading_pair})["quantityDecimals"]
425
401
 
426
402
  @staticmethod
427
403
  def __closest_valid_price(price: float) -> float:
@@ -445,7 +421,14 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
445
421
 
446
422
  return float(corrected)
447
423
 
448
- @retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
424
+ def get_balance(self, base: str) -> dict[str, float]:
425
+ balance = self.bitvavo.balance({"symbol": base})
426
+ if balance:
427
+ balance = balance[0]
428
+ else:
429
+ balance = {"available": 0, "inOrder": 0}
430
+ return balance
431
+
449
432
  def place_limit_buy_order(
450
433
  self, base: str, limit_price: float, amount_in_quote: float
451
434
  ) -> Order:
@@ -468,30 +451,23 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
468
451
  },
469
452
  )
470
453
  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
454
+ error_code = response["errorCode"]
455
+ if error_code in [107, 108, 109]:
456
+ time.sleep(3)
457
+ order = self.place_limit_buy_order(base, limit_price, amount_in_quote)
458
+ return order
459
+ elif error_code == 216:
460
+ balance = self.get_balance(base)
461
+ raise InsufficientBalanceError(balance, amount_in_base)
462
+ elif error_code == 217:
463
+ raise InsufficientOrderValueError()
464
+ else:
465
+ raise Exception(str(response))
481
466
 
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
467
  order = order_from_bitvavo_response(response)
491
468
  self.state.add_order(order)
492
469
  return order
493
470
 
494
- @retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
495
471
  def place_limit_sell_order(
496
472
  self, base: str, limit_price: float, amount_in_base: float
497
473
  ) -> Order:
@@ -499,7 +475,7 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
499
475
 
500
476
  quantity_decimals = self.quantity_decimals(base)
501
477
 
502
- factor = 1 / (10 ** quantity_decimals)
478
+ factor = 1 / (10**quantity_decimals)
503
479
  amount_in_base = round(
504
480
  (float(amount_in_base) // factor) * factor,
505
481
  quantity_decimals,
@@ -518,33 +494,24 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
518
494
  },
519
495
  )
520
496
  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
497
+ error_code = response["errorCode"]
498
+ if error_code in [107, 108, 109]:
499
+ time.sleep(3)
500
+ order = self.place_limit_sell_order(base, limit_price, amount_in_base)
501
+ return order
502
+ elif error_code == 216:
503
+ balance = self.get_balance(base)
504
+ raise InsufficientBalanceError(balance, amount_in_base)
505
+ elif error_code == 217:
506
+ raise InsufficientOrderValueError()
507
+ else:
508
+ raise Exception(str(response))
531
509
 
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
510
  order = order_from_bitvavo_response(response)
541
511
  self.state.add_order(order)
542
512
  return order
543
513
 
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:
514
+ def place_market_buy_order(self, base: str, amount_in_quote: float) -> Order:
548
515
  trading_pair = base + "-EUR"
549
516
 
550
517
  amount_in_quote = str(round(float(amount_in_quote), 2))
@@ -556,34 +523,30 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
556
523
  body={"amountQuote": amount_in_quote, "operatorId": self.operator_id},
557
524
  )
558
525
  if "errorCode" in response:
526
+ error_code = response["errorCode"]
527
+ if error_code in [107, 108, 109]:
528
+ time.sleep(3)
529
+ order = self.place_market_buy_order(base, amount_in_quote)
530
+ return order
531
+ elif error_code == 216:
532
+ balance = self.get_balance("EUR")
533
+ raise InsufficientBalanceError(balance, amount_in_quote)
534
+ elif error_code == 217:
535
+ raise InsufficientOrderValueError()
536
+ else:
537
+ raise Exception(str(response))
559
538
 
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
539
  order = order_from_bitvavo_response(response)
574
540
  time.sleep(3)
575
541
  order = self.get_order_info(order)
576
542
  self.state.process_filled_order(order)
577
543
  return order
578
544
 
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:
545
+ def place_market_sell_order(self, base: str, amount_in_base: float) -> Order:
583
546
  trading_pair = base + "-EUR"
584
547
  quantity_decimals = self.quantity_decimals(base)
585
548
 
586
- factor = 1 / (10 ** quantity_decimals)
549
+ factor = 1 / (10**quantity_decimals)
587
550
  amount_in_base = round(
588
551
  (float(amount_in_base) // factor) * factor,
589
552
  quantity_decimals,
@@ -595,47 +558,33 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
595
558
  body={"amount": str(amount_in_base), "operatorId": self.operator_id},
596
559
  )
597
560
  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)}")
561
+ error_code = response["errorCode"]
562
+ if error_code in [107, 108, 109]:
563
+ time.sleep(3)
564
+ order = self.place_market_sell_order(base, amount_in_base)
565
+ return order
566
+ elif error_code == 216:
567
+ balance = self.get_balance(base)
568
+ raise InsufficientBalanceError(balance, amount_in_base)
569
+ elif error_code == 217:
570
+ raise InsufficientOrderValueError()
571
+ else:
572
+ raise Exception(str(response))
573
+
610
574
  order = order_from_bitvavo_response(response)
611
575
  time.sleep(3)
612
576
  order = self.get_order_info(order)
613
577
  self.state.process_filled_order(order)
614
578
  return order
615
579
 
616
- @retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
617
- def get_order_info(
618
- self, order: Order
619
- ) -> Order:
580
+ def get_order_info(self, order: Order) -> Order:
620
581
 
621
- response = self.bitvavo.getOrder(
622
- market=order.market, orderId=order.order_id
623
- )
582
+ response = self.bitvavo.getOrder(market=order.market, orderId=order.order_id)
624
583
  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)
584
+ if response["errorCode"] == 240:
585
+ raise GetOrderInfoError(response)
636
586
  return order_from_bitvavo_response(response)
637
587
 
638
- @retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
639
588
  def cancel_order(self, order: Order) -> Order:
640
589
  response = self.bitvavo.cancelOrder(
641
590
  market=order.market,
@@ -643,23 +592,6 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
643
592
  operatorId=self.operator_id,
644
593
  )
645
594
  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}")
595
+ if response["errorCode"] != 240:
596
+ raise GetOrderInfoError(response)
665
597
  self.state.cancel_order(order)
dijkies/performance.py CHANGED
@@ -1,12 +1,10 @@
1
- from abc import ABC, abstractmethod
2
-
3
- import numpy as np
4
- from pandas.core.series import Series as PandasSeries
5
-
6
1
  import uuid
2
+ from abc import ABC, abstractmethod
7
3
  from datetime import datetime
8
4
 
5
+ import numpy as np
9
6
  from pandas.core.series import Series
7
+ from pandas.core.series import Series as PandasSeries
10
8
  from pydantic import BaseModel
11
9
 
12
10
  from dijkies.executors import State
dijkies/strategy.py CHANGED
@@ -1,10 +1,20 @@
1
1
  import inspect
2
2
  from abc import ABC, abstractmethod
3
+ from datetime import datetime, timedelta
3
4
 
5
+ import pandas as pd
4
6
  from pandas.core.frame import DataFrame as PandasDataFrame
5
7
 
6
- from dijkies.executors import ExchangeAssetClient
7
8
  from dijkies.data_pipeline import DataPipeline
9
+ from dijkies.exceptions import (
10
+ DataTimeWindowShorterThanSuggestedAnalysisWindowError,
11
+ InvalidExchangeAssetClientError,
12
+ InvalidTypeForTimeColumnError,
13
+ MissingOHLCVColumnsError,
14
+ TimeColumnNotDefinedError,
15
+ )
16
+ from dijkies.executors import BacktestExchangeAssetClient, ExchangeAssetClient
17
+ from dijkies.performance import PerformanceInformationRow
8
18
 
9
19
 
10
20
  class Strategy(ABC):
@@ -56,13 +66,64 @@ class Strategy(ABC):
56
66
  def analysis_dataframe_size_in_minutes(self) -> int:
57
67
  pass
58
68
 
59
- @property
60
- @abstractmethod
61
- def exchange(self) -> str:
62
- pass
63
-
64
69
  def get_data_pipeline(self) -> DataPipeline:
65
70
  """
66
71
  implement this method for deployement
67
72
  """
68
73
  raise NotImplementedError()
74
+
75
+ def backtest(self, data: PandasDataFrame) -> PandasDataFrame:
76
+ """
77
+ This method runs the backtest. It expects data, this should have the following properties:
78
+ """
79
+
80
+ # validate args
81
+
82
+ if "time" not in data.columns:
83
+ raise TimeColumnNotDefinedError()
84
+
85
+ if not pd.api.types.is_datetime64_any_dtype(data.time):
86
+ raise InvalidTypeForTimeColumnError()
87
+
88
+ lookback_in_min = self.analysis_dataframe_size_in_minutes
89
+ timespan_data_in_min = (data.time.max() - data.time.min()).total_seconds() / 60
90
+
91
+ if lookback_in_min > timespan_data_in_min:
92
+ raise DataTimeWindowShorterThanSuggestedAnalysisWindowError()
93
+
94
+ if not {"open", "high", "low", "close", "volume"}.issubset(data.columns):
95
+ raise MissingOHLCVColumnsError()
96
+
97
+ if not isinstance(self.executor, BacktestExchangeAssetClient):
98
+ raise InvalidExchangeAssetClientError()
99
+
100
+ start_time = data.iloc[0].time + timedelta(minutes=lookback_in_min)
101
+ simulation_df: PandasDataFrame = data.loc[data.time >= start_time]
102
+ start_candle = simulation_df.iloc[0]
103
+ start_value_in_quote = self.state.total_value_in_quote(start_candle.open)
104
+ result: list[PerformanceInformationRow] = []
105
+
106
+ def get_analysis_df(
107
+ data: PandasDataFrame, current_time: datetime, look_back_in_min: int
108
+ ) -> PandasDataFrame:
109
+ start_analysis_df = current_time - timedelta(minutes=look_back_in_min)
110
+
111
+ analysis_df = data.loc[
112
+ (data.time >= start_analysis_df) & (data.time <= current_time)
113
+ ]
114
+
115
+ return analysis_df
116
+
117
+ for _, candle in simulation_df.iterrows():
118
+ analysis_df = get_analysis_df(data, candle.time, lookback_in_min)
119
+ self.executor.update_current_candle(candle)
120
+
121
+ self.run(analysis_df)
122
+
123
+ result.append(
124
+ PerformanceInformationRow.from_objects(
125
+ candle, start_candle, self.state, start_value_in_quote
126
+ )
127
+ )
128
+
129
+ return pd.DataFrame([r.model_dump() for r in result])