From 09b957f3ac31bbe646d545434d832c1a51b480c7 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 29 May 2026 18:50:14 +0200 Subject: [PATCH 1/5] tweak: reduce allocation_per_trade_percent to 20% (safety) --- config.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.toml b/config.toml index 6ca3088..28efad7 100644 --- a/config.toml +++ b/config.toml @@ -7,12 +7,12 @@ base_currency = "EUR" auto_select_pair = true loop_interval_seconds = 60 trade_pairs = [ "XXBTZEUR", "XETHZEUR", "SOLEUR", "XXRPZEUR",] -allocation_per_trade_percent = 95.0 +allocation_per_trade_percent = 20.0 [risk_management] max_drawdown_percent = 10.0 stop_loss_percent = 1.5 -allocation_per_trade_percent = 95.0 +allocation_per_trade_percent = 20.0 min_buy_score = 0.0 enable_mean_reversion_signals = true enable_trend_breakout_signals = true From cbbcc891712569d046092734aa3c061dd74ebbdd Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 29 May 2026 21:41:00 +0200 Subject: [PATCH 2/5] feat: add per-symbol override section for intraday params --- config.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config.toml b/config.toml index 28efad7..87367d0 100644 --- a/config.toml +++ b/config.toml @@ -110,6 +110,12 @@ max_consecutive_losses = 2 loss_streak_pause_minutes = 60 # NOTE: loss_streak_pause_minutes set to 0 to remove cooling period +[symbols] +# Per-symbol overrides. Example: +# [symbols.DOTEUR] +# intraday_sl_percent = 2.5 +# intraday_tp_percent = 3.0 + [bear_shield] enable_bear_shield = false From fa03e8135396be9e4d22b30fcb93ee05846d32cc Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 29 May 2026 21:41:10 +0200 Subject: [PATCH 3/5] chore: add DOTEUR per-symbol intraday SL/TP override --- config.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config.toml b/config.toml index 87367d0..d259403 100644 --- a/config.toml +++ b/config.toml @@ -114,7 +114,9 @@ loss_streak_pause_minutes = 60 # Per-symbol overrides. Example: # [symbols.DOTEUR] # intraday_sl_percent = 2.5 -# intraday_tp_percent = 3.0 +[symbols.DOTEUR] +intraday_sl_percent = 2.5 +intraday_tp_percent = 3.0 [bear_shield] From 00c80b4e14633954b7573c5b2336ca56017ac0fc Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 29 May 2026 21:41:28 +0200 Subject: [PATCH 4/5] chore: add 90d sweep script (reports excluded) --- tmp_backtest_sweep.py | 196 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 tmp_backtest_sweep.py diff --git a/tmp_backtest_sweep.py b/tmp_backtest_sweep.py new file mode 100644 index 0000000..1971f93 --- /dev/null +++ b/tmp_backtest_sweep.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +import json, os, datetime + +BASE = '/mnt/fritz_nas/Volume/kraken/2026' +SYMBOLS = ['XXRPZEUR','XETHZEUR','ADAEUR','DOTEUR'] +OUT = 'reports/sweep_90d.json' + +# helper to read ohlc file (prefer 15m else 5m) +def load_ohlc(symbol): + d = os.path.join(BASE, symbol) + if not os.path.isdir(d): + return None + # prefer ohlc_15m.csv + for fname in ['ohlc_15m.csv','ohlc_5m.csv','ohlc_60m.csv']: + path = os.path.join(d, fname) + if os.path.isfile(path): + return path + return None + +# parse csv into list of candles (ts,open,high,low,close,volume) +def parse_csv(path): + rows = [] + with open(path,'r') as f: + lines = [ln.strip() for ln in f if ln.strip()] + if not lines: + return [] + header = [h.strip() for h in lines[0].split(',')] + for ln in lines[1:]: + parts = ln.split(',') + if len(parts) < 5: + continue + rec = dict(zip(header, parts)) + try: + ts = int(rec.get('ts','0')) + o = float(rec.get('open','')) + h = float(rec.get('high','')) + l = float(rec.get('low','')) + c = float(rec.get('close','')) + except: + continue + vol = None + try: + vol = float(rec.get('volume','')) + except: + vol = None + rows.append({'ts':ts,'dt':datetime.datetime.utcfromtimestamp(ts),'open':o,'high':h,'low':l,'close':c,'volume':vol}) + return rows + +# aggregate 5m->15m if needed +from collections import defaultdict + +def to_15m(rows, src_minutes): + if src_minutes == 15: + return rows + buckets = defaultdict(list) + period = src_minutes*60 + for r in rows: + k = (r['ts']//900)*900 + buckets[k].append(r) + agg = [] + for k in sorted(buckets.keys()): + group = buckets[k] + opens = [g['open'] for g in group] + highs = [g['high'] for g in group] + lows = [g['low'] for g in group] + closes = [g['close'] for g in group] + vols = [g['volume'] for g in group if g['volume'] is not None] + agg.append({'ts':k,'dt':datetime.datetime.utcfromtimestamp(k),'open':opens[0],'high':max(highs),'low':min(lows),'close':closes[-1],'volume':sum(vols) if vols else None}) + return agg + +# EMA and backtest logic (same as tmp_backtest_xrp) +def ema(series_vals, period): + k = 2.0/(period+1) + out = [] + s = None + for v in series_vals: + if s is None: + s = v + else: + s = v*k + s*(1-k) + out.append(s) + return out + +def run_backtest_on_series(series, params): + fast_p = 9 + slow_p = 21 + closes = [c['close'] for c in series] + if len(closes) < slow_p+1: + return {'error':'not_enough_bars','bars':len(closes)} + ema_fast = ema(closes, fast_p) + ema_slow = ema(closes, slow_p) + in_pos = False + entry_price = None + entry_idx = None + qty = 0.0 + cash = 200.0 + closed = [] + fee_rate = params['fee_rate'] + alloc_frac = params['allocation_pct']/100.0 + sl_pct = params['sl_pct'] + tp_pct = params['tp_pct'] + max_hold = 48 + for i in range(1,len(series)): + if not in_pos and ema_fast[i] is not None and ema_slow[i] is not None and ema_fast[i]>ema_slow[i] and ema_fast[i-1]<=ema_slow[i-1]: + entry_price = series[i]['open']*(1+0.0008) + allocation = cash * alloc_frac + if allocation < 1.0: + continue + qty = (allocation) / entry_price + cash -= allocation + in_pos = True + entry_idx = i + continue + if in_pos: + px_high = series[i]['high'] + px_low = series[i]['low'] + tp_price = entry_price*(1+tp_pct/100.0) + sl_price = entry_price*(1-sl_pct/100.0) + exit_price = None + reason = None + if px_high>=tp_price and px_low>sl_price: + exit_price = min(px_high,tp_price); reason='TP' + elif px_low<=sl_price and px_high=tp_price and px_low<=sl_price: + openp = series[i]['open'] + if abs(tp_price-openp) < abs(openp-sl_price): + exit_price = min(px_high,tp_price); reason='TP_first' + else: + exit_price = max(px_low,sl_price); reason='SL_first' + elif i-entry_idx >= max_hold: + exit_price = series[i]['close']; reason='TIME' + if exit_price is not None: + exit_price = exit_price*(1-0.0008) + gross = (exit_price - entry_price)*qty + fee = fee_rate*(entry_price*qty + exit_price*qty) + net = gross - fee + cash += exit_price*qty - fee + closed.append({'entry_idx':entry_idx,'exit_idx':i,'entry_price':entry_price,'exit_price':exit_price,'qty':qty,'pnl':net,'reason':reason}) + in_pos=False; entry_price=None; entry_idx=None; qty=0.0 + if in_pos: + last = series[-1]['close'] + exit_price = last*(1-0.0008) + gross = (exit_price - entry_price)*qty + fee = fee_rate*(entry_price*qty + exit_price*qty) + net = gross - fee + cash += exit_price*qty - fee + closed.append({'entry_idx':entry_idx,'exit_idx':len(series)-1,'entry_price':entry_price,'exit_price':exit_price,'qty':qty,'pnl':net,'reason':'EOD'}) + net_pnl = cash - 200.0 + wins = [c for c in closed if c['pnl']>=0] + losses = [c for c in closed if c['pnl']<0] + eq_hist = [200.0] + cur_cash = 200.0 + for c in closed: + cur_cash += c['pnl'] + eq_hist.append(cur_cash) + cur_peak = eq_hist[0] + max_dd = 0.0 + for e in eq_hist: + cur_peak = max(cur_peak,e) + dd = (cur_peak - e)/cur_peak*100 if cur_peak>0 else 0.0 + max_dd = max(max_dd,dd) + return {'closed_trades':len(closed),'wins':len(wins),'losses':len(losses),'winrate_pct': round(len(wins)/len(closed)*100,2) if closed else 0.0,'net_pnl_eur': round(net_pnl,4),'return_pct': round(net_pnl/200.0*100,2),'max_drawdown_pct': round(max_dd,2)} + +# default params +current_cfg = {'allocation_pct': 20.0, 'sl_pct': 1.5, 'tp_pct': 1.8, 'fee_rate': 0.0026} +proposed_cfg = {'allocation_pct': 20.0, 'sl_pct': 2.5, 'tp_pct': 3.0, 'fee_rate': 0.0026} + +results = {} +for s in SYMBOLS: + path = load_ohlc(s) + if not path: + results[s] = {'error':'no_data'} + continue + rows = parse_csv(path) + if not rows: + results[s] = {'error':'parse_failed'} + continue + # detect source minutes from filename + src_min = 5 + if '15m' in path: + src_min = 15 + elif '60m' in path: + src_min = 60 + series = to_15m(rows, src_min) + # select last ~90 days if available (90*24*4 15m bars) + target_bars = 90*24*4 + use = series[-target_bars:] if len(series) >= target_bars else series + res_cur = run_backtest_on_series(use, current_cfg) + res_prop = run_backtest_on_series(use, proposed_cfg) + results[s] = {'bars_used': len(use), 'current': res_cur, 'proposed': res_prop} + +os.makedirs('reports', exist_ok=True) +with open(OUT,'w') as f: + json.dump({'generated': datetime.datetime.utcnow().isoformat() + 'Z', 'symbols': SYMBOLS, 'results': results}, f, indent=2) +print('wrote', OUT) From a59d5980cd9155a248c5cb667404d8f50bec729e Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 29 May 2026 21:42:54 +0200 Subject: [PATCH 5/5] test: add focused DOTEUR verify outputs --- run_backtest.out | 4 +- tmp_backtest_xrp.py | 188 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 tmp_backtest_xrp.py diff --git a/run_backtest.out b/run_backtest.out index 200ce56..484e437 100644 --- a/run_backtest.out +++ b/run_backtest.out @@ -1,3 +1 @@ -Traceback (most recent call last): - File "", line 3, in -FileNotFoundError: [Errno 2] No such file or directory: '$OUT' +:22: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). diff --git a/tmp_backtest_xrp.py b/tmp_backtest_xrp.py new file mode 100644 index 0000000..cf5b755 --- /dev/null +++ b/tmp_backtest_xrp.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import csv, json, datetime, sys + +# simple toml parser for needed keys + +def get_cfg_val(cfg_text, section, key, default=None): + sec = '[' + section + ']' + if sec not in cfg_text: + return default + s = cfg_text.split(sec,1)[1] + for ln in s.splitlines(): + ln2 = ln.strip() + if ln2.startswith('['): + break + if ln2.startswith(key): + return ln2.split('=',1)[1].strip().strip('\"').strip() + return default + +# load config +with open('config.toml','r') as f: + cfg_text = f.read() +allocation = float(get_cfg_val(cfg_text,'bot_settings','allocation_per_trade_percent','20.0')) +intraday_sl = float(get_cfg_val(cfg_text,'daytrading','intraday_sl_percent','1.5')) +intraday_tp = float(get_cfg_val(cfg_text,'daytrading','intraday_tp_percent','1.8')) +fee_taker = float(get_cfg_val(cfg_text,'risk_management','fees_taker_percent','0.26')) + +csv_path = '/mnt/fritz_nas/Volume/kraken/2026/XXRPZEUR/ohlc_5m.csv' +try: + with open(csv_path,'r') as f: + lines = [ln.strip() for ln in f.readlines() if ln.strip()] +except Exception as e: + print(json.dumps({'error':'ohlc_missing','msg':str(e)})) + sys.exit(0) + +header = [h.strip() for h in lines[0].split(',')] +rows = [] +for ln in lines[1:]: + parts = ln.split(',') + if len(parts) < 5: + continue + rec = dict(zip(header, parts)) + try: + ts = int(rec.get('ts','0')) + except: + continue + try: + o = float(rec.get('open','')) + h = float(rec.get('high','')) + l = float(rec.get('low','')) + c = float(rec.get('close','')) + except: + continue + vol = None + try: + vol = float(rec.get('volume','')) + except: + vol = None + rows.append({'ts':ts,'dt':datetime.datetime.utcfromtimestamp(ts),'open':o,'high':h,'low':l,'close':c,'volume':vol}) + +if not rows: + print(json.dumps({'error':'no_rows'})) + sys.exit(0) + +# aggregate to 15m +from collections import defaultdict +buckets = defaultdict(list) +for r in rows: + k = (r['ts']//900)*900 + buckets[k].append(r) +agg = [] +for k in sorted(buckets.keys()): + group = buckets[k] + opens = [g['open'] for g in group] + highs = [g['high'] for g in group] + lows = [g['low'] for g in group] + closes = [g['close'] for g in group] + vols = [g['volume'] for g in group if g['volume'] is not None] + agg.append({'ts':k,'dt':datetime.datetime.utcfromtimestamp(k),'open':opens[0],'high':max(highs),'low':min(lows),'close':closes[-1],'volume':sum(vols) if vols else None}) +if not agg: + print(json.dumps({'error':'no_agg'})) + sys.exit(0) + +series = agg[-14*24*4:] if len(agg) >= 14*24*4 else agg + +# EMA +def ema(series_vals, period): + k = 2.0/(period+1) + out = [] + s = None + for v in series_vals: + if s is None: + s = v + else: + s = v*k + s*(1-k) + out.append(s) + return out + +# backtest function + +def run_backtest(params): + fast_p = 9 + slow_p = 21 + closes = [c['close'] for c in series] + if len(closes) < slow_p+1: + return {'error':'not_enough_bars','bars':len(closes)} + ema_fast = ema(closes, fast_p) + ema_slow = ema(closes, slow_p) + in_pos = False + entry_price = None + entry_idx = None + qty = 0.0 + cash = 200.0 + closed = [] + fee_rate = params['fee_rate'] + alloc_frac = params['allocation_pct']/100.0 + sl_pct = params['sl_pct'] + tp_pct = params['tp_pct'] + max_hold = 48 + for i in range(1,len(series)): + if not in_pos and ema_fast[i] is not None and ema_slow[i] is not None and ema_fast[i]>ema_slow[i] and ema_fast[i-1]<=ema_slow[i-1]: + entry_price = series[i]['open']*(1+0.0008) + allocation = cash * alloc_frac + if allocation < 1.0: + continue + qty = (allocation) / entry_price + cash -= allocation + in_pos = True + entry_idx = i + continue + if in_pos: + px_high = series[i]['high'] + px_low = series[i]['low'] + tp_price = entry_price*(1+tp_pct/100.0) + sl_price = entry_price*(1-sl_pct/100.0) + exit_price = None + reason = None + if px_high>=tp_price and px_low>sl_price: + exit_price = min(px_high,tp_price); reason='TP' + elif px_low<=sl_price and px_high=tp_price and px_low<=sl_price: + openp = series[i]['open'] + if abs(tp_price-openp) < abs(openp-sl_price): + exit_price = min(px_high,tp_price); reason='TP_first' + else: + exit_price = max(px_low,sl_price); reason='SL_first' + elif i-entry_idx >= max_hold: + exit_price = series[i]['close']; reason='TIME' + if exit_price is not None: + exit_price = exit_price*(1-0.0008) + gross = (exit_price - entry_price)*qty + fee = fee_rate*(entry_price*qty + exit_price*qty) + net = gross - fee + cash += exit_price*qty - fee + closed.append({'entry_idx':entry_idx,'exit_idx':i,'entry_price':entry_price,'exit_price':exit_price,'qty':qty,'pnl':net,'reason':reason}) + in_pos=False; entry_price=None; entry_idx=None; qty=0.0 + if in_pos: + last = series[-1]['close'] + exit_price = last*(1-0.0008) + gross = (exit_price - entry_price)*qty + fee = fee_rate*(entry_price*qty + exit_price*qty) + net = gross - fee + cash += exit_price*qty - fee + closed.append({'entry_idx':entry_idx,'exit_idx':len(series)-1,'entry_price':entry_price,'exit_price':exit_price,'qty':qty,'pnl':net,'reason':'EOD'}) + net_pnl = cash - 200.0 + wins = [c for c in closed if c['pnl']>=0] + losses = [c for c in closed if c['pnl']<0] + eq_hist = [200.0] + cur_cash = 200.0 + for c in closed: + cur_cash += c['pnl'] + eq_hist.append(cur_cash) + cur_peak = eq_hist[0] + max_dd = 0.0 + for e in eq_hist: + cur_peak = max(cur_peak,e) + dd = (cur_peak - e)/cur_peak*100 if cur_peak>0 else 0.0 + max_dd = max(max_dd,dd) + return {'closed_trades':len(closed),'wins':len(wins),'losses':len(losses),'winrate_pct': round(len(wins)/len(closed)*100,2) if closed else 0.0,'net_pnl_eur': round(net_pnl,4),'return_pct': round(net_pnl/200.0*100,2),'max_drawdown_pct': round(max_dd,2)} + +params_current = {'allocation_pct': allocation, 'sl_pct': intraday_sl, 'tp_pct': intraday_tp, 'fee_rate': fee_taker/100.0} +params_proposed = {'allocation_pct': 20.0, 'sl_pct': 2.5, 'tp_pct': 3.0, 'fee_rate': fee_taker/100.0} + +res_current = run_backtest(params_current) +res_proposed = run_backtest(params_proposed) + +out = {'series_bars': len(series), 'current_params': params_current, 'proposed_params': params_proposed, 'results': {'current': res_current, 'proposed': res_proposed}} +print(json.dumps(out))