How to backtest portfolio compositions using backtrader? - python

I have a csv file / pandas dataframe which looks like this. It contains various portfolio compositions for a portfolio which is re-balanced everyday according to my own calculations.
date asset percentage
4-Jan-21 AAPL 12.00%
4-Jan-21 TSM 1.00%
4-Jan-21 IBM 31.00%
4-Jan-21 KO 15.00%
4-Jan-21 AMD 41.00%
5-Jan-21 DELL 23.00%
5-Jan-21 TSM 12.20%
5-Jan-21 IBM 15.24%
5-Jan-21 KO 1.50%
5-Jan-21 NKE 7.50%
5-Jan-21 TSLA 9.50%
5-Jan-21 CSCO 3.30%
5-Jan-21 JPM 27.76%
6-Jan-21 AMD 45%
6-Jan-21 BA 0.50%
6-Jan-21 ORCL 54.50%
7-Jan-21 AAPL 50.00%
7-Jan-21 KO 50.00%
...
I want to test a strategy with a 12 asset portfolio.
AAPL,TSM,IBM,KO,AMD,DELL,NKE,TSLA,CSCO,JPM,BA,ORCL
So let's say on 4Jan2021, the portfolio's composition would be 12% in apple, 1% in TSM.. etc. I want to be able to check the prices and know how many I should be holding.
The next day, 5Jan2021, the composition will change to 23% in Dell.. etc, if the stock isn't in this list means its 0% for that day.
I have been looking at backtrader as a backtesting platform, however, the code I have seen in the repo mostly shows how to do stuff with indicators, like SMA cross over, RSI...
My question is: Is it possible to create and test a portfolio based on these compositions I have so I can check the return of this strategy? It would check this frame, and know how many stocks in a ticker to buy or sell on that particular day.
So the universe of stocks I am buying or sell is AAPL,TSM,IBM,KO,AMD,DELL,NKE,TSLA,CSCO,JPM,BA,ORCL
So on 4-Jan-21 it might look like,
dictionary['4Jan2021'] = {'AAPL':0.12,
'TSM':0.01,
'IBM':0.31,
'KO':0.15,
'AMD':0.41,}
On 5-Jan-21 it will look like,
dictionary['5Jan2021'] = {'DELL':0.23,
'TSM':0.122,
'IBM':0.1524,
'KO':0.015,
'NKE':0.075,
'TSLA':0.095,
'CSCO':0.033,
'JPM':0.2776,}
If the ticker isnt there means its 0%.
The portfolio composition needs to change everyday.

