Skip to content

Commit

Permalink
Merge branch 'v7.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
enarjord committed Sep 8, 2024
2 parents 2698a52 + c854ee9 commit e036de7
Show file tree
Hide file tree
Showing 15 changed files with 214 additions and 178 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

:warning: **Used at one's own risk** :warning:

v7.0.0
v7.0.1


## Overview
Expand Down
2 changes: 1 addition & 1 deletion configs/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"max_n_cancellations_per_batch": 5,
"max_n_creations_per_batch": 3,
"minimum_coin_age_days": 30.0,
"noisiness_rolling_mean_window_size": 60,
"ohlcv_rolling_window": 60,
"pnls_max_lookback_days": 30.0,
"price_distance_threshold": 0.002,
"relative_volume_filter_clip_pct": 0.5,
Expand Down
17 changes: 16 additions & 1 deletion docs/backtesting.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# Backtesting

Coming soon...
Passivbot includes a backtester which will simulate the bot's behavior on past price data. Historical 1m candlestick data is automatically downloaded and cached for all coins.

## Usage

```shell
python3 src/backtest.py
```
Or
```shell
python3 src/backtest.py path/to/config.json
```
If no config is specified, it will default to `configs/template.json`

## Backtest Results

Metrics and plots are dumped to `backtests/{exchange}/`.
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ If a position is stuck, bot will use profits made on other positions to realize
- `leverage`: leverage set on exchange. Default 10.
- `max_n_cancellations_per_batch`: will cancel n open orders per execution
- `max_n_creations_per_batch`: will create n new orders per execution
- `minimum_market_age_days`: disallow coins younger than a given number of days
- `noisiness_rolling_mean_window_size`: number of minutes to look into the past to compute noisiness
- `minimum_coin_age_days`: disallow coins younger than a given number of days
- `ohlcv_rolling_window`: number of minutes to look into the past to compute volume and noisiness, used for dynamic coin selection in forager mode.
- noisiness is normalized relative range of 1m ohlcvs: `mean((high - low) / close)`
- in forager mode, bot will select coins with highest noisiness for opening positions
- `pnls_max_lookback_days`: how far into the past to fetch pnl history
Expand Down
21 changes: 20 additions & 1 deletion docs/optimizing.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# Optimizing

Coming soon...
Passivbot's config parameters may be automatically optimized by iterating many backtests and extracting the optimal config.

## Usage

```shell
python3 src/optimize.py
```
Or
```shell
python3 src/optimize.py path/to/config.json
```
If no config is specified, it will default to `configs/template.json`

## Optimizing Results

All backtest results produced by the optimizer are stored in `optimize_results/`

## Analyzing Results

After optimization is complete, the script `src/tools/extract_best_config.py` will be run, analyzing all the backtest results and dumping the best one to `optimize_results_analysis/`
3 changes: 3 additions & 0 deletions passivbot-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ fn passivbot_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(round_, m)?)?;
m.add_function(wrap_pyfunction!(round_up, m)?)?;
m.add_function(wrap_pyfunction!(round_dn, m)?)?;
m.add_function(wrap_pyfunction!(round_dynamic, m)?)?;
m.add_function(wrap_pyfunction!(round_dynamic_up, m)?)?;
m.add_function(wrap_pyfunction!(round_dynamic_dn, m)?)?;
m.add_function(wrap_pyfunction!(calc_diff, m)?)?;
m.add_function(wrap_pyfunction!(qty_to_cost, m)?)?;
m.add_function(wrap_pyfunction!(cost_to_qty, m)?)?;
Expand Down
33 changes: 33 additions & 0 deletions passivbot-rust/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@ pub fn round_dn(n: f64, step: f64) -> f64 {
round_to_decimal_places(result, 10)
}

#[pyfunction]
pub fn round_dynamic(n: f64, d: i32) -> f64 {
if n == 0.0 {
return n;
}
let shift = d - (n.abs().log10().floor() as i32) - 1;
let multiplier = 10f64.powi(shift);
let result = (n * multiplier).round() / multiplier;
round_to_decimal_places(result, 10)
}

#[pyfunction]
pub fn round_dynamic_up(n: f64, d: i32) -> f64 {
if n == 0.0 {
return n;
}
let shift = d - (n.abs().log10().floor() as i32) - 1;
let multiplier = 10f64.powi(shift);
let result = (n * multiplier).ceil() / multiplier;
round_to_decimal_places(result, 10)
}

#[pyfunction]
pub fn round_dynamic_dn(n: f64, d: i32) -> f64 {
if n == 0.0 {
return n;
}
let shift = d - (n.abs().log10().floor() as i32) - 1;
let multiplier = 10f64.powi(shift);
let result = (n * multiplier).floor() / multiplier;
round_to_decimal_places(result, 10)
}

