Skip to content

Commit 07e1ed0

Browse files
committed
The first backtest integration test!
1 parent 103a9e1 commit 07e1ed0

File tree

7 files changed

+138
-2
lines changed

7 files changed

+138
-2
lines changed

.github/workflows/tests.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,28 @@ jobs:
231231
runs-on: ubuntu-20.04
232232
needs:
233233
- Pre-Commit
234-
# - Exchange-Data
234+
- Binance-Exchange-Data
235+
# - Kucoin-Exchange-Data
235236

236237
steps:
237238
- uses: actions/checkout@v2
239+
240+
- name: Backtesting Variables
241+
id: dotenv
242+
uses: falti/[email protected]
243+
with:
244+
log-variables: true
245+
path:
246+
user_data/backtesting-binance.env
247+
248+
- name: Download Data Cache
249+
uses: actions/download-artifact@v2
250+
with:
251+
name: exchange-data-${{ steps.dotenv.outputs.exchange }}
252+
path: user_data/data/${{ steps.dotenv.outputs.exchange }}
253+
254+
- name: Build Tests Image
255+
run: docker-compose build tests
256+
238257
- name: Run Tests
239258
run: docker-compose run --rm tests

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
volumes:
1010
- "./:/testing"
1111
command: >
12-
python -m pytest -ra -vv -s /testing
12+
python -m pytest -ra -vv -s --log-cli-level=info tests/
1313
entrypoint: []
1414
working_dir: /testing
1515
backtesting:

tests/backtests/__init__.py

Whitespace-only changes.

tests/backtests/binance/__init__.py

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import functools
2+
3+
import pytest
4+
5+
from tests.backtests.helpers import exchange_backtest
6+
7+
8+
@pytest.fixture
9+
def backtest(tmp_path):
10+
return functools.partial(exchange_backtest, "binance", tmp_path)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def test_20210601_20210701(backtest):
2+
ret = backtest(start_date="20210601", end_date="20210701")
3+
assert ret.results.max_drawdown < 0.17

tests/backtests/helpers.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import json
2+
import logging
3+
import subprocess
4+
from types import SimpleNamespace
5+
6+
import attr
7+
8+
from tests.conftest import REPO_ROOT
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
@attr.s(frozen=True)
14+
class ProcessResult:
15+
"""
16+
This class serves the purpose of having a common result class which will hold the
17+
resulting data from a subprocess command.
18+
:keyword int exitcode:
19+
The exitcode returned by the process
20+
:keyword str stdout:
21+
The ``stdout`` returned by the process
22+
:keyword str stderr:
23+
The ``stderr`` returned by the process
24+
:keyword list,tuple cmdline:
25+
The command line used to start the process
26+
.. admonition:: Note
27+
Cast :py:class:`~saltfactories.utils.processes.ProcessResult` to a string to pretty-print it.
28+
"""
29+
30+
exitcode = attr.ib()
31+
stdout = attr.ib()
32+
stderr = attr.ib()
33+
cmdline = attr.ib(default=None, kw_only=True)
34+
35+
@exitcode.validator
36+
def _validate_exitcode(self, attribute, value):
37+
if not isinstance(value, int):
38+
raise ValueError(f"'exitcode' needs to be an integer, not '{type(value)}'")
39+
40+
def __str__(self):
41+
message = self.__class__.__name__
42+
if self.cmdline:
43+
message += f"\n Command Line: {self.cmdline}"
44+
if self.exitcode is not None:
45+
message += f"\n Exitcode: {self.exitcode}"
46+
if self.stdout or self.stderr:
47+
message += "\n Process Output:"
48+
if self.stdout:
49+
message += f"\n >>>>> STDOUT >>>>>\n{self.stdout}\n <<<<< STDOUT <<<<<"
50+
if self.stderr:
51+
message += f"\n >>>>> STDERR >>>>>\n{self.stderr}\n <<<<< STDERR <<<<<"
52+
return message + "\n"
53+
54+
55+
def exchange_backtest(
56+
exchange,
57+
tmp_path,
58+
start_date,
59+
end_date,
60+
pairlist=None,
61+
max_open_trades=5,
62+
stake_amount="unlimited",
63+
):
64+
exchange_config = f"user_data/{exchange}-usdt-static.json"
65+
json_results_file = tmp_path / "backtest-results.json"
66+
cmdline = [
67+
"freqtrade",
68+
"backtesting",
69+
f"--user-data=user_data",
70+
"--strategy-list=NostalgiaForInfinityNext",
71+
f"--timerange={start_date}-{end_date}",
72+
f"--max-open-trades={max_open_trades}",
73+
f"--stake-amount={stake_amount}",
74+
"--config=user_data/pairlists.json",
75+
f"--export-filename={json_results_file}",
76+
]
77+
if pairlist is None:
78+
cmdline.append(f"--config={exchange_config}")
79+
else:
80+
pairlist_config = {"exchange": {"name": exchange, "pair_whitelist": pairlist}}
81+
pairlist_config_file = tmp_path / "test-pairlist.json"
82+
pairlist_config_file.write(json.dumps(pairlist_config))
83+
cmdline.append(f"--config={pairlist_config_file}")
84+
log.info("Running cmdline '%s' on '%s'", " ".join(cmdline), REPO_ROOT)
85+
proc = subprocess.run(
86+
cmdline, check=False, shell=False, cwd=REPO_ROOT, text=True, capture_output=True
87+
)
88+
ret = ProcessResult(
89+
exitcode=proc.returncode,
90+
stdout=proc.stdout.strip(),
91+
stderr=proc.stderr.strip(),
92+
cmdline=cmdline,
93+
)
94+
log.info("Command Result:\n%s", ret)
95+
assert ret.exitcode == 0
96+
generated_results_file = list(tmp_path.rglob("backtest-results-*.json"))[0]
97+
results_data = json.loads(generated_results_file.read_text())
98+
data = {
99+
"stdout": ret.stdout.strip(),
100+
"stderr": ret.stderr.strip(),
101+
"comparison": results_data["strategy_comparison"],
102+
"results": results_data["strategy"]["NostalgiaForInfinityNext"],
103+
}
104+
return json.loads(json.dumps(data), object_hook=lambda d: SimpleNamespace(**d))

0 commit comments

Comments
 (0)