// OrderBookFuzzyStrangle_v1.c
// Zorro S / lite-C -- SPY long strangle with underlying order-book filter.
// Requires synchronized historical .t2 book snapshots and .t8 options chains
// for Test/Train; GET_BOOK and options support from broker for Trade.
// Research prototype: validate fills, execution, and broker behavior in demo.
#include <contract.c>
#define BOOK_HANDLE 1
#define VOL_PERIOD 20
#define MIN_BOOK_QUOTES 6
#define MIN_DTE 30
#define EXIT_DTE 10
#define MIN_IMBALANCE 0.15
#define EXIT_IMBALANCE 0.07
#define MIN_FUZZY 0.65
#define MOVE_MULTIPLIER 1.10
#define MAX_DEBIT_USD 1500.0
#define PROFIT_FRACTION 0.50
#define LOSS_FRACTION 0.35
// Average absolute close-to-close change, in SPY dollars per hourly bar.
var meanAbsoluteMove(vars Prices, int Period)
{
var Sum = 0;
int i;
for(i = 1; i <= Period; i++)
Sum += abs(Prices[i-1] - Prices[i]);
return Sum / Period;
}
// Returns 1 if a usable book snapshot exists and fills the imbalance output.
int readBookImbalance(var Spot, var* Imbalance)
{
int N = orderUpdate(0,BOOK_HANDLE); // History\SPY.t2 or broker GET_BOOK
if(N < MIN_BOOK_QUOTES) return 0;
T2* Quotes = dataStr(BOOK_HANDLE,OrderRow,0);
if(!Quotes) return 0;
var Distance = 0.01*Spot; // visible liquidity within +/-1% of SPY
if(orderCVD(Quotes,N,Distance) < MIN_BOOK_QUOTES) return 0;
// cpd returns values in 0..100 normalized to the larger book side.
var BidDepth = cpd(-(Spot-Distance));
var AskDepth = cpd(Spot+Distance);
var Total = BidDepth+AskDepth;
if(Total <= 0) return 0;
*Imbalance = (BidDepth-AskDepth)/Total;
return 1;
}
// Returns 1 if open options should be liquidated on risk/expiration rules.
int exitByRisk()
{
var Cost = 0;
var OpenPnL = 0;
int ExpireSoon = 0;
int Legs = 0;
for(current_trades) {
if(!TradeIsOpen || !TradeIsLong || !TradeIsContract) continue;
Cost += TradePriceOpen*TradeUnits;
OpenPnL += TradeProfit;
if(contractDays(ThisTrade) <= EXIT_DTE) ExpireSoon = 1;
Legs++;
}
if(!Legs) return 0;
if(ExpireSoon) return 1;
if(Cost <= 0) return 0;
if(OpenPnL >= PROFIT_FRACTION*Cost) return 1;
if(OpenPnL <= -LOSS_FRACTION*Cost) return 1;
return 0;
}
void buyStrangle(var Spot, var AvgMove)
{
// Dynamic OTM strike targets in dollars. Select nearest listed strikes.
var Gap = max(0.01*Spot,8*AvgMove);
var CallTarget = Spot+Gap;
var PutTarget = Spot-Gap;
if(PutTarget <= 0) return;
// 6 means select by strike (the 4th contractFind parameter is mandatory).
CONTRACT* Call = contractFind(CALL,MIN_DTE,CallTarget,6);
CONTRACT* Put = contractFind(PUT,MIN_DTE,PutTarget,6);
if(!Call || !Put) return;
if(Call->fStrike <= Spot || Put->fStrike >= Spot) return;
if(Call->Expiry != Put->Expiry) return;
if(contractDays(Call) <= EXIT_DTE || contractDays(Put) <= EXIT_DTE)
return;
if(contractPrice(Call) <= 0) return;
if(contractPrice(Put) <= 0) return;
if(Call->fAsk <= 0 || Put->fAsk <= 0) return;
if(Call->fBid <= 0 || Put->fBid <= 0) return;
var Debit = (Call->fAsk+Put->fAsk)*Multiplier;
if(Debit <= 0 || Debit > MAX_DEBIT_USD) return;
if(combo(Call,1,Put,1,0,0,0,0) != 2) return;
MarginCost = comboMargin(1,3); // Stock/ETF options; long positions.
// The broker receives the combo when the second leg is submitted, if
// SET_COMBO_LEG is supported. Otherwise separate orders can be submitted.
enterLong(comboLeg(1));
enterLong(comboLeg(2));
printf("\nStrangle order attempted: SPY %.2f, call %.2f, put %.2f, debit %.2f",
Spot,Call->fStrike,Put->fStrike,Debit);
}
void run()
{
set(LOGFILE);
BarPeriod = 60;
LookBack = 100;
StartDate = 2020;
EndDate = 2024;
History = ".t8"; // Workshop 8: historical options/unadjusted underlying.
assetList("AssetsIB");
asset("SPY");
algo("OBStrangle");
Multiplier = 100; // Standard SPY option contract; verify with broker.
Capital = 100000; // Research backtest account, not a live risk control.
if(is(EXITRUN)) return;
static int LastChainDate = 0;
if(is(INITRUN)) {
LastChainDate = 0;
return;
}
vars Prices = series(priceClose());
var AvgMove = meanAbsoluteMove(Prices,VOL_PERIOD);
if(AvgMove <= 0) return;
FuzzyRange = max(0.01,0.25*AvgMove);
var Bullish = max(risingF(Prices),valleyF(Prices));
var Bearish = max(fallingF(Prices),peakF(Prices));
// In historical tests, options must be refreshed every bar.
// For live mode, cache the costly IB chain download for the session day.
int Today = ymd(wdate(0));
if(!Live || Today != LastChainDate || NumContracts <= 0) {
if(!contractUpdate(Asset,0,CALL|PUT)) return;
LastChainDate = Today;
}
// These exits must work even when the current order book is unavailable.
if(NumOpenLong > 0 && exitByRisk()) {
exitLong("cp"); // Only call and put contracts of this asset/algo.
return;
}
var Imbalance = 0;
if(!readBookImbalance(priceClose(),&Imbalance)) return;
plot("Order book imbalance",Imbalance,NEW,BLUE);
if(NumOpenLong > 0 || NumPendingLong > 0) {
// Fuzzy bearish price signal plus disappearing book pressure.
if(Bearish > MIN_FUZZY && abs(Imbalance) < EXIT_IMBALANCE)
exitLong("cp");
return;
}
// Require a meaningful move, visible bid/ask pressure, and corresponding
// fuzzy price direction. Long strangle still buys BOTH call and put.
if(abs(Prices[0]-Prices[1]) < MOVE_MULTIPLIER*AvgMove) return;
if(abs(Imbalance) < MIN_IMBALANCE) return;
if(Imbalance > 0 && Bullish >= MIN_FUZZY)
buyStrangle(priceClose(),AvgMove);
else if(Imbalance < 0 && Bearish >= MIN_FUZZY)
buyStrangle(priceClose(),AvgMove);
}