The first thing you will want to do it load your targets with your datas. I like
personally to attach the target to the dataline as I add it to backtrader.
tickers = {"FB": 0.25, "MSFT": 0.4, "TSLA": 0.35}
for ticker, target in tickers.items():
data = bt.feeds.YahooFinanceData(
dataname=ticker,
timeframe=bt.TimeFrame.Days,
fromdate=datetime.datetime(2019, 1, 1),
todate=datetime.datetime(2020, 12, 31),
reverse=False,
)
data.target = target
cerebro.adddata(data, name=ticker)
In next you will want to go through each data, and determine the current allocation. If the current allocation is too far from the desired allocation (threshold) you trade all datas.
Notice there is a buffer variable. This will reduce the overall value of the account for calculating units to trade. This helps avoid margin.
You will use a dictionary to track this information.
def next(self):
track_trades = dict()
total_value = self.broker.get_value() * (1 - self.p.buffer)
for d in self.datas:
track_trades[d] = dict()
value = self.broker.get_value(datas=[d])
allocation = value / total_value
units_to_trade = (d.target - allocation) * total_value / d.close[0]
track_trades[d]["units"] = units_to_trade
# Can check to make sure there is enough distance away from ideal to trade.
track_trades[d]["threshold"] = abs(d.target - allocation) > self.p.threshold
Check all the thresholds to determine if trading. If any of datas need trading, then all need trading.
rebalance = False
for values in track_trades.values():
if values['threshold']:
rebalance = True
if not rebalance:
return
Finally, execute your trades. Always sell first to generate cash in the account and avoid margins.
# Sell shares first
for d, value in track_trades.items():
if value["units"] < 0:
self.sell(d, size=value["units"])
# Buy shares second
for d, value in track_trades.items():
if value["units"] > 0:
self.buy(d, size=value["units"])
Here is the all of the code for your reference.
import datetime
import backtrader as bt
class Strategy(bt.Strategy):
params = (
("buffer", 0.05),
("threshold", 0.025),
)
def log(self, txt, dt=None):
""" Logging function fot this strategy"""
dt = dt or self.data.datetime[0]
if isinstance(dt, float):
dt = bt.num2date(dt)
print("%s, %s" % (dt.date(), txt))
def print_signal(self):
self.log(
f"o {self.datas[0].open[0]:7.2f} "
f"h {self.datas[0].high[0]:7.2f} "
f"l {self.datas[0].low[0]:7.2f} "
f"c {self.datas[0].close[0]:7.2f} "
f"v {self.datas[0].volume[0]:7.0f} "
)
def notify_order(self, order):
""" Triggered upon changes to orders. """
# Suppress notification if it is just a submitted order.
if order.status == order.Submitted:
return
# Print out the date, security name, order number and status.
type = "Buy" if order.isbuy() else "Sell"
self.log(
f"{order.data._name:<6} Order: {order.ref:3d} "
f"Type: {type:<5}\tStatus"
f" {order.getstatusname():<8} \t"
f"Size: {order.created.size:9.4f} Price: {order.created.price:9.4f} "
f"Position: {self.getposition(order.data).size:5.2f}"
)
if order.status == order.Margin:
return
# Check if an order has been completed
if order.status in [order.Completed]:
self.log(
f"{order.data._name:<6} {('BUY' if order.isbuy() else 'SELL'):<5} "
# f"EXECUTED for: {dn} "
f"Price: {order.executed.price:6.2f} "
f"Cost: {order.executed.value:6.2f} "
f"Comm: {order.executed.comm:4.2f} "
f"Size: {order.created.size:9.4f} "
)
def notify_trade(self, trade):
"""Provides notification of closed trades."""
if trade.isclosed:
self.log(
"{} Closed: PnL Gross {}, Net {},".format(
trade.data._name,
round(trade.pnl, 2),
round(trade.pnlcomm, 1),
)
)
def next(self):
track_trades = dict()
total_value = self.broker.get_value() * (1 - self.p.buffer)
for d in self.datas:
track_trades[d] = dict()
value = self.broker.get_value(datas=[d])
allocation = value / total_value
units_to_trade = (d.target - allocation) * total_value / d.close[0]
track_trades[d]["units"] = units_to_trade
# Can check to make sure there is enough distance away from ideal to trade.
track_trades[d]["threshold"] = abs(d.target - allocation) > self.p.threshold
rebalance = False
for values in track_trades.values():
if values['threshold']:
rebalance = True
if not rebalance:
return
# Sell shares first
for d, value in track_trades.items():
if value["units"] < 0:
self.sell(d, size=value["units"])
# Buy shares second
for d, value in track_trades.items():
if value["units"] > 0:
self.buy(d, size=value["units"])
if __name__ == "__main__":
cerebro = bt.Cerebro()
tickers = {"FB": 0.25, "MSFT": 0.4, "TSLA": 0.35}
for ticker, target in tickers.items():
data = bt.feeds.YahooFinanceData(
dataname=ticker,
timeframe=bt.TimeFrame.Days,
fromdate=datetime.datetime(2019, 1, 1),
todate=datetime.datetime(2020, 12, 31),
reverse=False,
)
data.target = target
cerebro.adddata(data, name=ticker)
cerebro.addstrategy(Strategy)
# Execute
cerebro.run()
####################################
############# EDIT ###############
####################################
There was an additional requiest for adding in variable allocations per day per security. The following code accomplishes that.
import datetime
import backtrader as bt
class Strategy(bt.Strategy):
params = (
("buffer", 0.05),
("threshold", 0.025),
)
def log(self, txt, dt=None):
""" Logging function fot this strategy"""
dt = dt or self.data.datetime[0]
if isinstance(dt, float):
dt = bt.num2date(dt)
print("%s, %s" % (dt.date(), txt))
def print_signal(self):
self.log(
f"o {self.datas[0].open[0]:7.2f} "
f"h {self.datas[0].high[0]:7.2f} "
f"l {self.datas[0].low[0]:7.2f} "
f"c {self.datas[0].close[0]:7.2f} "
f"v {self.datas[0].volume[0]:7.0f} "
)
def notify_order(self, order):
""" Triggered upon changes to orders. """
# Suppress notification if it is just a submitted order.
if order.status == order.Submitted:
return
# Print out the date, security name, order number and status.
type = "Buy" if order.isbuy() else "Sell"
self.log(
f"{order.data._name:<6} Order: {order.ref:3d} "
f"Type: {type:<5}\tStatus"
f" {order.getstatusname():<8} \t"
f"Size: {order.created.size:9.4f} Price: {order.created.price:9.4f} "
f"Position: {self.getposition(order.data).size:5.2f}"
)
if order.status == order.Margin:
return
# Check if an order has been completed
if order.status in [order.Completed]:
self.log(
f"{order.data._name:<6} {('BUY' if order.isbuy() else 'SELL'):<5} "
# f"EXECUTED for: {dn} "
f"Price: {order.executed.price:6.2f} "
f"Cost: {order.executed.value:6.2f} "
f"Comm: {order.executed.comm:4.2f} "
f"Size: {order.created.size:9.4f} "
)
def notify_trade(self, trade):
"""Provides notification of closed trades."""
if trade.isclosed:
self.log(
"{} Closed: PnL Gross {}, Net {},".format(
trade.data._name,
round(trade.pnl, 2),
round(trade.pnlcomm, 1),
)
)
def __init__(self):
for d in self.datas:
d.target = {
datetime.datetime.strptime(date, "%d-%b-%y").date(): allocation
for date, allocation in d.target.items()
}
def next(self):
date = self.data.datetime.date()
track_trades = dict()
total_value = self.broker.get_value() * (1 - self.p.buffer)
for d in self.datas:
if date not in d.target:
if self.getposition(d):
self.close(d)
continue
target_allocation = d.target[date]
track_trades[d] = dict()
value = self.broker.get_value(datas=[d])
current_allocation = value / total_value
net_allocation = target_allocation - current_allocation
units_to_trade = (
(net_allocation) * total_value / d.close[0]
)
track_trades[d]["units"] = units_to_trade
# Can check to make sure there is enough distance away from ideal to trade.
track_trades[d]["threshold"] = abs(net_allocation) > self.p.threshold
rebalance = False
for values in track_trades.values():
if values["threshold"]:
rebalance = True
if not rebalance:
return
# Sell shares first
for d, value in track_trades.items():
if value["units"] < 0:
self.sell(d, size=value["units"])
# Buy shares second
for d, value in track_trades.items():
if value["units"] > 0:
self.buy(d, size=value["units"])
if __name__ == "__main__":
cerebro = bt.Cerebro()
allocations = [
("AAPL", "4-Jan-21", 0.300),
("TSM", "4-Jan-21", 0.200),
("IBM", "4-Jan-21", 0.300),
("KO", "4-Jan-21", 0.2000),
("AMD", "4-Jan-21", 0.1000),
("DELL", "5-Jan-21", 0.200),
("TSM", "5-Jan-21", 0.20),
("IBM", "5-Jan-21", 0.1),
("KO", "5-Jan-21", 0.1),
("NKE", "5-Jan-21", 0.15),
("TSLA", "5-Jan-21", 0.10),
("CSCO", "5-Jan-21", 0.050),
("JPM", "5-Jan-21", 0.1),
("AMD", "6-Jan-21", 0.25),
("BA", "6-Jan-21", 0.25),
("ORCL", "6-Jan-21", 0.50),
("AAPL", "7-Jan-21", 0.5000),
("KO", "7-Jan-21", 0.5000),
]
ticker_names = list(set([alls[0] for alls in allocations]))
targets = {ticker: {} for ticker in ticker_names}
for all in allocations:
targets[all[0]].update({all[1]: all[2]})
for ticker, target in targets.items():
data = bt.feeds.YahooFinanceData(
dataname=ticker,
timeframe=bt.TimeFrame.Days,
fromdate=datetime.datetime(2020, 12, 21),
todate=datetime.datetime(2021, 1, 8),
reverse=False,
)
data.target = target
cerebro.adddata(data, name=ticker)
cerebro.addstrategy(Strategy)
cerebro.broker.setcash(1000000)
# Execute
cerebro.run()

