118 lines
3.5 KiB
Go
118 lines
3.5 KiB
Go
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
|
||
}
|
||
|
||
// 不在此持 portfolioMu:GetPortfolio / 下单路径里的 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
|
||
}
|