Insights/Insights & Articles/Building an HFT Scalping Bot: Dynamic Lot Sizing & Spread Filtering

Building an HFT Scalping Bot: Dynamic Lot Sizing & Spread Filtering

Let us kill a misconception upfront: "HFT" on MetaTrader doesn't mean nanosecond execution or co-located servers. You're not competing with Citadel. What it does mean is taking a high volume of small, positive-expectancy trades with tight risk controls and aggressive capital efficiency.

Mar 11, 2026

Building an HFT Scalping Bot: Dynamic Lot Sizing & Spread Filtering

Building an HFT Scalping Bot: Dynamic Lot Sizing & Spread Filtering

Let us kill a misconception upfront: "HFT" on MetaTrader doesn't mean nanosecond execution or co-located servers. You're not competing with Citadel. What it does mean is taking a high volume of small, positive-expectancy trades with tight risk controls and aggressive capital efficiency.

This article dissects a real production scalper we built – one that ran backtests across gold, forex crosses, and crypto from 2017 to 2022. The signal itself is almost embarrassingly simple. The secret sauce is in the lot sizing, spread gating, and equity management.

What This Thing Actually Does

Seven steps. That's the whole logic:

  1. Wait for a new trading day. No open positions.
  2. Check the spread. If it's too wide, do nothing.
  3. Buy the dip – enter when price drops below the previous bar's low.
  4. Size aggressively. Lots scale directly with account balance.
  5. Hit the equity target? Flatten everything.
  6. Hit the loss limit? Flatten everything.
  7. Tomorrow, recalculate based on the new balance. Repeat.

The edge isn't hiding in a clever indicator. It's in how the pieces – scaling, spread control, equity exits – work together.

Dynamic Lot Sizing: Where the Money Lives

Most EAs use fixed lots. That's fine for learning. It's terrible for making money. This scalper ties lot size directly to account balance – the bigger the account grows, the harder it swings.

MQL4 Implementation

// Input parameters for the scaling engine
input double ProfitFactor      = 0.8;    // Profit target multiplier
input double LossFactor        = 3.0;    // Max loss multiplier
input double AccountMultiplier = 0.5;    // Account scaling factor
input int    MaxTrades         = 100;    // Max trades per day
 
// Dynamic lot and target calculation – recalculated per tick
void RecalculateParameters()
{
    double balance = AccountBalance();
 
    // Core lot formula: scales linearly with balance
    // $10,000 balance → ~7.6 standard lots
    // $1,000 balance → ~0.76 lots
    double baseLot = balance * 2.0 / 1316.0;
 
    // Apply account multiplier for risk scaling
    g_lotSize = baseLot * 0.7 * AccountMultiplier;
 
    // Profit target: also scales with balance
    g_profitTarget = balance * 2.0 * 0.3 * 0.7 * AccountMultiplier * ProfitFactor / 11.844;
 
    // Max loss exit: wider than profit target (asymmetric R:R)
    g_maxLoss = balance * 2.0 * 0.6 * 0.7 * AccountMultiplier * LossFactor / 3.948;
}

The formula explained:

$$\text{LotSize} = \frac{\text{Balance} \times 2 \times 0.7 \times \text{AccountMultiplier}}{1316}$$

The divisor (1316) is calibrated to gold's margin requirements. Change the instrument, change the divisor. The AccountMultiplier input (default 0.5) is your risk dial – crank it up for aggression, dial it down to sleep at night.

Why is the loss limit wider than the profit target? Because this strategy needs room to breathe. Trades dip before they rip. A 0.8 profit factor with a 3.0 loss factor means you're hitting profits frequently while only rarely touching the loss floor.

Lot Splitting: When You Outgrow Your Broker

Good problem to have: your account grows and the lot formula spits out numbers your broker won't accept in a single order. Solution: split automatically.

MQL4 Implementation

