Skip to content

Commit 77be6d0

Browse files
committed
Update readme
1 parent 327509d commit 77be6d0

File tree

1 file changed

+249
-62
lines changed

1 file changed

+249
-62
lines changed

README.md

Lines changed: 249 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,6 @@
1414
</div>
1515

1616
The 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:
7067
investing-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

7976
This 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

161322
if __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

Comments
 (0)