feat: 手续费改为逐笔USD累算 + Vite React前端 + system_orders表 + README
This commit is contained in:
@@ -520,7 +520,7 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
|
||||
longPnl := (longCurrent - longAvg) / longAvg * 100
|
||||
shortPnl := (shortAvg - shortCurrent) / shortAvg * 100
|
||||
totalFees := 2 * (takerFees[ExBitget] + takerFees[ExHyperLiquid]) // 开仓 + 平仓手续费
|
||||
totalFees := float64(2+pos.ScaleLevels) * (takerFees[ExBitget] + takerFees[ExHyperLiquid]) // 开仓(含加仓) + 平仓手续费
|
||||
netPnl := longPnl + shortPnl - totalFees
|
||||
|
||||
elapsed := time.Since(pos.StartedAt)
|
||||
@@ -905,15 +905,35 @@ func (t *Trader) GetClosedTrades() []TradeRecord {
|
||||
return r
|
||||
}
|
||||
|
||||
// persistTrade saves a completed trade to SQLite.
|
||||
// persistTrade saves a completed trade to SQLite, with per-leg orders and system_orders.
|
||||
func (t *Trader) persistTrade(pos *ArbPosition, exitSpread float64, convergence, exitReason string, netPnl, longPnl, shortPnl, totalFees float64) {
|
||||
var entrySpread, fe float64
|
||||
var entrySpread float64
|
||||
if pos.LongLeg != nil {
|
||||
entrySpread = pos.EntrySpread
|
||||
}
|
||||
fe = totalFees / 2 // split into entry/exit halves
|
||||
|
||||
now := time.Now()
|
||||
tradeUnit := t.cfg.TradeAmountUSD
|
||||
|
||||
// Pre-calculate all fees BEFORE saving the trade
|
||||
totalFeeEntryUSD := 0.0
|
||||
for range pos.LongEntryPrices {
|
||||
totalFeeEntryUSD += tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
|
||||
}
|
||||
for range pos.ShortEntryPrices {
|
||||
totalFeeEntryUSD += tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
|
||||
}
|
||||
totalLongShares := 0.0
|
||||
for _, p := range pos.LongEntryPrices {
|
||||
totalLongShares += tradeUnit / p
|
||||
}
|
||||
totalShortShares := 0.0
|
||||
for _, p := range pos.ShortEntryPrices {
|
||||
totalShortShares += tradeUnit / p
|
||||
}
|
||||
totalFeeExitUSD := totalLongShares*pos.LongLeg.ExitPrice*takerFees[pos.LongLeg.Exchange]/100 +
|
||||
totalShortShares*pos.ShortLeg.ExitPrice*takerFees[pos.ShortLeg.Exchange]/100
|
||||
|
||||
dbTrade := &db.TradeRecord{
|
||||
Coin: pos.Coin,
|
||||
Direction: pos.Direction,
|
||||
@@ -928,8 +948,8 @@ func (t *Trader) persistTrade(pos *ArbPosition, exitSpread float64, convergence,
|
||||
ShortExit: &pos.ShortLeg.ExitPrice,
|
||||
LongPnl: &longPnl,
|
||||
ShortPnl: &shortPnl,
|
||||
FeeEntry: &fe,
|
||||
FeeExit: &fe,
|
||||
FeeEntry: &totalFeeEntryUSD,
|
||||
FeeExit: &totalFeeExitUSD,
|
||||
NetPnl: &netPnl,
|
||||
AmountUSD: pos.AmountUSD,
|
||||
ScaleCount: pos.ScaleLevels,
|
||||
@@ -938,8 +958,145 @@ func (t *Trader) persistTrade(pos *ArbPosition, exitSpread float64, convergence,
|
||||
OpenedAt: pos.StartedAt,
|
||||
ClosedAt: &now,
|
||||
}
|
||||
if _, err := t.db.SaveTrade(dbTrade); err != nil {
|
||||
tradeID, err := t.db.SaveTrade(dbTrade)
|
||||
if err != nil {
|
||||
log.Printf("[Trader] Failed to save trade to DB: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save per-leg order records
|
||||
// Long leg: entry (buy), scales (buy), exit (sell)
|
||||
status := "filled"
|
||||
var longEntryOrderIDs []int64
|
||||
for i, p := range pos.LongEntryPrices {
|
||||
shares := tradeUnit / p
|
||||
orderType := "entry"
|
||||
if i > 0 {
|
||||
orderType = "scale"
|
||||
}
|
||||
fee := tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
|
||||
oid, oErr := t.db.SaveOrder(&db.OrderRecord{
|
||||
TradeID: tradeID,
|
||||
Leg: "long",
|
||||
Type: orderType,
|
||||
Exchange: pos.LongLeg.Exchange,
|
||||
Side: "buy",
|
||||
Price: &p,
|
||||
Size: &shares,
|
||||
Fee: &fee,
|
||||
Status: &status,
|
||||
CreatedAt: pos.StartedAt,
|
||||
})
|
||||
if oErr != nil {
|
||||
log.Printf("[Trader] Failed to save long entry order: %v", oErr)
|
||||
} else {
|
||||
longEntryOrderIDs = append(longEntryOrderIDs, oid)
|
||||
}
|
||||
}
|
||||
// Long exit (sell)
|
||||
longExitShares := totalLongShares
|
||||
longExitFee := totalLongShares * pos.LongLeg.ExitPrice * takerFees[pos.LongLeg.Exchange] / 100
|
||||
status = "filled"
|
||||
var longExitOrderID int64
|
||||
if oid, oErr := t.db.SaveOrder(&db.OrderRecord{
|
||||
TradeID: tradeID,
|
||||
Leg: "long",
|
||||
Type: "exit",
|
||||
Exchange: pos.LongLeg.Exchange,
|
||||
Side: "sell",
|
||||
Price: &pos.LongLeg.ExitPrice,
|
||||
Size: &longExitShares,
|
||||
Fee: &longExitFee,
|
||||
Status: &status,
|
||||
CreatedAt: now,
|
||||
}); oErr != nil {
|
||||
log.Printf("[Trader] Failed to save long exit order: %v", oErr)
|
||||
} else {
|
||||
longExitOrderID = oid
|
||||
}
|
||||
|
||||
// Short leg: entry (sell), scales (sell), exit (buy)
|
||||
var shortEntryOrderIDs []int64
|
||||
for i, p := range pos.ShortEntryPrices {
|
||||
shares := tradeUnit / p
|
||||
orderType := "entry"
|
||||
if i > 0 {
|
||||
orderType = "scale"
|
||||
}
|
||||
fee := tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
|
||||
oid, oErr := t.db.SaveOrder(&db.OrderRecord{
|
||||
TradeID: tradeID,
|
||||
Leg: "short",
|
||||
Type: orderType,
|
||||
Exchange: pos.ShortLeg.Exchange,
|
||||
Side: "sell",
|
||||
Price: &p,
|
||||
Size: &shares,
|
||||
Fee: &fee,
|
||||
Status: &status,
|
||||
CreatedAt: pos.StartedAt,
|
||||
})
|
||||
if oErr != nil {
|
||||
log.Printf("[Trader] Failed to save short entry order: %v", oErr)
|
||||
} else {
|
||||
shortEntryOrderIDs = append(shortEntryOrderIDs, oid)
|
||||
}
|
||||
}
|
||||
// Short exit (buy)
|
||||
shortExitShares := totalShortShares
|
||||
shortExitFee := totalShortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
|
||||
var shortExitOrderID int64
|
||||
if oid, oErr := t.db.SaveOrder(&db.OrderRecord{
|
||||
TradeID: tradeID,
|
||||
Leg: "short",
|
||||
Type: "exit",
|
||||
Exchange: pos.ShortLeg.Exchange,
|
||||
Side: "buy",
|
||||
Price: &pos.ShortLeg.ExitPrice,
|
||||
Size: &shortExitShares,
|
||||
Fee: &shortExitFee,
|
||||
Status: &status,
|
||||
CreatedAt: now,
|
||||
}); oErr != nil {
|
||||
log.Printf("[Trader] Failed to save short exit order: %v", oErr)
|
||||
} else {
|
||||
shortExitOrderID = oid
|
||||
}
|
||||
|
||||
// Save system orders linking long+short legs
|
||||
es := pos.EntrySpread
|
||||
for i := 0; i < len(longEntryOrderIDs) && i < len(shortEntryOrderIDs); i++ {
|
||||
sysType := "entry"
|
||||
if i > 0 {
|
||||
sysType = "scale"
|
||||
}
|
||||
if _, sErr := t.db.SaveSystemOrder(&db.SystemOrderRecord{
|
||||
TradeID: tradeID,
|
||||
Type: sysType,
|
||||
Status: "filled",
|
||||
Spread: &es,
|
||||
LongPrice: &pos.LongEntryPrices[i],
|
||||
ShortPrice: &pos.ShortEntryPrices[i],
|
||||
LongOrderID: &longEntryOrderIDs[i],
|
||||
ShortOrderID: &shortEntryOrderIDs[i],
|
||||
CreatedAt: pos.StartedAt,
|
||||
}); sErr != nil {
|
||||
log.Printf("[Trader] Failed to save entry system order: %v", sErr)
|
||||
}
|
||||
}
|
||||
// Exit system order
|
||||
if _, sErr := t.db.SaveSystemOrder(&db.SystemOrderRecord{
|
||||
TradeID: tradeID,
|
||||
Type: "exit",
|
||||
Status: "filled",
|
||||
Spread: &exitSpread,
|
||||
LongPrice: &pos.LongLeg.ExitPrice,
|
||||
ShortPrice: &pos.ShortLeg.ExitPrice,
|
||||
LongOrderID: &longExitOrderID,
|
||||
ShortOrderID: &shortExitOrderID,
|
||||
CreatedAt: now,
|
||||
}); sErr != nil {
|
||||
log.Printf("[Trader] Failed to save exit system order: %v", sErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1004,7 +1161,7 @@ func (t *Trader) blacklistCoin(pos *ArbPosition, bgP, hlP, diffPct float64, noti
|
||||
shortAvg := weightedAvgPrice(pos.ShortEntryPrices, t.cfg.TradeAmountUSD)
|
||||
longPnl := (longCurrent - longAvg) / longAvg * 100
|
||||
shortPnl := (shortAvg - shortCurrent) / shortAvg * 100
|
||||
totalFees := 2 * (takerFees[ExBitget] + takerFees[ExHyperLiquid])
|
||||
totalFees := float64(2+pos.ScaleLevels) * (takerFees[ExBitget] + takerFees[ExHyperLiquid])
|
||||
netPnl := longPnl + shortPnl - totalFees
|
||||
|
||||
pos.ExitDiffPct = diffPct
|
||||
@@ -1074,4 +1231,4 @@ func (t *Trader) RemoveBlacklist(coin string) {
|
||||
defer t.mu.Unlock()
|
||||
delete(t.blacklist, coin)
|
||||
log.Printf("[Trader] ✅ %s: Removed from blacklist", coin)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user