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
|
@@ -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"])
|