Skip to content

Feat/order size #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Strategy(metaclass=ABCMeta):
`backtesting.backtesting.Strategy.next` to define
your own strategy.
"""

def __init__(self, broker, data):
self._indicators = []
self._broker = broker # type: _Broker
Expand Down Expand Up @@ -182,7 +183,7 @@ def next(self):
super().next()
"""

def buy(self, price=None, *, sl=None, tp=None):
def buy(self, price=None, *, sl=None, tp=None, size=None):
"""
Let the strategy close any current position and
use _all available funds_ to
Expand All @@ -192,12 +193,15 @@ def buy(self, price=None, *, sl=None, tp=None):
one at take-profit price (`tp`; limit order).

If `price` is not set, market price is assumed.

If `size` is not set, entire holding cash is assumed.
"""
self._broker.buy(price and float(price),
sl and float(sl),
tp and float(tp))
tp and float(tp),
size and float(size))

def sell(self, price=None, *, sl=None, tp=None):
def sell(self, price=None, *, sl=None, tp=None, size=None):
"""
Let the strategy close any current position and
use _all available funds_ to
Expand All @@ -207,10 +211,13 @@ def sell(self, price=None, *, sl=None, tp=None):
one at take-profit price (`tp`; limit order).

If `price` is not set, market price is assumed.

If `size` is not set, entire holding cash is assumed.
"""
self._broker.sell(price and float(price),
sl and float(sl),
tp and float(tp))
tp and float(tp),
size and float(size))

@property
def equity(self):
Expand Down Expand Up @@ -267,16 +274,18 @@ class Orders:
`backtesting.backtesting.Orders.set_sl` and
`backtesting.backtesting.Orders.set_tp`.
"""

def __init__(self, broker):
self._broker = broker
self._entry = self._sl = self._tp = self._close = self._is_long = None
self._entry = self._sl = self._tp = self._close = self._is_long = self._size = None

def _update(self, entry, sl, tp, is_long=True):
def _update(self, entry, sl, tp, size, is_long=True):
self._entry = entry and float(entry) or _MARKET_PRICE
self._sl = sl and float(sl) or None
self._tp = tp and float(tp) or None
self._close = False
self._is_long = is_long
self._size = size and float(size) or None

@property
def is_long(self):
Expand Down Expand Up @@ -367,6 +376,7 @@ class Position:
if self.position:
... # we have a position, either long or short
"""

def __init__(self, broker):
self._broker = broker

Expand Down Expand Up @@ -459,13 +469,13 @@ def __init__(self, *, data, cash, commission, margin, trade_on_close, length):
def __repr__(self):
return '<Broker: {:.0f}{:+.1f}>'.format(self._cash, self.position.pl)

def buy(self, price=None, sl=None, tp=None):
def buy(self, price=None, sl=None, tp=None, size: float = None):
assert (sl or -np.inf) <= (price or self.last_close) <= (tp or np.inf), "For long orders should be: SL ({}) < BUY PRICE ({}) < TP ({})".format(sl, price or self.last_close, tp) # noqa: E501
self.orders._update(price, sl, tp)
self.orders._update(price, sl, tp, size)

def sell(self, price=None, sl=None, tp=None):
def sell(self, price=None, sl=None, tp=None, size: float = None):
assert (tp or -np.inf) <= (price or self.last_close) <= (sl or np.inf), "For short orders should be: TP ({}) < BUY PRICE ({}) < SL ({})".format(tp, price or self.last_close, sl) # noqa: E501
self.orders._update(price, sl, tp, is_long=False)
self.orders._update(price, sl, tp, size, is_long=False)

def close(self):
self.orders.cancel()
Expand Down Expand Up @@ -493,7 +503,8 @@ def _open_position(self, price, is_long):

i, price = self._get_market_price(price)

position = float(self._cash * self._leverage / (price * (1 + self._commission)))
size = self._cash if self.orders._size is None else self.orders._size
position = float(size * self._leverage / (price * (1 + self._commission)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this position computation correct? You effectively replace former cash with size, but size actually corresponds to cash / price?

I think when size is given, position might better be float(size) if enough_cash else error?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention for the size variable being computed as such is because:

  1. If size is not specified when performing an order, it will default to the default behaviour of using entire holding cash amount.
  2. I did not notice there was a margin option to define a leveraged trade. (Only found out about it after reading: How to use backtesting.py for forex trading #10 ) Was initially under the assumption that all trades were on a 1:1 leverage. Under my assumption, i believed that specifying the margin option wasn't needed in order to define a "leveraged trade". In fact, by specifying an order size that is larger than your holding cash amount would technically mean you enter a trade on cross margin. (which was why I did not implement a check for if enough_cash). Reason for this assumption is largely influenced by Trading View's pine script, and how "leveraged trades" would be entered under their backtesting tools.

So my thoughts are as follows, and would need some clarification:

  1. Does the library actually calculate liquidations. If it does, the margin option would come in handy for that. A quick look into the code base shows how margin is used to calculate leverage amount. But margin and leverage are simply being used in reporting functions and likely no where else?
  2. Would it make sense to have margin be calculated dynamically if order size was specified, and larger than than holding cash. In that case, we could dynamically calculate a cross margin on init instead.

Would love to take a stab at pyramiding too, there are indeed many use cases for that. Let me know your thoughts. I might be working on a path different than the philosophy of this library. :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the library actually calculate liquidations.

It simulates them.

# Log account equity for the equity curve
equity = self.equity
self.log.equity[i] = equity
# Hovever, if negative, set all to 0 and stop the simulation
if equity < 0:
self._close_position()
self._cash = 0
self.log.equity[i:] = 0
raise _OutOfMoneyError

margin= therefore represents initial and maintenance margin of the trades. It's used only in stats and plots so the interested user gets a rough idea on how leveraged their strategy is allowed to be to still remain afloat.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our case, position is actually equal to size when the user requests it. Thus, we only need to deduct the commission and then confirm we have (leveraged) cash to cover it all.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think? 😅

self._position = position if is_long else -position
self._position_open_price = price
self._position_open_i = i
Expand Down Expand Up @@ -580,6 +591,7 @@ class Backtest:
instance, or `backtesting.backtesting.Backtest.optimize` to
optimize it.
"""

def __init__(self,
data: pd.DataFrame,
strategy: type(Strategy),
Expand Down
12 changes: 12 additions & 0 deletions backtesting/test/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ def next(self):
if self.position:
self.position.close()

class SingleFixedSizeTrade(Strategy):
def init(self):
self._done = False

def next(self):
if not self._done:
self.buy(size=1000)
self._done = True
if self.position:
self.position.close()

class SinglePosition(Strategy):
def init(self):
pass
Expand All @@ -287,6 +298,7 @@ def next(self):

for strategy in (SmaCross,
SingleTrade,
SingleFixedSizeTrade,
SinglePosition,
NoTrade):
with self.subTest(strategy=strategy.__name__):
Expand Down