mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
503 lines
12 KiB
Go
503 lines
12 KiB
Go
// Package trading provides a unified HFT trading SDK with multi-venue support.
|
|
package trading
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
// Executor is the interface for execution algorithms.
|
|
type Executor interface {
|
|
Execute(ctx context.Context) ([]Order, error)
|
|
}
|
|
|
|
// =============================================================================
|
|
// TWAP Executor
|
|
// =============================================================================
|
|
|
|
// TwapExecutor implements Time-Weighted Average Price execution.
|
|
type TwapExecutor struct {
|
|
client *Client
|
|
symbol string
|
|
side Side
|
|
totalQuantity decimal.Decimal
|
|
numSlices int
|
|
interval time.Duration
|
|
}
|
|
|
|
// TwapConfig configures TWAP execution.
|
|
type TwapConfig struct {
|
|
Symbol string
|
|
Side Side
|
|
TotalQuantity decimal.Decimal
|
|
DurationSeconds int
|
|
NumSlices int
|
|
}
|
|
|
|
// NewTwapExecutor creates a TWAP executor.
|
|
func NewTwapExecutor(client *Client, cfg TwapConfig) *TwapExecutor {
|
|
interval := time.Duration(cfg.DurationSeconds/cfg.NumSlices) * time.Second
|
|
return &TwapExecutor{
|
|
client: client,
|
|
symbol: cfg.Symbol,
|
|
side: cfg.Side,
|
|
totalQuantity: cfg.TotalQuantity,
|
|
numSlices: cfg.NumSlices,
|
|
interval: interval,
|
|
}
|
|
}
|
|
|
|
// Execute runs the TWAP strategy.
|
|
func (e *TwapExecutor) Execute(ctx context.Context) ([]Order, error) {
|
|
sliceQty := e.totalQuantity.Div(decimal.NewFromInt(int64(e.numSlices)))
|
|
var orders []Order
|
|
|
|
for i := 0; i < e.numSlices; i++ {
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
remaining := e.totalQuantity.Sub(sliceQty.Mul(decimal.NewFromInt(int64(i))))
|
|
qty := decimal.Min(sliceQty, remaining)
|
|
|
|
if qty.LessThanOrEqual(decimal.Zero) {
|
|
break
|
|
}
|
|
|
|
var order Order
|
|
var err error
|
|
|
|
if e.side == SideBuy {
|
|
order, err = e.client.Buy(ctx, e.symbol, qty)
|
|
} else {
|
|
order, err = e.client.Sell(ctx, e.symbol, qty)
|
|
}
|
|
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
orders = append(orders, order)
|
|
|
|
// Wait for next slice (except for last)
|
|
if i < e.numSlices-1 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
case <-time.After(e.interval):
|
|
}
|
|
}
|
|
}
|
|
|
|
return orders, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// VWAP Executor
|
|
// =============================================================================
|
|
|
|
// VwapExecutor implements Volume-Weighted Average Price execution.
|
|
type VwapExecutor struct {
|
|
client *Client
|
|
symbol string
|
|
side Side
|
|
totalQuantity decimal.Decimal
|
|
participationRate decimal.Decimal
|
|
maxDuration time.Duration
|
|
checkInterval time.Duration
|
|
}
|
|
|
|
// VwapConfig configures VWAP execution.
|
|
type VwapConfig struct {
|
|
Symbol string
|
|
Side Side
|
|
TotalQuantity decimal.Decimal
|
|
ParticipationRate decimal.Decimal // e.g., 0.1 = 10% of volume
|
|
MaxDurationSeconds int
|
|
}
|
|
|
|
// NewVwapExecutor creates a VWAP executor.
|
|
func NewVwapExecutor(client *Client, cfg VwapConfig) *VwapExecutor {
|
|
return &VwapExecutor{
|
|
client: client,
|
|
symbol: cfg.Symbol,
|
|
side: cfg.Side,
|
|
totalQuantity: cfg.TotalQuantity,
|
|
participationRate: cfg.ParticipationRate,
|
|
maxDuration: time.Duration(cfg.MaxDurationSeconds) * time.Second,
|
|
checkInterval: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Execute runs the VWAP strategy.
|
|
func (e *VwapExecutor) Execute(ctx context.Context) ([]Order, error) {
|
|
var orders []Order
|
|
remaining := e.totalQuantity
|
|
start := time.Now()
|
|
|
|
for remaining.GreaterThan(decimal.Zero) {
|
|
// Check timeout
|
|
if time.Since(start) > e.maxDuration {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// Get current volume
|
|
ticker, err := e.client.Ticker(ctx, e.symbol)
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
|
|
volume := decimal.NewFromInt(1000) // Default
|
|
if ticker.Volume24H != nil && !ticker.Volume24H.IsZero() {
|
|
volume = *ticker.Volume24H
|
|
}
|
|
|
|
// Calculate slice based on participation rate
|
|
// hourlyVolume = volume / 24
|
|
// sliceVolume = hourlyVolume * participationRate / (3600 / checkInterval)
|
|
hourlyVolume := volume.Div(decimal.NewFromInt(24))
|
|
checksPerHour := decimal.NewFromFloat(float64(time.Hour) / float64(e.checkInterval))
|
|
sliceVolume := hourlyVolume.Mul(e.participationRate).Div(checksPerHour)
|
|
qty := decimal.Min(sliceVolume, remaining)
|
|
|
|
if qty.GreaterThan(decimal.Zero) {
|
|
var order Order
|
|
|
|
if e.side == SideBuy {
|
|
order, err = e.client.Buy(ctx, e.symbol, qty)
|
|
} else {
|
|
order, err = e.client.Sell(ctx, e.symbol, qty)
|
|
}
|
|
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
orders = append(orders, order)
|
|
remaining = remaining.Sub(order.FilledQuantity)
|
|
}
|
|
|
|
// Wait for next check
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
case <-time.After(e.checkInterval):
|
|
}
|
|
}
|
|
|
|
return orders, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Iceberg Executor
|
|
// =============================================================================
|
|
|
|
// IcebergExecutor implements iceberg order execution.
|
|
type IcebergExecutor struct {
|
|
client *Client
|
|
symbol string
|
|
side Side
|
|
totalQuantity decimal.Decimal
|
|
visibleQuantity decimal.Decimal
|
|
price decimal.Decimal
|
|
venue string
|
|
pollInterval time.Duration
|
|
}
|
|
|
|
// IcebergConfig configures iceberg execution.
|
|
type IcebergConfig struct {
|
|
Symbol string
|
|
Side Side
|
|
TotalQuantity decimal.Decimal
|
|
VisibleQuantity decimal.Decimal
|
|
Price decimal.Decimal
|
|
Venue string
|
|
}
|
|
|
|
// NewIcebergExecutor creates an iceberg executor.
|
|
func NewIcebergExecutor(client *Client, cfg IcebergConfig) *IcebergExecutor {
|
|
return &IcebergExecutor{
|
|
client: client,
|
|
symbol: cfg.Symbol,
|
|
side: cfg.Side,
|
|
totalQuantity: cfg.TotalQuantity,
|
|
visibleQuantity: cfg.VisibleQuantity,
|
|
price: cfg.Price,
|
|
venue: cfg.Venue,
|
|
pollInterval: 500 * time.Millisecond,
|
|
}
|
|
}
|
|
|
|
// Execute runs the iceberg strategy.
|
|
func (e *IcebergExecutor) Execute(ctx context.Context) ([]Order, error) {
|
|
var orders []Order
|
|
remaining := e.totalQuantity
|
|
opts := []OrderOption{}
|
|
if e.venue != "" {
|
|
opts = append(opts, WithVenue(e.venue))
|
|
}
|
|
|
|
for remaining.GreaterThan(decimal.Zero) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
qty := decimal.Min(e.visibleQuantity, remaining)
|
|
|
|
var order Order
|
|
var err error
|
|
|
|
if e.side == SideBuy {
|
|
order, err = e.client.LimitBuy(ctx, e.symbol, qty, e.price, opts...)
|
|
} else {
|
|
order, err = e.client.LimitSell(ctx, e.symbol, qty, e.price, opts...)
|
|
}
|
|
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
|
|
// Wait for fill (simplified - would need order status polling)
|
|
for order.IsOpen() {
|
|
select {
|
|
case <-ctx.Done():
|
|
// Cancel remaining order
|
|
e.client.CancelOrder(ctx, order.OrderID, order.Symbol, order.Venue)
|
|
return orders, ctx.Err()
|
|
case <-time.After(e.pollInterval):
|
|
// In real implementation, refresh order status here
|
|
break
|
|
}
|
|
break // Simplified: assume filled after one poll
|
|
}
|
|
|
|
remaining = remaining.Sub(order.FilledQuantity)
|
|
orders = append(orders, order)
|
|
}
|
|
|
|
return orders, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Sniper Executor
|
|
// =============================================================================
|
|
|
|
// SniperExecutor waits for a price target before executing.
|
|
type SniperExecutor struct {
|
|
client *Client
|
|
symbol string
|
|
side Side
|
|
quantity decimal.Decimal
|
|
targetPrice decimal.Decimal
|
|
timeout time.Duration
|
|
pollInterval time.Duration
|
|
}
|
|
|
|
// SniperConfig configures sniper execution.
|
|
type SniperConfig struct {
|
|
Symbol string
|
|
Side Side
|
|
Quantity decimal.Decimal
|
|
TargetPrice decimal.Decimal
|
|
TimeoutSeconds int
|
|
}
|
|
|
|
// NewSniperExecutor creates a sniper executor.
|
|
func NewSniperExecutor(client *Client, cfg SniperConfig) *SniperExecutor {
|
|
return &SniperExecutor{
|
|
client: client,
|
|
symbol: cfg.Symbol,
|
|
side: cfg.Side,
|
|
quantity: cfg.Quantity,
|
|
targetPrice: cfg.TargetPrice,
|
|
timeout: time.Duration(cfg.TimeoutSeconds) * time.Second,
|
|
pollInterval: 100 * time.Millisecond,
|
|
}
|
|
}
|
|
|
|
// Execute runs the sniper strategy.
|
|
func (e *SniperExecutor) Execute(ctx context.Context) ([]Order, error) {
|
|
deadline := time.Now().Add(e.timeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
ticker, err := e.client.Ticker(ctx, e.symbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shouldExecute := false
|
|
|
|
if e.side == SideBuy {
|
|
if ticker.Ask != nil && ticker.Ask.LessThanOrEqual(e.targetPrice) {
|
|
shouldExecute = true
|
|
}
|
|
} else {
|
|
if ticker.Bid != nil && ticker.Bid.GreaterThanOrEqual(e.targetPrice) {
|
|
shouldExecute = true
|
|
}
|
|
}
|
|
|
|
if shouldExecute {
|
|
var order Order
|
|
|
|
if e.side == SideBuy {
|
|
order, err = e.client.Buy(ctx, e.symbol, e.quantity)
|
|
} else {
|
|
order, err = e.client.Sell(ctx, e.symbol, e.quantity)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []Order{order}, nil
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(e.pollInterval):
|
|
}
|
|
}
|
|
|
|
// Timeout - no execution
|
|
return nil, ErrTimeout
|
|
}
|
|
|
|
// =============================================================================
|
|
// POV (Percentage of Volume) Executor
|
|
// =============================================================================
|
|
|
|
// PovExecutor implements Percentage of Volume execution.
|
|
type PovExecutor struct {
|
|
client *Client
|
|
symbol string
|
|
side Side
|
|
totalQuantity decimal.Decimal
|
|
participationRate decimal.Decimal
|
|
maxDuration time.Duration
|
|
minOrderSize decimal.Decimal
|
|
checkInterval time.Duration
|
|
}
|
|
|
|
// PovConfig configures POV execution.
|
|
type PovConfig struct {
|
|
Symbol string
|
|
Side Side
|
|
TotalQuantity decimal.Decimal
|
|
ParticipationRate decimal.Decimal // e.g., 0.05 = 5% of volume
|
|
MaxDurationSeconds int
|
|
MinOrderSize decimal.Decimal
|
|
}
|
|
|
|
// NewPovExecutor creates a POV executor.
|
|
func NewPovExecutor(client *Client, cfg PovConfig) *PovExecutor {
|
|
minOrder := cfg.MinOrderSize
|
|
if minOrder.IsZero() {
|
|
minOrder = decimal.NewFromFloat(0.001)
|
|
}
|
|
|
|
return &PovExecutor{
|
|
client: client,
|
|
symbol: cfg.Symbol,
|
|
side: cfg.Side,
|
|
totalQuantity: cfg.TotalQuantity,
|
|
participationRate: cfg.ParticipationRate,
|
|
maxDuration: time.Duration(cfg.MaxDurationSeconds) * time.Second,
|
|
minOrderSize: minOrder,
|
|
checkInterval: 10 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Execute runs the POV strategy.
|
|
func (e *PovExecutor) Execute(ctx context.Context) ([]Order, error) {
|
|
var orders []Order
|
|
remaining := e.totalQuantity
|
|
start := time.Now()
|
|
var lastVolume *decimal.Decimal
|
|
|
|
for remaining.GreaterThan(decimal.Zero) {
|
|
if time.Since(start) > e.maxDuration {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
ticker, err := e.client.Ticker(ctx, e.symbol)
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
|
|
if ticker.Volume24H == nil || ticker.Volume24H.IsZero() {
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
case <-time.After(e.checkInterval):
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Calculate volume delta
|
|
var volumeDelta decimal.Decimal
|
|
if lastVolume != nil {
|
|
volumeDelta = ticker.Volume24H.Sub(*lastVolume)
|
|
if volumeDelta.IsNegative() {
|
|
volumeDelta = decimal.Zero // Volume reset (24h window)
|
|
}
|
|
} else {
|
|
// First iteration: estimate from check interval
|
|
volumeDelta = ticker.Volume24H.Mul(decimal.NewFromFloat(float64(e.checkInterval) / float64(24*time.Hour)))
|
|
}
|
|
|
|
lastVolume = ticker.Volume24H
|
|
|
|
// Calculate order size as participation rate of volume
|
|
qty := volumeDelta.Mul(e.participationRate)
|
|
qty = decimal.Min(qty, remaining)
|
|
qty = decimal.Max(qty, decimal.Zero)
|
|
|
|
if qty.GreaterThanOrEqual(e.minOrderSize) {
|
|
var order Order
|
|
|
|
if e.side == SideBuy {
|
|
order, err = e.client.Buy(ctx, e.symbol, qty)
|
|
} else {
|
|
order, err = e.client.Sell(ctx, e.symbol, qty)
|
|
}
|
|
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
orders = append(orders, order)
|
|
remaining = remaining.Sub(order.FilledQuantity)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return orders, ctx.Err()
|
|
case <-time.After(e.checkInterval):
|
|
}
|
|
}
|
|
|
|
return orders, nil
|
|
}
|