Version 1 of 1 · · Current version

rebalance

Bringing a portfolio back to its targets. Each position is valued, compared with its target weight, and a guarded rule decides: buy, sell or leave it. Differences under 20% of the target (relative) are treated as on target, and trades are in whole shares, so small drifts don't churn the book. Buys are paid from the cash the sells free up, less a reserve kept aside; a buy the rest can't cover is trimmed, and one it can't start at all is skipped with a reason.

Valuing

$when pf.value (positions: Array, cash: Float) {
  total = positions map (p => p.shares * p.price) fold (s = cash) (x => s + x)
  payload = positions map (p => p + {value: p.shares * p.price, weight: p.shares * p.price / total}) 
}

Deciding, one position at a time

$when pf.decide (p: Object, total: Float) if ((p.weight - p.target) / p.target > 0.20) {
  n = ((p.weight - p.target) * total / p.price) as Int
  payload = {ticker: p.ticker, action: "sell", shares: n, amount: n * p.price}
}
$when pf.decide (p: Object, total: Float) if ((p.target - p.weight) / p.target > 0.20) {
  n = ((p.target - p.weight) * total / p.price) as Int
  payload = {ticker: p.ticker, action: "buy", shares: n, amount: n * p.price, price: p.price}
}
$when pf.decide (p: Object, total: Float) if (not ((p.weight - p.target) / p.target > 0.20) and not ((p.target - p.weight) / p.target > 0.20)) {
  payload = {ticker: p.ticker, action: "hold", shares: 0, amount: 0.0}
}

Paying for the buys

$when pf.fund (trade: Object, cash: Float) if (trade.action != "buy") {
  payload = trade
}
$when pf.fund (trade: Object, cash: Float) if (trade.action == "buy" and cash <= 0.0) {
  diesel.throw (code = "E_CASH", msg = "no cash left for ${trade.ticker}")
}
$when pf.fund (trade: Object, cash: Float) if (trade.action == "buy" and cash > 0.0 and trade.amount > cash) {
  n = (cash / trade.price) as Int
  payload = trade + {shares: n, amount: n * trade.price, trimmed: true}
}
$when pf.fund (trade: Object, cash: Float) if (trade.action == "buy" and cash > 0.0 and trade.amount <= cash) {
  payload = trade
}

The rebalance

Sells come first, so their cash can pay for the buys.

$when pf.rebalance (positions: Array, cash: Float, reserve: Float) {
  valued = pf.value(positions = positions, cash = cash)
  total = valued map (p => p.value) fold (s = cash) (x => s + x)
  trades = valued map (p => pf.decide(p = p, total = total))
  sells = trades filter (t => t.action == "sell")
  ctx.cash = sells map (t => t.amount) fold (s = cash - reserve) (x => s + x)
  buys = trades filter (t => t.action == "buy") map (t => pf.pay(trade = t))
  payload = {total: total, sells: sells, buys: buys, holds: trades filter (t => t.action == "hold") map (t => t.ticker), cashLeft: ctx.cash + reserve}
}
$when pf.pay (trade: Object) {
  try {
    t = pf.fund(trade = trade, cash = ctx.cash)
    ctx.cash = ctx.cash - t.amount
    payload = t
  } catch (e) {
    payload = trade + {skipped: e.message}
  }
}

Try it: rebalance, or in the Fiddle.