kernc / backtesting.py

:mag_right: :chart_with_upwards_trend: :snake: :moneybag: Backtest trading strategies in Python.
https://kernc.github.io/backtesting.py/
GNU Affero General Public License v3.0
5.3k stars 1.04k forks source link

Fractional Shares / Currencies not supported #1159

Closed realfishsam closed 1 month ago

realfishsam commented 1 month ago

Expected Behavior

Fractional shares are supported by numerous brokers. Here is a list of some: Charles Schwab Fidelity Investments Interactive Brokers Robinhood E-Trade Merrill Edge Vanguard Tastytrade SoFi Active Investing WellsTrade

If you are trading currencies, it makes sense to allow for a dollar and a quarter. So, why isn't it allowed?

Hence, the expected behavior when calling self.buy(size=1.1) is not an error, but rather the absence of any error. Printing output['_trades'] should result in this output:

    Size  EntryBar  ExitBar  EntryPrice  ExitPrice         PnL  ReturnPct  \
0   -1.1        63       75   168.68196     179.13  -11.492844  -0.061939   
1    1.1        75       85   179.48826     182.00    2.762914   0.013994   
2   -1.1        85       88   181.63600     187.45   -6.395400  -0.032009   
3    1.1        88      110   187.82490     179.27   -9.410390  -0.045547   
4   -1.1       110      119   178.91146     196.96  -19.853394  -0.100880   
..   ...       ...      ...         ...        ...         ...        ...   
89   1.1      1947     1960   611.57070     588.72  -25.135770  -0.037364   
90  -1.1      1960     1983   587.54256     580.01    8.285816   0.012820   
91   1.1      1983     2059   581.17002     705.58  136.850978   0.214068   
92  -1.1      2059     2087   704.16884     702.24    2.121724   0.002739   
93   1.1      2087     2147   703.64448     797.80  103.571072   0.133811   

    EntryTime   ExitTime Duration  
0  2004-11-17 2004-12-06  19 days  
1  2004-12-06 2004-12-20  14 days  
2  2004-12-20 2004-12-23   3 days  
3  2004-12-23 2005-01-26  34 days  
4  2005-01-26 2005-02-08  13 days  
..        ...        ...      ...  
89 2012-05-11 2012-05-31  20 days  
90 2012-05-31 2012-07-03  33 days  
91 2012-07-03 2012-10-19 108 days  
92 2012-10-19 2012-12-03  45 days  
93 2012-12-03 2013-03-01  88 days  

[94 rows x 10 columns]

Given that the size of a buy or sell order is a fraction.

Actual Behavior

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[2], [line 27](vscode-notebook-cell:?execution_count=2&line=27)
     [20](vscode-notebook-cell:?execution_count=2&line=20)             self.sell(size=1.1)                 # changed to fractional
     [23](vscode-notebook-cell:?execution_count=2&line=23) bt = Backtest(GOOG, SmaCross,
     [24](vscode-notebook-cell:?execution_count=2&line=24)               cash=10000, commission=.002,
     [25](vscode-notebook-cell:?execution_count=2&line=25)               exclusive_orders=True)
---> [27](vscode-notebook-cell:?execution_count=2&line=27) output = bt.run()
     [28](vscode-notebook-cell:?execution_count=2&line=28) bt.plot()

File /opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:1170, in Backtest.run(self, **kwargs)
   [1167](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:1167)         break
   [1169](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:1169)     # Next tick, a moment before bar close
-> [1170](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:1170)     strategy.next()
   [1171](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:1171) else:
   [1172](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:1172)     # Close any remaining open trades so they produce some stats
   [1173](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:1173)     for trade in broker.trades:

Cell In[2], [line 20](vscode-notebook-cell:?execution_count=2&line=20)
     [18](vscode-notebook-cell:?execution_count=2&line=18)     self.buy(size=1.1)                  # changed to fractional
     [19](vscode-notebook-cell:?execution_count=2&line=19) elif crossover(self.sma2, self.sma1):
---> [20](vscode-notebook-cell:?execution_count=2&line=20)     self.sell(size=1.1)

File /opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:223, in Strategy.sell(self, size, limit, stop, sl, tp)
    [212](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:212) def sell(self, *,
    [213](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:213)          size: float = _FULL_EQUITY,
    [214](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:214)          limit: float = None,
    [215](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:215)          stop: float = None,
    [216](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:216)          sl: float = None,
    [217](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:217)          tp: float = None):
    [218](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:218)     """
    [219](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:219)     Place a new short order. For explanation of parameters, see `Order` and its properties.
    [220](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:220) 
    [221](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:221)     See also `Strategy.buy()`.
    [222](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:222)     """
--> [223](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:223)     assert 0 < size < 1 or round(size) == size, \
    [224](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:224)         "size must be a positive fraction of equity, or a positive whole number of units"
    [225](https://file+.vscode-resource.vscode-cdn.net/opt/homebrew/lib/python3.10/site-packages/backtesting/backtesting.py:225)     return self._broker.new_order(-size, limit, stop, sl, tp)

AssertionError: size must be a positive fraction of equity, or a positive whole number of units

Steps to Reproduce

  1. Copy and paste the Backtesting.py snippet into your IDE
  2. Change argument size in self.buy() and/or self.sell() to a floating point number
  3. An assertion error will occur.

from backtesting import Backtest, Strategy
from backtesting.lib import crossover

from backtesting.test import SMA, GOOG

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

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

    def next(self):
        if crossover(self.sma1, self.sma2):
            self.buy(size=1.1)
        elif crossover(self.sma2, self.sma1):
            self.sell(size=1.1)

bt = Backtest(GOOG, SmaCross,
              cash=10000, commission=.002,
              exclusive_orders=True)

output = bt.run()
bt.plot()

Additional info

realfishsam commented 1 month ago

This closes the issue: https://github.com/kernc/backtesting.py/pull/1160