1GRAINS = [ZW=F ZC=F ZS=F ZM=F ZR=F KE=F]
2SOFTS = [SB=F KC=F CC=F]
3THRESHOLD = 8 # index points defining a sustained state
4HOLD = 6 months
6function positions(bar):
7 soi = feature(soi_3m)
9 la_nina_began = crossed_above(soi, +THRESHOLD, within HOLD of bar)
10 el_nino_began = crossed_below(soi, -THRESHOLD, within HOLD of bar)
12 basket = []
13 if la_nina_began: basket += GRAINS
14 if el_nino_began: basket += SOFTS
16 if basket is empty:
17 return {} # no event open, stand aside
19 return equal_risk(basket, bar)
21# ── execution ────────────────────────────────────────────────────────
23COST_PER_TRADE = 15 bps of the notional that changes hands
25function on_bar(bar, book):
26 if not bar.is_month_end:
27 return # decisions are made monthly only
29 target = positions(bar) # the rule above
30 target = scale_to_gross(target, 1.0)
32 rebalance(book, target, bar)
34function scale_to_gross(w, limit):
35 gross = sum(abs(w)) # total capital at work
37 if gross == 0:
38 return w # flat is a valid target
40 return w * limit / gross # leverage fixed, never implicit
42function rebalance(book, target, bar):
43 for symbol in union(book.symbols, target.symbols):
44 delta = target[symbol] - book.weight(symbol)
46 if delta > 0:
47 buy(symbol, delta, fill = next_bar.close)
48 else if delta < 0:
49 sell(symbol, -delta, fill = next_bar.close)
51 book.charge(COST_PER_TRADE * abs(delta))
53# Orders are filled at the next month's close, never the one the decision
54# was made on. There is no stop_loss(), take_profit(), limit order or
55# position cap: a holding changes only when positions() returns a
56# different target at the next month end.