Skip to content
This repository has been archived by the owner on Dec 3, 2024. It is now read-only.

Commit

Permalink
Merge pull request #16 from Newtoniano/min-volume-filter
Browse files Browse the repository at this point in the history
  • Loading branch information
sleeyax authored Apr 17, 2024
2 parents 5bc4500 + 31d4e72 commit 36c60f2
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 70 deletions.
4 changes: 4 additions & 0 deletions config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ trading_options:
# Set to 0 to disable.
cool_off_delay: 0

# Trading volume threshold in pair_with currency for a coin to be considered for trading.
# For Binance, this value refers to the 24h quote asset trading volume of the coin.
min_quote_volume_traded: 100000

# Configuration for trailing stop loss.
trailing_stop_options:
# Whether to enable trailing stop loss.
Expand Down
42 changes: 42 additions & 0 deletions internal/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Bot struct {
market market.Market
db database.Database
volatilityWindow *VolatilityWindow
tradeVolumes market.TradeVolumes
config *config.Configuration
botLog *zap.SugaredLogger
buyLog *zap.SugaredLogger
Expand Down Expand Up @@ -52,6 +53,12 @@ func (b *Bot) Start(ctx context.Context) {
defer b.flushLogs()
b.botLog.Info("Bot started. Press CTRL + C to quit.")

if b.config.TradingOptions.MinQuoteVolumeTraded != 0.0 {
if err := b.updateVolumeTraded(ctx); err != nil {
panic(fmt.Sprintf("failed to load initial volume traded: %s", err))
}
}

var wg sync.WaitGroup
wg.Add(2)

Expand All @@ -72,11 +79,20 @@ func (b *Bot) buy(ctx context.Context, wg *sync.WaitGroup) {
panic(fmt.Sprintf("failed to load initial latest coins: %s", err))
}

ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
b.buyLog.Debug("Bot stopped buying coins.")
return
case <-ticker.C:
// We want to update the volume traded every hour to avoid API rate limiting. This can be a configurable option in the future.
if err := b.updateVolumeTraded(ctx); err != nil {
b.buyLog.Errorf("Failed to update volume traded: %s.", err)
continue
}
default:
// Wait until the next recheck interval.
lastRecord := b.volatilityWindow.GetLatestRecord()
Expand All @@ -97,6 +113,12 @@ func (b *Bot) buy(ctx context.Context, wg *sync.WaitGroup) {
volatileCoins := b.volatilityWindow.IdentifyVolatileCoins(b.config.TradingOptions.ChangeInPrice)
b.buyLog.Infof("Found %d volatile coins.", len(volatileCoins))
for _, volatileCoin := range volatileCoins {

if !volatileCoin.Coin.IsAvailableForTrading(b.config.TradingOptions.AllowList, b.config.TradingOptions.DenyList, b.config.TradingOptions.PairWith, b.config.TradingOptions.MinQuoteVolumeTraded) {
b.buyLog.Debugf("Coin %s is not available for trading. Skipping.", volatileCoin.Symbol)
continue
}

b.buyLog.Infof("Coin %s has gained %.2f%% within the last %d minutes.", volatileCoin.Symbol, volatileCoin.Percentage, b.config.TradingOptions.TimeDifference)

// Skip if the coin has already been bought.
Expand Down Expand Up @@ -358,6 +380,20 @@ func (b *Bot) getProfitOrLossText(priceChangePercentage float64) string {
return profitOrLossText
}

// updateVolumeTraded fetches the volume traded of all coins from the market and stores them in the CoinVolumes map.
func (b *Bot) updateVolumeTraded(ctx context.Context) error {
b.botLog.Debug("Fetching volume traded of all coins.")

volumeTraded, err := b.market.GetCoinsVolume(ctx)
if err != nil {
return err
}

b.tradeVolumes = volumeTraded

return nil
}

// updateLatestCoins fetches the latest coins from the market and appends them to the volatilityWindow.
func (b *Bot) updateLatestCoins(ctx context.Context) error {
b.botLog.Debug("Fetching latest coins.")
Expand All @@ -367,6 +403,12 @@ func (b *Bot) updateLatestCoins(ctx context.Context) error {
return err
}

for symbol, coin := range coins {
if quoteVolume, ok := b.tradeVolumes[symbol]; ok {
coin.QuoteVolumeTraded = quoteVolume
}
}

b.volatilityWindow.AddRecord(coins)

return nil
Expand Down
78 changes: 51 additions & 27 deletions internal/bot/bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

type mockMarket struct {
coinsIndex int
coins []market.CoinMap
coins []market.Coins
cancel context.CancelFunc
}

Expand All @@ -23,7 +23,7 @@ var _ market.Market = (*mockMarket)(nil)

func newMockMarket(cancel context.CancelFunc) *mockMarket {
return &mockMarket{
coins: make([]market.CoinMap, 0),
coins: make([]market.Coins, 0),
cancel: cancel,
}
}
Expand All @@ -40,7 +40,11 @@ func (m *mockMarket) Sell(ctx context.Context, coin string, quantity float64) (m
panic("implement me")
}

func (m *mockMarket) GetCoins(_ context.Context) (market.CoinMap, error) {
func (m *mockMarket) GetCoinsVolume(_ context.Context) (market.TradeVolumes, error) {
panic("implement me")
}

func (m *mockMarket) GetCoins(_ context.Context) (market.Coins, error) {
if m.coinsIndex >= len(m.coins) {
m.cancel()
return nil, fmt.Errorf("no coins found at index %d", m.coinsIndex)
Expand All @@ -53,7 +57,7 @@ func (m *mockMarket) GetCoins(_ context.Context) (market.CoinMap, error) {
return coins, nil
}

func (m *mockMarket) AddCoins(coins market.CoinMap) {
func (m *mockMarket) AddCoins(coins market.Coins) {
m.coins = append(m.coins, coins)
}

Expand Down Expand Up @@ -128,41 +132,61 @@ func TestBot_buy(t *testing.T) {
EnableTestMode: true,
LoggingOptions: config.LoggingOptions{Enable: false},
TradingOptions: config.TradingOptions{
ChangeInPrice: 10, // 10%
PairWith: "USDT",
Quantity: 10, // trade 10 USDT
ChangeInPrice: 10, // 10%
PairWith: "USDT",
Quantity: 10, // trade 10 USDT
MinQuoteVolumeTraded: 100_000,
},
}

m := newMockMarket(cancel)
m.AddCoins(market.CoinMap{
m.AddCoins(market.Coins{
"BTC": market.Coin{
Symbol: "BTC",
Price: 10_000,
Symbol: "BTCUSDT",
Price: 10_000,
QuoteVolumeTraded: 50_000,
},
"ETH": market.Coin{
Symbol: "ETH",
Price: 10_000,
Symbol: "ETHUSDT",
Price: 10_000,
QuoteVolumeTraded: 80_000,
},
})
m.AddCoins(market.CoinMap{
m.AddCoins(market.Coins{
"BTC": market.Coin{
Symbol: "BTC",
Price: 10_500,
Symbol: "BTCUSDT",
Price: 10_500,
QuoteVolumeTraded: 100_000,
},
"ETH": market.Coin{
Symbol: "ETH",
Price: 9_000,
Symbol: "ETHUSDT",
Price: 9_000,
QuoteVolumeTraded: 20_000,
},
})
m.AddCoins(market.CoinMap{
m.AddCoins(market.Coins{
"BTC": market.Coin{
Symbol: "BTC",
Price: 11_000,
Symbol: "BTCUSDT",
Price: 11_000,
QuoteVolumeTraded: 120_000,
},
"ETH": market.Coin{
Symbol: "ETH",
Price: 10_000,
Symbol: "ETHUSDT",
Price: 10_000,
QuoteVolumeTraded: 400_000,
},
})
// price change above threshold but not enough trading volume
m.AddCoins(market.Coins{
"BTC": market.Coin{
Symbol: "BTCUSDT",
Price: 14_000,
QuoteVolumeTraded: 30_000,
},
"ETH": market.Coin{
Symbol: "ETHUSDT",
Price: 13_000,
QuoteVolumeTraded: 40_000,
},
})

Expand All @@ -176,7 +200,7 @@ func TestBot_buy(t *testing.T) {

orders := db.GetOrders(models.BuyOrder, m.Name())
assert.Equal(t, 1, len(orders))
assert.Equal(t, "BTC", orders[0].Symbol)
assert.Equal(t, "BTCUSDT", orders[0].Symbol)
assert.Equal(t, 0.0009091, orders[0].Volume)
}

Expand All @@ -197,7 +221,7 @@ func TestBot_sell(t *testing.T) {
}

m := newMockMarket(cancel)
m.AddCoins(market.CoinMap{
m.AddCoins(market.Coins{
"XTZUSDT": market.Coin{
Symbol: "XTZUSDT",
Price: 1.295,
Expand Down Expand Up @@ -249,19 +273,19 @@ func TestBot_sell_with_trailing_stop_loss(t *testing.T) {
}

m := newMockMarket(cancel)
m.AddCoins(market.CoinMap{
m.AddCoins(market.Coins{
"BTC": market.Coin{
Symbol: "BTC",
Price: 11_000,
},
})
m.AddCoins(market.CoinMap{
m.AddCoins(market.Coins{
"BTC": market.Coin{
Symbol: "BTC",
Price: 11_050,
},
})
m.AddCoins(market.CoinMap{
m.AddCoins(market.Coins{
"BTC": market.Coin{
Symbol: "BTC",
Price: 9000,
Expand Down
4 changes: 2 additions & 2 deletions internal/bot/volatility_window.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const UnlimitedVolatilityWindowLength = 0

type VolatilityWindowRecord struct {
time time.Time
coins market.CoinMap
coins market.Coins
}

type VolatilityWindow struct {
Expand All @@ -36,7 +36,7 @@ func (h *VolatilityWindow) Size() int {
}

// AddRecord adds a new record to the volatilityWindow.
func (h *VolatilityWindow) AddRecord(coins market.CoinMap) {
func (h *VolatilityWindow) AddRecord(coins market.Coins) {
if l := h.Size(); l == h.maxLength && l != UnlimitedVolatilityWindowLength {
// remove everything except the last record
// h.records = h.records[h.maxLength-1:]
Expand Down
24 changes: 12 additions & 12 deletions internal/bot/volatility_window_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ func TestVolatilityWindow_Size(t *testing.T) {

func TestVolatilityWindow_Min(t *testing.T) {
window := NewVolatilityWindow(3)
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 10000.0},
})
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 8000.0},
})
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 20000.0},
})
m := window.Min("BTCUSDT")
Expand All @@ -30,13 +30,13 @@ func TestVolatilityWindow_Min(t *testing.T) {

func TestVolatilityWindow_Max(t *testing.T) {
window := NewVolatilityWindow(3)
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 10000.0},
})
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 20000.0},
})
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 8000.0},
})
m := window.Max("BTCUSDT")
Expand All @@ -47,41 +47,41 @@ func TestVolatilityWindow_IdentifyVolatileCoins(t *testing.T) {
// Basic percentage increase check.
window := NewVolatilityWindow(UnlimitedVolatilityWindowLength)
percentage := 15.0
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 20_000.0},
})
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 23_000.0},
})
v := window.IdentifyVolatileCoins(percentage)
assert.Equal(t, percentage, v["BTCUSDT"].Percentage)

// This coin is already identified as volatile above.
// a sudden spike in price shouldn't affect the result within the current time window.
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"BTCUSDT": {Price: 25_000.0},
})
v = window.IdentifyVolatileCoins(percentage)
assert.Equal(t, percentage, v["BTCUSDT"].Percentage)

// A brand-new coin should not yet be volatile.
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"ETHUSDT": {Price: 3000},
})
v = window.IdentifyVolatileCoins(percentage)
vv, ok := v["ETHUSDT"]
assert.Equal(t, false, ok, vv.Percentage)

// Test price drop.
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"ETHUSDT": {Price: 2000},
})
v = window.IdentifyVolatileCoins(percentage)
vv, ok = v["ETHUSDT"]
assert.Equal(t, false, ok, vv.Percentage)

// Test price increase
window.AddRecord(market.CoinMap{
window.AddRecord(market.Coins{
"ETHUSDT": {Price: 10_000},
})
v = window.IdentifyVolatileCoins(percentage)
Expand Down
4 changes: 4 additions & 0 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ type TradingOptions struct {
// Set to 0 to disable.
CoolOffDelay int `mapstructure:"cool_off_delay"`

// The minimum 24h quote asset volume traded of the coin.
// This is to avoid buying coins with very low trading volume.
MinQuoteVolumeTraded float64 `mapstructure:"min_quote_volume_traded"`

// Configuration for trailing stop loss.
TrailingStopOptions TrailingStopOptions `mapstructure:"trailing_stop_options"`

Expand Down
Loading

0 comments on commit 36c60f2

Please sign in to comment.