#define MAX_LOT_PER_ORDER 100000  // Broker's max lot per single order
 
void PlaceBuyWithSplitting()
{
    double remainingLots = g_lotSize;
 
    while (remainingLots > 0)
    {
        double orderLot;
 
        if (remainingLots > MAX_LOT_PER_ORDER)
        {
            orderLot = MAX_LOT_PER_ORDER;
            remainingLots -= MAX_LOT_PER_ORDER;
        }
        else
        {
            orderLot = remainingLots;
            remainingLots = 0;
        }
 
        int ticket = OrderSend(Symbol(), OP_BUY, orderLot, Ask, 0,
                               0, 0, NULL, MAGIC_BUY, 0, clrBlue);
 
        if (ticket < 0)
            Print("Order failed. Error: ", GetLastError());
    }
}

A 250K-lot order becomes three orders automatically. We've run production EAs that routinely hit six-figure lots. Without this pattern, the broker just rejects you and the EA sits there confused.

The Spread Gate: Non-Negotiable

This is what separates a scalper that works from one that bleeds. A 2-pip spread on a 5-pip target eats 40% of your edge before the trade even opens. Think about that.

MQL4 Implementation

// Entry condition: ONLY trade when spread is tight
void OnTick()
{
    RecalculateParameters();
 
    // SPREAD GATE – this is non-negotiable for scalpers
    double spread = Ask - Bid;
    if (spread > 0.002)  // Max 0.2 pips (for gold/GC)
        return;          // Do nothing. Wait for tighter spread.
 
    // Only enter ONCE per day (day filter reset)
    static int lastTradeDay = 0;
    if (Day() != lastTradeDay && OrdersTotal() == 0)
    {
        // DIP-BUY ENTRY: current price below previous bar's low
        if (Ask < Low[1] && g_tradeCount < MaxTrades)
        {
            PlaceBuyWithSplitting();
            g_lastEntryPrice = Close[0];
            g_tradeCount++;
        }
    }
 
    // EQUITY-BASED EXITS
    CheckEquityExits();
}

Python Equivalent – Spread-Gated Entry

class HFTScalper:
    def __init__(self, profit_factor=0.8, loss_factor=3.0, account_mult=0.5):
        self.profit_factor = profit_factor
        self.loss_factor = loss_factor
        self.account_mult = account_mult
        self.last_entry_price = None
        self.trade_count = 0
 
    def calculate_parameters(self, balance):
        """Recalculate lot size and targets based on current balance."""
        self.lot_size = balance * 2 * 0.7 * self.account_mult / 1316
        self.profit_target = (balance * 2 * 0.3 * 0.7
                              * self.account_mult * self.profit_factor / 11.844)
        self.max_loss = (balance * 2 * 0.6 * 0.7
                         * self.account_mult * self.loss_factor / 3.948)
 
    def should_enter(self, ask, bid, previous_low, max_spread=0.002):
        """Check if entry conditions are met."""
        spread = ask - bid
        if spread > max_spread:
            return False  # Spread too wide
 
        if ask < previous_low:
            return True  # Price dipped below previous bar's low
 
        return False
 
    def check_exit(self, equity, balance):
        """Check equity-based exit conditions."""
        if equity > balance + self.profit_target:
            return 'take_profit'
        if equity < balance - self.max_loss:
            return 'stop_loss'
        return None

Equity-Based Exit: The Heartbeat

No per-trade stop losses. No per-trade take profits. This strategy monitors total account equity against dynamic targets. When the combined portfolio hits the number, everything closes.

Why do it this way? With up to 100 simultaneous positions, individual stops would trigger independently – a cascading mess of partial closes. The equity-based approach treats the whole stack as one bet and makes one clean decision.

MQL4 Implementation

