feat: 实际成交价追踪 + reduceOnly保护 + 数据库增强
- Bitget GetTradeFee 返回实际成交均价(weighted avg fill price)及手续费, 支持最多5次重试 - HyperLiquid ParseFillFromResponse 提取订单成交均价, 新增 GetSize/GetBalance 方法 - 开仓/加仓/平仓均从交易所获取实际成交价替代WS估算价 - HL 平仓使用 reduceOnly 防止反向开仓 - 所有 OrderID 保存到数据库 orders 表 - 加仓零成交检测及实际手续费获取 - PnL 计算修正为按USD计算手续费 - 新增 GetAllClosedTrades / GetClosedStats 数据库查询 - 服务器重启 restore 未完成交易修复(DBTradeID 缺失) - 黑名单强平添加 USD PnL/手续费预计算 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c0cafb0400
commit
f97ac16b1c
@@ -168,32 +168,77 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro
|
||||
return string(respJSON), nil
|
||||
}
|
||||
|
||||
// PlaceMarketCloseOrder closes a position on HL with reduceOnly protection.
|
||||
// Uses the SDK's MarketClose which sets ReduceOnly=true to prevent accidental reversals.
|
||||
// sz is the size string (same format as PlaceMarketOrder). Pass "0" or "" to close full position.
|
||||
func (h *HyperLiquidTrade) PlaceMarketCloseOrder(coin, sz string) (string, error) {
|
||||
if !h.configured {
|
||||
return "", fmt.Errorf("HL not configured")
|
||||
}
|
||||
if err := h.initExchange(); err != nil {
|
||||
return "", fmt.Errorf("init: %w", err)
|
||||
}
|
||||
|
||||
var size *float64
|
||||
if f, err := strconv.ParseFloat(sz, 64); err == nil && f > 0 {
|
||||
size = &f
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
log.Printf("[Order] HL MarketClose | coin=%s size=%v reduceOnly=true slippage=0.05", coin, size)
|
||||
result, err := h.exchange.MarketClose(ctx, coin, size, nil, 0.05, nil, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("market close: %w", err)
|
||||
}
|
||||
respJSON, _ := json.Marshal(result)
|
||||
return string(respJSON), nil
|
||||
}
|
||||
|
||||
// EstimateFeeFromResponse calculates the fee using the response's filled size × price
|
||||
// × configured taker rate. This is NOT an actual fee from HL — HL does not return
|
||||
// fee amounts in the order response. The result is equivalent to estimating from
|
||||
// TradeAmountUSD, but more accurate for partial fills since it uses actual filled sz/px.
|
||||
func (h *HyperLiquidTrade) EstimateFeeFromResponse(orderResponseJSON string, takerFeePct float64) (feeUSD float64, err error) {
|
||||
// HL MarketOpen returns a single OrderStatus object (NOT wrapped in statuses array):
|
||||
// {"resting":..., "filled":{"totalSz":"82.5","avgPx":"0.12153","oid":52463955193}, "error":...}
|
||||
var resp struct {
|
||||
Resting *json.RawMessage `json:"resting,omitempty"`
|
||||
Filled *struct {
|
||||
Filled *struct {
|
||||
TotalSz string `json:"totalSz"`
|
||||
AvgPx string `json:"avgPx"`
|
||||
} `json:"filled,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(orderResponseJSON), &resp); err != nil || resp.Filled == nil {
|
||||
return 0, fmt.Errorf("no filled data in response")
|
||||
}
|
||||
sz, _ := strconv.ParseFloat(resp.Filled.TotalSz, 64)
|
||||
px, _ := strconv.ParseFloat(resp.Filled.AvgPx, 64)
|
||||
if sz > 0 && px > 0 {
|
||||
return sz * px * takerFeePct / 100, nil
|
||||
}
|
||||
return 0, fmt.Errorf("no filled status in response")
|
||||
}
|
||||
|
||||
// ParseFillFromResponse extracts the average fill price and total filled size
|
||||
// from an HL MarketOpen/MarketClose response. Returns (avgFillPrice, filledSize, error).
|
||||
func (h *HyperLiquidTrade) ParseFillFromResponse(orderResponseJSON string) (avgPrice, filledSize float64, err error) {
|
||||
var resp struct {
|
||||
Filled *struct {
|
||||
TotalSz string `json:"totalSz"`
|
||||
AvgPx string `json:"avgPx"`
|
||||
} `json:"filled,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(orderResponseJSON), &resp); err != nil {
|
||||
return 0, fmt.Errorf("parse: %w", err)
|
||||
return 0, 0, fmt.Errorf("parse: %w", err)
|
||||
}
|
||||
if resp.Filled != nil {
|
||||
sz, _ := strconv.ParseFloat(resp.Filled.TotalSz, 64)
|
||||
px, _ := strconv.ParseFloat(resp.Filled.AvgPx, 64)
|
||||
if sz > 0 && px > 0 {
|
||||
return sz * px * takerFeePct / 100, nil
|
||||
return px, sz, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no filled status in response")
|
||||
return 0, 0, fmt.Errorf("no filled data in response")
|
||||
}
|
||||
|
||||
func (h *HyperLiquidTrade) GetBalance() (float64, error) {
|
||||
|
||||
Reference in New Issue
Block a user