Skip to content

Commit e85080f

Browse files
committed
Fix backtesting portfolio configuration
1 parent 27bbd5b commit e85080f

File tree

21 files changed

+807
-523
lines changed

21 files changed

+807
-523
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from dotenv import load_dotenv
2+
3+
from investing_algorithm_framework import create_app, PortfolioConfiguration, \
4+
TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \
5+
CCXTTickerMarketDataSource, MarketCredential, AzureBlobStorageStateHandler
6+
7+
load_dotenv()
8+
9+
# Define market data sources OHLCV data for candles
10+
bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource(
11+
identifier="BTC-ohlcv",
12+
market="BITVAVO",
13+
symbol="BTC/EUR",
14+
time_frame="2h",
15+
window_size=200
16+
)
17+
# Ticker data for orders, trades and positions
18+
bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource(
19+
identifier="BTC-ticker",
20+
market="BITVAVO",
21+
symbol="BTC/EUR",
22+
)
23+
app = create_app(state_handler=AzureBlobStorageStateHandler())
24+
app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h)
25+
algorithm = Algorithm()
26+
app.add_market_credential(MarketCredential(market="bitvavo"))
27+
app.add_portfolio_configuration(
28+
PortfolioConfiguration(
29+
market="bitvavo",
30+
trading_symbol="EUR",
31+
initial_balance=20
32+
)
33+
)
34+
app.add_algorithm(algorithm)
35+
36+
@algorithm.strategy(
37+
# Run every two hours
38+
time_unit=TimeUnit.HOUR,
39+
interval=2,
40+
# Specify market data sources that need to be passed to the strategy
41+
market_data_sources=[bitvavo_btc_eur_ticker, "BTC-ohlcv"]
42+
)
43+
def perform_strategy(algorithm: Algorithm, market_data: dict):
44+
# By default, ohlcv data is passed as polars df in the form of
45+
# {"<identifier>": <dataframe>} https://pola.rs/,
46+
# call to_pandas() to convert to pandas
47+
polars_df = market_data["BTC-ohlcv"]
48+
print(f"I have access to {len(polars_df)} candles of ohlcv data")

investing_algorithm_framework/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
pretty_print_backtest_reports_evaluation, load_backtest_reports, \
1212
RESERVED_BALANCES, APP_MODE, AppMode, DATETIME_FORMAT, \
1313
load_backtest_report, BacktestDateRange, convert_polars_to_pandas, \
14-
DateRange, get_backtest_report, DEFAULT_LOGGING_CONFIG
14+
DateRange, get_backtest_report, DEFAULT_LOGGING_CONFIG, \
15+
BacktestReport
1516
from investing_algorithm_framework.infrastructure import \
1617
CCXTOrderBookMarketDataSource, CCXTOHLCVMarketDataSource, \
1718
CCXTTickerMarketDataSource, CSVOHLCVMarketDataSource, \
@@ -88,5 +89,6 @@
8889
"is_divergence",
8990
"get_backtest_report",
9091
"AzureBlobStorageStateHandler",
91-
"DEFAULT_LOGGING_CONFIG"
92+
"DEFAULT_LOGGING_CONFIG",
93+
"BacktestReport"
9294
]

investing_algorithm_framework/app/algorithm.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,14 @@ def name(self):
163163
def data_sources(self):
164164
return self._data_sources
165165

166-
@property
167-
def identifier(self):
168-
"""
169-
Function to get a config instance. This allows users when
170-
having access to the algorithm instance also to read the
171-
configs of the app.
172-
"""
173-
return self.configuration_service.config
174-
175166
@property
176167
def config(self):
177168
"""
178169
Function to get a config instance. This allows users when
179170
having access to the algorithm instance also to read the
180171
configs of the app.
181172
"""
182-
return self.configuration_service.config
173+
return self.configuration_service.get_config()
183174

184175
@property
185176
def description(self):
@@ -489,6 +480,16 @@ def get_portfolio(self, market=None) -> Portfolio:
489480

490481
return self.portfolio_service.find({{"market": market}})
491482

483+
def get_portfolios(self):
484+
"""
485+
Function to get all portfolios of the algorithm. This function
486+
will return all portfolios of the algorithm.
487+
488+
Returns:
489+
List[Portfolio]: A list of all portfolios of the algorithm
490+
"""
491+
return self.portfolio_service.get_all()
492+
492493
def get_unallocated(self, market=None) -> float:
493494
"""
494495
Function to get the unallocated balance of the portfolio. This
@@ -834,7 +835,7 @@ def get_position_percentage_of_portfolio(
834835
return (position.amount * ticker["bid"] / total) * 100
835836

836837
def get_position_percentage_of_portfolio_by_net_size(
837-
self, symbol, market=None, identifier=None
838+
self, symbol, market=None, identifier=None
838839
) -> float:
839840
"""
840841
Returns the percentage of the portfolio that is allocated to a

