diff --git a/README.md b/README.md index 31a7a7f..32853bd 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,13 @@ SQLite (`data/trades.db`),三张核心表: ## 版本历史 -### v1.2 (当前) +### v1.2.1 (当前) +- ✨ scale-in 后 leg.EntryPrice 更新为加权平均(`weightedAvgPrice`) +- ✨ 进程重启恢复仓位时,从 `orders` 表加载 scale prices,重建完整价格切片 +- ✨ 新增 `GetScalePrices()` DB 方法 +- 见 v1.2: + +### v1.2 - ✨ `system_orders` 表,记录系统级开仓/加仓/平仓 - ✨ 手续费改为逐笔累加 USD,不再用百分比估算 - ✨ Vite + React 前端,支持热加载 diff --git a/db/trade_repo.go b/db/trade_repo.go index 0535370..7448a7d 100644 --- a/db/trade_repo.go +++ b/db/trade_repo.go @@ -235,6 +235,31 @@ func scanTrades(rows *sql.Rows) ([]TradeRecord, error) { return trades, rows.Err() } +// GetScalePrices returns scale-in order prices for a trade, grouped by leg. +func (d *DB) GetScalePrices(tradeID int64) (longPrices, shortPrices []float64, err error) { + rows, err := d.Query(`SELECT leg, price FROM orders + WHERE trade_id=? AND type='scale' AND price IS NOT NULL + ORDER BY id`, tradeID) + if err != nil { + return nil, nil, err + } + defer rows.Close() + for rows.Next() { + var leg string + var price float64 + if err := rows.Scan(&leg, &price); err != nil { + return nil, nil, err + } + switch leg { + case "long": + longPrices = append(longPrices, price) + case "short": + shortPrices = append(shortPrices, price) + } + } + return longPrices, shortPrices, rows.Err() +} + // GetClosedStats returns convergence counts from the database. func (d *DB) GetClosedStats() (converged, diverged, flat, total int, err error) { if err = d.QueryRow("SELECT COUNT(*) FROM trades WHERE status='closed'").Scan(&total); err != nil { diff --git a/trader.go b/trader.go index 1777493..a3f1dad 100644 --- a/trader.go +++ b/trader.go @@ -547,6 +547,10 @@ func (t *Trader) checkScaleIn(pos *ArbPosition, bgP, hlP, diffPct float64, store pos.LongEntryPrices = append(pos.LongEntryPrices, longPrice) pos.ShortEntryPrices = append(pos.ShortEntryPrices, shortPrice) + // Update leg EntryPrice to reflect weighted average across all scale levels + pos.LongLeg.EntryPrice = weightedAvgPrice(pos.LongEntryPrices, t.cfg.TradeAmountUSD) + pos.ShortLeg.EntryPrice = weightedAvgPrice(pos.ShortEntryPrices, t.cfg.TradeAmountUSD) + // Persist scale orders to DB immediately if t.db != nil && pos.DBTradeID > 0 { now := time.Now() @@ -1172,6 +1176,20 @@ func (t *Trader) restoreOpenPositions() { } pos.ShortEntryPrices = []float64{*tr.ShortEntry} } + + // Restore scale-in prices from orders table for correct weighted average + scaleLong, scaleShort, err := t.db.GetScalePrices(tr.ID) + if err == nil { + pos.LongEntryPrices = append(pos.LongEntryPrices, scaleLong...) + pos.ShortEntryPrices = append(pos.ShortEntryPrices, scaleShort...) + // Refresh leg EntryPrice to reflect all scale levels + if len(pos.LongEntryPrices) > 1 { + pos.LongLeg.EntryPrice = weightedAvgPrice(pos.LongEntryPrices, t.cfg.TradeAmountUSD) + } + if len(pos.ShortEntryPrices) > 1 { + pos.ShortLeg.EntryPrice = weightedAvgPrice(pos.ShortEntryPrices, t.cfg.TradeAmountUSD) + } + } t.positions[tr.Coin] = pos // Prevent immediate re-trading of the same coin t.lastTradeTime[tr.Coin] = tr.OpenedAt