Skip to main content
By default, backtesting.py assumes whole-unit position sizing. For assets where fractional quantities are common — Bitcoin, other cryptocurrencies, or high-priced stocks where you may want to trade micro-lots — FractionalBacktest from backtesting.lib provides a transparent workaround.

Why fractional trading matters

Consider Bitcoin at 60,000percoin.With60,000 per coin. With 10,000 of starting capital and whole-unit sizing, you cannot buy even a single unit, making the backtest meaningless. Fractional trading lets you express positions as fractions of a unit (e.g. 0.001 BTC), which reflects how crypto exchanges actually operate. A similar issue arises with high-priced equities (e.g. Amazon, Google) when backtesting with small amounts of capital.

FractionalBacktest

FractionalBacktest is a drop-in replacement for Backtest that transparently scales prices by fractional_unit before running the simulation, then un-scales trade data in the results:
class FractionalBacktest(Backtest):
    def __init__(self, data, *args, fractional_unit=1/100e6, **kwargs):
        ...
Internally it applies the transformation:
data[['Open', 'High', 'Low', 'Close']] *= fractional_unit
data['Volume'] /= fractional_unit
This converts prices into units of fractional_unit, so that buying one “unit” in the simulation corresponds to buying one fractional_unit of the real asset. The run() method reverses this scaling on trade prices and sizes before returning results.

The fractional_unit parameter

ValueAssetMeaning
1/100e6 (default)Bitcoin1 satoshi (10⁻⁸ BTC)
1/1e6Bitcoin1 micro-BTC (μBTC)
1/1000Stocks1 milli-share
1/100General1 cent-share

Basic Bitcoin example

from backtesting import Strategy
from backtesting.lib import FractionalBacktest, crossover
from backtesting.test import BTCUSD, SMA


class SmaCross(Strategy):
    n1 = 10
    n2 = 20

    def init(self):
        self.sma1 = self.I(SMA, self.data.Close, self.n1)
        self.sma2 = self.I(SMA, self.data.Close, self.n2)

    def next(self):
        if crossover(self.sma1, self.sma2):
            self.position.close()
            self.buy()
        elif crossover(self.sma2, self.sma1):
            self.position.close()
            self.sell()


# Use FractionalBacktest instead of Backtest
# Default fractional_unit=1/100e6 (satoshi precision)
bt = FractionalBacktest(BTCUSD, SmaCross, cash=10_000, commission=.001)
stats = bt.run()
print(stats)
The returned stats and trade data are automatically rescaled back to original BTC prices and quantities — you read results in familiar BTC terms.

Custom fractional unit

For micro-BTC (μBTC) trading, where the smallest unit is 0.000001 BTC:
bt = FractionalBacktest(
    BTCUSD,
    SmaCross,
    cash=10_000,
    commission=.001,
    fractional_unit=1/1e6   # 1 micro-BTC
)
stats = bt.run()
For milli-share trading of a high-priced stock:
from backtesting.test import GOOG

bt = FractionalBacktest(
    GOOG,
    SmaCross,
    cash=500,
    commission=.002,
    fractional_unit=1/1000   # 1 milli-share
)
stats = bt.run()

How results are reported

After calling run(), FractionalBacktest reverses the price scaling on all trade records:
  • trades['Size'] is multiplied by fractional_unit → reported in real asset units
  • trades['EntryPrice'] and trades['ExitPrice'] are divided by fractional_unit → reported in real currency
  • trades['TP'] and trades['SL'] are similarly un-scaled
Indicators declared with overlay=True are also un-scaled so they align correctly with the price chart.
stats = bt.run()
print(stats['_trades'][['Size', 'EntryPrice', 'ExitPrice', 'PnL']].head())
FractionalBacktest inherits all parameters from Backtest: cash, commission, margin, trade_on_close, exclusive_orders, hedging, and finalize_trades. You can use optimization and plotting exactly as you would with a standard Backtest instance.

Combining with optimization

FractionalBacktest supports optimize() identically to Backtest:
bt = FractionalBacktest(BTCUSD, SmaCross, cash=10_000, commission=.001)

stats, heatmap = bt.optimize(
    n1=range(5, 30, 5),
    n2=range(10, 60, 5),
    constraint=lambda p: p.n1 < p.n2,
    maximize='Sharpe Ratio',
    return_heatmap=True
)
print(stats._strategy)
Choosing an inappropriate fractional_unit that is too large may cause position sizing to behave unexpectedly. As a rule of thumb, set fractional_unit so that the scaled price fits comfortably within a normal equity range (e.g. 0.010.01–1000 per scaled unit).

Build docs developers (and LLMs) love