Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add script for estimating the optimal interval #86

Merged
merged 7 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ install: check-uv
##
.PHONY: dev
dev: check-uv
$(UV) pip install -e ".[dev,test,backtest]"
$(UV) pip install -e ".[dev,test,backtest]" -r doc/requirements.txt

## ======= T E S T I N G =======================================================
## test Run the unit tests
Expand Down
7 changes: 3 additions & 4 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@
copyright = "2025, Benjamin Thomas Schwertfeger" # noqa: A001 # pylint: disable=redefined-builtin
author = "Benjamin Thomas Schwertfeger"

# to import the package
parent_directory: Path = Path("..").resolve()
sys.path.insert(0, str(parent_directory))
# Import the local package
sys.path.insert(0, str(Path("..").resolve() / "src"))

rst_epilog = ""
# Read link all targets from file
Expand Down Expand Up @@ -72,7 +71,7 @@ def setup(app: Any) -> None: # noqa: ARG001,ANN401
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "sphinx_rtd_theme"
html_theme = "sphinx_book_theme" # "sphinx_rtd_theme"
html_static_path = ["_static"]
html_context = {
"display_github": True,
Expand Down
5 changes: 3 additions & 2 deletions doc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ psycopg2-binary~=2.9
python-kraken-sdk>=3.1.3,<3.2
requests~=2.32
setuptools_scm
sphinx
sphinx<8.2.0 # doesn't work with nbsphinx
# sphinx-rtd-theme
sphinx-book-theme
sphinx-click
sphinx-rtd-theme
sqlalchemy~=2.0
8 changes: 1 addition & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,6 @@ classifiers = [
[project.optional-dependencies]
dev = ["mypy", "black", "ruff"]
test = ["pytest", "pytest-cov", "pytest-asyncio"]
backtest = [
"ipython",
"ipykernel", # python -m ipykernel install --user --name=kraken-infinity-grid
"jupyter",
"pandas",
"tqdm",
]

[project.scripts]
kraken-infinity-grid = "kraken_infinity_grid.cli:cli"
Expand Down Expand Up @@ -254,6 +247,7 @@ task-tags = ["todo", "TODO", "fixme", "FIXME"]
]
"tools/*.py" = [
"T201", # `print` found
"E501", # Line too long
]
"tools/generate_ws_messages.py" = [
"S311", # pseudo-random-generator
Expand Down
2 changes: 1 addition & 1 deletion src/kraken_infinity_grid/gridbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def __init__( # pylint: disable=too-many-arguments

self.max_investment: float = config["max_investment"]
self.n_open_buy_orders: int = config["n_open_buy_orders"]
self.fee: float | None = None
self.fee: float | None = config.get("fee")
self.base_currency: str = config["base_currency"]
self.quote_currency: str = config["quote_currency"]

Expand Down
1 change: 1 addition & 0 deletions src/kraken_infinity_grid/order_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ def new_buy_order(
)

# Compute the target volume for the upcoming buy order.
# NOTE: The fee is respected while placing the sell order
volume = float(
self.__s.trade.truncate(
amount=Decimal(self.__s.amount_per_grid) / Decimal(order_price),
Expand Down
22 changes: 15 additions & 7 deletions tools/backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ def __init__(
self.instance.market = self.api
self.instance.trade = self.api

self.summary_balances: float
self.summary_market_price: float

async def run(self: Self, prices: Iterable) -> None:
"""
Run the configured strategy against a series of prices.
Expand Down Expand Up @@ -419,25 +422,30 @@ def summary(self: Self) -> None:
"""
Print the summary of the backtest.
"""
self.compute_summary()
print("*" * 80)
print(f"Strategy: {self.instance.strategy}")
print(f"Final price: {self.instance.ticker.last}")
print(f"Executed buy orders: {self.api.n_exec_buy_orders}")
print(f"Executed sell orders: {self.api.n_exec_sell_orders}")
print("Final balances:")

balances = self.api.get_balances()
print(f"{self.instance.base_currency}: {balances[self.api.base]}")
print(f"{self.instance.quote_currency}: {balances[self.api.quote]}")
market_price = (
balances[self.api.base]["balance"] * self.instance.ticker.last
+ balances[self.api.quote]["balance"]
print(f"{self.instance.base_currency}: {self.summary_balances[self.api.base]}")
print(
f"{self.instance.quote_currency}: {self.summary_balances[self.api.quote]}",
)
print(
f"Market price: {market_price} {self.instance.quote_currency}",
f"Market price: {self.summary_market_price} {self.instance.quote_currency}",
)
print("*" * 80)

def compute_summary(self: Self) -> None:
self.summary_balances = self.api.get_balances()
self.summary_market_price = (
self.summary_balances[self.api.base]["balance"] * self.instance.ticker.last
+ self.summary_balances[self.api.quote]["balance"]
)


async def main() -> None:
"""
Expand Down
186 changes: 186 additions & 0 deletions tools/backtesting/optimal_interval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 Benjamin Thomas Schwertfeger
# All rights reserved.
# https://github.com/btschwertfeger
#

"""
This script performs backtesting to find the optimal interval for a grid trading
strategy using historical OHLC data from the Kraken API. The script iterates
through different intervals and evaluates the performance of the strategy to
determine the best interval.

This script provides a rough estimation of the optimal interval based on hourly
data of the last 30 days. One should keep in mind, that the price does move way
more than only in hourly intervals!

Example:

(The output depends on the adjustments made in this script)

>>> python3 -m venv venv
>>> source venv/bin/activate
>>> pip install --compile asyncio python-kraken-sdk pandas prettytable
>>> python3 optimal_interval.py

Results (sorted by Market Price):
Data time range: 2025-01-12 08:00:00 to 2025-02-11 07:00:00
+----------------+--------------------+---------+--------+--------------------+---------------------+-----------------------+------------------------+-------------------+-------------------+
| Interval | Market Price (EUR) | N Sells | N Buys | Trade volume (EUR) | Volume-based profit | BTC (total) | BTC (hold trade) | EUR (total) | EUR (hold trade) |
+----------------+--------------------+---------+--------+--------------------+---------------------+-----------------------+------------------------+-------------------+-------------------+
| 0.0210 (2.10%) | 1000005.4665 | 18 | 24 | 1050 | 0.5206% | 0.0016282999999999998 | 0.00150382 | 999849.9654728107 | 75.54282935119095 |
| 0.0220 (2.20%) | 1000005.2172 | 17 | 22 | 975 | 0.5351% | 0.00136391 | 0.00124384 | 999874.9652039555 | 75.51128815186323 |
| 0.0330 (3.30%) | 1000004.3978 | 8 | 10 | 450 | 0.9773% | 0.0005697400000000001 | 0.0004874100000000001 | 999949.9881832895 | 75.24845152932751 |
| 0.0200 (2.00%) | 1000004.3962 | 17 | 23 | 1000 | 0.4396% | 0.0016170800000000001 | 0.00150357 | 999849.9667006223 | 75.49813897806229 |
| 0.0250 (2.50%) | 1000003.4176 | 11 | 16 | 675 | 0.5063% | 0.0013450100000000002 | 0.0012453400000000002 | 999874.9704892357 | 75.36020184157425 |
| 0.0350 (3.50%) | 1000003.1625 | 6 | 8 | 350 | 0.9036% | 0.0005568 | 0.00048724999999999994 | 999949.9886699338 | 75.19879946928025 |
| 0.0310 (3.10%) | 1000003.1294 | 7 | 11 | 450 | 0.6954% | 0.00108014 | 0.00099589 | 999899.9771240605 | 75.25522636289202 |
| 0.0230 (2.30%) | 1000003.1137 | 12 | 17 | 725 | 0.4295% | 0.0013418300000000002 | 0.00124484 | 999874.9703002557 | 75.40516251525602 |
| 0.0260 (2.60%) | 1000003.0944 | 10 | 15 | 625 | 0.4951% | 0.0013416300000000001 | 0.00124435 | 999874.970116059 | 75.35526006792955 |
| 0.0340 (3.40%) | 1000002.9820 | 6 | 8 | 350 | 0.8520% | 0.0005549099999999999 | 0.0004874100000000001 | 999949.9886272204 | 75.19853153376624 |
| 0.0240 (2.40%) | 1000002.4204 | 10 | 15 | 625 | 0.3873% | 0.00133455 | 0.0012450900000000001 | 999874.9721661004 | 75.3363423329422 |
| 0.0270 (2.70%) | 1000002.3290 | 8 | 13 | 525 | 0.4436% | 0.0013336099999999998 | 0.00124651 | 999874.9705948302 | 75.30492621440575 |
| 0.0300 (3.00%) | 1000002.0550 | 6 | 10 | 400 | 0.5138% | 0.0010689 | 0.0009940599999999997 | 999899.9761401109 | 75.249501106859 |
| 0.0280 (2.80%) | 1000002.0340 | 7 | 12 | 475 | 0.4282% | 0.0013305300000000003 | 0.00124677 | 999874.9697224711 | 75.27950104504197 |
| 0.0320 (3.20%) | 1000001.9165 | 5 | 9 | 350 | 0.5476% | 0.00106743 | 0.0009958900000000001 | 999899.9779652994 | 75.22375147674826 |
| 0.0290 (2.90%) | 1000001.8094 | 6 | 10 | 400 | 0.4524% | 0.0010662999999999998 | 0.00099406 | 999899.9788310586 | 75.24885925780153 |
| 0.0370 (3.70%) | 1000001.3350 | 3 | 6 | 225 | 0.5933% | 0.00079951 | 0.0007450200000000001 | 999924.9826145548 | 75.1676288614165 |
| 0.0390 (3.90%) | 1000001.0436 | 2 | 5 | 175 | 0.5963% | 0.00079645 | 0.0007480900000000001 | 999924.9833821958 | 75.14193082649103 |
| 0.0380 (3.80%) | 1000000.7396 | 2 | 5 | 175 | 0.4226% | 0.00079327 | 0.0007462600000000001 | 999924.983137572 | 75.14208339901779 |
| 0.0360 (3.60%) | 1000000.7305 | 3 | 6 | 225 | 0.3246% | 0.0007931699999999999 | 0.00074036 | 999924.983513722 | 75.1674896032595 |
+----------------+--------------------+---------+--------+--------------------+---------------------+-----------------------+------------------------+-------------------+-------------------+
"""

import asyncio
import logging
import warnings

import pandas as pd
import prettytable
from backtesting import Backtest
from kraken.spot import Market

warnings.filterwarnings("ignore", category=DeprecationWarning)


def get_historical_data(pair: str, interval: str = "60") -> pd.DataFrame:
"""
Fetch historical OHLC data for a given cryptocurrency pair and interval.

Args:
pair (str): The cryptocurrency pair, e.g., 'XXBTZUSD'. interval (str):
The interval in minutes, e.g., '60' for 60 minutes or '1440' for one
day.

Returns:
pd.DataFrame: A DataFrame containing the OHLC data.
"""
df = pd.DataFrame(
Market().get_ohlc(pair=pair, interval=interval)[pair], # 720 entries
columns=["time", "open", "high", "low", "close", "vwap", "volume", "count"],
).astype(float)
df["time"] = pd.to_datetime(df["time"], unit="s")
return df


async def main() -> None:
"""
Main function to perform backtesting and find the optimal interval for the
grid trading strategy.
"""
df = get_historical_data("XXBTZEUR")
amount_per_grid = 25 # Quote currency
fee = 0.00025 # FIXME: Values near zero doesn't work.
step = 0.001
interval = 0.02 - step # Should be larger than fee * 2
initial_hold = 1000000.0
results = {}

while (interval := interval + step) <= 0.04:
bt = Backtest(
strategy_config={
"strategy": "GridHODL",
"userref": 1,
"name": "Intervaltester",
"interval": interval,
"amount_per_grid": amount_per_grid,
"max_investment": initial_hold,
"n_open_buy_orders": 3,
"base_currency": "BTC",
"quote_currency": "EUR",
"fee": fee,
},
db_config={"sqlite_file": ":memory:"},
balances={
"balances": {
"XXBT": {"balance": "0.0", "hold_trade": "0.0"},
"ZEUR": {"balance": str(initial_hold), "hold_trade": "0.0"},
},
},
)
try:
await bt.run(prices=df["close"])
finally:
await bt.instance.async_close()
await bt.instance.stop()
bt.compute_summary()
results[interval] = {
"market_price": bt.summary_market_price,
"n_buys": bt.api.n_exec_buy_orders,
"n_sells": bt.api.n_exec_sell_orders,
bt.instance.base_currency: bt.summary_balances[bt.api.base],
bt.instance.quote_currency: bt.summary_balances[bt.api.quote],
}

table = prettytable.PrettyTable()
table.field_names = [
"Interval",
f"Market Price ({bt.instance.quote_currency})",
"N Sells",
"N Buys",
f"Trade volume ({bt.instance.quote_currency})",
"Volume-based profit",
f"{bt.instance.base_currency} (total)",
f"{bt.instance.base_currency} (hold trade)",
f"{bt.instance.quote_currency} (total)",
f"{bt.instance.quote_currency} (hold trade)",
]

for interval, data in dict(
sorted(results.items(), key=lambda x: x[1]["market_price"], reverse=True),
).items():
trade_volume = (data["n_buys"] + data["n_sells"]) * amount_per_grid
volume_based_profit = (
"0.0000%"
if trade_volume == 0
else f"{((data['market_price'] - initial_hold) / trade_volume) * 100:.4f}%"
)
table.add_row(
[
f"{interval:.4f} ({interval * 100:.2f}%)",
f"{data['market_price']:.4f}",
data["n_sells"],
data["n_buys"],
trade_volume,
volume_based_profit,
data[bt.instance.base_currency]["balance"],
data[bt.instance.base_currency]["hold_trade"],
data[bt.instance.quote_currency]["balance"],
data[bt.instance.quote_currency]["hold_trade"],
],
)

print("\nResults (sorted by Market Price):")
print(f"Data time range: {df['time'].iloc[0]} to {df['time'].iloc[-1]}")
print(table)


if __name__ == "__main__":
logging.basicConfig(
format="%(asctime)s %(levelname)8s | %(message)s",
datefmt="%Y/%m/%d %H:%M:%S",
level=logging.WARNING,
)
asyncio.run(main())
8 changes: 8 additions & 0 deletions tools/backtesting/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ipykernel
ipython
jupyter # python -m ipykernel install --user --name=kraken-infinity-grid
kraken-infinity-grid
pandas
prettytable
python-kraken-sdk
tqdm
1 change: 1 addition & 0 deletions tools/calculate_drawdown.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- mode: python; coding: utf-8 -*-
# !/usr/bin/env python3
#
# Copyright (C) 2024 Benjamin Thomas Schwertfeger
# All rights reserved.
Expand Down
Loading