Skip to content

Commit 9bb1333

Browse files
committed
initial commit
v1.0.0 release gitignore v1.0.0 Release
0 parents  commit 9bb1333

18 files changed

+744
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.python-version
2+
.env
3+
.venv
4+
__pycache__
5+
output
6+
market_data
7+
*.yaml

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Change log
2+
3+
## v1.0.0 - 2023-08-30
4+
5+
- Initial release

LICENSE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) [2023]
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# AI Trading Prototype Backtester
2+
3+
This project is a backtester for the sentiment-analysis cryptocurrency trading strategy that pairs with the live trading bot, [ai-trading-prototype](https://github.com/binance/ai-trading-prototype/). This backtester is designed to run accurate simulations for a given date range on a chosen trading symbol.
4+
5+
![Diagram](../assets/diagram.png?raw=true)
6+
7+
## Disclaimer
8+
9+
This trading bot backtester does not provide financial advice or endorse trading strategies. Users assume all risks, as past results from historical data do not guarantee future returns; the creators bear no responsibility for losses. Please seek guidance from financial experts before trading or investing. By using this project you accept these conditions.
10+
11+
## Features
12+
13+
* A flexible trading strategy builder.
14+
* Customisable Backtesting strategy.
15+
* Automated data downloader using `data.binance.vision`.
16+
* Detailed backtest results and performance assessment, including HTML visualisations of all trades.
17+
18+
## Installation
19+
20+
1. Clone the repository
21+
```
22+
git clone https://github.com/binance/ai-trading-prototype-backtester
23+
```
24+
2. Move into the cloned directory
25+
```
26+
cd ai-trading-prototype-backtester
27+
```
28+
3. Install dependencies
29+
```
30+
pip install -r requirements.txt
31+
```
32+
33+
## Usage
34+
### Configuration
35+
36+
All configurations are stored in `config.yaml.example`. You can specify:
37+
* `symbol`: The trading symbol/ pair. Example: `ETHUSDT`.
38+
* `kline_interval`: The interval of the candlesticks. Valid intervals are: `1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h` or `1d`
39+
* `start_date` and `end_date`: The range of dates to backtest on. Format: `YYYY-MM-DD`.
40+
* `sentiment_data`: The path to the sentiment data file. Default: `./sentiment_data/sentiment_data.csv`.
41+
* `start_balance`: The starting balance in quote currency (eg. USDT). Default: `100000`.
42+
* `order_size`: The order size in base currency (eg. BTC). Default: `0.01`.
43+
* `total_quantity_limit`: The maximum quantity of base currency (eg. BTC) that can be held at any given time. Default: `1`.
44+
* `commission`: The trading fee/commission ratio. Default: `0.002`.
45+
* `logging_level`: The logging detail level. Default: `INFO`.
46+
47+
Please also ensure that the sentiment data file `sentiment_data.csv` is present and formatted as `"headline source","headline collected timestamp (ms)","headline published timestamp (ms)","headline","sentiment"`.
48+
49+
#### Strategy Configuration Profiles
50+
51+
Within the `strategy_configuration` directory, there are 3 extra configuration files. Each file corresponds to a different strategy/risk level (Aggressive, Conservative and Standard). These can be used to quickly test different parameters with varying degrees of risk.
52+
53+
### Run the backtester as module
54+
55+
```
56+
python -m aitradingprototypebacktester
57+
```
58+
59+
#### How it works
60+
61+
* During the backtesting process, the backtester checks `/sentiment_data/sentiment_data.csv` for a published headline at each kline/candlestick interval.
62+
* If a headline was published during the current time-period, it reads the sentiment of the headline.
63+
* By default, the backtester uses the `successive_strategy` which is defined in `aitradingprototypebacktester/strategy/successive_strategy.py`. The details of how this strategy works is as follows:
64+
* If the sentiment was "bullish" (meaning it is expected that the price would increase) it will attempt a BUY order of size = `order_size` of base currency, so long as the `total_quantity_limit` (max. quantity that can be held at any given time) has not yet been reached.
65+
* If the sentiment was "bearish" (meaning that a fall in price is expected), it will attempt to SELL `order_size` quantity of the base currency, so long as the current base currency balance is > 0.
66+
* Once the backtest is complete, it will return a detailed table of results along with an HTML visualisation of all the buys and sells plotted against the trading symbol's price throughout the period.
67+
* An image, `backtest_result.png`, will also be generated. This is a static view of the HTML visualisation.
68+
* If there is a position still open at the end of the backtest, it will be closed at the open price of the last kline in the backtesting period.
69+
70+
## Backtest Results
71+
72+
* As outlined in the `How it works` section above, the backtester will output several files as a result of each backtest:
73+
* `output/raw/backtest_result.txt` - A summary of the backtest.
74+
* `output/raw/backtest_trades.txt` - A detailed list of each individual trade executed during the backtest.
75+
* `output/visualisation/dynamic_report.html` - HTML visualisation of all the buys and sells plotted against the trading symbol's price throughout the period.
76+
* `output/visualisation/backtest_result.png` - A static view of the HTML visualisation (image below).
77+
78+
![Backtest Result](../assets/backtest_result.png?raw=true)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import logging
2+
import os
3+
4+
import bokeh
5+
from backtesting import Backtest
6+
7+
from aitradingprototypebacktester.config_loader import load_config
8+
from aitradingprototypebacktester.data_downloader import download_binance_data
9+
from aitradingprototypebacktester.strategy_manager import StrategyManager
10+
11+
12+
def initialise_config(config):
13+
"""
14+
Loads configuration values from config.yaml and returns neccessary variables
15+
"""
16+
symbol = config["symbol"]
17+
kline_interval = config["kline_interval"]
18+
start_date = config["start_date"]
19+
end_date = config["end_date"]
20+
start_balance = int(config["start_balance"])
21+
commission = float(config["commission"])
22+
logging_level = config["logging_level"]
23+
return (
24+
symbol,
25+
kline_interval,
26+
start_date,
27+
end_date,
28+
start_balance,
29+
commission,
30+
logging_level,
31+
)
32+
33+
34+
def create_directories():
35+
"""
36+
Check if "output/visualisation" and "output/raw" directories exist, if not create them
37+
"""
38+
if not os.path.exists("output/visualisation"):
39+
os.makedirs("output/visualisation")
40+
if not os.path.exists("output/raw"):
41+
os.makedirs("output/raw")
42+
43+
44+
def write_results(results):
45+
"""
46+
Write the backtest results and individual trades to output/raw directory separate text files.
47+
"""
48+
create_directories()
49+
with open("output/raw/backtest_result.txt", "w") as file:
50+
file.write(str(results))
51+
with open("output/raw/backtest_trades.txt", "w") as file:
52+
file.write(str(results._trades))
53+
54+
55+
def convert_from_satoshi(results, bt):
56+
"""
57+
Convert the columns `Size`, `EntryPrice`, `ExitPrice`, `Open`, `High`, `Low`, `Close`, and `Volume` from satoshis back to their original values.
58+
59+
Args:
60+
results (object): The `results` object that contains the trades data.
61+
bt (object): The `bt` object that contains the data for the strategy.
62+
63+
Returns:
64+
tuple: A tuple containing the modified `results` object and the modified `bt` object.
65+
"""
66+
# Convert columns: Size, EntryPrice, ExitPrice, back from satoshis:
67+
results._trades = results._trades.assign(
68+
Size=results._trades.Size / 1e6,
69+
EntryPrice=results._trades.EntryPrice * 1e6,
70+
ExitPrice=results._trades.ExitPrice * 1e6,
71+
)
72+
bt._data = bt._data.assign(
73+
Open=results._strategy._data._Data__df.Open * 1e6,
74+
High=results._strategy._data._Data__df.High * 1e6,
75+
Low=results._strategy._data._Data__df.Low * 1e6,
76+
Close=results._strategy._data._Data__df.Close * 1e6,
77+
Volume=results._strategy._data._Data__df.Volume / 1e6,
78+
)
79+
return results, bt
80+
81+
82+
if __name__ == "__main__":
83+
"""
84+
- Loads configuration values from config.yaml
85+
- Downloads binance kline/candlestick data from data.binance.vision
86+
- Creates a Backtest instance with the trading data and trading strategy
87+
- Runs backtest, outputs results and creates html + png visualisation of results
88+
- Saves raw backtest results and individual trades
89+
"""
90+
config = load_config("config.yaml")
91+
(
92+
symbol,
93+
kline_interval,
94+
start_date,
95+
end_date,
96+
start_balance,
97+
commission,
98+
logging_level,
99+
) = initialise_config(config)
100+
logging.basicConfig(level=logging_level, format="%(message)s") # Initialise Logging
101+
102+
kline_data = download_binance_data(
103+
symbol, kline_interval, start_date, end_date, logging_level
104+
)
105+
kline_data = (kline_data / 1e6).assign(
106+
Volume=kline_data.Volume * 1e6 # Convert relevant columns to satoshis
107+
)
108+
109+
bt = Backtest(
110+
kline_data,
111+
StrategyManager,
112+
cash=start_balance,
113+
commission=commission,
114+
exclusive_orders=False,
115+
)
116+
logging.info("Running Backtest...")
117+
118+
results = bt.run()
119+
results, bt = convert_from_satoshi(
120+
results, bt
121+
) # Convert relevant columns back from satoshis
122+
logging.info(results)
123+
124+
write_results(results)
125+
plot = bt.plot(resample=False, filename="output/visualisation/dynamic_report.html")
126+
bokeh.io.export.export_png(
127+
plot, filename="output/visualisation/backtest_result.png"
128+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import yaml
2+
3+
4+
def load_config(file_path: str) -> dict:
5+
"""
6+
Load configuration file and return the content as a dictionary.
7+
"""
8+
with open(file_path, "r") as file:
9+
config = yaml.safe_load(file)
10+
return config
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import datetime
2+
import logging
3+
import os
4+
from io import BytesIO
5+
from zipfile import ZipFile
6+
7+
import pandas as pd
8+
import requests
9+
10+
11+
def get_kline_data(symbol, kline_interval, date_str):
12+
"""
13+
Fetches the binance data for given symbol and kline-interval for a particular day
14+
"""
15+
url = f"https://data.binance.vision/data/spot/daily/klines/{symbol.upper()}/{kline_interval}/{symbol.upper()}-{kline_interval}-{date_str}.zip"
16+
response = requests.get(url)
17+
return response
18+
19+
20+
def parse_data(response):
21+
"""
22+
Parses the fetched data and appends it into the list 'data'
23+
"""
24+
data = []
25+
with ZipFile(BytesIO(response.content)) as zip_file:
26+
for file in zip_file.namelist():
27+
with zip_file.open(file) as f:
28+
df = pd.read_csv(f, usecols=range(6))
29+
df.columns = ["Timestamp", "Open", "High", "Low", "Close", "Volume"]
30+
data.append(df)
31+
return data
32+
33+
34+
def process_data(data):
35+
"""
36+
Processes the list into a DataFrame
37+
"""
38+
df = pd.concat(data)
39+
df.columns = ["Timestamp", "Open", "High", "Low", "Close", "Volume"]
40+
df["Timestamp"] = pd.to_datetime(df["Timestamp"], unit="ms")
41+
df.set_index("Timestamp", inplace=True)
42+
df.sort_index(ascending=True, inplace=True)
43+
return df
44+
45+
46+
def load_csv_data(file_path):
47+
"""
48+
Loads the DataFrame from the specified csv file.
49+
"""
50+
return pd.read_csv(file_path, index_col=[0])
51+
52+
53+
def download_binance_data(symbol, kline_interval, start_date, end_date, logging_level):
54+
"""
55+
Downloads Binance data for a given symbol and date range.
56+
57+
Args:
58+
symbol (str): The symbol to download data for.
59+
kline_interval (str): The interval of the kline data.
60+
start_date (datetime.date): The start date of the data to download.
61+
end_date (datetime.date): The end date of the data to download.
62+
"""
63+
logging.basicConfig(level=logging_level, format="%(message)s") # Initialise Logging
64+
market_data_path = f"market_data/{symbol.upper()}/{kline_interval}"
65+
66+
# Check if the downloads directory exists, create it if it doesn't
67+
if not os.path.exists(market_data_path):
68+
os.makedirs(market_data_path)
69+
70+
data = []
71+
total_days = (end_date - start_date).days + 1
72+
downloaded_days = 0
73+
74+
while start_date <= end_date:
75+
date_str = start_date.strftime("%Y-%m-%d")
76+
file_path = f"{market_data_path}/{date_str}.csv"
77+
78+
# Check if data is already downloaded
79+
if os.path.isfile(file_path):
80+
df = load_csv_data(file_path)
81+
data.append(df)
82+
logging.info(f"Loaded data for {date_str} from disk.")
83+
downloaded_days += 1
84+
else:
85+
response = get_kline_data(symbol, kline_interval, date_str)
86+
87+
if response.status_code == 200:
88+
data += parse_data(response)
89+
90+
# Save processed data to csv file
91+
data[-1].to_csv(file_path)
92+
93+
downloaded_days += 1
94+
logging.info(
95+
f"Downloaded and saved kline data for {date_str}\nProgress: {downloaded_days}/{total_days} days ({(downloaded_days/total_days)*100:.2f}%) downloaded.\n"
96+
)
97+
else:
98+
logging.error(
99+
f"Failed to download kline data for date {date_str}. Status code: {response.status_code}"
100+
)
101+
downloaded_days += 1
102+
pass
103+
104+
start_date += datetime.timedelta(days=1)
105+
return process_data(data)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from enum import Enum
2+
3+
4+
class Sentiment(Enum):
5+
BULLISH = "bullish"
6+
BEARISH = "bearish"
7+
UNKNOWN = "unknown"

0 commit comments

Comments
 (0)