Related

How do I add a daterange to open trades during when backtesting with backtrader?

I am trying to backtest a strategy where trades are only opened during 8.30 to 16.00 using backtrader.
Using the below attempt my code is running but returning no trades so my clsoing balane is the same as the opening. If I remove this filter my code is running correctly and trades are opening and closing so it is definitely the issue. Can anyone please help?
I have tried adding the datetime column of the data to a data feed using the below code:
` def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
self.datatime = mdates.num2date(self.datas[0].datetime)
self.datatsi = self.datas[0].tsi
self.datapsar = self.datas[0].psar
self.databbmiddle = self.datas[0].bbmiddle
self.datastlower = self.datas[0].stlower
self.datastupper = self.datas[0].stupper
# To keep track of pending orders
self.order = None`
I then used the following code to try filter by this date range:
# Check if we are in the market
if not self.position:
current_time = self.datatime[0].time()
if datetime.time(8, 30) < current_time < datetime.time(16, 0):
if self.datatsi < 0 and self.datastupper[0] > self.dataclose[0] and self.datastlower[1] < self.dataclose[1] and self.dataclose[0] < self.databbmiddle[0] and self.datapsar[0] > self.dataclose[0]:
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
os = self.sell_bracket(size=100,price=sp1, stopprice=sp2, limitprice=sp3)
self.orefs = [o.ref for o in os]
else:
o1 = self.buy(exectype=bt.Order.Limit,price=bp1,transmit=False)
print('{}: Oref {} / Buy at {}'.format(self.datetime.date(), o1.ref, bp1))
o2 = self.sell(exectype=bt.Order.Stop,price=bp2,parent=o1,transmit=False)
print('{}: Oref {} / Sell Stop at {}'.format(self.datetime.date(), o2.ref, bp2))
o3 = self.sell(exectype=bt.Order.Limit,price=bp3,parent=o1,transmit=True)
print('{}: Oref {} / Sell Limit at {}'.format(self.datetime.date(), o3.ref, bp3))
self.orefs = [o1.ref, o2.ref, o3.ref] # self.sell(size=100, exectype=bt.Order.Limit, price=self.data.close[0]+16, parent=self.order, parent_bracket=bt.Order.Market)

