1414</div >
1515
1616The Investing Algorithm Framework is a Python-based framework built to streamline the entire lifecycle of quantitative trading strategies from signal generation and backtesting to live deployment.
17- It offers a complete quantitative workflow, featuring two dedicated backtesting engines:
18-
19- * A vectorized backtest engine for fast signal research and prototyping
20-
21- * An event-based backtest engine for realistic and accurate strategy evaluation
22-
23- The framework supports live trading across multiple exchanges and offers flexible deployment options, including Azure Functions and AWS Lambda.
24- Designed for extensibility, it allows you to integrate custom strategies, data providers, and order executors, enabling support for any exchange or broker.
25- It natively supports multiple data formats, including OHLCV, ticker, and custom datasets with seamless compatibility for both Pandas and Polars DataFrames.
26-
27-
2817
2918## Sponsors
3019
@@ -43,6 +32,7 @@ It natively supports multiple data formats, including OHLCV, ticker, and custom
4332- [x] Event-Driven Backtest Engine: Accurate and realistic backtesting with event-driven architecture.
4433- [x] Vectorized Backtest Engine: Fast signal research and prototyping with vectorized operations.
4534- [x] Permutation testing: Run permutation tests to evaluate the strategy statistical significance.
35+ - [x] Metric tracking and backtest reports evaluation/comparison: Track and compare key performance metrics like CAGR, Sharpe ratio, max drawdown, and more (See example usage for a complete list of metrics the framework collects).
4636- [x] Backtest Reporting: Generate detailed reports to analyse and compare backtests.
4737- [x] Live Trading: Execute trades in real-time with support for multiple exchanges via ccxt.
4838- [x] Portfolio Management: Manage portfolios, trades, and positions with persistence via SQLite.
@@ -52,6 +42,13 @@ It natively supports multiple data formats, including OHLCV, ticker, and custom
5242- [x] Web API: Interact with your bot via REST API.
5343- [x] PyIndicators Integration: Perform technical analysis directly on your dataframes.
5444- [x] Extensibility: Add custom strategies, data providers, order executors so you can connect your trading bot to your favorite exchange or broker.
45+ - [x] Modular Design: Build your bot using modular components for easy customization and maintenance.
46+ - [x] Multiple exchanges and brokers: ** Detailed guides and API references to help you get started and make the most of the framework.
47+ and offers flexible deployment options, including Azure Functions and AWS Lambda.
48+ Designed for extensibility, it allows you to integrate custom strategies, data providers, and order executors, enabling support for any exchange or broker.
49+ It natively supports multiple data formats, including OHLCV, ticker, and custom datasets with seamless compatibility for both Pandas and Polars DataFrames.
50+
51+
5552
5653## 🚀 Quickstart
5754
@@ -70,10 +67,10 @@ Run the following command to set up your project:
7067investing-algorithm-framewor init
7168```
7269
73- For a web-enabled version :
70+ For a aws lambda compatible project, run :
7471
7572``` bash
76- investing-algorithm-framework init --web
73+ investing-algorithm-framework init --type aws_lambda
7774```
7875
7976This will create:
@@ -97,74 +94,264 @@ the 20, 50 and 100 period exponential moving averages (EMA) and the
9794> You can install it using pip: pip install pyindicators.
9895
9996``` python
100- import logging.config
101- from dotenv import load_dotenv
97+ from typing import Dict, Any
98+ from datetime import datetime, timezone
10299
103- from pyindicators import ema, rsi, crossunder, crossover, is_above
100+ import pandas as pd
101+ from pyindicators import ema, rsi, crossover, crossunder
104102
105- from investing_algorithm_framework import create_app, TimeUnit, Context, BacktestDateRange, \
106- DEFAULT_LOGGING_CONFIG , TradingStrategy, SnapshotInterval, BacktestReport, DataSource
103+ from investing_algorithm_framework import TradingStrategy, DataSource, \
104+ TimeUnit, DataType, PositionSize, create_app, RESOURCE_DIRECTORY , \
105+ BacktestDateRange, BacktestReport
107106
108- load_dotenv()
109- logging.config.dictConfig(DEFAULT_LOGGING_CONFIG )
110- logger = logging.getLogger(__name__ )
111107
112- app = create_app()
113- # Registered bitvavo market, credentials are read from .env file by default
114- app.add_market(market = " BITVAVO" , trading_symbol = " EUR" , initial_balance = 100 )
115-
116- class MyStrategy (TradingStrategy ):
117- interval = 2
108+ class RSIEMACrossoverStrategy (TradingStrategy ):
118109 time_unit = TimeUnit.HOUR
119- data_sources = [
120- DataSource(data_type = " OHLCV" , market = " bitvavo" , symbol = " BTC/EUR" , window_size = 200 , time_frame = " 2h" , identifier = " BTC-ohlcv" , pandas = True ),
110+ interval = 2
111+ symbols = [" BTC" ]
112+ position_sizes = [
113+ PositionSize(
114+ symbol = " BTC" , percentage_of_portfolio = 20.0
115+ ),
116+ PositionSize(
117+ symbol = " ETH" , percentage_of_portfolio = 20.0
118+ )
121119 ]
122- symbols = [" BTC/EUR" ]
123120
124- def run_strategy (self , context : Context, data ):
121+ def __init__ (
122+ self ,
123+ time_unit : TimeUnit,
124+ interval : int ,
125+ market : str ,
126+ rsi_time_frame : str ,
127+ rsi_period : int ,
128+ rsi_overbought_threshold ,
129+ rsi_oversold_threshold ,
130+ ema_time_frame ,
131+ ema_short_period ,
132+ ema_long_period ,
133+ ema_cross_lookback_window : int = 10
134+ ):
135+ self .rsi_time_frame = rsi_time_frame
136+ self .rsi_period = rsi_period
137+ self .rsi_result_column = f " rsi_ { self .rsi_period} "
138+ self .rsi_overbought_threshold = rsi_overbought_threshold
139+ self .rsi_oversold_threshold = rsi_oversold_threshold
140+ self .ema_time_frame = ema_time_frame
141+ self .ema_short_result_column = f " ema_ { ema_short_period} "
142+ self .ema_long_result_column = f " ema_ { ema_long_period} "
143+ self .ema_crossunder_result_column = " ema_crossunder"
144+ self .ema_crossover_result_column = " ema_crossover"
145+ self .ema_short_period = ema_short_period
146+ self .ema_long_period = ema_long_period
147+ self .ema_cross_lookback_window = ema_cross_lookback_window
148+ data_sources = []
149+
150+ for symbol in self .symbols:
151+ full_symbol = f " { symbol} /EUR "
152+ data_sources.append(
153+ DataSource(
154+ identifier = f " { symbol} _rsi_data " ,
155+ data_type = DataType.OHLCV ,
156+ time_frame = self .rsi_time_frame,
157+ market = market,
158+ symbol = full_symbol,
159+ pandas = True ,
160+ window_size = 800
161+ )
162+ )
163+ data_sources.append(
164+ DataSource(
165+ identifier = f " { symbol} _ema_data " ,
166+ data_type = DataType.OHLCV ,
167+ time_frame = self .ema_time_frame,
168+ market = market,
169+ symbol = full_symbol,
170+ pandas = True ,
171+ window_size = 800
172+ )
173+ )
125174
126- if context.has_open_orders(target_symbol = " BTC" ):
127- logger.info(" There are open orders, skipping strategy iteration." )
128- return
175+ super ().__init__ (
176+ data_sources = data_sources, time_unit = time_unit, interval = interval
177+ )
178+
179+ self .buy_signal_dates = {}
180+ self .sell_signal_dates = {}
181+
182+ for symbol in self .symbols:
183+ self .buy_signal_dates[symbol] = []
184+ self .sell_signal_dates[symbol] = []
185+
186+ def _prepare_indicators (
187+ self ,
188+ rsi_data ,
189+ ema_data
190+ ):
191+ """
192+ Helper function to prepare the indicators
193+ for the strategy. The indicators are calculated
194+ using the pyindicators library: https://github.com/coding-kitties/PyIndicators
195+ """
196+ ema_data = ema(
197+ ema_data,
198+ period = self .ema_short_period,
199+ source_column = " Close" ,
200+ result_column = self .ema_short_result_column
201+ )
202+ ema_data = ema(
203+ ema_data,
204+ period = self .ema_long_period,
205+ source_column = " Close" ,
206+ result_column = self .ema_long_result_column
207+ )
208+ # Detect crossover (short EMA crosses above long EMA)
209+ ema_data = crossover(
210+ ema_data,
211+ first_column = self .ema_short_result_column,
212+ second_column = self .ema_long_result_column,
213+ result_column = self .ema_crossover_result_column
214+ )
215+ # Detect crossunder (short EMA crosses below long EMA)
216+ ema_data = crossunder(
217+ ema_data,
218+ first_column = self .ema_short_result_column,
219+ second_column = self .ema_long_result_column,
220+ result_column = self .ema_crossunder_result_column
221+ )
222+ rsi_data = rsi(
223+ rsi_data,
224+ period = self .rsi_period,
225+ source_column = " Close" ,
226+ result_column = self .rsi_result_column
227+ )
228+
229+ return ema_data, rsi_data
230+
231+ def generate_buy_signals (self , data : Dict[str , Any]) -> Dict[str , pd.Series]:
232+ """
233+ Generate buy signals based on the moving average crossover.
234+
235+ data (Dict[str, Any]): Dictionary containing all the data for
236+ the strategy data sources.
237+
238+ Returns:
239+ Dict[str, pd.Series]: A dictionary where keys are symbols and values
240+ are pandas Series indicating buy signals (True/False).
241+ """
242+
243+ signals = {}
244+
245+ for symbol in self .symbols:
246+ ema_data_identifier = f " { symbol} _ema_data "
247+ rsi_data_identifier = f " { symbol} _rsi_data "
248+ ema_data, rsi_data = self ._prepare_indicators(
249+ data[ema_data_identifier].copy(),
250+ data[rsi_data_identifier].copy()
251+ )
129252
130- data = data[" BTC-ohlcv" ]
131- data = ema(data, source_column = " Close" , period = 20 , result_column = " ema_20" )
132- data = ema(data, source_column = " Close" , period = 50 , result_column = " ema_50" )
133- data = ema(data, source_column = " Close" , period = 100 , result_column = " ema_100" )
134- data = crossunder(data, first_column = " ema_50" , second_column = " ema_100" , result_column = " crossunder_50_20" )
135- data = crossover(data, first_column = " ema_50" , second_column = " ema_100" , result_column = " crossover_50_20" )
136- data = rsi(data, source_column = " Close" , period = 14 , result_column = " rsi_14" )
253+ # crossover confirmed
254+ ema_crossover_lookback = ema_data[
255+ self .ema_crossover_result_column].rolling(
256+ window = self .ema_cross_lookback_window
257+ ).max().astype(bool )
137258
138- if context.has_position(" BTC" ) and self .sell_signal(data):
139- context.create_limit_sell_order(
140- " BTC" , percentage_of_position = 100 , price = data[" Close" ].iloc[- 1 ]
141- )
142- return
259+ # use only RSI column
260+ rsi_oversold = rsi_data[self .rsi_result_column] \
261+ < self .rsi_oversold_threshold
262+
263+ buy_signal = rsi_oversold & ema_crossover_lookback
264+ buy_signals = buy_signal.fillna(False ).astype(bool )
265+ signals[symbol] = buy_signals
266+
267+ # Get all dates where there is a sell signal
268+ buy_signal_dates = buy_signals[buy_signals].index.tolist()
269+
270+ if buy_signal_dates:
271+ self .buy_signal_dates[symbol] += buy_signal_dates
272+
273+ return signals
274+
275+ def generate_sell_signals (self , data : Dict[str , Any]) -> Dict[str , pd.Series]:
276+ """
277+ Generate sell signals based on the moving average crossover.
278+
279+ Args:
280+ data (Dict[str, Any]): Dictionary containing all the data for
281+ the strategy data sources.
282+
283+ Returns:
284+ Dict[str, pd.Series]: A dictionary where keys are symbols and values
285+ are pandas Series indicating sell signals (True/False).
286+ """
143287
144- if not context.has_position(" BTC" ) and self .buy_signal(data):
145- context.create_limit_buy_order(
146- " BTC" , percentage_of_portfolio = 20 , price = data[" Close" ].iloc[- 1 ]
288+ signals = {}
289+ for symbol in self .symbols:
290+ ema_data_identifier = f " { symbol} _ema_data "
291+ rsi_data_identifier = f " { symbol} _rsi_data "
292+ ema_data, rsi_data = self ._prepare_indicators(
293+ data[ema_data_identifier].copy(),
294+ data[rsi_data_identifier].copy()
147295 )
148- return
149296
150- def buy_signal (self , data ) -> bool :
151- return False
297+ # Confirmed by crossover between short-term EMA and long-term EMA
298+ # within a given lookback window
299+ ema_crossunder_lookback = ema_data[
300+ self .ema_crossunder_result_column].rolling(
301+ window = self .ema_cross_lookback_window
302+ ).max().astype(bool )
152303
153- def sell_signal (self , data ) -> bool :
154- return False
304+ # use only RSI column
305+ rsi_overbought = rsi_data[self .rsi_result_column] \
306+ >= self .rsi_overbought_threshold
307+
308+ # Combine both conditions
309+ sell_signal = rsi_overbought & ema_crossunder_lookback
310+ sell_signal = sell_signal.fillna(False ).astype(bool )
311+ signals[symbol] = sell_signal
312+
313+ # Get all dates where there is a sell signal
314+ sell_signal_dates = sell_signal[sell_signal].index.tolist()
315+
316+ if sell_signal_dates:
317+ self .sell_signal_dates[symbol] += sell_signal_dates
318+
319+ return signals
155320
156- date_range = BacktestDateRange(
157- start_date = " 2023-08-24 00:00:00" , end_date = " 2023-12-02 00:00:00"
158- )
159- app.add_strategy(MyStrategy)
160321
161322if __name__ == " __main__" :
162- # Run the backtest with a daily snapshot interval for end-of-day granular reporting
323+ app = create_app()
324+ app.add_strategy(
325+ RSIEMACrossoverStrategy(
326+ time_unit = TimeUnit.HOUR ,
327+ interval = 2 ,
328+ market = " bitvavo" ,
329+ rsi_time_frame = " 2h" ,
330+ rsi_period = 14 ,
331+ rsi_overbought_threshold = 70 ,
332+ rsi_oversold_threshold = 30 ,
333+ ema_time_frame = " 2h" ,
334+ ema_short_period = 12 ,
335+ ema_long_period = 26 ,
336+ ema_cross_lookback_window = 10
337+ )
338+ )
339+
340+ # Market credentials for coinbase for both the portfolio connection and data sources will
341+ # be read from .env file, when not registering a market credential object in the app.
342+ app.add_market(
343+ market = " bitvavo" ,
344+ trading_symbol = " EUR" ,
345+ )
346+ backtest_range = BacktestDateRange(
347+ start_date = datetime(2023 , 1 , 1 , tzinfo = timezone.utc),
348+ end_date = datetime(2024 , 6 , 1 , tzinfo = timezone.utc)
349+ )
163350 backtest = app.run_backtest(
164- backtest_date_range = date_range , initial_amount = 100 , snapshot_interval = SnapshotInterval. DAILY
351+ backtest_date_range = backtest_range , initial_amount = 1000
165352 )
166- backtest_report = BacktestReport(backtests = [ backtest] )
167- backtest_report .show()
353+ report = BacktestReport(backtest)
354+ report .show(backtest_date_range = backtest_range, browser = True )
168355```
169356
170357> You can find more examples [ here] ( ./examples ) folder.
0 commit comments