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.
@@ -0,0 +1,235 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from pandas.core.frame import DataFrame as PandasDataFrame
4
+
5
+ import logging
6
+
7
+ import pandas as pd
8
+ import requests
9
+ from binance.client import Client
10
+
11
+ import threading
12
+ import time
13
+ from concurrent.futures import ThreadPoolExecutor, as_completed
14
+
15
+ from python_bitvavo_api.bitvavo import Bitvavo
16
+
17
+ from dijkies.logger import get_logger
18
+
19
+
20
+ class ExchangeMarketAPI(ABC):
21
+ @abstractmethod
22
+ def get_candles(
23
+ self,
24
+ base: str,
25
+ interval_in_minutes: int,
26
+ lookback_in_minutes: int,
27
+ ) -> PandasDataFrame:
28
+ pass
29
+
30
+ @abstractmethod
31
+ def get_price(self, base: str) -> float:
32
+ pass
33
+
34
+
35
+ class BinanceMarketAPI(ExchangeMarketAPI):
36
+ def __init__(
37
+ self,
38
+ logger: logging.Logger = get_logger(),
39
+ ):
40
+ self.logger = logger
41
+ self.binance_data_client = Client()
42
+
43
+ def get_candles(
44
+ self,
45
+ base: str = "BTC",
46
+ interval_in_minutes: int = 60,
47
+ lookback_in_minutes: int = 24 * 62 * 60,
48
+ ) -> PandasDataFrame:
49
+ trading_pair = base + "USDT"
50
+ interval = f"{int(interval_in_minutes / 60)}h"
51
+ lookback = f"{lookback_in_minutes} min ago UTC"
52
+ df = pd.DataFrame(
53
+ self.binance_data_client.get_historical_klines(
54
+ symbol=trading_pair, interval=interval, start_str=lookback
55
+ )
56
+ )
57
+
58
+ df = df.iloc[:, :6]
59
+ df.columns = ["time", "open", "high", "low", "close", "volume"]
60
+ df.time = pd.to_datetime(df.time, unit="ms", utc=True)
61
+ df[["open", "high", "low", "close", "volume"]] = df[
62
+ ["open", "high", "low", "close", "volume"]
63
+ ].astype(float)
64
+
65
+ return df
66
+
67
+ def get_price(self, base: str = "BTC") -> float:
68
+ trading_pair = base + "USDT"
69
+ key = f"https://api.binance.com/api/v3/ticker/price?symbol={trading_pair}"
70
+ data = requests.get(key)
71
+ data = data.json()
72
+ return float(data["price"]) # type: ignore
73
+
74
+
75
+ class BitvavoMarketAPI(ExchangeMarketAPI):
76
+ def __init__(
77
+ self,
78
+ logger: logging.Logger = get_logger(),
79
+ max_workers: int = 4,
80
+ rate_limit_threshold: int = 50,
81
+ ):
82
+ self.logger = logger
83
+ self.bitvavo_data_client = Bitvavo()
84
+ self.max_workers = max_workers
85
+ self.rate_limit_threshold = rate_limit_threshold
86
+ self._lock = threading.Lock() # shared rate-limit lock
87
+
88
+ def _wait_if_rate_limited(self):
89
+ """Check Bitvavo remaining limit and sleep if it's too low."""
90
+ remaining = self.bitvavo_data_client.getRemainingLimit()
91
+ if remaining < self.rate_limit_threshold:
92
+ self.logger.warning(
93
+ f"💤 Rate limit low ({remaining}). Sleeping 60s to recover."
94
+ )
95
+ time.sleep(60)
96
+
97
+ def _fetch_candle_chunk(
98
+ self, trading_pair: str, interval: str, start: int, end: int
99
+ ) -> PandasDataFrame:
100
+ """Fetch a single chunk of candles and return as DataFrame."""
101
+ options = {"start": f"{start}", "end": f"{end}"}
102
+
103
+ with self._lock:
104
+ # Ensure only one thread checks/sleeps for rate limit at a time
105
+ self._wait_if_rate_limited()
106
+
107
+ try:
108
+ candles = self.bitvavo_data_client.candles(trading_pair, interval, options)
109
+ except Exception as e:
110
+ self.logger.error(f"⚠️ Error fetching candles ({start}-{end}): {e}")
111
+ return pd.DataFrame(
112
+ [], columns=["time", "open", "high", "low", "close", "volume"]
113
+ )
114
+
115
+ if not candles:
116
+ return pd.DataFrame(
117
+ [], columns=["time", "open", "high", "low", "close", "volume"]
118
+ )
119
+
120
+ df = pd.DataFrame(
121
+ candles, columns=["time", "open", "high", "low", "close", "volume"]
122
+ )
123
+ df.time = pd.to_datetime(df.time, unit="ms", utc=True)
124
+ df[["open", "high", "low", "close", "volume"]] = df[
125
+ ["open", "high", "low", "close", "volume"]
126
+ ].astype(float)
127
+ df = df.iloc[::-1].reset_index(drop=True)
128
+ return df
129
+
130
+ def get_candles(
131
+ self,
132
+ base: str = "BTC",
133
+ interval_in_minutes: int = 60,
134
+ lookback_in_minutes: int = 60 * 24 * 60,
135
+ ) -> PandasDataFrame:
136
+ trading_pair = base + "-EUR"
137
+
138
+ # Determine correct Bitvavo interval
139
+ if interval_in_minutes < 3:
140
+ corrected_interval = 1
141
+ interval = "1m"
142
+ elif interval_in_minutes < 10:
143
+ corrected_interval = 5
144
+ interval = "5m"
145
+ elif interval_in_minutes < 23:
146
+ corrected_interval = 15
147
+ interval = "15m"
148
+ elif interval_in_minutes < 45:
149
+ corrected_interval = 30
150
+ interval = "30m"
151
+ elif interval_in_minutes < 90:
152
+ corrected_interval = 60
153
+ interval = "1h"
154
+ elif interval_in_minutes < 180:
155
+ corrected_interval = 120
156
+ interval = "2h"
157
+ elif interval_in_minutes < 300:
158
+ corrected_interval = 240
159
+ interval = "4h"
160
+ elif interval_in_minutes < 420:
161
+ corrected_interval = 360
162
+ interval = "6h"
163
+ elif interval_in_minutes < 600:
164
+ corrected_interval = 480
165
+ interval = "8h"
166
+ elif interval_in_minutes < 1080:
167
+ corrected_interval = 720
168
+ interval = "12h"
169
+ elif interval_in_minutes < 4 * 1440:
170
+ corrected_interval = 1440
171
+ interval = "1d"
172
+ elif interval_in_minutes < 19 * 1440:
173
+ corrected_interval = 1440 * 7
174
+ interval = "1W"
175
+ else:
176
+ corrected_interval = 1440 * 30
177
+ interval = "1M"
178
+
179
+ now = int(time.time() * 1000)
180
+ start = now - lookback_in_minutes * 60000
181
+
182
+ # Split into chunks
183
+ chunk_size_minutes = (
184
+ corrected_interval * 1440
185
+ ) # 1440 x interval minutes per chunk
186
+ chunks = []
187
+ s = start
188
+ while s < now:
189
+ e = s + chunk_size_minutes * 60000
190
+ chunks.append((s, e))
191
+ s = e - corrected_interval * 60000 # small overlap to prevent gaps
192
+
193
+ self.logger.info(
194
+ f"""
195
+ 📮 Fetching {len(chunks)} chunks in parallel (interval={interval},
196
+ max_workers={self.max_workers})
197
+ """
198
+ )
199
+
200
+ results = []
201
+ with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
202
+ future_to_range = {
203
+ executor.submit(
204
+ self._fetch_candle_chunk, trading_pair, interval, s, e
205
+ ): (s, e)
206
+ for (s, e) in chunks
207
+ }
208
+
209
+ for future in as_completed(future_to_range):
210
+ s, e = future_to_range[future]
211
+ try:
212
+ df = future.result()
213
+ results.append(df)
214
+ except Exception as exc:
215
+ self.logger.error(f"❌ Chunk {s}-{e} generated an exception: {exc}")
216
+
217
+ if not results:
218
+ return pd.DataFrame(
219
+ [], columns=["time", "open", "high", "low", "close", "volume"]
220
+ )
221
+
222
+ all_df = (
223
+ pd.concat(results)
224
+ .drop_duplicates()
225
+ .sort_values("time")
226
+ .reset_index(drop=True)
227
+ )
228
+ self.logger.info(f"✅ Retrieved {len(all_df)} candles successfully.")
229
+ return all_df
230
+
231
+ def get_price(self, base: str = "BTC") -> float:
232
+ trading_pair = base + "-EUR"
233
+ price = self.bitvavo_data_client.tickerPrice({"market": trading_pair})
234
+
235
+ return float(price["price"])