Getting Expired Options Contract pricing from Interactive Brokers

I am looking to reconstruct Expired Options pricing with the help of ib_insync library and Interactive Brokers available data..
Because IB provides OPTION_IMPLIED_VOLATILITY as an output for reqHistoricalData I was thinking proceeding this way:
Have a function to infer Expired Options Contract Prices from Black-Scholes Model:
def black_scholes(stock_price,strike_price,vol,time,rate,right="Call"):
d1 = (np.log(stock_price/strike_price) + (rate + 0.5* vol**2)*time)/(vol*np.sqrt(time))
d2 = (np.log(stock_price/strike_price) + (rate - 0.5* vol**2)*time)/(vol*np.sqrt(time))
nd1 = norm.cdf(d1)
nd2 = norm.cdf(d2)
n_d1 = norm.cdf(-1*d1)
n_d2 = norm.cdf(-1*d2)
if right.capitalize()[0] == "C":
return round((stock_price*nd1) - (strike_price*np.exp(-1*rate*time)*nd2),2)
else:
return round((strike_price*np.exp(-1*rate*time)*n_d2) - (stock_price*n_d1),2)
Using the contract on the underlying stock assuming I have a valid ib connection opened elsewhere in my code to retrieve the data
def get_stock_history(symbol,whattoshow_string):
contract = Stock(symbol, 'SMART', 'USD')
ib.reqMarketDataType(2)
bars = ib.reqHistoricalData(
contract,
endDateTime='',
durationStr='2 Y',
barSizeSetting='1 Hour',
whatToShow=whattoshow_string,
useRTH=True,
formatDate=1)
ib.sleep(5)
df = util.df(bars)
df['date'] = pd.to_datetime(df['date'] ).dt.date
return df
I also have a handy function to compute maturity in the BSM based on an hourly time decay:
def hourCount(DF, expiry):
DF["maturity"] = ((dt.datetime.strptime(expiry, "%Y-%m-%d") - pd.to_datetime(DF.index))/pd.Timedelta(hours=1))/(365*24)
I could then get the data as below assuming I have an expiration date and strike from elsewhere I wish to backtest:
strike = 148
expiration_date = '2022-12-02'
symbol = 'AAPL'
historicalData = get_stock_history(symbol,'ADJUSTED_LAST')
impVolData = get_stock_history(symbol,'OPTION_IMPLIED_VOLATILITY')
option_price_call = pd.DataFrame(columns=["open","high","low","close"])
option_price_put = pd.DataFrame(columns=["open","high","low","close"])
hourCount(historicalData, expiration_date)
hourCount(impVolData, expiration_date)
historicalData = historicalData [(historicalData["maturity"] > 0)]
impVolData = impVolData[(impVolData["maturity"] > 0)]
for column in ["open","high","low","close"]:
option_price_call[column] = black_scholes(historicalData[column], strike, impVolData[column], historicalData["maturity"], 0.03,right="Call")
option_price_put[column] = black_scholes(historicalData[column], strike, impVolData[column], historicalData["maturity"], 0.03,right="Put")
Would that be a good approach to reconstruct/backtest the Expired Options contract pricing or am I overlooking something here? and maybe a smarter way to achieve this operation?
Thanks in advance for your suggestions! (y)

