-
Notifications
You must be signed in to change notification settings - Fork 60
/
cascadebot.go
207 lines (167 loc) · 6.09 KB
/
cascadebot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Copyright Andrius Sutas BitfinexLendingBot [at] motoko [dot] sutas [dot] eu
// Strategy inspired by: https://github.com/ah3dce/cascadebot
// Unlike the original cascadebot strategy, we start from FRR + Increment rate,
// so that the start lending rate settings would adapt dynamically to the lendbook
// and prevent offers from sitting unlent for long.
package main
import (
"errors"
"log"
"math"
"strconv"
"strings"
"time"
"github.com/eAndrius/bitfinex-go"
)
// CascadeBotConf ...
type CascadeBotConf struct {
StartDailyLendRateFRRInc float64
MinDailyLendRate float64
ReductionIntervalMinutes float64
ReduceDailyLendRate float64
ExponentialDecayMult float64
LendPeriod int
}
const (
cancel = iota
lend
)
// CascadeBotAction ...
type CascadeBotAction struct {
Action int
OfferID int
Amount, YearlyRate float64
Period int
}
// CascadeBotActions ...
type CascadeBotActions []CascadeBotAction
func strategyCascadeBot(bconf BotConfig, dryRun bool) (err error) {
api := bconf.API
conf := bconf.Strategy.CascadeBot
activeWallet := strings.ToLower(bconf.Bitfinex.ActiveWallet)
// Do sanity check: Is MinDailyLendRate set?
if conf.MinDailyLendRate <= 0.003 { // 0.003% daily == 1.095% yearly
log.Println("\tWARNING: minimum daily lend rate is low (" + strconv.FormatFloat(conf.MinDailyLendRate, 'f', -1, 64) + "%)")
}
// Get all active offers
log.Println("\tGetting all active offers...")
allOffers, err := api.ActiveOffers()
if err != nil {
return
}
// Filter only relevant offers
log.Println("\tKeeping only " + activeWallet + " lend offers...")
var offers bitfinex.Offers
for _, o := range allOffers {
if strings.ToLower(o.Currency) == activeWallet && strings.ToLower(o.Direction) == "lend" {
offers = append(offers, o)
}
}
log.Println("\tGetting current lendbook for FRR...")
lendbook, err := api.Lendbook(activeWallet, 0, 10000)
if err != nil {
return
}
FRR := 1.0
for _, o := range lendbook.Asks {
if o.FRR {
FRR = o.Rate / 365
break
}
}
// Sanity check: is the daily lend rate sane?
if FRR+conf.StartDailyLendRateFRRInc >= 0.5 {
log.Println("\tWARNING: Starting daily lend rate (" +
strconv.FormatFloat(FRR+conf.StartDailyLendRateFRRInc, 'f', -1, 64) + " %/day) is unusually high")
}
log.Println("\tGetting current wallet balance...")
balance, err := api.WalletBalances()
if err != nil {
return errors.New("Failed to get wallet funds: " + err.Error())
}
// Calculate minimum loan size
minLoan := bconf.Bitfinex.MinLoanUSD
if activeWallet != "usd" {
log.Println("\tGetting current " + activeWallet + " ticker...")
ticker, err := api.Ticker(activeWallet + "usd")
if err != nil {
return errors.New("Failed to get ticker: " + err.Error())
}
minLoan = bconf.Bitfinex.MinLoanUSD / ticker.Mid
}
// Sanity check: is there anything to lend?
walletAmount := balance[bitfinex.WalletKey{"deposit", activeWallet}].Amount
if walletAmount < minLoan {
log.Println("\tWARNING: Wallet amount (" +
strconv.FormatFloat(walletAmount, 'f', -1, 64) + " " + activeWallet + ") is less than the allowed minimum (" +
strconv.FormatFloat(minLoan, 'f', -1, 64) + " " + activeWallet + ")")
}
// Determine available funds for trading
available := balance[bitfinex.WalletKey{"deposit", activeWallet}].Available
// Check if we need to limit our usage
if bconf.Bitfinex.MaxActiveAmount >= 0 {
available = math.Min(available, bconf.Bitfinex.MaxActiveAmount)
}
actions := cascadeBotGetActions(available, minLoan, FRR, offers, conf)
// Execute the actions
for _, a := range actions {
if a.Action == cancel {
log.Println("\tCanceling offer ID: " + strconv.Itoa(a.OfferID))
if !dryRun {
err = api.CancelOffer(a.OfferID)
if err != nil {
return errors.New("Failed to cancel offer: " + err.Error())
}
}
} else if a.Action == lend {
log.Println("\tPlacing offer: " +
strconv.FormatFloat(a.Amount, 'f', -1, 64) + " " + activeWallet + " @ " +
strconv.FormatFloat(a.YearlyRate/365, 'f', -1, 64) + " %/day for " + strconv.Itoa(a.Period) + " days")
if !dryRun {
_, err = api.NewOffer(strings.ToUpper(activeWallet), a.Amount, a.YearlyRate, a.Period, bitfinex.LEND)
if err != nil {
return errors.New("Failed to place new offer: " + err.Error())
}
}
}
}
log.Println("\tRun done.")
return
}
func cascadeBotGetActions(fundsAvailable, minLoan, dailyFRR float64, activeOffers bitfinex.Offers, conf CascadeBotConf) (actions CascadeBotActions) {
// Update lend rates where needed
for _, o := range activeOffers {
// Check if we need to update the offer based on its timestamp
offerDurationMinutes := (time.Now().Unix() - int64(o.Timestamp)) / 60
if offerDurationMinutes >= int64(conf.ReductionIntervalMinutes) {
// Cancel the offer first
actions = append(actions,
CascadeBotAction{Action: cancel, OfferID: o.ID})
// Check if there is enough amount remaining so that we can re-lend it,
// otherwise the offer's amount will just go back to the wallet
// and be lent at the "starting" daily rate
if o.RemainingAmount >= minLoan {
// Adjust rate only one step
// (e.g. to prevent offer going immediately to a minimum rate in the event of connection failure)
newDailyRate := o.Rate / 365
// Linear reduction
newDailyRate -= conf.ReduceDailyLendRate
// Exponential reduction
newDailyRate = (newDailyRate-conf.MinDailyLendRate)*conf.ExponentialDecayMult + conf.MinDailyLendRate
// Force minimum rate in case of wrong exponential decay user parameters
newRate := math.Max(newDailyRate, conf.MinDailyLendRate) * 365
// Make new offer at a different rate
actions = append(actions, CascadeBotAction{Action: lend,
YearlyRate: newRate, Amount: o.RemainingAmount, Period: o.Period})
} else {
fundsAvailable += o.RemainingAmount
}
}
}
// Are there spare funds to offer at the "starting" daily amount?
if fundsAvailable >= minLoan {
actions = append(actions, CascadeBotAction{Action: lend,
YearlyRate: (dailyFRR + conf.StartDailyLendRateFRRInc) * 365, Amount: fundsAvailable, Period: conf.LendPeriod})
}
return
}