investing_algorithm_framework/app/app.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,9 @@ def _initialize_algorithm_for_backtest(self, algorithm):
498498
if portfolio_configuration_service.count() == 0:
499499
raise OperationalException("No portfolios configured")
500500

501+
# Initialize all portfolios that are registered
502+
503+
501504
strategy_orchestrator_service = \
502505
self.container.strategy_orchestrator_service()
503506
market_credential_service = self.container.market_credential_service()
@@ -518,17 +521,6 @@ def _initialize_algorithm_for_backtest(self, algorithm):
518521
trade_service=self.container.trade_service(),
519522
)
520523

521-
# Create all portfolios
522-
portfolio_configuration_service = self.container \
523-
.portfolio_configuration_service()
524-
portfolio_configurations = portfolio_configuration_service.get_all()
525-
portfolio_service = self.container.portfolio_service()
526-
527-
for portfolio_configuration in portfolio_configurations:
528-
portfolio_service.create_portfolio_from_configuration(
529-
portfolio_configuration
530-
)
531-
532524
def run(
533525
self,
534526
payload: dict = None,
@@ -774,20 +766,24 @@ def get_portfolio_configurations(self):
774766

775767
def run_backtest(
776768
self,
777-
algorithm,
778769
backtest_date_range: BacktestDateRange,
770+
initial_amount=None,
779771
pending_order_check_interval=None,
780-
output_directory=None
772+
output_directory=None,
773+
algorithm: Algorithm = None
781774
) -> BacktestReport:
782775
"""
783776
Run a backtest for an algorithm. This method should be called when
784777
running a backtest.
785778
786779
Parameters:
787-
algorithm: The algorithm to run a backtest for (instance of
788-
Algorithm)
789780
backtest_date_range: The date range to run the backtest for
790781
(instance of BacktestDateRange)
782+
initial_amount: The initial amount to start the backtest with.
783+
This will be the amount of trading currency that the backtest
784+
portfolio will start with.
785+
algorithm: The algorithm to run a backtest for (instance of
786+
Algorithm)
791787
pending_order_check_interval: str - pending_order_check_interval:
792788
The interval at which to check pending orders (e.g. 1h, 1d, 1w)
793789
output_directory: str - The directory to
@@ -796,8 +792,11 @@ def run_backtest(
796792
Returns:
797793
Instance of BacktestReport
798794
"""
799-
logger.info("Initializing backtest")
800-
self.algorithm = algorithm
795+
if algorithm is not None:
796+
self.algorithm = algorithm
797+
798+
if self.algorithm is None:
799+
raise OperationalException("No algorithm registered")
801800

802801
self._initialize_app_for_backtest(
803802
backtest_date_range=backtest_date_range,
@@ -807,18 +806,20 @@ def run_backtest(
807806
self._initialize_algorithm_for_backtest(
808807
algorithm=self.algorithm
809808
)
809+
810810
backtest_service = self.container.backtest_service()
811-
configuration_service = self.container.configuration_service()
812-
config = configuration_service.get_config()
813-
backtest_service.resource_directory = config[RESOURCE_DIRECTORY]
814811

815812
# Run the backtest with the backtest_service and collect the report
816813
report = backtest_service.run_backtest(
817-
algorithm=self.algorithm, backtest_date_range=backtest_date_range
814+
algorithm=self.algorithm,
815+
initial_amount=initial_amount,
816+
backtest_date_range=backtest_date_range
818817
)
819818
backtest_report_writer_service = self.container \
820819
.backtest_report_writer_service()
821820

821+
config = self.container.configuration_service().get_config()
822+
822823
if output_directory is None:
823824
output_directory = os.path.join(
824825
config[RESOURCE_DIRECTORY], "backtest_reports"

investing_algorithm_framework/dependency_container.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,11 @@ class DependencyContainer(containers.DeclarativeContainer):
126126
BacktestService,
127127
configuration_service=configuration_service,
128128
order_service=order_service,
129-
portfolio_repository=portfolio_repository,
129+
portfolio_service=portfolio_service,
130130
performance_service=performance_service,
131131
position_repository=position_repository,
132132
market_data_source_service=market_data_source_service,
133+
portfolio_configuration_service=portfolio_configuration_service,
133134
)
134135
backtest_report_writer_service = providers.Factory(
135136
BacktestReportWriterService,

investing_algorithm_framework/domain/models/backtesting/backtest_report.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -358,15 +358,7 @@ def time_unit(self, value):
358358
self._time_unit = value
359359

360360
def get_runs_per_day(self):
361-
362-
if self.time_unit is None:
363-
return 0
364-
elif TimeUnit.SECOND.equals(self.time_unit):
365-
return 86400 / self.interval
366-
elif TimeUnit.MINUTE.equals(self.time_unit):
367-
return 1440 / self.interval
368-
else:
369-
return 24 / self.interval
361+
return self.number_of_runs / self.number_of_days
370362

371363
@property
372364
def backtest_start_date(self):
@@ -525,6 +517,9 @@ def get_growth_percentage(self) -> float:
525517
def get_trading_symbol(self) -> str:
526518
return self.trading_symbol
527519

520+
def get_initial_unallocated(self) -> float:
521+
return self.initial_unallocated
522+
528523
def add_symbol(self, symbol):
529524

530525
if symbol not in self.symbols:

investing_algorithm_framework/domain/models/portfolio/portfolio.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@
22

33

44
class Portfolio(BaseModel):
5+
"""
6+
Portfolio base class.
7+
8+
A portfolio is a collection of positions that are managed by an algorithm.
9+
10+
Attributes:
11+
* identifier: str, unique identifier of the portfolio
12+
* trading_symbol: str, trading symbol of the portfolio
13+
* unallocated: float, the size of the trading symbol that is not
14+
allocated. For example, if the trading symbol is USDT and the unallocated
15+
is 1000, it means that the portfolio has 1000 USDT that is not allocated to any position.
16+
* net_size: float, net size of the portfolio is the initial balance of the
17+
portfolio plus the all the net gains of the trades. The
18+
* realized: float, the realized gain of the portfolio is the sum of all the
19+
realized gains of the trades.
20+
* total_revenue: float, the total revenue of the portfolio is the sum
21+
of all the orders (price * size)
22+
* total_cost: float, the total cost of the portfolio is the sum of all the
23+
costs of the trades (price * size (for buy orders) or -price * size (for sell orders))
24+
* total_net_gain: float, the total net gain of the portfolio is the sum of
25+
all the net gains of the trades
26+
* total_trade_volume: float, the total trade volume of the
27+
portfolio is the sum of all the sizes of the trades
28+
* market: str, the market of the portfolio (e.g. BITVAVO, BINANCE)
29+
* created_at: datetime, the datetime when the portfolio was created
30+
* updated_at: datetime, the datetime when the portfolio was last updated
31+
* initialized: bool, whether the portfolio is initialized or not
32+
* initial_balance: float, the initial balance of the portfolio
33+
"""
534

635
def __init__(
736
self,

investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@
66

77

88
class PortfolioConfiguration(BaseModel):
9+
"""
10+
This class represents a portfolio configuration. It is used to
11+
configure the portfolio that the user wants to create.
12+
13+
The portfolio configuration will have the following attributes:
14+
- market: The market where the portfolio will be created
15+
- trading_symbol: The trading symbol of the portfolio
16+
- track_from: The date from which the portfolio will be tracked
17+
- identifier: The identifier of the portfolio
18+
- initial_balance: The initial balance of the portfolio
19+
20+
For backtesting, a portfolio configuration is used to create a
21+
portfolio that will be used to simulate the trading of the algorithm. if
22+
the user does not provide an initial balance, the portfolio will be created
23+
with a balance of according to the initial balanace of the PortfolioConfiguration class.
24+
"""
925

1026
def __init__(
1127
self,
@@ -22,7 +38,9 @@ def __init__(
2238
self._initial_balance = initial_balance
2339

2440
if self.identifier is None:
25-
self._identifier = market.lower()
41+
self._identifier = market.upper()
42+
else:
43+
self._identifier = identifier.upper()
2644

2745
if track_from:
2846
self._track_from = parse(track_from)
@@ -35,8 +53,8 @@ def __init__(
3553
@property
3654
def market(self):
3755

38-
if hasattr(self._market, "lower"):
39-
return self._market.lower()
56+
if hasattr(self._market, "upper"):
57+
return self._market.upper()
4058

4159
return self._market
4260

investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def _apply_query_params(self, db, query, query_params):
2020
query = query.filter_by(market=market_query_param.upper())
2121

2222
if identifier_query_param:
23-
query = query.filter_by(identifier=identifier_query_param.lower())
23+
query = query.filter_by(identifier=identifier_query_param.upper())
2424

2525
if position_query_param:
2626
position = db.query(SQLPosition)\

investing_algorithm_framework/infrastructure/repositories/repository.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def delete(self, object_id):
7171
try:
7272
delete_object = self.get(object_id)
7373
db.delete(delete_object)
74+
db.commit()
7475
return delete_object
7576
except SQLAlchemyError as e:
7677
logger.error(e)

0 commit comments

Comments
 (0)