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.
This commit is contained in:
tinkle-community
2026-06-11 00:45:06 +08:00
parent 41c2625bb2
commit 332ddf61ef
2 changed files with 15 additions and 7 deletions

View File

@@ -143,7 +143,11 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) *
weightedSum += ot.Price * matchQty weightedSum += ot.Price * matchQty
matchedQty += 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() { if entryTime.IsZero() {
entryTime = ot.Time entryTime = ot.Time
@@ -151,6 +155,7 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) *
remainingQty -= matchQty remainingQty -= matchQty
ot.Quantity -= matchQty ot.Quantity -= matchQty
ot.Fee -= feePortion
// Remove fully consumed open trade // Remove fully consumed open trade
if ot.Quantity <= dustQuantityEpsilon { if ot.Quantity <= dustQuantityEpsilon {

View File

@@ -154,12 +154,15 @@ func TestRebuildPositionsFromTrades_PartialClose(t *testing.T) {
if !floatsClose(records[0].Fee, 0.3) { if !floatsClose(records[0].Fee, 0.3) {
t.Errorf("records[0].Fee = %v, want 0.3", records[0].Fee) t.Errorf("records[0].Fee = %v, want 0.3", records[0].Fee)
} }
// NOTE: documents current behavior. The open trade's Fee field is not // Second partial close consumes the remaining half of the open trade:
// reduced when partially consumed, so the second close re-attributes the // exit fee 0.1 + remaining entry fee 0.2 = 0.3. Total entry fee attributed
// full remaining ratio of the original fee: 0.1 + 0.4*(1/1) = 0.5. // across both closes must equal the 0.4 actually paid.
// Total attributed entry fee across both closes is 0.6 > 0.4 actually paid. if !floatsClose(records[1].Fee, 0.3) {
if !floatsClose(records[1].Fee, 0.5) { t.Errorf("records[1].Fee = %v, want 0.3", records[1].Fee)
t.Errorf("records[1].Fee = %v, want 0.5 (current over-attribution behavior)", 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)
} }
} }