Add orderbook calculator and verify func

This commit is contained in:
Adrian Gallagher
2019-06-30 22:09:19 +10:00
parent 7dbfcb311c
commit 7112a89491
14 changed files with 1452 additions and 275 deletions

View File

@@ -725,7 +725,10 @@ func (e *Base) IsAssetTypeSupported(asset asset.Item) bool {
// PrintEnabledPairs prints the exchanges enabled asset pairs
func (e *Base) PrintEnabledPairs() {
for k, v := range e.CurrencyPairs.Pairs {
log.Infof("Asset type %v:", k)
log.Infof("\t Enabled pairs: %v", v.Enabled)
log.Infof("%s Asset type %v:\n\t Enabled pairs: %v",
e.Name, strings.ToUpper(k.String()), v.Enabled)
}
}
// GetBase returns the exchange base
func (e *Base) GetBase() *Base { return e }

View File

@@ -63,4 +63,5 @@ type IBotExchange interface {
GetDefaultConfig() (*config.ExchangeConfig, error)
GetSubscriptions() ([]WebsocketChannelSubscription, error)
AuthenticateWebsocket() error
GetBase() *Base
}

View File

@@ -0,0 +1,227 @@
package orderbook
import (
"errors"
"fmt"
"sort"
math "github.com/thrasher-/gocryptotrader/common/math"
log "github.com/thrasher-/gocryptotrader/logger"
)
// WhaleBombResult returns the whale bomb result
type WhaleBombResult struct {
Amount float64
MinimumPrice float64
MaximumPrice float64
PercentageGainOrLoss float64
Orders orderSummary
Status string
}
// WhaleBomb finds the amount required to target a price
func (o *Base) WhaleBomb(priceTarget float64, buy bool) (*WhaleBombResult, error) {
if priceTarget < 0 {
return nil, errors.New("price target is invalid")
}
if buy {
a, orders := o.findAmount(priceTarget, true)
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
var err error
if max < priceTarget {
err = errors.New("unable to hit price target due to insufficient orderbook items")
}
status := fmt.Sprintf("Buying %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
a, o.Pair.Quote.String(), o.Pair.Base.String(), min, max,
math.CalculatePercentageGainOrLoss(max, min), len(orders))
return &WhaleBombResult{
Amount: a,
Orders: orders,
MinimumPrice: min,
MaximumPrice: max,
Status: status,
}, err
}
a, orders := o.findAmount(priceTarget, false)
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
var err error
if min > priceTarget {
err = errors.New("unable to hit price target due to insufficient orderbook items")
}
status := fmt.Sprintf("Selling %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
a, o.Pair.Base.String(), o.Pair.Quote.String(), max, min,
math.CalculatePercentageGainOrLoss(min, max), len(orders))
return &WhaleBombResult{
Amount: a,
Orders: orders,
MinimumPrice: min,
MaximumPrice: max,
Status: status,
}, err
}
// OrderSimulationResult returns the order simulation result
type OrderSimulationResult WhaleBombResult
// SimulateOrder simulates an order
func (o *Base) SimulateOrder(amount float64, buy bool) *OrderSimulationResult {
if buy {
orders, amt := o.buy(amount)
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
pct := math.CalculatePercentageGainOrLoss(max, min)
status := fmt.Sprintf("Buying %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
amount, o.Pair.Quote.String(), o.Pair.Base.String(), min, max,
pct, len(orders))
return &OrderSimulationResult{
Orders: orders,
Amount: amt,
MinimumPrice: min,
MaximumPrice: max,
PercentageGainOrLoss: pct,
Status: status,
}
}
orders, amt := o.sell(amount)
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
pct := math.CalculatePercentageGainOrLoss(min, max)
status := fmt.Sprintf("Selling %f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
amount, o.Pair.Base.String(), o.Pair.Quote.String(), max, min,
pct, len(orders))
return &OrderSimulationResult{
Orders: orders,
Amount: amt,
MinimumPrice: min,
MaximumPrice: max,
PercentageGainOrLoss: pct,
Status: status,
}
}
type orderSummary []Item
func (o orderSummary) Print() {
for x := range o {
log.Debugf("Order: Price: %f Amount: %f", o[x].Price, o[x].Amount)
}
}
func (o orderSummary) MinimumPrice(reverse bool) float64 {
if len(o) != 0 {
sortOrdersByPrice(&o, reverse)
return o[0].Price
}
return 0
}
func (o orderSummary) MaximumPrice(reverse bool) float64 {
if len(o) != 0 {
sortOrdersByPrice(&o, reverse)
return o[0].Price
}
return 0
}
// ByPrice used for sorting orders by order date
type ByPrice orderSummary
func (b ByPrice) Len() int { return len(b) }
func (b ByPrice) Less(i, j int) bool { return b[i].Price < b[j].Price }
func (b ByPrice) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
// sortOrdersByPrice the caller function to sort orders
func sortOrdersByPrice(o *orderSummary, reverse bool) {
if reverse {
sort.Sort(sort.Reverse(ByPrice(*o)))
} else {
sort.Sort(ByPrice(*o))
}
}
func (o *Base) findAmount(price float64, buy bool) (float64, orderSummary) {
var orders orderSummary
var amt float64
if buy {
asks := o.Asks
for x := range asks {
if asks[x].Price >= price {
amt += asks[x].Price * asks[x].Amount
orders = append(orders, Item{
Price: asks[x].Price,
Amount: asks[x].Amount,
})
return amt, orders
}
orders = append(orders, Item{
Price: asks[x].Price,
Amount: asks[x].Amount,
})
amt += asks[x].Price * asks[x].Amount
}
return amt, orders
}
for x := range o.Bids {
if o.Bids[x].Price <= price {
amt += o.Bids[x].Amount
orders = append(orders, Item{
Price: o.Bids[x].Price,
Amount: o.Bids[x].Amount,
})
break
}
orders = append(orders, Item{
Price: o.Bids[x].Price,
Amount: o.Bids[x].Amount,
})
amt += o.Bids[x].Amount
}
return amt, orders
}
func (o *Base) buy(amount float64) (orders orderSummary, baseAmount float64) {
var processedAmt float64
for x := range o.Asks {
subtotal := o.Asks[x].Price * o.Asks[x].Amount
if processedAmt+subtotal >= amount {
diff := amount - processedAmt
subAmt := diff / o.Asks[x].Price
orders = append(orders, Item{
Price: o.Asks[x].Price,
Amount: subAmt,
})
baseAmount += subAmt
break
}
processedAmt += subtotal
baseAmount += o.Asks[x].Amount
orders = append(orders, Item{
Price: o.Asks[x].Price,
Amount: o.Asks[x].Amount,
})
}
return
}
func (o *Base) sell(amount float64) (orders orderSummary, quoteAmount float64) {
var processedAmt float64
for x := range o.Bids {
if processedAmt+o.Bids[x].Amount >= amount {
diff := amount - processedAmt
orders = append(orders, Item{
Price: o.Bids[x].Price,
Amount: diff,
})
quoteAmount += diff * o.Bids[x].Price
break
}
processedAmt += o.Bids[x].Amount
quoteAmount += o.Bids[x].Amount * o.Bids[x].Price
orders = append(orders, Item{
Price: o.Bids[x].Price,
Amount: o.Bids[x].Amount,
})
}
return
}

View File

@@ -0,0 +1,79 @@
package orderbook
import (
"testing"
"github.com/thrasher-/gocryptotrader/currency"
)
func testSetup() Base {
return Base{
ExchangeName: "a",
Pair: currency.NewPair(currency.BTC, currency.USD),
Asks: []Item{
{Price: 7000, Amount: 1},
{Price: 7001, Amount: 2},
},
Bids: []Item{
{Price: 6999, Amount: 1},
{Price: 6998, Amount: 2},
},
}
}
func TestWhaleBomb(t *testing.T) {
t.Parallel()
b := testSetup()
// invalid price amout
_, err := b.WhaleBomb(-1, true)
if err == nil {
t.Error("unexpected result")
}
// valid
b.WhaleBomb(7001, true)
// invalid
b.WhaleBomb(7002, true)
// valid
b.WhaleBomb(6998, false)
// invalid
b.WhaleBomb(6997, false)
}
func TestSimulateOrder(t *testing.T) {
t.Parallel()
b := testSetup()
b.SimulateOrder(8000, true)
b.SimulateOrder(1.5, false)
}
func TestOrderSummary(t *testing.T) {
var o orderSummary
if p := o.MaximumPrice(false); p != 0 {
t.Error("unexpected result")
}
if p := o.MinimumPrice(false); p != 0 {
t.Error("unexpected result")
}
o = orderSummary{
{Price: 1337, Amount: 1},
{Price: 9001, Amount: 1},
}
if p := o.MaximumPrice(false); p != 1337 {
t.Error("unexpected result")
}
if p := o.MaximumPrice(true); p != 9001 {
t.Error("unexpected result")
}
if p := o.MinimumPrice(false); p != 1337 {
t.Error("unexpected result")
}
if p := o.MinimumPrice(true); p != 9001 {
t.Error("unexpected result")
}
o.Print()
}

View File

@@ -2,6 +2,7 @@ package orderbook
import (
"errors"
"sort"
"sync"
"time"
@@ -47,6 +48,45 @@ type Orderbook struct {
ExchangeName string
}
type byOBPrice []Item
func (a byOBPrice) Len() int { return len(a) }
func (a byOBPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byOBPrice) Less(i, j int) bool { return a[i].Price < a[j].Price }
// Verify ensures that the orderbook items are correctly sorted
// Bids should always go from a high price to a low price and
// asks should always go from a low price to a higher price
func (o *Base) Verify() {
var lastPrice float64
var sortBids, sortAsks bool
for x := range o.Bids {
if lastPrice != 0 && o.Bids[x].Price >= lastPrice {
sortBids = true
break
}
lastPrice = o.Bids[x].Price
}
lastPrice = 0
for x := range o.Asks {
if lastPrice != 0 && o.Asks[x].Price <= lastPrice {
sortAsks = true
break
}
lastPrice = o.Asks[x].Price
}
if sortBids {
sort.Sort(sort.Reverse(byOBPrice(o.Bids)))
}
if sortAsks {
sort.Sort((byOBPrice(o.Asks)))
}
}
// TotalBidsAmount returns the total amount of bids and the total orderbook
// bids value
func (o *Base) TotalBidsAmount() (amountCollated, total float64) {
@@ -168,6 +208,8 @@ func (o *Base) Process() error {
o.LastUpdated = time.Now()
}
o.Verify()
orderbook, err := GetByExchange(o.ExchangeName)
if err != nil {
CreateNewOrderbook(o.ExchangeName, o, o.AssetType)

View File

@@ -12,6 +12,28 @@ import (
log "github.com/thrasher-/gocryptotrader/logger"
)
func TestVerify(t *testing.T) {
t.Parallel()
b := Base{
ExchangeName: "TestExchange",
Pair: currency.NewPair(currency.BTC, currency.USD),
Bids: []Item{
{Price: 100}, {Price: 101}, {Price: 99},
},
Asks: []Item{
{Price: 100}, {Price: 99}, {Price: 101},
},
}
b.Verify()
if r := b.Bids[1].Price; r != 100 {
t.Error("unexpected result")
}
if r := b.Asks[1].Price; r != 100 {
t.Error("unexpected result")
}
}
func TestCalculateTotalBids(t *testing.T) {
t.Parallel()
currency := currency.NewPairFromStrings("BTC", "USD")

View File

@@ -0,0 +1,24 @@
package simulator
import (
"testing"
"github.com/thrasher-/gocryptotrader/currency"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
"github.com/thrasher-/gocryptotrader/exchanges/bitstamp"
)
func TestSimulate(t *testing.T) {
b := bitstamp.Bitstamp{}
b.SetDefaults()
b.Verbose = false
o, err := b.FetchOrderbook(currency.NewPair(currency.BTC, currency.USD), asset.Spot)
if err != nil {
t.Error(err)
}
r := o.SimulateOrder(10000000, true)
t.Log(r.Status)
r = o.SimulateOrder(2171, false)
t.Log(r.Status)
}