#[pyfunction]
pub fn calc_diff(x: f64, y: f64) -> f64 {
if y == 0.0 {
Expand Down
87 changes: 19 additions & 68 deletions src/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
load_config,
dump_config,
coin_to_symbol,
add_arguments_recursively,
update_config_with_args,
format_config,
)
from pure_funcs import (
get_template_live_config,
Expand Down Expand Up @@ -111,46 +114,9 @@ def check_nested(d0, d1):
return check_nested(dict0, dict1)


def add_argparse_args_to_config(config, args):
for key, value in vars(args).items():
try:
if value is None:
continue
if key == "symbols":
symbols = sorted([xf for x in set(value.split(",")) if (xf := coin_to_symbol(x))])
if symbols != sorted(set(config["live"]["approved_coins"])):
logging.info(f"new symbols: {symbols}")
config["live"]["approved_coins"] = [coin_to_symbol(x) for x in symbols]
config["backtest"]["symbols"] = config["live"]["approved_coins"]
elif key in config["backtest"]:
if not isinstance(config["backtest"][key], dict):
if config["backtest"][key] != value:
logging.info(f"changing backtest {key} {config['backtest'][key]} -> {value}")
config["backtest"][key] = value
elif key in config["optimize"]:
if not isinstance(config["optimize"][key], dict):
if config["optimize"][key] != value:
logging.info(f"changing optimize {key} {config['optimize'][key]} -> {value}")
config["optimize"][key] = value
elif key in config["optimize"]["bounds"]:
new_value = [value, value]
if config["optimize"]["bounds"][key] != new_value:
logging.info(f"fixing optimizing bound {key} to {value}")
config["optimize"]["bounds"][key] = new_value
elif key in config["optimize"]["limits"]:
old_value = config["optimize"]["limits"][key]
if old_value != value:
logging.info(f"changing optimizing limit {key} from {old_value} to {value}")
config["optimize"]["limits"][key] = value
except Exception as e:
raise Exception(f"failed to add argparse arg to config {key}: {e}")
return config


async def prepare_hlcvs_mss(config):
results_path = oj(
config["backtest"]["base_dir"],
"forager",
config["backtest"]["exchange"],
"",
)
Expand Down Expand Up @@ -246,36 +212,8 @@ def plot_forager(results_path, symbols: [str], fdf: pd.DataFrame, bal_eq, hlcs):
plt.savefig(oj(plots_dir, f"{symbol}.png"))


def add_argparse_args_backtest(parser):
parser_items = [
("s", "symbols", "symbols", str, ", comma separated (SYM1USDT,SYM2USDT,...)"),
("e", "exchange", "exchange", str, ""),
("sd", "start_date", "start_date", str, ""),
(
"ed",
"end_date",
"end_date",
str,
", if end date is 'now', will use current date as end date",
),
("sb", "starting_balance", "starting_balance", float, ""),
("bd", "base_dir", "base_dir", str, ""),
]
for k0, k1, d, t, h in parser_items:
parser.add_argument(
*[f"-{k0}", f"--{k1}"] + ([f"--{k1.replace('_', '-')}"] if "_" in k1 else []),
type=t,
required=False,
dest=d,
default=None,
help=f"specify {k1}{h}, overriding value from hjson config.",
)
args = parser.parse_args()
return args


def calc_preferred_coins(hlcvs, config):
w_size = config["live"]["noisiness_rolling_mean_window_size"]
w_size = config["live"]["ohlcv_rolling_window"]
n_coins = hlcvs.shape[1]

# Calculate noisiness indices
Expand Down Expand Up @@ -327,9 +265,22 @@ async def main():
)
parser = argparse.ArgumentParser(prog="backtest", description="run forager backtest")
parser.add_argument("config_path", type=str, default=None, help="path to hjson passivbot config")
args = add_argparse_args_backtest(parser)
template_config = get_template_live_config("v7")
del template_config["optimize"]
keep_live_keys = {
"approved_coins",
"minimum_coin_age_days",
"ohlcv_rolling_window",
"relative_volume_filter_clip_pct",
}
for key in sorted(template_config["live"]):
if key not in keep_live_keys:
del template_config["live"][key]
add_arguments_recursively(parser, template_config)
args = parser.parse_args()
config = load_config("configs/template.hjson" if args.config_path is None else args.config_path)
config = add_argparse_args_to_config(config, args)
update_config_with_args(config, args)
config = format_config(config)
symbols, hlcvs, mss, results_path = await prepare_hlcvs_mss(config)
config["backtest"]["symbols"] = symbols
preferred_coins = calc_preferred_coins(hlcvs, config)
Expand Down
2 changes: 1 addition & 1 deletion src/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from dateutil import parser
from tqdm import tqdm

from njit_funcs import calc_samples, round_up, round_dn, round_
from njit_funcs import calc_samples
from procedures import (
prepare_backtest_config,
make_get_filepath,
Expand Down
83 changes: 33 additions & 50 deletions src/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import asyncio
import argparse
import multiprocessing
import subprocess
from multiprocessing import shared_memory
from backtest import (
prepare_hlcvs_mss,
prep_backtest_args,
add_argparse_args_to_config,
calc_preferred_coins,
)
from pure_funcs import (
Expand Down Expand Up @@ -214,57 +214,16 @@ def cleanup(self):
self.shared_preferred_coins.unlink()


def add_argparse_args_optimize(parser):
parser_items = [
("s", "symbols", "symbols", str, ", comma separated (SYM1USDT,SYM2USDT,...)"),
("e", "exchange", "exchange", str, ""),
("sd", "start_date", "start_date", str, ""),
(
"ed",
"end_date",
"end_date",
str,
", if end date is 'now', will use current date as end date",
),
("sb", "starting_balance", "starting_balance", float, ""),
("bd", "base_dir", "base_dir", str, ""),
("c", "n_cpus", "n_cpus", int, ""),
("i", "iters", "iters", int, ""),
("p", "population_size", "population_size", int, ""),
]
template = get_template_live_config("v7")
shortened_already_added = set([x[0] for x in parser_items])
for key in list(template["optimize"]["bounds"]) + list(template["optimize"]["limits"]):
shortened = "".join([x[0] for x in key.split("_")])
if shortened in shortened_already_added:
for i in range(100):
shortened = "".join([x[0] for x in key.split("_")]) + str(i)
if shortened not in shortened_already_added:
break
else:
raise Exception(f"too many duplicates of shortened key {key}")
parser_items.append((shortened, key, key, float, ", fixing optimizing bounds"))
shortened_already_added.add(shortened)
for k0, k1, d, t, h in parser_items:
parser.add_argument(
*[f"-{k0}", f"--{k1}"] + ([f"--{k1.replace('_', '-')}"] if "_" in k1 else []),
type=t,
required=False,
dest=d,
default=None,
help=f"specify {k1}{h}, overriding value from config.",
)
def add_extra_options(parser):
parser.add_argument(
"-t",
"--start",
type=str,
required=False,
dest="starting_configs",
default=None,
help="start with given live configs. single json file or dir with multiple json files",
help="Start with given live configs. Single json file or dir with multiple json files",
)
args = parser.parse_args()
return args


def get_starting_configs(starting_configs: str):
Expand Down Expand Up @@ -296,13 +255,24 @@ def configs_to_individuals(cfgs):

async def main():
manage_rust_compilation()
parser = argparse.ArgumentParser(prog="optimize", description="run forager optimizer")
parser = argparse.ArgumentParser(prog="optimize", description="run optimizer")
parser.add_argument(
"config_path", type=str, default=None, nargs="?", help="path to json passivbot config"
)
# add_arguments_recursively(parser, get_template_live_config("v7"))
# args = parser.parse_args()
args = add_argparse_args_optimize(parser)
template_config = get_template_live_config("v7")
del template_config["bot"]
keep_live_keys = {
"approved_coins",
"minimum_coin_age_days",
"ohlcv_rolling_window",
"relative_volume_filter_clip_pct",
}
for key in sorted(template_config["live"]):
if key not in keep_live_keys:
del template_config["live"][key]
add_arguments_recursively(parser, template_config)
add_extra_options(parser)
args = parser.parse_args()
signal.signal(signal.SIGINT, signal_handler)
logging.basicConfig(
format="%(asctime)s %(levelname)-8s %(message)s",
Expand All @@ -315,7 +285,9 @@ async def main():
else:
logging.info(f"loading config {args.config_path}")
config = load_config(args.config_path)
config = add_argparse_args_to_config(config, args)
old_config = deepcopy(config)
update_config_with_args(config, args)
config = format_config(config)
symbols, hlcvs, mss, results_path = await prepare_hlcvs_mss(config)
config["backtest"]["symbols"] = symbols
preferred_coins = calc_preferred_coins(hlcvs, config)
Expand All @@ -325,7 +297,7 @@ async def main():
coins_fname = "_".join(coins) if len(coins) <= 6 else f"{len(coins)}_coins"
hash_snippet = uuid4().hex[:8]
config["results_filename"] = make_get_filepath(
f"opt_results_forager/{date_fname}_{coins_fname}_{hash_snippet}_all_results.txt"
f"optimize_results/{date_fname}_{coins_fname}_{hash_snippet}_all_results.txt"
)
try:
evaluator = Evaluator(hlcs, preferred_coins, config, mss)
Expand Down Expand Up @@ -426,6 +398,17 @@ def create_individual():
print(logbook)

logging.info(f"Optimization complete.")
try:
logging.info(f"Extracting best config...")
result = subprocess.run(
["python3", "src/tools/extract_best_config.py", config["results_filename"], "-v"],
check=True,
capture_output=True,
text=True,
)
print(result.stdout)
except Exception as e:
logging.error(f"failed to extract best config {e}")
########
except Exception as e:
traceback.print_exc()
Expand Down
Loading

0 comments on commit e036de7

Please sign in to comment.