From 332ddf61ef30029679ca6b704ff81af12f0853cb Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 11 Jun 2026 00:45:06 +0800 Subject: [PATCH] fix(trader): stop over-attributing entry fees on partial position closes The FIFO matcher reduced an open trade's remaining quantity but not its remaining fee, so each subsequent partial close re-attributed entry fee that earlier closes had already counted (e.g. open 2.0 with fee 0.4, two 1.0 closes attributed 0.6 total). Deduct the consumed fee portion alongside the quantity so attributed fees sum to the fee actually paid. --- trader/position_rebuild.go | 7 ++++++- trader/position_rebuild_test.go | 15 +++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/trader/position_rebuild.go b/trader/position_rebuild.go index ac7b5518..1522b0a0 100644 --- a/trader/position_rebuild.go +++ b/trader/position_rebuild.go @@ -143,7 +143,11 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) * weightedSum += ot.Price * matchQty matchedQty += matchQty - totalEntryFee += ot.Fee * (matchQty / ot.Quantity) + // Attribute the entry fee proportionally and deduct the consumed + // portion from the open trade, so a later partial close cannot + // re-attribute fee that was already counted. + feePortion := ot.Fee * (matchQty / ot.Quantity) + totalEntryFee += feePortion if entryTime.IsZero() { entryTime = ot.Time @@ -151,6 +155,7 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) * remainingQty -= matchQty ot.Quantity -= matchQty + ot.Fee -= feePortion // Remove fully consumed open trade if ot.Quantity <= dustQuantityEpsilon { diff --git a/trader/position_rebuild_test.go b/trader/position_rebuild_test.go index 2df811a6..f5c9c8b7 100644 --- a/trader/position_rebuild_test.go +++ b/trader/position_rebuild_test.go @@ -154,12 +154,15 @@ func TestRebuildPositionsFromTrades_PartialClose(t *testing.T) { if !floatsClose(records[0].Fee, 0.3) { t.Errorf("records[0].Fee = %v, want 0.3", records[0].Fee) } - // NOTE: documents current behavior. The open trade's Fee field is not - // reduced when partially consumed, so the second close re-attributes the - // full remaining ratio of the original fee: 0.1 + 0.4*(1/1) = 0.5. - // Total attributed entry fee across both closes is 0.6 > 0.4 actually paid. - if !floatsClose(records[1].Fee, 0.5) { - t.Errorf("records[1].Fee = %v, want 0.5 (current over-attribution behavior)", records[1].Fee) + // Second partial close consumes the remaining half of the open trade: + // exit fee 0.1 + remaining entry fee 0.2 = 0.3. Total entry fee attributed + // across both closes must equal the 0.4 actually paid. + if !floatsClose(records[1].Fee, 0.3) { + t.Errorf("records[1].Fee = %v, want 0.3", records[1].Fee) + } + totalEntryFee := records[0].Fee + records[1].Fee - 0.2 // subtract the two exit fees + if !floatsClose(totalEntryFee, 0.4) { + t.Errorf("total attributed entry fee = %v, want 0.4 (fee actually paid)", totalEntryFee) } }