Skip to main content

August 12, 2026

eToro deploy vs rotate: idle cash not rotate

A live eToro book sat in cash while the allocator returned success. I force a deploy, park leftover in indexes, and name the fee skip when a rotate waits.

Sander Korf8 min read

The live eToro allocator rebalances a real book: cash, indexes, overnight what-ifs, skip reasons. The operator watching the account sees idle cash while the job returns 200, or leftover weight looping through the same buy. I had shipped a net-edge gate that treated sitting in cash like a fee-sensitive rotate, so a small book never bought when it needed to.

That gate had a dollar floor, a skip path, and a shouldRebalance flag. On a small live eToro book, a 10% tilt could not clear the floor. The flag stayed false. The skip reason even lied: no_significant_delta. There was a cash delta. The gate 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, so the math skipped a deploy the account needed.

I had mixed two questions into one boolean. Is the book empty enough that not buying is itself the cost? Is this tilt worth the round-trip fees? A rotate can wait on fees. Idle cash is already a position, even when it does not show up as a name weight. I put the fee hurdle on rotates. An empty or nearly empty book skips that hurdle, because sitting out is the expensive move.

Force a deploy, then hurdle a rotate

Idle cash at or above 15% of equity, or zero positions, buys the core book and bypasses the dollar floor. Transaction cost is not the veto on that path.

Rotate still 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.

Without a max-name delta, rotate does not fire. Same if the hurdle fails. Force-deploy ignores both of those checks.

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;
}

In Deploy vs rotate, drag implied 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 you should see a skip, not silent cash.

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.

Measure cash on the full book, not a lane

I split rebalance into an all lane and a crypto lane. The crypto lane only sees BTC and ETH, which is the right filter for eToro orders and the wrong filter for cash.

I fed those lane-filtered weights into impliedCashWeight. An empty crypto slice then looks like 100% cash, so 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.

A small BTC/ETH sleeve on a full book still has to 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 whether the book is idle. shouldForceCryptoSleeveDeploy asks whether the sleeve is below the floor on that same full book. Mix those two and I either spam deploy on a full book, or sit still while a sleeve starves.

Finish leftover into SPY and QQQ

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. Next run, that leftover tripped the 15% deploy gate. The gate bought the core. The core hit the caps. Caps parked cash. Cash retriggered deploy. High-stress runs made it louder: a safe-haven sleeve that was not fully invested fought the cash-deploy gate on every pass, because residual parked as cash looked under-deployed to the next run.

fillIndexLeftover(allocation, 1) is what stops that loop. It subtracts sumAllocationWeights from the invested floor (default 1). Leftover at or below DUST_WEIGHT returns the allocation unchanged. Anything above dust is split onto SPY and QQQ: spyAdd = leftover * SPY_LEFTOVER_SHARE, the rest on QQQ, then pruneDustWeights. After that the book is fully invested, so implied cash is gone and the 15% deploy gate has nothing left to fire on.

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 I make. Residual weight after clamps is unfinished math, so I finish it into an index before the deploy gate looks at the book.

Overnight fees stay off the open ticket

I asked eToro what-if for a rotate. The reply looked like one number. It mixed two clocks.

Markup, market spread, and transaction fee hit when I open. Overnight fee, weekend fee, and stamp duty hit later if I hold. One payload. Two clocks.

My first pass was lazy. I replaced the entire modeled round-trip cost with the what-if total. What-if only prices opens, so close and trim vanished. Overnight sat in the open hurdle, and a rotate that would pay those hold fees either way looked more expensive than sitting still. I shipped that number into the rotate gate. Names that should have flipped stayed put.

Open-ticket is markup plus market spread plus transaction fee. That is the hurdle.

Hold cost is logged, not hurdled. Overnight, weekend, stamp duty. Those accrue if I 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 hurdle

Per-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 a missing quote. A zero hurdle will rotate the book on a missing price.

In Open ticket vs hold cost, toggle which fee buckets feed the rotate hurdle. When hold costs leak into the open ticket, rotates look too expensive and the gate stays shut even when turnover would pay for itself.

What-if buckets

Local toy book. Naive dumps the whole quote into the rotate hurdle. Split keeps overnight out of the gate.

Accounting
What-if health
Open-ticket, hold, and close costs by symbol
SymbolOpen ticketHold (logged)Modeled closeHurdle
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.

Name the skip when fees eat the edge

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 label every day.

The economics object already knew both facts: meetsDeltaThreshold and meetsNetEdgeThreshold. I 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.

In Skip reason fork, slide delta and cost until delta passes but net edge fails. The label should flip from no_significant_delta to net_edge_below_cost_hurdle instead of hiding fee vetoes behind the same string.

Skip reason fork

Cash-deploy already passed. Flip the two economics flags. The skip string is what the dashboard and Slack histogram read. `net_edge_below_cost_hurdle` means no eToro fill.

skipReasonWhenNotRebalancing

net_edge_below_cost_hurdle

Weights 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 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 pin fixtures to the live shape now: a few names, real cash, a 10% tilt that used to vanish under the floor.

A live eToro book that sits in cash after a 200 is usually a gate that treated deploy like a rotate. I split those paths. Leftover after clamps still has to land in an index, or the next run will "discover" idle cash I just invented.


Happy coding! Sander