Files
coin/internal/logic/run.go
2026-05-11 16:24:13 +08:00

118 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"log"
"strconv"
"time"
"git.apinb.com/quant/coin/internal/models"
"github.com/shopspring/decimal"
)
// 以下为可调参数,改数值即可调整策略行为(改后重新编译运行)。
const (
// pollInterval 两次完整轮询之间的间隔(拉余额、价格、判单、写库)
pollInterval = 60 * time.Second
// maxDipAdds 相对当前均价最多加仓次数(不含首仓)
maxDipAdds = 4
// profitArmPct 浮盈达到该比例后进入「跟踪峰值 PnL」从峰值回落 gridStartPct 后再清仓
profitArmPct = 0.05
// gridStartPct 加仓/清仓前相对缓存极值的最小回撤幅度(与未实现盈亏比例同量纲),过滤噪声
gridStartPct = 0.005
)
// dipAddDrawdowns[k] 为已完成 k 次加仓后,下一次加仓须达到的相对当前加权均价的跌幅(第 1 次 5%、第 2 次 15%…)。
var dipAddDrawdowns = [...]float64{0.05, 0.15, 0.30, 0.50}
// Run 单轮现货策略1) 拉最新标价 2) 结合 portfolio 算未实现盈亏 3) 盈侧走跟踪止盈清仓,亏侧走超跌加仓。
func Run(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
if err := RefreshAccount(); err != nil {
log.Printf("logic: Run 刷新账户余额失败: %v", err)
}
// 1. 最新价格ListPrices 与标的 ticker
prices, err := BinanceClient.NewListPricesService().Symbols(Symbols).Do(ctx)
if err != nil {
return err
}
priceBySymbol := make(map[string]float64, len(prices))
for _, p := range prices {
v, err := strconv.ParseFloat(p.Price, 64)
if err != nil {
continue
}
priceBySymbol[p.Symbol] = v
}
// 不在此持 portfolioMuGetPortfolio / 下单路径里的 loadPortfolio 也会抢同一把锁,会死锁。
portfolio := GetPortfolio()
for symbol, pos := range portfolio {
price, ok := priceBySymbol[symbol]
if !ok {
continue
}
// 计算PNL盈利侧始终可止盈加仓仅在未打满档时判断
pnl := spotUnrealizedPnLPct(pos, price)
if pnl >= profitArmPct {
trySpotRallySell(pos, pnl, price)
continue
}
if pos.DipAddsDone >= maxDipAdds {
continue
}
draw := dipAddDrawdowns[pos.DipAddsDone]
if pnl < -draw {
AddSpotPosition(pos, pnl, price)
}
}
// 配置里有、但库中无 status=0 持仓时补首仓(止盈/删档后仅靠 boot 一次不够)
pf := GetPortfolio()
for _, sym := range Symbols {
if _, ok := pf[sym]; ok {
continue
}
if err := CreateNewSpotPosition(sym); err != nil {
log.Printf("logic: %s 无有效持仓,补首仓失败: %v", sym, err)
}
}
return nil
}
// spotUnrealizedPnLPct 相对持仓均价的未实现盈亏比例;(现价-成本)/成本;无有效成本时返回 0。
func spotUnrealizedPnLPct(st *models.SpotPosition, price float64) float64 {
if st == nil || st.BuyCostPrice <= 0 || price <= 0 {
return 0
}
return (price - st.BuyCostPrice) / st.BuyCostPrice
}
// formatQtyToLotStep 将数量按 stepSize 向下取整,满足 Binance LOT_SIZE买卖共用过小则返回 ok=false。
func formatQtyToLotStep(qty float64, stepSize string) (string, bool, error) {
if qty <= 0 {
return "", false, nil
}
step, err := decimal.NewFromString(stepSize)
if err != nil {
return "", false, err
}
q := decimal.NewFromFloat(qty)
n := q.Div(step).Floor()
out := n.Mul(step)
if out.LessThanOrEqual(decimal.Zero) {
return "", false, nil
}
return out.String(), true, nil
}
func parseFloat(s string) float64 {
v, _ := strconv.ParseFloat(s, 64)
return v
}