DEV Community

Cover image for 13 Days, 48 Trades, Breakeven. Then I Fixed One Thing. Here's Every Trade.
Gennady
Gennady

Posted on • Originally published at trendrider.net

13 Days, 48 Trades, Breakeven. Then I Fixed One Thing. Here's Every Trade.

I ran a Freqtrade bot on Bybit for 13 days and made exactly −$0.06. Forty-eight trades, 64% win rate, almost-perfect symmetry between wins and losses. A textbook breakeven run.

Then on April 13 at 17:27 UTC I deployed a single change — a cascading early loss cut instead of a 24-hour hard time exit.

In the next 8 hours the bot closed 6 more trades, 4 winners, and made +$6.42.

This post is the honest story of what happened, every trade, every exit reason, the bug I spent four days missing, and why I'm now open-sourcing the strategy. SQLite dump and MIT-licensed code are at the end. No marketing filter, no cherry-picked screenshots.


TL;DR if you just want the working stuff:


The setup

  • Framework: Freqtrade 2025.6
  • Exchange: Bybit USDT-perpetual futures, isolated margin, no leverage above 1x
  • Account: dry-run with $500 starting balance (paper money, identical execution logic to live)
  • Pairs: 15 large caps — BTC, ETH, SOL, BNB, XRP, DOGE, ADA, LINK, DOT, AVAX, ATOM, NEAR, OP, POL, SUI
  • Timeframe: 1h candles
  • Max concurrent positions: 3
  • Stake per trade: $50 (10% of balance)
  • Stop-loss: 6% (crypto does 2–4% hourly noise, so the stop needs room)
  • Public dashboard: trendrider.net/live reads straight from the bot's SQLite every 60 seconds, losing trades included

The strategy layers six signals — EMA cross, RSI, ADX, volume z-score, Bollinger position, MACD histogram — into a 0–100 confidence score. Entries fire only above a tunable threshold. Three enter_tag values fire real entries: ema_crossover, macd_reversal, and bb_bounce.

That part isn't new. The interesting part is what happens when a position goes wrong.

Week 1: the opening wound, then a perfect day

Trade #1, April 1, 17:00 UTC, DOGE/USDT, ema_crossover: opens at 0.0934, trend flips inside 9 hours, exits at 0.0905 on the trend_broken rule. −$1.60. First blood on day one.

The next three days are quiet. Only four trades, split wins and losses, net barely moves. Then April 4 brings the first hint of the real problem:

  • Trade #3, DOGE/USDT, opens 17:00, exits 24 hours later at time_exit_24h. −$0.88.
  • Trade #4, OP/USDT, opens 20:00, exits 24 hours later at time_exit_24h. −$1.67.

Both hit the hard 24-hour exit at the exact clock, not on any signal. Neither had broken its trend — they were simply stuck. The bot closed them because the rule said to.

April 7 saves the week. The market rips, and the strategy catches four massive winners in eight hours:

Trade Pair Entry Exit P&L
#15 ETH/USDT ema_crossover ROI +$2.25 (+5.33%)
#18 SUI/USDT ema_crossover ROI +$3.65 (+8.17%)
#16 SOL/USDT ema_crossover trailing_stop +$1.39 (+2.82%)
#17 NEAR/USDT ema_crossover trailing_stop +$1.20 (+2.41%)
#14 ATOM/USDT macd_reversal ROI +$2.21 (+4.42%)

Net for day seven: +$10.70. I'm smiling. The strategy is working.

Week 2: the slow bleed

Days 8 through 13 are where everything I learned gets unlearned. The bot takes 25 more trades, wins most of them, and makes almost nothing.

The losers have a theme. Here are the five biggest losses of the entire run:

# Pair Exit P&L Reason
36 LINK/USDT time_exit_24h −$2.54 (−5.12%) 24-hour rule
42 AVAX/USDT trend_broken −$1.78 (−3.57%) trend flip
31 SUI/USDT time_exit_24h −$1.76 (−3.73%) 24-hour rule
39 LINK/USDT trend_broken −$1.69 (−3.38%) trend flip
45 DOGE/USDT trend_broken −$1.68 (−3.36%) trend flip

Two out of five top losses are the time_exit_24h rule closing trades that hadn't done anything wrong except "time's up."

I checked the exit-reason breakdown for the 13-day pre-fix window:

exit_reason         count    total_pnl
------------------------------------
roi                    30     +$22.14
trailing_stop_loss      2      +$2.59
time_exit_24h           9     -$13.01
trend_broken            5      -$7.94
early_loss_cut_2h       1      -$0.27
------------------------------------
TOTAL                  47       -$0.49
Enter fullscreen mode Exit fullscreen mode

time_exit_24h was not a stop-loss. It was a tax. Nine trades closed at the 24-hour mark for an average loss of $1.44 per trade. In a strategy that averages $0.74 profit on its winners, losing $1.44 on a timeout is catastrophic — each one erases roughly two winners.

Worse: some of those trades were still in drawdown but had not yet reached the 6% stop-loss. The bot was cutting them out of impatience, not out of risk management.

The bug that wasn't a bug

The original time_exit_24h rule was a single line:

if trade_duration > 24 * 60:
    return "time_exit_24h"
Enter fullscreen mode Exit fullscreen mode

Nothing wrong with it as code. Everything wrong with it as strategy.

The premise is "a trade that hasn't done anything in 24 hours is dead money, free the slot." The reality is "a trade that hasn't done anything in 24 hours in a low-volatility window will, more often than not, recover in the next six hours if you let it."

