Skip to content

Commit

Permalink
Merge pull request #476 from enarjord/v7.2.8
Browse files Browse the repository at this point in the history
v7.2.8 new scoring metrics
  • Loading branch information
enarjord authored Nov 25, 2024
2 parents 76c8de8 + 25c090b commit 165394c
Show file tree
Hide file tree
Showing 15 changed files with 321 additions and 191 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.2.7
v7.2.8


## Overview
Expand Down
103 changes: 52 additions & 51 deletions configs/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,56 @@
"exchange": "binance",
"start_date": "2021-05-01",
"starting_balance": 100000.0},
"bot": {"long": {"close_grid_markup_range": 0.0016219,
"close_grid_min_markup": 0.012842,
"close_grid_qty_pct": 0.65242,
"close_trailing_grid_ratio": 0.021638,
"close_trailing_qty_pct": 0.88439,
"close_trailing_retracement_pct": 0.028672,
"close_trailing_threshold_pct": 0.065293,
"ema_span_0": 465.26,
"bot": {"long": {"close_grid_markup_range": 0.0013425,
"close_grid_min_markup": 0.0047292,
"close_grid_qty_pct": 0.85073,
"close_trailing_grid_ratio": 0.037504,
"close_trailing_qty_pct": 0.54254,
"close_trailing_retracement_pct": 0.021623,
"close_trailing_threshold_pct": 0.065009,
"ema_span_0": 469.33,
"ema_span_1": 1120.5,
"entry_grid_double_down_factor": 2.3744,
"entry_grid_spacing_pct": 0.052341,
"entry_grid_spacing_weight": 0.070271,
"entry_initial_ema_dist": -0.0059754,
"entry_initial_qty_pct": 0.029454,
"entry_trailing_grid_ratio": -0.28169,
"entry_trailing_retracement_pct": 0.0024748,
"entry_trailing_threshold_pct": -0.051708,
"filter_relative_volume_clip_pct": 0.51416,
"filter_rolling_window": 60.0,
"n_positions": 10.675,
"total_wallet_exposure_limit": 0.95859,
"unstuck_close_pct": 0.071741,
"unstuck_ema_dist": -0.053527,
"unstuck_loss_allowance_pct": 0.033558,
"unstuck_threshold": 0.49002},
"short": {"close_grid_markup_range": 0.0049057,
"close_grid_min_markup": 0.013579,
"close_grid_qty_pct": 0.6168,
"close_trailing_grid_ratio": 0.88873,
"close_trailing_qty_pct": 0.97705,
"close_trailing_retracement_pct": 0.095287,
"close_trailing_threshold_pct": -0.060579,
"ema_span_0": 819.23,
"ema_span_1": 246.39,
"entry_grid_double_down_factor": 2.3062,
"entry_grid_spacing_pct": 0.072015,
"entry_grid_spacing_weight": 1.4565,
"entry_initial_ema_dist": -0.072047,
"entry_initial_qty_pct": 0.072205,
"entry_trailing_grid_ratio": -0.02319,
"entry_trailing_retracement_pct": 0.017338,
"entry_trailing_threshold_pct": -0.084177,
"filter_relative_volume_clip_pct": 0.5183,
"filter_rolling_window": 68.072,
"n_positions": 1.1534,
"total_wallet_exposure_limit": 0.209,
"unstuck_close_pct": 0.052695,
"unstuck_ema_dist": -0.026947,
"unstuck_loss_allowance_pct": 0.046017,
"unstuck_threshold": 0.58422}},
"entry_grid_double_down_factor": 2.2661,
"entry_grid_spacing_pct": 0.05224,
"entry_grid_spacing_weight": 0.070246,
"entry_initial_ema_dist": -0.015187,
"entry_initial_qty_pct": 0.032679,
"entry_trailing_grid_ratio": -0.29357,
"entry_trailing_retracement_pct": 0.002646,
"entry_trailing_threshold_pct": -0.043522,
"filter_relative_volume_clip_pct": 0.51429,
"filter_rolling_window": 330.17,
"n_positions": 5.2399,
"total_wallet_exposure_limit": 1.2788,
"unstuck_close_pct": 0.05968,
"unstuck_ema_dist": -0.027416,
"unstuck_loss_allowance_pct": 0.035915,
"unstuck_threshold": 0.45572},
"short": {"close_grid_markup_range": 0.0020933,
"close_grid_min_markup": 0.016488,
"close_grid_qty_pct": 0.93256,
"close_trailing_grid_ratio": 0.035892,
"close_trailing_qty_pct": 0.98975,
"close_trailing_retracement_pct": 0.0042704,
"close_trailing_threshold_pct": -0.046918,
"ema_span_0": 1174.4,
"ema_span_1": 1217.3,
"entry_grid_double_down_factor": 2.0966,
"entry_grid_spacing_pct": 0.070355,
"entry_grid_spacing_weight": 1.5293,
"entry_initial_ema_dist": -0.090036,
"entry_initial_qty_pct": 0.07003,
"entry_trailing_grid_ratio": 0.075994,
"entry_trailing_retracement_pct": 0.023943,
"entry_trailing_threshold_pct": -0.079098,
"filter_relative_volume_clip_pct": 0.49361,
"filter_rolling_window": 57.016,
"n_positions": 1.1103,
"total_wallet_exposure_limit": 0.0,
"unstuck_close_pct": 0.063395,
"unstuck_ema_dist": -0.025704,
"unstuck_loss_allowance_pct": 0.04867,
"unstuck_threshold": 0.58437}},
"live": {"approved_coins": [],
"auto_gs": true,
"coin_flags": {},
Expand Down Expand Up @@ -128,9 +128,10 @@
"crossover_probability": 0.7,
"iters": 30000,
"limits": {"lower_bound_drawdown_worst": 0.25,
"lower_bound_equity_balance_diff_mean": 0.01,
"lower_bound_drawdown_worst_mean_1pct": 0.15,
"lower_bound_equity_balance_diff_mean": 0.02,
"lower_bound_loss_profit_ratio": 0.6},
"mutation_probability": 0.2,
"n_cpus": 5,
"population_size": 500,
"scoring": ["mdg", "sharpe_ratio"]}}
"scoring": ["mdg", "sortino_ratio"]}}
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ When optimizing, parameter values are within the lower and upper bounds.
- Default values are median daily gain and Sharpe ratio.
- The script uses the NSGA-II algorithm (Non-dominated Sorting Genetic Algorithm II) for multi-objective optimization.
- The fitness function is set up to minimize both objectives (converted to negative values internally).
- Options: adg, mdg, sharpe_ratio, sortino_ratio, omega_ratio, calmar_ratio, sterling_ratio
- Examples: ["mdg", "sharpe_ratio"], ["adg", "sortino_ratio"], ["sortino_ratio", "omega_ratio"]

