BBGO is a Go framework for crypto trading strategies. From 24 September to 30 September 2026, main took twelve commits from dboyliao, eleven of them in the xfundingv2 funding strategy. The net diff is seven files, 93 insertions, and 78 deletions, concentrated on how a round matches the spot leg to the futures leg while a TWAP order is still live.
Position sync forgets the trade id ¶
An xfundingv2 round is two TWAP workers. While the round is opening, the futures target should be the negation of the spot fill. While it is closing, the spot target should be the negation of the futures fill. Those writes used to sit behind syncedSpotTrades and syncedFuturesTrades on ArbitrageRoundSyncState.
The removal of those maps also drops the GetOrder check. syncFuturesPosition and syncSpotPosition no longer take a trade. They read FilledPosition and set the other leg. A buy adds quantity and a sell subtracts it, so a short futures fill is negative.
futureTargetPosition := filledSpotPosition.Neg()
r.futuresWorker.SetTargetPosition(futureTargetPosition)
The old caller marked the trade id as synced after the call returned, including when GetOrder missed and the helper wrote nothing. That trade id never retried.
Later the same day, prepareOpening, prepareClosing, and rebalanceOpening call those helpers instead of inlining SetTargetPosition. round_sync.go no longer stores the maps. encoding/json ignores unknown keys, so an old round blob still loads, and the next save drops the fields. A bad FilledPosition becomes the hedge target on the next sync.
Rebalance keeps an overshot spot long ¶
On 24 September the target source moved twice. The first patch set the futures target from available futures collateral, and shrank the spot target when spot collateral exceeded the remaining spot quantity. A later patch the same day replaced both rules with fills. If the spot fill plus the futures target is not zero, the futures target becomes the negation of the spot fill. On close, the spot target becomes the negation of the futures fill.
Overshoot handling differs on the way in and on the way out. If the spot fill is greater than the spot target, rebalanceOpening raises the spot target to the fill, so the spot worker does not sell the extra size, and futures sync shorts the larger fill. Buys are positive, so this catches a long that ran past its target.
On close, rebalanceClosing negates the futures fill and clamps the result at zero.
newTarget := futuresFilledPosition.Neg()
if newTarget.Sign() < 0 {
newTarget = fixedpoint.Zero
}
Without that clamp, a futures fill that crossed through zero would give the spot worker a negative target and open a short during a close. The clamp lives only in rebalanceClosing. prepareClosing calls syncSpotPosition and assigns the raw negation. prepareOpening syncs the futures target to the spot fill and leaves an overshot spot target in place, so a resume can still sell size that rebalanceOpening would have kept.
A failed collateral transfer logs a warning and lets rebalanceOpening continue. TransferInAmount and the Slack notice update only when the transfer from spot to futures succeeds. The old path returned the error and skipped the target rewrite and the margin check. A rejected transfer can now resize a position the futures account cannot margin.
The futures UpdateAccount call now sits immediately before that margin comparison. It was moved there once the target rewrite stopped reading the balance. Required margin is the best bid times the remaining futures quantity, divided by the multiplier on the round sync state.
The TWAP direction check was dead code ¶
Tick is supposed to cancel a resting order whose side fights RemainingQuantity. Remaining is target minus filled, and it goes negative when the worker needs to sell.
SideType.Int returns 1 for a buy and negative 1 for a sell. orderSide returns buy when remaining is positive and sell otherwise. The old test multiplied remaining.Sign() by orderSide(remaining).Int(). Both factors come from the same remaining value, so the product stays positive and the cancel branch could not run.
Once rebalance rewrites a target under a live slice, the old order stays on the book until the slice interval ends.
The fix multiplies the live order side by the remaining sign.
if w.activeOrder != nil && w.activeOrder.Side.Int()*remaining.Sign() < 0 {
If the order still has quantity, Tick cancels it, then syncs and clears the local handle. A cancel error returns and leaves that handle in place.
Quote sweep and the holding interval field ¶
Strategy.rebalance used to move positive quote currency from the futures account back to spot whenever the configured futures direction was short. It now does that only when ActiveRounds is empty. One open round, on any symbol, blocks the sweep for the whole account, so quote can sit on the futures wallet for the life of an unrelated round. The close path in round.go still transfers quote that negative unrealized PnL has locked on the futures account.
Round notifications now take currentTime. For RoundOpening and RoundReady, the Slack field Min Holding Intervals prints NumHoldingIntervals over the configured minimum. The count is 0 when StartAt is zero, or when FundingIntervalStart is still ahead of currentTime. Otherwise it is how many whole funding intervals fit between FundingIntervalStart and currentTime. That anchor is the funding grid captured at construction, so a late open can show 1 at the next boundary. A partial interval does not count, and closing cards omit the field.
Call sites pass the tick time. Startup forces a round into closing with time.Now, and round_fee.go passes currentTime when a pending round starts. Dynamic holding can still change the denominator. This commit adds the numerator. A fork of NewNotification or NewCriticalNotification must pass currentTime or it will not compile.
String output for every state except closing now places the target beside the fill. The closing format still prints fills only. A log line during close hides the target rebalanceClosing just wrote. The Slack card still shows both.
What to watch ¶
After a restart, prepareOpening and prepareClosing skip the overshoot snap and the zero clamp. The long futures close is still an empty TODO.
Any entry in ActiveRounds blocks the quote sweep, including a round on another symbol. A round that failed to close leaves quote on the futures wallet until that map is empty.
Closing log lines still omit targets. Use the Slack card for a bad close, and read the hold count as funding intervals since FundingIntervalStart.
The DeepWiki badge on the README is the only commit outside the strategy. It does not change how the bot runs.