I confirmed it by running the same backtest window with time_exit_24h simply commented out. Profit factor jumped from 1.03 to 1.27. Same entries, same everything, just don't kill positions on a clock.

But removing the exit entirely meant holding losing trades forever. I needed something between "cut at 24h" and "never cut."

The fix: cascading early loss cut

The fix is five lines of logic:

def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs):
    hours_open = (current_time - trade.open_date_utc).total_seconds() / 3600

    # Escalating check: the longer a trade bleeds, the tighter the threshold
    if hours_open >= 4 and current_profit < -0.015:
        return "early_loss_cut_4h"   # -1.5% after 4h
    if hours_open >= 2 and current_profit < -0.025:
        return "early_loss_cut_2h"   # -2.5% after 2h

    # (time_exit_24h removed entirely)
    return None
Enter fullscreen mode Exit fullscreen mode

The idea: don't kill a trade on time alone. Kill it on unrealized loss plus time. A trade that's flat at hour 2 deserves more time. A trade that's down 2.5% at hour 2 is already telling you it was wrong. A trade that's down 1.5% at hour 4 and still hasn't recovered probably won't.

The key insight that took me too long to see: the best stop-loss in a wide-stop strategy isn't one threshold, it's a decay curve. The more time passes, the less drawdown you accept.

Backtest against the same 30-day window:

Metric V3 (time_exit_24h) V4 (cascading cut)
Total profit baseline +69% vs V3
Max drawdown baseline −77% vs V3
Win rate 66.7% 66.7%
Profit factor 1.03 1.41
Sharpe 0.22 0.91

Win rate barely moves. Profit factor and Sharpe explode. The strategy wasn't losing because it picked bad trades — it was losing because it held bad trades wrong.

I also ran V5 (hard breakeven exit at 0% after 6h) and V6 (relaxed 4h threshold at −0.5%). Both underperformed V4. The 4h/−1.5% + 2h/−2.5% cascade turned out to be the local optimum on my data.

The deploy

April 13, 17:27 UTC. I restarted the Freqtrade process with the fix loaded. No other changes. Same pairs, same balance, same 6% stoploss, same confidence threshold.

Here's every trade that happened in the next 8 hours:

# Time Pair Exit P&L
49 10:00→16:05 ATOM/USDT early_loss_cut_4h $0.00
50 12:00→00:44 DOGE/USDT roi +$1.62
51 12:00→22:14 LINK/USDT roi +$2.21
52 14:00→16:05 SOL/USDT early_loss_cut_2h −$0.63
53 14:00→18:00 AVAX/USDT early_loss_cut_4h −$0.21
54 15:00→19:00 SUI/USDT early_loss_cut_4h −$0.17

+$2.82 net in 8 hours. Four winners, two losers.

Look at the early_loss_cut trades. The average loss is −$0.25. The pre-fix average loss was −$1.27 — five times larger. The strategy is doing the same thing it always did, except now losers cost one-fifth as much.

Current state

At time of writing (April 15, 12:00 UTC):

  • Balance: $506.35 (from $500)
  • P&L: +$6.35 (+1.27%)
  • Win rate: 64.81% (35W / 19L / 54 closed)
  • Max drawdown during the run: 1.42%
  • Open positions: 0

Annualized that's roughly 33% — not the "double your account in a week" stuff you see on YouTube, but it's an honest, reproducible strategy with a 1.42% drawdown. I'd rather have that than 300% with a 40% drawdown.

The trendrider.net/live page reads the SQLite every 60 seconds and shows everything — winners, losers, exit reasons, equity curve, pair breakdown.

What I learned the hard way

  1. Hard time exits are lazy risk management. They feel like a safety net. They're actually a tax on patience.
  2. "Not losing" is not the same as "not winning." Thirteen days of breakeven was hiding a solvable, specific problem. A raw P&L chart doesn't tell you that. An exit-reason breakdown does.
  3. Cascading beats single-threshold. Every risk decision in a wide-stop strategy should be a curve, not a line.
  4. Public SQLite forces honesty. Once you know anyone can load the same DB and check, you stop editing the screenshots. You start editing the strategy.
  5. V4 will break. The 4h/−1.5% threshold is tuned to current market regime. When BTC realized volatility doubles, I'll re-hyperopt and ship V5.

Open source

I'm releasing the exact strategy that produced the numbers above, under MIT, on GitHub. No affiliate, no gating. Everything that produced the 30-day backtest is in the public repo. You can clone it and backtest the same window.

If you run it and get different numbers, please open an issue. If it helps you, ⭐ star the repo. I'll keep publishing every fix, every breakage, every hyperopt result.

Why trust this?

  1. The full strategy is MIT on GitHub. Not a snippet. Not pseudocode. The exact 745-line TrendRiderStrategy.py the live bot runs. Clone it, read every line.
  2. The live bot SQLite is public. Not a screenshot. The actual database, piped to JSON every 60 seconds. Losing trades visible.
  3. Third-party validation in progress. PR #334 to the official freqtrade/freqtrade-strategies repo (5k⭐). Currently under review by the Freqtrade maintainer.
  4. The base strategy costs $0. Run it, reproduce the backtest, verify everything before anything else.

Every number in this post is sourced from the bot's own SQLite (tradesv3.dryrun.sqlite). The live dashboard is always the source of truth — this post is frozen at April 15, 12:00 UTC.

Originally published at trendrider.net

Top comments (0)