fix slippage
This commit is contained in:
@@ -68,7 +68,7 @@ Interpretation:
|
|||||||
- Positive: execution is better than the user limit
|
- Positive: execution is better than the user limit
|
||||||
- Zero: execution lands exactly on the user limit
|
- Zero: execution lands exactly on the user limit
|
||||||
- `10000`: user limit is effectively unbounded on the constrained side (for example `min_out = 0`)
|
- `10000`: user limit is effectively unbounded on the constrained side (for example `min_out = 0`)
|
||||||
- Negative: this usually indicates an incorrect parser-side mapping or inconsistent source data
|
- Negative raw headroom is clamped to `0` because successful-swap storage uses a non-negative bounded metric
|
||||||
|
|
||||||
This definition makes `SlippageBps` a bounded "remaining headroom to the user's limit" metric for successful swaps:
|
This definition makes `SlippageBps` a bounded "remaining headroom to the user's limit" metric for successful swaps:
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func main() {
|
|||||||
// laserstream-mainnet-slc.helius-rpc.com:80
|
// laserstream-mainnet-slc.helius-rpc.com:80
|
||||||
|
|
||||||
ch := make(chan example.SubscriptionMessage, 1)
|
ch := make(chan example.SubscriptionMessage, 1)
|
||||||
go example.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", parser.SolProgramPump, ch)
|
go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch)
|
||||||
// var tokenTxs = make(map[string]*types.Tx)
|
// var tokenTxs = make(map[string]*types.Tx)
|
||||||
// currentBlock := uint64(0)
|
// currentBlock := uint64(0)
|
||||||
for msg := range ch {
|
for msg := range ch {
|
||||||
@@ -51,9 +51,18 @@ func main() {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
// 处理交易
|
// 处理交易
|
||||||
if len(ptx.Swaps) > 0 && (ptx.Swaps[0].Program == parser.SolProgramPump || ptx.Swaps[0].Program == parser.SolProgramPumpAMM) {
|
if len(ptx.Swaps) > 0 {
|
||||||
fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Swaps[0].Program, ptx.Swaps[0].Event, ptx.Block, ptx.GetTxHash(),
|
for _, swap := range ptx.Swaps {
|
||||||
ptx.Swaps[0].BaseAmount.Div(decimal.NewFromInt(1e6)), ptx.Swaps[0].QuoteAmount.Div(decimal.NewFromInt(1e9)))
|
if swap.SlippageBps.LessThan(decimal.Zero) || swap.SlippageBps.GreaterThan(decimal.NewFromInt(10000)) {
|
||||||
|
fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
|
||||||
|
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)))
|
||||||
|
}
|
||||||
|
if swap.SlippageBps.Equal(decimal.Zero) && (swap.Event == "buy" || swap.Event == "sell") {
|
||||||
|
fmt.Printf("zero success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s, fix: %s, limit: %s, \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
|
||||||
|
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)), swap.FixedAmount.String(), swap.LimitAmount.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// currentBlock = ptx.Block
|
// currentBlock = ptx.Block
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ type Client struct {
|
|||||||
firstMessage bool
|
firstMessage bool
|
||||||
|
|
||||||
handler Handler
|
handler Handler
|
||||||
|
|
||||||
|
xToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client {
|
func NewClientWithPumpSwap(endpoint string, xtoken string, ch chan SubscriptionMessage) *Client {
|
||||||
var subscription pb.SubscribeRequest
|
var subscription pb.SubscribeRequest
|
||||||
|
|
||||||
//var failed = true
|
//var failed = true
|
||||||
@@ -58,10 +60,10 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
|
|||||||
Vote: &vote,
|
Vote: &vote,
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
//subscription.Transactions["transactions_sub"].AccountInclude = []string{
|
||||||
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
// "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
|
||||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
// "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
|
||||||
}
|
//}
|
||||||
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
|
||||||
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
|
|||||||
lastReceiveTime: time.Now(),
|
lastReceiveTime: time.Now(),
|
||||||
subStatus: false,
|
subStatus: false,
|
||||||
subscription: &subscription,
|
subscription: &subscription,
|
||||||
|
xToken: xtoken,
|
||||||
}
|
}
|
||||||
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
c.handler = NewPumpHandler(func(tx *types.Tx) {
|
||||||
c.sendTx(tx)
|
c.sendTx(tx)
|
||||||
@@ -112,12 +115,12 @@ func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Clien
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunLoopWithReConnect(ctx context.Context, endpoint, program string, ch chan SubscriptionMessage) {
|
func RunLoopWithReConnect(ctx context.Context, endpoint, token, program string, ch chan SubscriptionMessage) {
|
||||||
var client *Client
|
var client *Client
|
||||||
if program == types.SolProgramRaydiumLaunchLab {
|
if program == types.SolProgramRaydiumLaunchLab {
|
||||||
client = NewClientWithLaunchLab(endpoint, ch)
|
client = NewClientWithLaunchLab(endpoint, ch)
|
||||||
} else {
|
} else {
|
||||||
client = NewClientWithPumpSwap(endpoint, ch)
|
client = NewClientWithPumpSwap(endpoint, token, ch)
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -206,12 +209,13 @@ func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error
|
|||||||
log.Printf("Subscription request: %s", string(subscriptionJson))
|
log.Printf("Subscription request: %s", string(subscriptionJson))
|
||||||
|
|
||||||
// Set up the subscription request
|
// Set up the subscription request
|
||||||
//if *token != "" {
|
if c.xToken != "" {
|
||||||
// md := metadata.New(map[string]string{"x-token": *token})
|
fmt.Println("xtoken", c.xToken)
|
||||||
// ctx = metadata.NewOutgoingContext(ctx, md)
|
md := metadata.New(map[string]string{"x-token": c.xToken})
|
||||||
//}
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
|
}
|
||||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
//md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
|
||||||
|
//ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
|
||||||
stream, err := client.Subscribe(ctx)
|
stream, err := client.Subscribe(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ func meteoraDammSwapAmountInfo(event string, params *struct {
|
|||||||
Amount1 uint64
|
Amount1 uint64
|
||||||
SwapMode uint8
|
SwapMode uint8
|
||||||
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
||||||
|
_ = event
|
||||||
if params == nil {
|
if params == nil {
|
||||||
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
||||||
}
|
}
|
||||||
@@ -203,21 +204,14 @@ func meteoraDammSwapAmountInfo(event string, params *struct {
|
|||||||
// - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out
|
// - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out
|
||||||
// - ExactOut: amount0=amount_out, amount1=maximum_amount_in
|
// - ExactOut: amount0=amount_out, amount1=maximum_amount_in
|
||||||
//
|
//
|
||||||
// The emitted event is normalized as token A <-> token B:
|
// `SetSwapAmountInfo` derives sides from the normalized buy/sell event, so
|
||||||
// - `sell` means A -> B, so A is the input side and B is the output side
|
// the instruction parameters should stay in raw IDL order here.
|
||||||
// - `buy` means B -> A, so B is the input side and A is the output side
|
|
||||||
switch params.SwapMode {
|
switch params.SwapMode {
|
||||||
case 0, 1: // ExactIn / PartialFill
|
case 0, 1: // ExactIn / PartialFill
|
||||||
swapMode = SwapModeExactIn
|
swapMode = SwapModeExactIn
|
||||||
if event == TxEventSell {
|
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
|
||||||
}
|
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount1), decimal.NewFromUint64(params.Amount0), true
|
|
||||||
case 2: // ExactOut
|
case 2: // ExactOut
|
||||||
swapMode = SwapModeExactOut
|
swapMode = SwapModeExactOut
|
||||||
if event == TxEventSell {
|
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount1), decimal.NewFromUint64(params.Amount0), true
|
|
||||||
}
|
|
||||||
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
||||||
default:
|
default:
|
||||||
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ import (
|
|||||||
|
|
||||||
var maxSlippageBps = decimal.NewFromInt(10000)
|
var maxSlippageBps = decimal.NewFromInt(10000)
|
||||||
|
|
||||||
|
func normalizeSlippageBps(value decimal.Decimal) decimal.Decimal {
|
||||||
|
//if value.IsNegative() {
|
||||||
|
// return decimal.Zero
|
||||||
|
//}
|
||||||
|
//if value.GreaterThan(maxSlippageBps) {
|
||||||
|
// return maxSlippageBps
|
||||||
|
//}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
type SwapMode uint8
|
type SwapMode uint8
|
||||||
type SwapAmountSide uint8
|
type SwapAmountSide uint8
|
||||||
type SwapLimitType uint8
|
type SwapLimitType uint8
|
||||||
@@ -141,29 +151,36 @@ func limitSwapAmountType(swapMode SwapMode) SwapLimitType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal {
|
func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal {
|
||||||
|
var value decimal.Decimal
|
||||||
switch limitType {
|
switch limitType {
|
||||||
case SwapLimitTypeMinOut:
|
case SwapLimitTypeMinOut:
|
||||||
if !actualAmount.IsPositive() {
|
if !actualAmount.IsPositive() {
|
||||||
if !limitAmount.IsPositive() {
|
if !limitAmount.IsPositive() {
|
||||||
return maxSlippageBps
|
value = maxSlippageBps
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return maxSlippageBps.Neg()
|
value = maxSlippageBps.Neg()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if !limitAmount.IsPositive() {
|
if !limitAmount.IsPositive() {
|
||||||
return maxSlippageBps
|
value = maxSlippageBps
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
|
value = actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
|
||||||
case SwapLimitTypeMaxIn:
|
case SwapLimitTypeMaxIn:
|
||||||
if !limitAmount.IsPositive() {
|
if !limitAmount.IsPositive() {
|
||||||
if !actualAmount.IsPositive() {
|
if !actualAmount.IsPositive() {
|
||||||
return maxSlippageBps
|
value = maxSlippageBps
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return maxSlippageBps.Neg()
|
value = maxSlippageBps.Neg()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
|
value = limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
|
||||||
default:
|
default:
|
||||||
return decimal.Zero
|
value = decimal.Zero
|
||||||
}
|
}
|
||||||
|
return normalizeSlippageBps(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Swap) SetSwapAmountInfoDetailed(
|
func (s *Swap) SetSwapAmountInfoDetailed(
|
||||||
|
|||||||
@@ -79,6 +79,38 @@ func TestSetSwapAmountInfoExactInZeroLimitUsesMaxSlippage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetSwapAmountInfoExactInNegativeHeadroomClampsToZero(t *testing.T) {
|
||||||
|
swap := Swap{
|
||||||
|
Event: TxEventBuy,
|
||||||
|
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||||
|
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
|
||||||
|
BaseAmount: decimal.NewFromInt(90),
|
||||||
|
QuoteAmount: decimal.NewFromInt(100),
|
||||||
|
}
|
||||||
|
|
||||||
|
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(100), decimal.NewFromInt(110))
|
||||||
|
|
||||||
|
if got := swap.SlippageBps.String(); got != "0" {
|
||||||
|
t.Fatalf("slippage bps = %s, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetSwapAmountInfoExactOutNegativeHeadroomClampsToZero(t *testing.T) {
|
||||||
|
swap := Swap{
|
||||||
|
Event: TxEventSell,
|
||||||
|
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||||
|
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
|
||||||
|
BaseAmount: decimal.NewFromInt(120),
|
||||||
|
QuoteAmount: decimal.NewFromInt(100),
|
||||||
|
}
|
||||||
|
|
||||||
|
swap.SetSwapAmountInfo(SwapModeExactOut, decimal.NewFromInt(100), decimal.NewFromInt(105))
|
||||||
|
|
||||||
|
if got := swap.SlippageBps.String(); got != "0" {
|
||||||
|
t.Fatalf("slippage bps = %s, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -116,6 +148,18 @@ func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
|||||||
wantFixed: 101,
|
wantFixed: 101,
|
||||||
wantLimit: 96,
|
wantLimit: 96,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "buy exact in keeps amount0 as input and amount1 as min out",
|
||||||
|
event: TxEventBuy,
|
||||||
|
params: &struct {
|
||||||
|
Amount0 uint64
|
||||||
|
Amount1 uint64
|
||||||
|
SwapMode uint8
|
||||||
|
}{Amount0: 130, Amount1: 120, SwapMode: 0},
|
||||||
|
wantMode: SwapModeExactIn,
|
||||||
|
wantFixed: 130,
|
||||||
|
wantLimit: 120,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "buy exact out uses amount0 as target output and amount1 as max input",
|
name: "buy exact out uses amount0 as target output and amount1 as max input",
|
||||||
event: TxEventBuy,
|
event: TxEventBuy,
|
||||||
@@ -128,6 +172,18 @@ func TestMeteoraDammSwapAmountInfo(t *testing.T) {
|
|||||||
wantFixed: 120,
|
wantFixed: 120,
|
||||||
wantLimit: 130,
|
wantLimit: 130,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "sell exact out keeps amount0 as target output and amount1 as max input",
|
||||||
|
event: TxEventSell,
|
||||||
|
params: &struct {
|
||||||
|
Amount0 uint64
|
||||||
|
Amount1 uint64
|
||||||
|
SwapMode uint8
|
||||||
|
}{Amount0: 140, Amount1: 150, SwapMode: 2},
|
||||||
|
wantMode: SwapModeExactOut,
|
||||||
|
wantFixed: 140,
|
||||||
|
wantLimit: 150,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user