An allocator that talks to eToro. Cash, indexes, overnight what-ifs, skip reasons. No storefront. No cart. If the gate is wrong, idle cash sits, leftover weight loops, or a fee hurdle pretends there was no delta.
A book that stays in cash while the job returns 200. Or a bot that buys the core, hits a cap, parks leftover as cash, then buys again. Same account. Same night. That is the failure, not a clever risk policy.
I shipped an eToro allocator whose net-edge gate treated sitting in cash like a fee-sensitive rotate. Same dollar floor. Same skip path. Same shouldRebalance flag.
On a small live eToro book, a 10% tilt could not clear that floor. The flag stayed false. The skip reason even lied: no_significant_delta. There was a delta. Cash. The gate just refused to name it.
The economics helper compared notional turnover to a dollar floor sized for a bigger book. A 10% tilt on a small book is a few units of cash. The floor was a hard dollar. Math said skip. Reality said deploy.
I had mixed two questions into one boolean.
Those are not the same decision. A rotate can wait. Idle cash cannot. Fee hurdles belong on rotates. They do not belong on an empty, or nearly empty, book. Not buying is a position. It just does not show up as a name weight.
Idle cash at or above 15% of equity, or zero positions, must buy the core book. That path bypasses the dollar floor. Sitting out is the expensive move. Transaction cost is not the veto.
Rotate still earns its keep. It needs a max-name delta and a hurdle of max(equity × 0.03%, cost × 1.1, regime floor). A cheaper sleeve gets a lower floor so an equity-sized hurdle cannot block a cheap trade.
I keep the rotate path honest: no max-name delta, no trade. Hurdle fails, no trade. Force-deploy ignores both. That is the point of the split.
export const CASH_DEPLOY_THRESHOLD = 0.15;
export function shouldForceCashDeploy(allocation: Allocation): boolean {
if (countPositivePositions(allocation) === 0) return true;
return impliedCashWeight(allocation) >= CASH_DEPLOY_THRESHOLD;
}
export function shouldExecuteAfterLiveEconomics(input: GateInput): boolean {
if (shouldForceCashDeploy(input.fullBookLiveAllocation)) return true;
return input.economics.meetsNetEdgeThreshold;
}Cash gate
Idle cash at 15% or an empty book must deploy. Rotate still needs the fee hurdle.
Force-deploy line at 15%.
Decision
Skip
Book is invested enough, and the tilt does not clear the fee hurdle.
Drag idle cash. At 15% or on an empty book, the gate forces a deploy. Below that, rotate only if the tilt clears the fee hurdle. Otherwise skip.
I split rebalance into an all lane and a crypto lane. The crypto lane only sees BTC and ETH. That filter is correct for eToro orders. It is wrong for cash.
I fed those lane-filtered weights into impliedCashWeight. An empty crypto slice then looks like 100% cash. The crypto lane force-deploys every run, even when the equity book is fully invested. The bot keeps buying because the slice is empty, not because I am sitting in cash.
The inverse bug is just as real. A small BTC/ETH sleeve on a full book must still force a crypto buy when that sleeve sits below the floor. Crisis is the exception. Everything else should top up the sleeve.
Cash-deploy and sleeve-deploy always see fullBookLiveAllocation. Lane filters apply to what I may trade, not to whether the eToro book is empty.
// full book, never lane-filtered weights
if (shouldForceCashDeploy(input.fullBookLiveAllocation)) return true;
if (input.lane === 'crypto' && shouldForceCryptoSleeveDeploy(input.fullBookLiveAllocation, input.regime)) {
return true;
}shouldForceCashDeploy asks: is the book idle? shouldForceCryptoSleeveDeploy asks: is the sleeve below the floor on that same full book? Mix those two and I will either spam deploy on a full book, or sit still while a sleeve starves.
I used to treat leftover eToro weight as cash. After name caps at 25%, high-risk caps, and overlay clamps, whatever did not survive those rules sat as implied cash. The book looked conservative. It was unfinished math.
That leftover then tripped the 15% deploy gate on the next run. The gate bought the core. The core hit the caps. Caps parked cash. Cash retriggered deploy. Again. And again. A control loop that fights itself is not a risk control. It is a bug wearing a policy badge.
High-stress runs made it louder. A safe-haven sleeve that was not fully invested fought the cash-deploy gate on every pass. If you park residual as cash, you teach the next run that it is under-deployed.
The fix is boring. After caps and clamps, leftover is an index. fillIndexLeftover(allocation, 1) measures shortfall against a fully invested floor, then dumps it into SPY and QQQ. Dust gets pruned.
export function fillIndexLeftover(allocation: Allocation, investedFloor = 1): Allocation {
const leftover = investedFloor - sumAllocationWeights(allocation);
if (leftover <= DUST_WEIGHT) return allocation;
const spyAdd = leftover * SPY_LEFTOVER_SHARE;
next.SPY = (next.SPY ?? 0) + spyAdd;
next.QQQ = (next.QQQ ?? 0) + (leftover - spyAdd);
return pruneDustWeights(next);
}Tests assert the core is fully invested. They do not require SPY or QQQ to be present. Top-ranked names can already fill the eToro book. Cash is a decision. Residual weight after clamps is unfinished math. Finish it into an index, then the deploy gate has nothing to chew on.
I asked eToro what-if for a rotate. The reply looked like one number. It was not.
Markup, market spread, transaction fee: cash you pay now to open. Overnight fee, weekend fee, stamp duty: cash you pay later if you hold. One payload. Two clocks.
First pass was lazy. I replaced the entire modeled round-trip cost with the what-if total. What-if only prices opens. Close and trim vanished. Overnight sat in the open hurdle, so a rotate that would pay those fees either way looked more expensive than sitting still. I shipped that number into the rotate gate. Names that should have flipped stayed put. The book looked disciplined. It was just badly labeled.
Open-ticket is markup plus market spread plus transaction fee. That is the hurdle.
Hold cost is logged, not hurdled. Overnight, weekend, stamp duty. You pay them if you keep the name. They are not a reason to skip the rotate.
Modeled close and trim always get added back. The what-if never priced them.
const OPEN_TICKET_COST_TYPES = new Set(['markup', 'marketSpread', 'transactionFee']);
estimatedTransactionCost = etoroOpenTicketCost + modeledCloseTransactionCost;
// overnight/weekend/stamp duty → holdCostNotional, not the hurdlePer-symbol eToro what-if failures fall back to the model rate for that name only. If every what-if fails, I keep the full model total. A zero is not a quote, and a zero hurdle will happily rotate you into nothing.
What-if buckets
Local toy book. Naive dumps the whole quote into the rotate hurdle. Split keeps overnight out of the gate.
| Symbol | Open ticket | Hold (logged) | Modeled close | Hurdle |
|---|---|---|---|---|
| ACME | $12.10 | $0.00 | $0.00 | $12.10 |
| BETA | $9.60 | $0.00 | $0.00 | $9.60 |
Rotate hurdle: $21.70
Hold cost logged, not hurdled: $0.00
Naive: overnight sits in the ticket and close is gone.
Ops could not tell "weights barely moved" from "we wanted to trade but fees ate the edge." Both landed in no_significant_delta. The dashboard skip streak climbed. Zero eToro fills. Same sleepy string.
The economics object already knew both facts: meetsDeltaThreshold and meetsNetEdgeThreshold. We just threw the second one away. The fork only runs after cash-deploy has already been allowed through.
export function skipReasonWhenNotRebalancing(economics: Economics): RebalanceSkipReason {
if (economics?.meetsDeltaThreshold && !economics.meetsNetEdgeThreshold) {
return 'net_edge_below_cost_hurdle';
}
return 'no_significant_delta';
}no_significant_delta stays for the tiny tilt. net_edge_below_cost_hurdle is the fee-gate: the bot wanted size, eToro would have taken the order, fees ate the edge, so no fill.
Skip reason fork
skipReasonWhenNotRebalancing
net_edge_below_cost_hurdleWeights moved enough. Fees ate the edge. net_edge_below_cost_hurdle. No eToro fill. Not a quiet book.
The daily summary now prints implied cash percent, a skip histogram, modeled-versus-eToro cost drift, and a rotating-light if there are zero verified eToro fills for two weekdays while the kill switch is off. Quiet book with the switch off is not a vibe. It is an alarm.
The golden tests lived on oversized fantasies. At that scale a 10% tilt always cleared the floor. On the live eToro book it never did. I now pin fixtures to the live shape: a few names, real cash, a 10% tilt that used to vanish under the floor.
If your fee gate cannot tell "must deploy" from "nice to rotate", it will keep a live eToro book in cash forever. Measure cash on the full book. Finish leftover into an index. Keep overnight out of the ticket. Name the skip.
Happy coding!
Sander