void CheckEquityExits()
{
    double balance = AccountBalance();
    double equity = AccountEquity();
 
    // PROFIT TARGET: equity exceeds balance by target amount
    if (equity > balance + g_profitTarget)
    {
        CloseAllBuyOrders();
        CloseAllSellOrders();
        // Reset balance reference to new equity level
        // Next cycle calculates fresh targets from new balance
        Print("PROFIT TARGET HIT. New balance: ", AccountBalance());
    }
 
    // LOSS LIMIT: equity drops below balance by max loss amount
    if (equity < balance - g_maxLoss)
    {
        CloseAllBuyOrders();
        CloseAllSellOrders();
        Print("MAX LOSS HIT. Closing all. Balance: ", AccountBalance());
    }
}

All positions live and die together. That's the design.

Trailing the Entry Price

Subtle but powerful: as price rises after your entry, the EA tracks the move and re-anchors for the next dip-buy at a higher level:

// In OnTick(), after entry:
if (Close[0] > g_lastEntryPrice)
{
    g_lastEntryPrice = Bid;  // Update reference price upward
}

So the bot follows momentum rather than fighting it. Dip-buys happen at progressively higher prices during an uptrend. Not counter-trend – with the trend.

The Complete Strategy Parameters

ParameterDefaultPurpose
ProfitFactor0.8How aggressively to target profit (smaller = more conservative)
LossFactor3.0How much drawdown to tolerate (larger = more patient)
AccountMultiplier0.5Overall risk scaling (0.1 = very safe, 1.0 = aggressive)
MaxTrades100Maximum entries per day
MaxSpread0.002Maximum allowed spread for entry

Suggested Parameter Sets

Conservative (recommended for starters):

ProfitFactorLossFactorAccountMultiplierMaxTrades
0.51.50.320

Balanced (used in backtesting):

ProfitFactorLossFactorAccountMultiplierMaxTrades
0.83.00.5100

Aggressive (high-risk, high-return):

ProfitFactorLossFactorAccountMultiplierMaxTrades
1.55.01.0100

What the Backtests Showed

We ran this across multiple instruments and timeframes. Here's the honest picture:

InstrumentPeriodTimeframeAccount StartKey Result
Gold (GC)2017-2021H1$1,000High yield configuration – aggressive scaling on gold futures
GBPCHF2019-2022H1$200Tested with spread limits of 100, 125, and 150 points
GBPNZD2019-2022H1/H4$200Tested with 150 and 200 point spread limits
AUDCHF2019-2022H1$200Tight spread CHF crosses
BTCUSD2019-2022H1$1,000Crypto volatility adaptation

Takeaways that actually matter:

  1. Spread dominates everything. Loosen the spread filter and results fall off a cliff.
  2. CHF crosses were the winners. Low volatility, tight spreads, predictable dip patterns. Boring markets make the best scalping playgrounds.
  3. H1 was the sweet spot. H4 was too slow for dip-buying. M15 generated too much noise.
  4. Account size matters. The dynamic scaling formula needs at least $1,000 to produce meaningful lot sizes.

The Honest Warning

We're going to be blunt because we like you:

  • Your broker matters more than your code. Execution speed, spread widening during news, slippage – these directly determine whether this strategy makes or loses money.
  • Dynamic sizing cuts both ways. When the account shrinks, lots shrink too. But during the drawdown? Those lots were still sized off the peak balance. It hurts.
  • The 3× loss factor means uncomfortable drawdowns. You will sit through red. By design.
  • Those calibration numbers (1316, 11.844, 3.948) are instrument-specific. They're not magic constants. They were fitted to gold. You can't just slap them on EUR/USD and expect the same behavior.

Demo first. Always.

Want a Scalper Calibrated to Your Setup?

We've built HFT scalpers for gold, forex majors, crosses, crypto, and futures. The lot formulas, spread thresholds, and equity targets get calibrated specifically for your broker, instrument, and risk appetite. No copy-paste – custom.

Get Your Custom Scalper Built →

Algorithmic Trading Insights by Quantumcona.

1of4