Skip to content

Commit 097e251

Browse files
committed
Fix datetime formatting and support configuration
1 parent 3da38ac commit 097e251

File tree

13 files changed

+107
-45
lines changed

13 files changed

+107
-45
lines changed

examples/backtest/algorithm/strategy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def apply_strategy(self, algorithm: Algorithm, market_data):
7171
fast = ti.sma(df['Close'].to_numpy(), self.fast)
7272
slow = ti.sma(df['Close'].to_numpy(), self.slow)
7373
trend = ti.sma(df['Close'].to_numpy(), self.trend)
74-
price = ticker_data['bid']
74+
price = ticker_data["bid"]
7575

7676
if not algorithm.has_position(target_symbol) \
7777
and is_crossover(fast, slow) \

investing_algorithm_framework/app/app.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ def _initialize_app_for_backtest(
279279
market_data_source.to_backtest_market_data_source()
280280
for market_data_source in market_data_sources
281281
]
282+
283+
for market_data_source in backtest_market_data_sources:
284+
market_data_source.config = self.config
285+
282286
self.container.market_data_source_service.override(
283287
BacktestMarketDataSourceService(
284288
market_data_sources=backtest_market_data_sources,
@@ -787,6 +791,7 @@ def run_backtests(
787791
return reports
788792

789793
def add_market_data_source(self, market_data_source):
794+
market_data_source.config = self.config
790795
self._market_data_source_service.add(market_data_source)
791796

792797
def add_market_credential(self, market_credential: MarketCredential):

investing_algorithm_framework/dependency_container.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class DependencyContainer(containers.DeclarativeContainer):
6868
)
6969
order_service = providers.Factory(
7070
OrderService,
71+
configuration_service=configuration_service,
7172
order_repository=order_repository,
7273
order_fee_repository=order_fee_repository,
7374
portfolio_repository=portfolio_repository,
@@ -125,6 +126,7 @@ class DependencyContainer(containers.DeclarativeContainer):
125126
)
126127
backtest_service = providers.Factory(
127128
BacktestService,
129+
configuration_service=configuration_service,
128130
order_service=order_service,
129131
portfolio_repository=portfolio_repository,
130132
performance_service=performance_service,

investing_algorithm_framework/domain/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class Config(dict):
7777
SQLITE_INITIALIZED = False
7878
BACKTEST_DATA_DIRECTORY_NAME = "backtest_data"
7979
SYMBOLS = None
80+
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
8081

8182
def __init__(self, resource_directory=None):
8283
super().__init__()

investing_algorithm_framework/domain/models/order/order.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22

33
from dateutil.parser import parse
4-
from dateutil.tz import gettz
54

65
from investing_algorithm_framework.domain.exceptions import \
76
OperationalException
@@ -308,10 +307,7 @@ def from_ccxt_order(ccxt_order):
308307
remaining=ccxt_order.get("remaining", None),
309308
cost=ccxt_order.get("cost", None),
310309
fee=OrderFee.from_ccxt_fee(ccxt_order.get("fee", None)),
311-
created_at=parse(
312-
ccxt_order.get("datetime", None),
313-
tzinfos={"UTC": gettz("UTC")}
314-
)
310+
created_at=parse(ccxt_order.get("datetime", None))
315311
)
316312

317313
def __repr__(self):

investing_algorithm_framework/domain/services/market_data_sources.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ def __init__(
3434
self._backtest_data_start_date = backtest_data_start_date
3535
self._backtest_data_index_date = backtest_data_index_date
3636

37+
@property
38+
def config(self):
39+
return self._config
40+
41+
@config.setter
42+
def config(self, value):
43+
self._config = value
44+
3745
def _data_source_exists(self, file_path):
3846
"""
3947
Function to check if the data source exists.
@@ -61,8 +69,6 @@ def _data_source_exists(self, file_path):
6169

6270
return True
6371
except Exception as e:
64-
logger.error(f"Error reading {file_path}")
65-
logger.error(e)
6672
return False
6773

6874
def write_data_to_file_path(self, data_file, data):
@@ -151,10 +157,10 @@ def backtest_data_index_date(self, value):
151157
class MarketDataSource(ABC):
152158

153159
def __init__(
154-
self,
155-
identifier,
156-
market,
157-
symbol,
160+
self,
161+
identifier,
162+
market,
163+
symbol,
158164
):
159165
self._identifier = identifier
160166
self._market = market
@@ -168,6 +174,14 @@ def initialize(self, config):
168174
def identifier(self):
169175
return self._identifier
170176

177+
@property
178+
def config(self):
179+
return self._config
180+
181+
@config.setter
182+
def config(self, value):
183+
self._config = value
184+
171185
def get_identifier(self):
172186
return self.identifier
173187

investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
2+
import logging
23
import os
34
from datetime import timedelta
45

56
import polars
7+
from dateutil import parser
68

79
from investing_algorithm_framework.domain import RESOURCE_DIRECTORY, \
810
BACKTEST_DATA_DIRECTORY_NAME, DATETIME_FORMAT_BACKTESTING, \
@@ -59,7 +61,7 @@ def __init__(
5961
start_date_func=start_date_func,
6062
end_date=end_date,
6163
end_date_func=end_date_func,
62-
window_size=window_size
64+
window_size=window_size,
6365
)
6466

6567
def prepare_data(
@@ -80,7 +82,6 @@ def prepare_data(
8082
When downloading the data it will use the ccxt library.
8183
"""
8284
# Calculating the backtest data start date
83-
8485
difference = self.end_date - self.start_date
8586
total_minutes = 0
8687

@@ -127,7 +128,10 @@ def prepare_data(
127128
)
128129

129130
# Get the OHLCV data from the ccxt market service
130-
market_service = CCXTMarketService(self.market_credential_service)
131+
market_service = CCXTMarketService(
132+
market_credential_service=self.market_credential_service,
133+
)
134+
market_service.config = config
131135
ohlcv = market_service.get_ohlcv(
132136
symbol=self.symbol,
133137
time_frame=self.timeframe,
@@ -171,6 +175,7 @@ def get_data(self, backtest_index_date, **kwargs):
171175
from_timestamp = backtest_index_date - timedelta(
172176
minutes=self.total_minutes_timeframe
173177
)
178+
datetime_format = self._config["DATETIME_FORMAT"]
174179
self.backtest_data_index_date = backtest_index_date\
175180
.replace(microsecond=0)
176181
from_timestamp = from_timestamp.replace(microsecond=0)
@@ -193,8 +198,8 @@ def get_data(self, backtest_index_date, **kwargs):
193198
file_path, columns=self.column_names, separator=","
194199
)
195200
df = df.filter(
196-
(df['Datetime'] >= from_timestamp.strftime(DATETIME_FORMAT))
197-
& (df['Datetime'] <= to_timestamp.strftime(DATETIME_FORMAT))
201+
(df['Datetime'] >= from_timestamp.strftime(datetime_format))
202+
& (df['Datetime'] <= to_timestamp.strftime(datetime_format))
198203
)
199204
return df
200205

@@ -209,6 +214,9 @@ def empty(self):
209214
def file_name(self):
210215
return self._create_file_path().split("/")[-1]
211216

217+
def write_data_to_file_path(self, data_file, data: polars.DataFrame):
218+
data.write_csv(data_file)
219+
212220

213221
class CCXTTickerBacktestMarketDataSource(
214222
TickerMarketDataSource, BacktestMarketDataSource
@@ -304,7 +312,10 @@ def prepare_data(
304312
)
305313

306314
# Get the OHLCV data from the ccxt market service
307-
market_service = CCXTMarketService(self.market_credential_service)
315+
market_service = CCXTMarketService(
316+
market_credential_service=self.market_credential_service
317+
)
318+
market_service.config = config
308319
ohlcv = market_service.get_ohlcv(
309320
symbol=self.symbol,
310321
time_frame=self.timeframe,
@@ -363,13 +374,12 @@ def get_data(self, **kwargs):
363374
# Filter the data based on the backtest index date and the end date
364375
df = polars.read_csv(file_path)
365376
df = df.filter(
366-
(df['Datetime'] >= backtest_index_date
367-
.strftime(DATETIME_FORMAT))
377+
(df['Datetime'] >= backtest_index_date.strftime(DATETIME_FORMAT))
368378
)
369-
370379
first_row = df.head(1)[0]
380+
first_row_datetime = parser.parse(first_row["Datetime"][0])
371381

372-
if first_row["Datetime"][0] > end_date.strftime(DATETIME_FORMAT):
382+
if first_row_datetime > end_date:
373383
logger.warning(
374384
f"No ticker data available for the given backtest "
375385
f"index date {backtest_index_date} and symbol {self.symbol} "
@@ -386,12 +396,17 @@ def get_data(self, **kwargs):
386396
"datetime": first_row["Datetime"][0],
387397
}
388398

399+
def write_data_to_file_path(self, data_file, data: polars.DataFrame):
400+
data.write_csv(data_file)
401+
389402

390403
class CCXTOHLCVMarketDataSource(OHLCVMarketDataSource):
391404

392405
def get_data(self, **kwargs):
393-
market_service = CCXTMarketService(self.market_credential_service)
394-
406+
market_service = CCXTMarketService(
407+
market_credential_service=self.market_credential_service,
408+
)
409+
market_service.config = self.config
395410
if self.start_date is None:
396411
raise OperationalException(
397412
"Either start_date or start_date_func should be set "
@@ -422,7 +437,10 @@ def to_backtest_market_data_source(self) -> BacktestMarketDataSource:
422437
class CCXTOrderBookMarketDataSource(OrderBookMarketDataSource):
423438

424439
def get_data(self, **kwargs):
425-
market_service = CCXTMarketService(self.market_credential_service)
440+
market_service = CCXTMarketService(
441+
market_credential_service=self.market_credential_service
442+
)
443+
market_service.config = self.config
426444
return market_service.get_order_book(
427445
symbol=self.symbol, market=self.market
428446
)
@@ -438,7 +456,8 @@ def __init__(
438456
identifier,
439457
market,
440458
symbol=None,
441-
backtest_timeframe=None
459+
backtest_timeframe=None,
460+
442461
):
443462
super().__init__(
444463
identifier=identifier,
@@ -448,7 +467,10 @@ def __init__(
448467
self._backtest_timeframe = backtest_timeframe
449468

450469
def get_data(self, **kwargs):
451-
market_service = CCXTMarketService(self.market_credential_service)
470+
market_service = CCXTMarketService(
471+
market_credential_service=self.market_credential_service
472+
)
473+
market_service.config = self.config
452474

453475
if self.market is None:
454476

investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import logging
2-
from typing import Dict
32
from datetime import datetime
43
from time import sleep
5-
import polars as pl
4+
from typing import Dict
5+
66
import ccxt
7+
import polars as pl
78
from dateutil import parser
8-
from dateutil.tz import gettz
99

1010
from investing_algorithm_framework.domain import OperationalException, Order, \
11-
CCXT_DATETIME_FORMAT, MarketService
11+
MarketService
1212

1313
logger = logging.getLogger(__name__)
1414

@@ -20,6 +20,19 @@ class CCXTMarketService(MarketService):
2020
msec = 1000
2121
minute = 60 * msec
2222

23+
def __init__(self, market_credential_service):
24+
super(CCXTMarketService, self).__init__(
25+
market_credential_service=market_credential_service,
26+
)
27+
28+
@property
29+
def config(self):
30+
return self._config
31+
32+
@config.setter
33+
def config(self, config):
34+
self._config = config
35+
2336
def initialize_exchange(self, market, market_credential):
2437
market = market.lower()
2538
if not hasattr(ccxt, market):
@@ -149,6 +162,7 @@ def get_order(self, order, market):
149162
def get_orders(self, symbol, market, since: datetime = None):
150163
market_credential = self.get_market_credential(market)
151164
exchange = self.initialize_exchange(market, market_credential)
165+
datetime_format = self.config["DATETIME_FORMAT"]
152166

153167
if not exchange.has['fetchOrders']:
154168
raise OperationalException(
@@ -157,7 +171,7 @@ def get_orders(self, symbol, market, since: datetime = None):
157171
)
158172

159173
if since is not None:
160-
since = exchange.parse8601(since.strftime(":%Y-%m-%d %H:%M:%S"))
174+
since = exchange.parse8601(datetime_format)
161175

162176
try:
163177
ccxt_orders = exchange.fetchOrders(symbol, since=since)
@@ -343,6 +357,7 @@ def get_closed_orders(
343357
def get_ohlcv(
344358
self, symbol, time_frame, from_timestamp, market, to_timestamp=None
345359
) -> pl.DataFrame:
360+
datetime_format = self.config["DATETIME_FORMAT"]
346361
market_credential = self.get_market_credential(market)
347362
exchange = self.initialize_exchange(market, market_credential)
348363

@@ -353,14 +368,14 @@ def get_ohlcv(
353368
)
354369

355370
from_time_stamp = exchange.parse8601(
356-
from_timestamp.strftime(CCXT_DATETIME_FORMAT)
371+
from_timestamp.strftime(datetime_format)
357372
)
358373

359374
if to_timestamp is None:
360375
to_timestamp = exchange.milliseconds()
361376
else:
362377
to_timestamp = exchange.parse8601(
363-
to_timestamp.strftime(CCXT_DATETIME_FORMAT)
378+
to_timestamp.strftime(datetime_format)
364379
)
365380
data = []
366381

@@ -374,17 +389,16 @@ def get_ohlcv(
374389
from_time_stamp = to_timestamp
375390

376391
for candle in ohlcv:
377-
datetime_stamp = parser.parse(
378-
exchange.iso8601(candle[0]),
379-
tzinfos={"UTC": gettz("UTC")}
392+
datetime_stamp = parser.parse(exchange.iso8601(candle[0]))
380393

381-
)
382394
to_timestamp_datetime = parser.parse(
383395
exchange.iso8601(to_timestamp),
384-
tzinfos={"UTC": gettz("UTC")}
385396
)
386397

387398
if datetime_stamp <= to_timestamp_datetime:
399+
datetime_stamp = datetime_stamp\
400+
.strftime(datetime_format)
401+
388402
data.append([datetime_stamp] + candle[1:])
389403

390404
sleep(exchange.rateLimit / 1000)

0 commit comments

Comments
 (0)