### Optimization Limits

Expand Down
138 changes: 116 additions & 22 deletions passivbot-rust/src/backtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,54 +1450,142 @@ pub fn analyze_backtest(fills: &[Fill], equities: &Vec<f64>) -> Analysis {
// Calculate daily equities
let mut daily_eqs = Vec::new();
let mut current_day = 0;
let mut sum = 0.0;
let mut count = 0;
let mut current_min = equities[0];

for (i, &equity) in equities.iter().enumerate() {
let day = i / 1440;
if day > current_day {
daily_eqs.push(sum / count as f64);
daily_eqs.push(current_min);
current_day = day;
sum = equity;
count = 1;
current_min = equity;
} else {
sum += equity;
count += 1;
current_min = current_min.min(equity);
}
}
if count > 0 {
daily_eqs.push(sum / count as f64);
if current_min != f64::INFINITY {
daily_eqs.push(current_min);
}

// Calculate daily percentage changes
let daily_eqs_pct_change: Vec<f64> =
daily_eqs.windows(2).map(|w| (w[1] - w[0]) / w[0]).collect();

// Calculate ADG and Sharpe ratio
// Calculate ADG and standard metrics
let adg = daily_eqs_pct_change.iter().sum::<f64>() / daily_eqs_pct_change.len() as f64;
// Calculate MDG
let mut sorted_pct_change = daily_eqs_pct_change.clone();
sorted_pct_change.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let mdg = if sorted_pct_change.len() % 2 == 0 {
(sorted_pct_change[sorted_pct_change.len() / 2 - 1]
+ sorted_pct_change[sorted_pct_change.len() / 2])
/ 2.0
} else {
sorted_pct_change[sorted_pct_change.len() / 2]
let mdg = {
let mut sorted_pct_change = daily_eqs_pct_change.clone();
sorted_pct_change.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
if sorted_pct_change.len() % 2 == 0 {
(sorted_pct_change[sorted_pct_change.len() / 2 - 1]
+ sorted_pct_change[sorted_pct_change.len() / 2])
/ 2.0
} else {
sorted_pct_change[sorted_pct_change.len() / 2]
}
};
// Calculate Sharpe Ratio

// Calculate variance and standard deviation
let variance = daily_eqs_pct_change
.iter()
.map(|&x| (x - adg).powi(2))
.sum::<f64>()
/ daily_eqs_pct_change.len() as f64;
let sharpe_ratio = adg / variance.sqrt();
let std_dev = variance.sqrt();

// Calculate Sharpe Ratio
let sharpe_ratio = if std_dev != 0.0 { adg / std_dev } else { 0.0 };

// Calculate Sortino Ratio (using downside deviation)
let downside_returns: Vec<f64> = daily_eqs_pct_change
.iter()
.filter(|&&x| x < 0.0)
.cloned()
.collect();
let downside_deviation = if !downside_returns.is_empty() {
(downside_returns.iter().map(|x| x.powi(2)).sum::<f64>() / downside_returns.len() as f64)
.sqrt()
} else {
0.0
};
let sortino_ratio = if downside_deviation != 0.0 {
adg / downside_deviation
} else {
0.0
};

// Calculate Omega Ratio (threshold = 0)
let (gains_sum, losses_sum) =
daily_eqs_pct_change
.iter()
.fold((0.0, 0.0), |(gains, losses), &ret| {
if ret >= 0.0 {
(gains + ret, losses)
} else {
(gains, losses + ret.abs())
}
});
let omega_ratio = if losses_sum != 0.0 {
gains_sum / losses_sum
} else {
f64::INFINITY
};

// Calculate Expected Shortfall (99%)
let expected_shortfall_1pct = {
let mut sorted_returns = daily_eqs_pct_change.clone();
sorted_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let cutoff_index = (daily_eqs_pct_change.len() as f64 * 0.01) as usize;
if cutoff_index > 0 {
sorted_returns[..cutoff_index]
.iter()
.map(|x| x.abs())
.sum::<f64>()
/ cutoff_index as f64
} else {
sorted_returns[0].abs()
}
};

// Calculate drawdowns
let drawdowns = calc_drawdowns(&equities);
let drawdowns = calc_drawdowns(&daily_eqs);
let drawdown_worst_mean_1pct = {
let mut sorted_drawdowns = drawdowns.clone();
sorted_drawdowns.sort_by(|a, b| b.abs().partial_cmp(&a.abs()).unwrap_or(Ordering::Equal));
let cutoff_index = std::cmp::max(1, (sorted_drawdowns.len() as f64 * 0.01) as usize);
let worst_n = std::cmp::min(cutoff_index, sorted_drawdowns.len());
sorted_drawdowns[..worst_n]
.iter()
.map(|x| x.abs())
.sum::<f64>()
/ worst_n as f64
};
let drawdown_worst = drawdowns
.iter()
.fold(f64::NEG_INFINITY, |a, &b| f64::max(a, b.abs()));

let calmar_ratio = if drawdown_worst != 0.0 {
adg / drawdown_worst
} else {
0.0
};

// Calculate Sterling Ratio (using average of worst N drawdowns)
let sterling_ratio = {
let mut sorted_drawdowns = drawdowns.clone();
sorted_drawdowns.sort_by(|a, b| b.abs().partial_cmp(&a.abs()).unwrap_or(Ordering::Equal));
let worst_n = std::cmp::min(10, sorted_drawdowns.len());
let avg_worst_drawdowns = sorted_drawdowns[..worst_n]
.iter()
.map(|x| x.abs())
.sum::<f64>()
/ worst_n as f64;
if avg_worst_drawdowns != 0.0 {
adg / avg_worst_drawdowns // Using raw daily gain instead of annualized
} else {
0.0
}
};

// Calculate equity-balance differences
let mut bal_eq = Vec::with_capacity(equities.len());
let mut fill_iter = fills.iter().peekable();
Expand Down Expand Up @@ -1542,7 +1630,13 @@ pub fn analyze_backtest(fills: &[Fill], equities: &Vec<f64>) -> Analysis {
adg,
mdg,
sharpe_ratio,
sortino_ratio,
omega_ratio,
expected_shortfall_1pct,
calmar_ratio,
sterling_ratio,
drawdown_worst,
drawdown_worst_mean_1pct,
equity_balance_diff_mean,
equity_balance_diff_max,
loss_profit_ratio,
Expand Down
9 changes: 9 additions & 0 deletions passivbot-rust/src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,16 @@ pub fn run_backtest(
py_analysis.set_item("adg", analysis.adg)?;
py_analysis.set_item("mdg", analysis.mdg)?;
py_analysis.set_item("sharpe_ratio", analysis.sharpe_ratio)?;
py_analysis.set_item("sortino_ratio", analysis.sortino_ratio)?;
py_analysis.set_item("omega_ratio", analysis.omega_ratio)?;
py_analysis.set_item("expected_shortfall_1pct", analysis.expected_shortfall_1pct)?;
py_analysis.set_item("calmar_ratio", analysis.calmar_ratio)?;
py_analysis.set_item("sterling_ratio", analysis.sterling_ratio)?;
py_analysis.set_item("drawdown_worst", analysis.drawdown_worst)?;
py_analysis.set_item(
"drawdown_worst_mean_1pct",
analysis.drawdown_worst_mean_1pct,
)?;
py_analysis.set_item(
"equity_balance_diff_mean",
analysis.equity_balance_diff_mean,
Expand Down
12 changes: 12 additions & 0 deletions passivbot-rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@ pub struct Analysis {
pub adg: f64,
pub mdg: f64,
pub sharpe_ratio: f64,
pub sortino_ratio: f64,
pub omega_ratio: f64,
pub expected_shortfall_1pct: f64,
pub calmar_ratio: f64,
pub sterling_ratio: f64,
pub drawdown_worst: f64,
pub drawdown_worst_mean_1pct: f64,
pub equity_balance_diff_mean: f64,
pub equity_balance_diff_max: f64,
pub loss_profit_ratio: f64,
Expand All @@ -229,7 +235,13 @@ impl Default for Analysis {
adg: 0.0,
mdg: 0.0,
sharpe_ratio: 0.0,
sortino_ratio: 0.0,
omega_ratio: 0.0,
expected_shortfall_1pct: 0.0,
calmar_ratio: 0.0,
sterling_ratio: 0.0,
drawdown_worst: 1.0,
drawdown_worst_mean_1pct: 1.0,
equity_balance_diff_mean: 1.0,
equity_balance_diff_max: 1.0,
loss_profit_ratio: 1.0,
Expand Down
4 changes: 3 additions & 1 deletion src/exchanges/binance.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
class BinanceBot(Passivbot):
def __init__(self, config: dict):
super().__init__(config)
self.custom_id_max_length = 36

def create_ccxt_sessions(self):
self.broker_code_spot = load_broker_code("binance_spot")
for ccx, ccxt_module in [("cca", ccxt_async), ("ccp", ccxt_pro)]:
exchange_class = getattr(ccxt_module, "binanceusdm")
Expand All @@ -47,7 +50,6 @@ def __init__(self, config: dict):
if self.broker_code_spot:
for key in ["spot", "margin"]:
getattr(self, ccx).options["broker"][key] = "x-" + self.broker_code_spot
self.custom_id_max_length = 36

async def print_new_user_suggestion(self):
res = None
Expand Down
Loading

0 comments on commit 165394c

Please sign in to comment.