binance api, i cant put an order with a price in dollar (not in quantity)

i try to put some orders at a specific price.
For example i would like to put 20 dollars to buy some ETHUSDT at 800 usdt but it gives me this error:
binance.exceptions.BinanceAPIException: APIError(code=-1106): Parameter 'quoteOrderQty' sent when not required.
there is my call function:
buy_order = client.create_order(
symbol = "ETHUSDT",
price = 800,
quoteOrderQty = 25,
side = client.SIDE_BUY,
type = client.ORDER_TYPE_LIMIT,
timeInForce = client.TIME_IN_FORCE_GTC)
but i dont have any error when i put this :
buy_order = client.create_test_order(symbol="ETHUSDT", side='BUY', type='MARKET', quoteOrderQty=10)
to be honnest actually i do that:
def putOrderBuy_at_price(self, symbol, amount, price):
monney_price = self.client.get_symbol_ticker(symbol=symbol)
# Calculate how much ETH $200 can buy
print(monney_price['price'])
i = 10
while i != 0:
try:
buy_quantity = round(amount / float(price), i)
print("-----------", buy_quantity)
#ETHUSDT
buy_order = self.client.create_order(
symbol = symbol,
price = price,
quantity = buy_quantity,
side = self.client.SIDE_BUY,
type = self.client.ORDER_TYPE_LIMIT,
timeInForce = self.client.TIME_IN_FORCE_GTC)
break
except Exception:
print(i)
i -= 1
And i think there is a better way for that.
Thanks for yours answers
quoteOrderQty is only for MARKET order.

How to get all tick ranges with non-zero liquidity to finally calculate Total Value Locked Uniswap V3?

