fix: 重启后加仓数据恢复 + scale_count/amount_usd 实时持久化
- restoreOpenPositions 从 orders 表 count 加仓次数,正确设置 ScaleLevels 和 AmountUSD - 加仓时立即持久化 amount_usd 和 scale_count 到 trades 表(不等到平仓) - PnL 图表支持单笔交易显示(原点+累计PnL连线) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
f97ac16b1c
commit
2f4d7869a9
@@ -118,6 +118,12 @@ func (d *DB) UpdateTradeEntry(id int64, t *TradeRecord) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTradeScale updates scale-in fields on an existing trade (amount_usd, scale_count).
|
||||||
|
func (d *DB) UpdateTradeScale(id int64, amountUSD float64, scaleCount int) error {
|
||||||
|
_, err := d.Exec("UPDATE trades SET amount_usd=?, scale_count=? WHERE id=?", amountUSD, scaleCount, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// GetOpenTrades returns all non-closed trades (status='open' or status='entering').
|
// GetOpenTrades returns all non-closed trades (status='open' or status='entering').
|
||||||
func (d *DB) GetOpenTrades() ([]TradeRecord, error) {
|
func (d *DB) GetOpenTrades() ([]TradeRecord, error) {
|
||||||
rows, err := d.Query(`SELECT id, coin, direction, status, entry_spread, exit_spread,
|
rows, err := d.Query(`SELECT id, coin, direction, status, entry_spread, exit_spread,
|
||||||
|
|||||||
@@ -651,6 +651,11 @@ function PnlChart() {
|
|||||||
// Compute cumulative PnL
|
// Compute cumulative PnL
|
||||||
const points = []
|
const points = []
|
||||||
let cum = 0
|
let cum = 0
|
||||||
|
// Prepend a zero point so single-trade chart still draws
|
||||||
|
if (data.length > 0) {
|
||||||
|
const t0 = new Date(data[0].ClosedAt).getTime() - 1000
|
||||||
|
points.push({ x: t0, y: 0 })
|
||||||
|
}
|
||||||
for (const t of data) {
|
for (const t of data) {
|
||||||
cum += 2 * (t.AmountUSD || 0) * (t.NetPnl || 0) / 100
|
cum += 2 * (t.AmountUSD || 0) * (t.NetPnl || 0) / 100
|
||||||
points.push({ x: new Date(t.ClosedAt).getTime(), y: cum })
|
points.push({ x: new Date(t.ClosedAt).getTime(), y: cum })
|
||||||
@@ -756,7 +761,7 @@ function PnlChart() {
|
|||||||
<section className="card card-wide" id="pnl-chart-card">
|
<section className="card card-wide" id="pnl-chart-card">
|
||||||
<h2>📈 总PnL成长曲线 <span className="text-dim" style={{fontSize:11}}>{data.length > 0 ? `$${totalPnl.toFixed(2)}` : ''}</span></h2>
|
<h2>📈 总PnL成长曲线 <span className="text-dim" style={{fontSize:11}}>{data.length > 0 ? `$${totalPnl.toFixed(2)}` : ''}</span></h2>
|
||||||
<div className="chart-container" style={{height:260}}>
|
<div className="chart-container" style={{height:260}}>
|
||||||
{data.length < 2 ? (
|
{data.length < 1 ? (
|
||||||
<div className="loading" style={{paddingTop:100}}>暂无数据...</div>
|
<div className="loading" style={{paddingTop:100}}>暂无数据...</div>
|
||||||
) : (
|
) : (
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -939,6 +940,8 @@ func (t *Trader) checkScaleIn(pos *ArbPosition, bgP, hlP, diffPct float64, store
|
|||||||
LongOrderID: &longOID, ShortOrderID: &shortOID,
|
LongOrderID: &longOID, ShortOrderID: &shortOID,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
})
|
})
|
||||||
|
// Persist updated amount_usd and scale_count immediately
|
||||||
|
t.db.UpdateTradeScale(pos.DBTradeID, pos.AmountUSD, pos.ScaleLevels)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[Trader] %s: Scale-in #%d executed | spread=%.4f%% (entry=%.4f%%) | total=$%.0f",
|
log.Printf("[Trader] %s: Scale-in #%d executed | spread=%.4f%% (entry=%.4f%%) | total=$%.0f",
|
||||||
@@ -1274,11 +1277,12 @@ func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) (s
|
|||||||
leg.OrderID = resp
|
leg.OrderID = resp
|
||||||
log.Printf("[ExRes] HL %s %s: size=%s", side, leg.Coin, szStr)
|
log.Printf("[ExRes] HL %s %s: size=%s", side, leg.Coin, szStr)
|
||||||
|
|
||||||
// Parse actual fill price and estimate fee from HL response
|
// Parse actual fill price, OID, and estimate fee from HL response
|
||||||
fillPrice, _, parseErr := t.hyperliquid.ParseFillFromResponse(resp)
|
fillPrice, _, oid, parseErr := t.hyperliquid.ParseFillFromResponse(resp)
|
||||||
if parseErr == nil && fillPrice > 0 {
|
if parseErr == nil && fillPrice > 0 {
|
||||||
leg.EntryPrice = fillPrice
|
leg.EntryPrice = fillPrice
|
||||||
log.Printf("[Fill] HL %s %s: actual fillPrice=%.6f", side, leg.Coin, fillPrice)
|
leg.OrderID = strconv.FormatInt(oid, 10)
|
||||||
|
log.Printf("[Fill] HL %s %s: actual fillPrice=%.6f oid=%d", side, leg.Coin, fillPrice, oid)
|
||||||
}
|
}
|
||||||
fee, fetchErr := t.hyperliquid.EstimateFeeFromResponse(resp, takerFees[ExHyperLiquid])
|
fee, fetchErr := t.hyperliquid.EstimateFeeFromResponse(resp, takerFees[ExHyperLiquid])
|
||||||
if fetchErr != nil {
|
if fetchErr != nil {
|
||||||
@@ -1531,11 +1535,12 @@ func (t *Trader) closeLeg(leg *PositionLeg) string {
|
|||||||
log.Printf("[ExRes] HL close %s %s: size=%s → response=%s", side, leg.Coin, leg.Size, resp)
|
log.Printf("[ExRes] HL close %s %s: size=%s → response=%s", side, leg.Coin, leg.Size, resp)
|
||||||
leg.OrderID = resp
|
leg.OrderID = resp
|
||||||
|
|
||||||
// Parse actual fill price from HL close response
|
// Parse actual fill price and OID from HL close response
|
||||||
fillPrice, _, parseErr := t.hyperliquid.ParseFillFromResponse(resp)
|
fillPrice, _, oid, parseErr := t.hyperliquid.ParseFillFromResponse(resp)
|
||||||
if parseErr == nil && fillPrice > 0 {
|
if parseErr == nil && fillPrice > 0 {
|
||||||
leg.ExitPrice = fillPrice
|
leg.ExitPrice = fillPrice
|
||||||
log.Printf("[Fill] HL close %s: actual exitPrice=%.6f", leg.Coin, fillPrice)
|
leg.OrderID = strconv.FormatInt(oid, 10)
|
||||||
|
log.Printf("[Fill] HL close %s: actual exitPrice=%.6f oid=%d", leg.Coin, fillPrice, oid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
leg.Closed = true
|
leg.Closed = true
|
||||||
@@ -1754,11 +1759,12 @@ func (t *Trader) placeOrderAt(leg *PositionLeg, side string, store *PriceStore,
|
|||||||
}
|
}
|
||||||
leg.OrderID = oid
|
leg.OrderID = oid
|
||||||
|
|
||||||
// Parse actual fill price from HL response
|
// Parse actual fill price and OID from HL response
|
||||||
fillPrice, _, parseErr := t.hyperliquid.ParseFillFromResponse(oid)
|
fillPrice, _, oidNum, parseErr := t.hyperliquid.ParseFillFromResponse(oid)
|
||||||
if parseErr == nil && fillPrice > 0 {
|
if parseErr == nil && fillPrice > 0 {
|
||||||
leg.EntryPrice = fillPrice
|
leg.EntryPrice = fillPrice
|
||||||
log.Printf("[Fill] HL scale %s %s: actual fillPrice=%.6f", side, leg.Coin, fillPrice)
|
leg.OrderID = strconv.FormatInt(oidNum, 10)
|
||||||
|
log.Printf("[Fill] HL scale %s %s: actual fillPrice=%.6f oid=%d", side, leg.Coin, fillPrice, oidNum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estimate fee from HL response
|
// Estimate fee from HL response
|
||||||
@@ -1957,6 +1963,11 @@ func (t *Trader) restoreOpenPositions() {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
pos.LongEntryPrices = append(pos.LongEntryPrices, scaleLong...)
|
pos.LongEntryPrices = append(pos.LongEntryPrices, scaleLong...)
|
||||||
pos.ShortEntryPrices = append(pos.ShortEntryPrices, scaleShort...)
|
pos.ShortEntryPrices = append(pos.ShortEntryPrices, scaleShort...)
|
||||||
|
// Restore ScaleLevels from actual scale order count
|
||||||
|
if len(scaleLong) > 0 {
|
||||||
|
pos.ScaleLevels = len(scaleLong)
|
||||||
|
pos.AmountUSD = tr.AmountUSD * (1 + float64(pos.ScaleLevels))
|
||||||
|
}
|
||||||
// Refresh leg EntryPrice to reflect all scale levels
|
// Refresh leg EntryPrice to reflect all scale levels
|
||||||
if len(pos.LongEntryPrices) > 1 {
|
if len(pos.LongEntryPrices) > 1 {
|
||||||
pos.LongLeg.EntryPrice = weightedAvgPrice(pos.LongEntryPrices, t.cfg.TradeAmountUSD)
|
pos.LongLeg.EntryPrice = weightedAvgPrice(pos.LongEntryPrices, t.cfg.TradeAmountUSD)
|
||||||
|
|||||||
Reference in New Issue
Block a user