Note: the code you enter is run for every tick. So it's often useful to provide some constraints on opening. But, we will likely want to check for adjustments or hitting profit target or stop-loss every tick.
This example may look complex, but it contains nearly all of what you need to know to create any trade plan. Take your time and read through it carefully.
-- Put Credit Spread: A comment like this at the first line will be used as a label for the run
-- Optional sim params (defaults: $40k and full archive date range)
sim_params.starting_cash = 40000
sim_params.start_date = "2019-01-02"
sim_params.end_date = "2023-12-29"
sim_params.commission = 1.08
sim_params.slippage = 3.00
sim_params.price = "mid"
if date.day_of_week == "Mon" and portfolio.n_open_trades < 5 then
trade = portfolio:new_trade()
dte = 90; qty = 1
trade:add_leg(Put("SP", Delta(-10), dte, -qty))
sp_strike = trade:leg("SP").strike
-- use the strike of the short put to determine placement of the LP.
-- here we make it a 50-wide Put Credit Spread.
trade:add_leg(Put("LP", Strike(sp_strike - 50), dte, qty))
-- the 1st 50 print statements will go to your javascript console (ctrl+shift+j)
-- this is a nice way to debug your code
print(trade)
end
-- nearly all strategies will want to check the trades each day for StopLoss or ProfitTarget
-- or adjustments. It's not possible do other methods on a `trade` variable after it has been closed.
for _, trade in portfolio:trades() do
if trade.pnl > 200 or trade.pnl < -500 then
trade:close(trade.pnl > 0 and "PT" or "SL") -- PT or SL
elseif trade.dit > 50 or trade.dte < 20 then
-- a string argument to close will be counted so we can track how many trades
-- were closed due to DIT or DTE. We could do the same above for PT and SL.
trade:close("DIT")
end
end
Creates a new empty trade in the portfolio.
Returns: Trade object
Number of open trades in the portfolio.
Returns: number
Returns an iterator over all active trades in the portfolio (yields index, trade).
Returns: iterator over Trade objects
for _, trade in portfolio:trades() do
print(trade.pnl)
end
Returns the PnL history of the portfolio.
Returns: table of numbers
local history = portfolio:history()
for i, pnl in ipairs(history) do print(i, pnl) end
Current portfolio profit and loss.
Returns: number
Current portfolio value (PnL + starting cash).
Returns: number
Current portfolio delta or theta.
Returns: number
if portfolio:delta() > 10 then -- Adjust if delta too high
-- Hedging logic
end
Most recently opened trade.
Returns: Trade object
Count events in the portfolio. These are reported to the user in a final table.
Parameters: key (string) - key to count, inc (number) - amount to increment by
portfolio:count("ProfitTargetHit", 2) -- increment the count of "ProfitTargetHit" by 2
portfolio:count("ProfitTargetHit") -- increment the count of "ProfitTargetHit" by 1
portfolio:count("ProfitTargetHit", -1) -- decrement the count of "ProfitTargetHit" by 1
Trade by ID.
Parameters: id (number) - ID of the trade
Returns: Trade object
Profit and loss of the trade.
Returns: number
Current mid value of the trade (sum of leg mid prices with quantity and multiplier).
Returns: number
if trade.mid < -2000 then trade:close() end
Cash used or made from the trade.
Returns: number
Show the risk graph of the trade in the risk-graph tab. Up to 200 risk graphs are allowed per run.
Returns: table
if trade.pnl < - 5000 -- let's see what's happening
trade:risk_graph()
end
Maximum days in trade.
Returns: number
Minimum days to expiration of the trade.
Returns: number
if trade.dte < 7 then trade:close() end
Id of the trade.
Returns: number
Delta or theta of the trade.
Returns: number
if math.abs(trade.delta) > 5 then
-- adjust the leg named LP to make the trade delta neutral (0). Accounts for quantity of named leg.
trade:adjust(TradeDelta(0), "LP")
end
Closes all legs in the trade.
Parameters: reason (string) - optional reason for closing the trade
if trade.pnl > 500 then trade:close("ProfitTargetHit") end
-- NOTE that we can't use trade variable after close (or erase).
Removes the trade from the portfolio and erases any commissions. Useful for filtering out bad data or invalid trade setups.
Adds a new leg to the trade.
Parameters: TradeLeg (created using Put() or Call())
trade:add_leg(Call("LC", Delta(30), 30, 1))
trade:add_leg(Put("SP", Delta(-30), 30, -1))
Checks if the trade has a leg with the given name.
Parameters: name (string) - name of the leg
Returns: boolean
if trade:has_leg("LC") then print("Has long call") end
Closes a specific leg in the trade.
name (string) - name of the leg to close
erase (boolean) - optional, default is false. if true, it's like the leg never existed
if trade:has_leg("LC") and trade:leg("LC").mid > 500 then
trade:close_leg("LC")
end
Adjusts a leg of the trade using a selector.
Parameters:
Returns: boolean (success)
-- Adjust leg "LP" to make trade delta -5. Accounts for quantity of named leg.
trade:adjust(TradeDelta(-5), "LP")
Gets a specific leg by name.
Parameters: name (string) - name of the leg
Returns: Leg object
local my_leg = trade:leg("LC")
print(my_leg.strike, my_leg.delta)
Creates a put option leg.
Parameters:
local short_put = Put("SP", Delta(-30), 45, -1)
Creates a call option leg.
Parameters: Same as Put()
Delta or theta of the leg.
Returns: number
local leg_delta = trade:leg("LC").delta
Days to expiration of the leg.
Returns: number
Expiration date of the leg as a string.
Returns: string
Mid price of the leg.
Returns: number
Name of the leg.
Returns: string
Quantity of the leg.
Returns: number
Side of the leg ("put" or "call").
Returns: string
Strike price of the leg.
Returns: number
Selects an option by delta value (use negative for puts).
Call("LC", Delta(30.0), 30, 1)
Put("SP", Delta(-30), 30, -1)
Adjusts a leg to achieve a specific trade-level delta. Accounts for quantity.
trade:adjust(TradeDelta(-5), "LP")
Selects an option by strike price.
Call("ATM", Strike(underlying_price), 30, 1)
Selects an option by mid price.
Call("Cheap", Mid(1.0), 30, 10)
Selects an option by theta value.
Call("HighDecay", Theta(-0.5), 30, -1)
Selects an option by vega value.
Call("VolSensitive", Vega(0.2), 45, 1)
Selects an option by gamma value.
Call("HighGamma", Gamma(0.05), 15, 1)
Returns: number. The EMA of the underlying price over the last period elements.
local ema = MA:EMA(20)
print(ema) -- 20-day EMA of underlying price
Returns: number. The SMA of the underlying price over the last period elements.
Current price of the underlying asset (index/stock/ETF).
Type: number
if underlying_price < 20 then -- VIX is low
-- Enter positions
end
simulation date with attributes: day_of_week, day, month, year, hour, minute, second
Type: date
if date.day_of_week == "Mon" and date.hour == 10 then -- Entry on Monday at 10am EST
-- Enter positions
-- Manage positions
end
Last trade opened. May be nil if no trades have been opened yet or if most recent trade has been closed.
Type: Trade
if last_trade ~= nil and last_trade.dit >= 3 then
-- open new trade ... not shown.
end
Global writable table that persists between ticks. Useful for storing data like starting values to compare against later.
-- ... note were only showing a subset of the code here. we'd also want to set O[trade.id] = 0 when we open the trade.
if trade.pnl > O[trade.id].pnl + 400 then -- if we made $400 in a single day (tick) then just close.
trade:close()
end
-- ... other operations. but can't use `trade` variable if it has been closed.
-- store the pnl of the trade at end of tick code.
O[trade.id] = trade.pnl
Simulation parameters. Set these before running (top-level assignments).
Type: SimParameters
Fields:
sim_params.starting_cash = 40000
sim_params.start_date = "2019-01-02"
sim_params.end_date = "2023-12-29"
sim_params.commission = 1.08
sim_params.slippage = 3.00
-- Daily ticks at 10:00 (or nearest available that day)
sim_params.archive = "SPX"
sim_params.tick_interval = "day"
sim_params.tick_time = "10:00"
-- Random daily time (one random time per run)
-- sim_params.tick_time = 9999
-- Hourly ticks
-- sim_params.tick_interval = "hour"
-- 15-minute ticks
-- sim_params.tick_interval = "15"
-- sim_params.tick_interval = "15-minute"
This lets the user create custom plots using any data available in the lua API. This can be very powerful for debugging trade performance and strategy backtesting. Use trace to add multiple series to the same plot.
The plot title can start with "main_" to plot on the second y-axis in the main chart.
Only scatter plots are supported on the main.
The default is to plot these in a User Plots tab.
Parameters:
-- show a histogram of how many trades we have open at any time.
plots:add("n_trades", portfolio.n_open_trades, "histogram", {bins = 20, color="red"})
-- show a scatter plot of how many trades we have open vs date.
plots:add("n_trades_time", portfolio.n_open_trades, "scatter", {date=tostring(date), color="#E500E5", symbol="x"})
-- after opening a trade, we can get the debit (or credit) and show a histogram.
plots:add("debit", trade.cash, "histogram")
-- plot trade pnl over time with one trace per trade id
for _, trade in portfolio:trades() do
plots:add("trade_pnl", trade.pnl, "scatter", {date=tostring(date), trace=tostring(trade.id)})
end
-- plot starting theta vs final Pnl.
if portfolio.n_open_trades < MAX_TRADES and (portfolio:last_trade() == nil or portfolio:last_trade().dit > 2) then
-- open a new trade ... not shown.
O[trade.id] = trade.theta
end
-- ... other operations.
for _, trade in portfolio:trades() do
if math.abs(trade.pnl) > 10000 or trade.dit > 50 then
plots:add("starting theta vs final pnl", O[trade.id], "scatter", {x=trade.pnl, symbol='o'})
trade:close()
end
end
-- Put Credit Spread Example
-- This is an example trade. It will look complex at first glance.
-- But, it contains most of the pieces to implement any strategy.
-- Optional sim params (defaults: $40k and full archive date range)
sim_params.starting_cash = 40000
sim_params.start_date = "2019-01-02"
sim_params.end_date = "2023-12-29"
sim_params.commission = 1.08
sim_params.slippage = 3.00
sim_params.price = "mid"
-- nearly all trades will want some sort of condition to open the trade.
-- this is often day of week and number of open trades.
if date.day_of_week == "Tue" and portfolio.n_open_trades < 5 and MA:EMA(10) > MA:EMA(20) then
trade = portfolio:new_trade()
dte = 45; qty = 2;
trade:add_leg(Put("SP", Delta(-10), dte, -qty))
-- use the strike of the short put to determine placement of the LP.
local sp = trade:leg("SP")
trade:add_leg(Put("LP", Strike(sp.strike - 50), dte, qty))
-- the 1st 200 print statements will go to your javascript console (ctrl+shift+j)
-- this is a nice way to debug your code
print(trade)
-- sometimes the strikes are not available so we end up with a trade
-- that is not 50-wide
local dist = sp.strike - trade:leg("LP").strike
-- make it like the trade never existed.
if dist > 75 or dist < 25 then trade:erase() end
end
-- nearly all strategies will want to check the trades each day for StopLoss or ProfitTarget
-- or adjustments.
for _, trade in portfolio:trades() do
if math.abs(trade.pnl) > 10000 then
trade:erase() -- can get rid of trades with bad data (but be careful!!).
elseif trade.pnl > 300 or trade.pnl < -2000 then
trade:close(trade.pnl > 0 and "PT" or "SL") -- PT or SL
elseif trade.dte < 2 then
trade:close("Days in Trade") -- the message to close gets logged to "User Counts tab"
end
end
-- VIX trading strategy example
local dte = 41
-- Open new trades on Mon/Wed/Fri when VIX is below 20
if (date.day_of_week == "Mon" or date.day_of_week == "Wed" or date.day_of_week == "Fri")
and underlying_price < 20 then
local trade = portfolio:new_trade()
-- Short call 2 points below current price
trade:add_leg(Call("Call", Strike(underlying_price - 2.0), dte, -1))
-- Long call at strike 32 for protection
trade:add_leg(Call("LC", Strike(32), dte, 2))
end
-- Manage existing trades
for _, trade in portfolio:trades() do
-- Close long call if mid price > 500
if trade:has_leg("LC") and trade:leg("LC").mid > 500 then
trade:close_leg("LC")
print("LC closed", date)
-- Close entire trade if less than 7 days to expiration
elseif trade.dte < 7 then
trade:close()
elseif trade.pnl > 600 or trade.pnl < -500 then
trade:close() -- PT or SL
end
end
-- IRON Condor Ex
function create_iron_condor(dte, short_delta, qty)
local trade = portfolio:new_trade()
local width = 25 -- points wide IC
-- Short call
trade:add_leg(Call("SC", Delta(short_delta), dte, -qty))
local sc_strike = trade:leg("SC").strike
-- Long call
trade:add_leg(Call("LC", Strike(sc_strike + width), dte, qty))
-- Check if the short call and long call have the same strike
if trade:leg("SC").strike == trade:leg("LC").strike then
portfolio:count("same-strike")
trade:close_leg("LC", true)
trade:add_leg(Call("LC", Strike(sc_strike + 2 * width), dte, qty)) -- could also close leg and re-open
-- now check again and erase trade if still same:
if trade:leg("SC").strike == trade:leg("LC").strike then
trade:erase()
return nil
end
print(trade)
end
-- log the width we ended up getting in user-counts
portfolio:count("width:" .. tostring(trade:leg("LC").strike - trade:leg("SC").strike))
-- Short put
trade:add_leg(Put("SP", Delta(-short_delta), dte, -qty))
local sp_strike = trade:leg("SP").strike
trade:add_leg(Put("LP", Strike(sp_strike - width), dte, qty))
return trade
end
-- Open new iron condor on Mondays
if date.day_of_week == "Mon" and date.hour == 10 then
local qty = 1
local dte = 45
local short_delta = 20
local ic = create_iron_condor(dte, short_delta, qty)
end
-- Strategy that dynamically adjusts delta based on day of week
-- Open a new trade on Monday
if date.day_of_week == "Mon" and portfolio.n_open_trades < 3 then
local trade = portfolio:new_trade()
local dte = 30
local qty = 1
trade:add_leg(Put("ShortPut", Delta(-30), dte, -qty))
trade:add_leg(Put("LongPut", Delta(-50), dte, qty))
end
-- Manage existing trades: adjust delta based on day and check exits
for _, trade in portfolio:trades() do
if trade.pnl > 1000 then
trade:close("Profit Target")
elseif trade.pnl < -1000 then
trade:close("Stop Loss")
elseif date.day_of_week == "Mon" then
trade:adjust(TradeDelta(-3), "ShortPut") -- Monday: bearish bias
elseif date.day_of_week == "Fri" then
trade:adjust(TradeDelta(2), "ShortPut") -- Friday: bullish bias
else
trade:adjust(TradeDelta(0), "ShortPut") -- Other days: neutral
end
end
-- Put Back Ratio
short_qty = 2
long_qty = 3
if date.day_of_week == "Tue" then
trade = portfolio:new_trade()
dte = 45;
trade:add_leg(Put("SP", Delta(-2), dte, -short_qty))
sp_mid = trade:leg("SP").mid
print("sp_mid:", sp_mid)
-- use the mid of the short put to determine placement of the LP.
trade:add_leg(Put("LP", Mid(sp_mid / long_qty - 1.0), dte, long_qty))
-- the 1st 50 print statements will go to your javascript console (ctrl+shift+j)
-- this is a nice way to debug your code
print(trade)
end
-- nearly all strategies will want to check the trades each day for StopLoss or ProfitTarget
-- or adjustments.
for _, trade in portfolio:trades() do
if math.abs(trade.pnl) > 10000 then
-- trade:erase() -- can get rid of trades with bad data (but be careful!!).
elseif trade.pnl > 1000 or trade.pnl < -2000 then
trade:close(trade.pnl > 0 and "PT" or "SL") -- PT or SL
end
end