The aim is to calculate the uniswap v3 pool's total value locked (TVL).
import json
from web3 import Web3
from collections import namedtuple
infura_url = 'https://mainnet.infura.io/v3/******'
web3 = Web3(Web3.HTTPProvider(infura_url))
def read_json_file(directory:str, file_name: str):
try:
file_path = directory + file_name
f_ = open(file_path, 'r')
except Exception as e:
print(f"Unable to open the {file_path} file")
raise e
else:
json_data = json.loads(f_.read())
return json_data
# uniswap_ETH_USDT.v3
abi = read_json_file('./', 'abis/uniswapV3Pool.json')
address = '0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36'
exchange_contract = web3.eth.contract(address=Web3.toChecksumAddress(address), abi=abi)
Tick = namedtuple("Tick", "liquidityGross liquidityNet feeGrowthOutside0X128 feeGrowthOutside1X128 tickCumulativeOutside secondsPerLiquidityOutsideX128 secondsOutside initialized")
amounts0 = 0
amounts1 = 0
liquidity = 0
slot0 = exchange_contract.functions.slot0().call()
sqrtPriceCurrent = slot0[0] / (1 << 96)
MIN_TICK = -887272
MAX_TICK = 887272
TICK_SPACING = exchange_contract.functions.tickSpacing().call()
def calculate_token0_amount(liquidity, sp, sa, sb):
sp = max(min(sp, sb), sa)
return liquidity * (sb - sp) / (sp * sb)
def calculate_token1_amount(liquidity, sp, sa, sb):
sp = max(min(sp, sb), sa)
return liquidity * (sp - sa)
for tick in range(MIN_TICK, MAX_TICK, TICK_SPACING):
tickRange = Tick(*exchange_contract.functions.ticks(tick).call())
liquidity += tickRange.liquidityNet
sqrtPriceLow = 1.0001 ** (tick // 2)
sqrtPriceHigh = 1.0001 ** ((tick + TICK_SPACING) // 2)
amounts0 += calculate_token0_amount(liquidity, sqrtPriceCurrent, sqrtPriceLow, sqrtPriceHigh)
amounts1 += calculate_token1_amount(liquidity, sqrtPriceCurrent, sqrtPriceLow, sqrtPriceHigh)
print(amounts0, amounts1, tick) # for better output, should correct for the amount of decimals before printing
This does print liquidity in MIN_TICK and MAX_TICK but takes a lot of time and waste web3 calls as it is iterating on zero liquidity ticks also. Right now these are hardcoded, here I want to know what can be the value of min-max so that range does not contain any zero liquidity tick.
Getting pair token balance of contracts
web3.eth.contract(address=token_address,abi=abi).functions.balanceOf(contract_address).call()
and then get current price of each token / USDT by calling function slot0 in pool tokenA/USDT & tokenB/USDT
slot0 = contract.functions.slot0().call()
sqrtPriceCurrent = slot0[0] / (1 << 96)
priceCurrent = sqrtPriceCurrent ** 2
decimal_diff = USDT_decimal - TOKEN_A_decimal
token_price = 10**(-decimal_diff)/( priceCurrent) if token0_address == USDT_address else priceCurrent/(10**decimal_diff)
Finally, TVL = sum(token_balance * token_price)
** Remember: check price from big pool
No offense but you are following a hard way, which needs to use TickBitmap to get the next initialized tick (Remember not all ticks are initialized unless necessary.)
Alternatively the easy way to get a pool's TVL is to query Uniswap V3's subgraph: like
{
pool(id: "0x4e68ccd3e89f51c3074ca5072bbac773960dfa36") {
id
token0 {symbol}
totalValueLockedToken0
token1 {symbol}
totalValueLockedToken1
}
}
(for some reason it doesn't show result if you put checksum address)
or
{
pools(first: 5) {
id
token0 {symbol}
totalValueLockedToken0
token1 {symbol}
totalValueLockedToken1
}
}

Running backtest on backtrader I get many BUY ORDERS CANCELLED/MARGIN/REJECTED when they should not

Many buy orders are cancelled in the backtests and I cannot find why. Looking at the Writefile log it seems the buy orders are created at the next day's close. In most cases the buy price is in the day's range but still not executed.
I have tried on different assets, different sizes of the data feed, different strategies, with the same result.
Running on Jupyter notebook. I include the code and the log.
Finally I changed the default parameter ('100') in AllInSizerInt() below 100 and it worked. I do not really understand why, I thought the sizer would get the cash position from the broker and adjust the order.
Here is the fix:
''''
python
#Add the sizer. We plan to go 'all in' every time
cerebro.addsizer(bt.sizers.AllInSizerInt, percents = 95)
''''
And here is the original code:
''''
python
#### Import databases
from datetime import datetime
import backtrader as bt
import backtrader.indicators as btind
abspath = '/mypath/'
logfile = 'abs_momentum.csv'
# Create a Strategy
class Abs_momentum(bt.Strategy):
alias = ('Abs_momentum',)
params = (
# period for momentum calculation
('lookback', 252),
('range', 100))
def __init__(self):
# keep track of close price in the series
self.data_close = self.datas[0].close
# keep track of pending orders/buy price/buy commission
self.order = None
self.price = None
self.comm = None
# add a momentum indicator
self.mom = btind.MomentumOsc(self.data_close, \
period = self.p.lookback, \
band = self.p.range)
self.buysig = self.mom
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if order.status in [order.Completed]:
if order.isbuy():
self.log(
'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
#self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
self.order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def next(self):
# do nothing if an order is pending
if self.order:
return
# check if there is already a position
if not self.position:
# buy condition
if self.buysig > 100:
self.log(f'BUY CREATED --- Price: {self.data_close[0]:.2f}')
self.order = self.buy(size = None)
else:
# sell condition
if self.buysig < 100:
self.log(f'SELL CREATED --- Price: {self.data_close[0]:.2f}')
self.order = self.sell(Size = None)
###
#### Download data from Yahoo Finance
data = bt.feeds.YahooFinanceData(dataname= 'SPY', \
fromdate=datetime(2018, 6, 15),\
todate=datetime(2021, 3, 17),\
reverse = False)
####
# create a Cerebro entity
cerebro = bt.Cerebro(stdstats = False)
### Set up the backtest
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Set the commission - 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)
#Add the sizer. We plan to go 'all in' every time
cerebro.addsizer(bt.sizers.AllInSizerInt)
#Add the strategy
cerebro.addstrategy(Abs_momentum)
#Add the observers to the plot
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)
#Write to a csv file
cerebro.addwriter(bt.WriterFile, out = (abspath + logfile), csv=True,\ data(csv) = False)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
###
### Plot the results
cerebro.plot(iplot=True, volume=False)
###
''''
Here is the log:
>Starting Portfolio Value: 100000.00
>2019-06-18, BUY CREATED --- Price: 282.95
>2019-06-19, Order Canceled/Margin/Rejected
>2019-06-19, BUY CREATED --- Price: 283.58
>2019-06-20, Order Canceled/Margin/Rejected
>2019-06-20, BUY CREATED --- Price: 286.29
>2019-06-21, Order Canceled/Margin/Rejected
>2019-06-21, BUY CREATED --- Price: 285.88
>2019-06-24, BUY EXECUTED, Price: 286.10, Cost: 99848.90, Comm 99.85
>2020-03-12, SELL CREATED --- Price: 243.56
>2020-03-13, SELL EXECUTED, Price: 258.27, Cost: 99848.90, Comm 90.14
>2020-03-13, OPERATION PROFIT, GROSS -9712.67, NET -9902.66
>2020-04-17, BUY CREATED --- Price: 283.04
>2020-04-20, BUY EXECUTED, Price: 279.06, Cost: 88741.08, Comm 88.74
>2020-04-20, SELL CREATED --- Price: 278.05
>2020-04-21, SELL EXECUTED, Price: 273.25, Cost: 88741.08, Comm 86.89
>2020-04-21, OPERATION PROFIT, GROSS -1847.58, NET -2023.21
>2020-04-29, BUY CREATED --- Price: 289.53
>2020-04-30, Order Canceled/Margin/Rejected
>2020-04-30, BUY CREATED --- Price: 286.83
>2020-05-01, Order Canceled/Margin/Rejected
>2020-05-06, BUY CREATED --- Price: 280.68
>2020-05-07, Order Canceled/Margin/Rejected
>2020-05-07, BUY CREATED --- Price: 284.07
>2020-05-08, Order Canceled/Margin/Rejected
>2020-05-08, BUY CREATED --- Price: 288.77
>2020-05-11, BUY EXECUTED, Price: 286.69, Cost: 87153.76, Comm 87.15
>Final Portfolio Value: 121189.86
The reason your adjustment from 100% size trade to 95% size trade corrected the problem is that trying to by all in 100% on a portfolio will always result in some margins along the way . This can be seen here in your logs:
>2019-06-19, Order Canceled/Margin/Rejected
The problem is you are calculating the number of shares to trade off the previous close. If the market price gaps up on the next bar, you won't have enough cash to buy. There is an option called cheat-on-open that allows a peak at the next open price to permit sizing of the shares. This helps.
But in reality, it's best to trade below 100%.

Categories

Resources