├── .dockerignore ├── .gitignore ├── strategies └── arbitrage │ ├── execution_test.go │ ├── validation_test.go │ ├── validation.go │ ├── execution.go │ ├── report.go │ ├── simulation.go │ └── simulation_test.go ├── assets └── gotrading.png ├── core ├── currency.go ├── math.go ├── core_suite_test.go ├── order_dispatched.go ├── orderbook.go ├── portfolio_test.go ├── currency_test.go ├── exchange_test.go ├── orderbook_test.go ├── currency_pair_test.go ├── exchange_mashup_test.go ├── currency_pair.go ├── order_test.go ├── hit.go ├── endpoint.go ├── exchange_mashup.go ├── nonce.go ├── exchange.go ├── order.go └── portfolio.go ├── reporting ├── report.go └── publisher.go ├── Dockerfile ├── graph ├── graph_suite_test.go ├── tree_test.go ├── path_test.go ├── path.go ├── tree.go ├── path_finder_test.go └── path_finder.go ├── exchanges ├── binance │ ├── binance_test.go │ └── binance.go ├── factory.go └── liqui │ └── liqui.go ├── Gopkg.toml ├── config.json ├── networking ├── batch.go └── gatling.go ├── main.go ├── README.md └── Gopkg.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /vendor 3 | -------------------------------------------------------------------------------- /strategies/arbitrage/execution_test.go: -------------------------------------------------------------------------------- 1 | package arbitrage 2 | -------------------------------------------------------------------------------- /strategies/arbitrage/validation_test.go: -------------------------------------------------------------------------------- 1 | package arbitrage 2 | -------------------------------------------------------------------------------- /assets/gotrading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgalabru/gotrading/HEAD/assets/gotrading.png -------------------------------------------------------------------------------- /core/currency.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // Currency represents a currency. 4 | type Currency string 5 | -------------------------------------------------------------------------------- /reporting/report.go: -------------------------------------------------------------------------------- 1 | package reporting 2 | 3 | type Report interface { 4 | Encode() ([]byte, error) 5 | Description() string 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | WORKDIR /go/src/gotrading 3 | COPY . . 4 | 5 | RUN go-wrapper download 6 | RUN go-wrapper install 7 | 8 | CMD ["go-wrapper", "run"] # ["app"] 9 | -------------------------------------------------------------------------------- /core/math.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "math" 4 | 5 | func Trunc8(m float64) float64 { 6 | return Trunc(m, 8) 7 | } 8 | 9 | func Trunc(m float64, d int) float64 { 10 | return math.Trunc(m*math.Pow(10, 8)) / math.Pow(10, 8) 11 | } 12 | -------------------------------------------------------------------------------- /core/core_suite_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestCore(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Core Suite") 13 | } 14 | -------------------------------------------------------------------------------- /graph/graph_suite_test.go: -------------------------------------------------------------------------------- 1 | package graph_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestStrategies(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Graph Suite") 13 | } 14 | -------------------------------------------------------------------------------- /core/order_dispatched.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "time" 4 | 5 | // OrderDispatched represents an order 6 | type OrderDispatched struct { 7 | Order *Order `json:"order"` 8 | SentAt time.Time `json:"sentAt"` 9 | ConfirmedAt time.Time `json:"confirmedAt"` 10 | IsSuccess bool `json:"isSuccess"` 11 | Message string `json:"error"` 12 | } 13 | -------------------------------------------------------------------------------- /graph/tree_test.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Tree", func() { 9 | 10 | BeforeEach(func() { 11 | }) 12 | 13 | Describe("Getting started with ginko", func() { 14 | Context("With 101 test", func() { 15 | It("should work", func() { 16 | Expect(1).To(Equal(1)) 17 | }) 18 | }) 19 | }) 20 | 21 | }) 22 | -------------------------------------------------------------------------------- /core/orderbook.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "time" 4 | 5 | // Orderbook represents an orderbook 6 | type Orderbook struct { 7 | CurrencyPair CurrencyPair `json:"pair"` 8 | Bids []Order `json:"bids"` 9 | Asks []Order `json:"asks"` 10 | StartedLastUpdateAt time.Time `json:"startedLastUpdateAt"` 11 | EndedLastUpdateAt time.Time `json:"endedLastUpdateAt"` 12 | } 13 | -------------------------------------------------------------------------------- /graph/path_test.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("PathFinder", func() { 9 | 10 | BeforeEach(func() { 11 | }) 12 | 13 | Describe("Getting started with ginko", func() { 14 | Context("With 101 test", func() { 15 | It("should work", func() { 16 | Expect(1).To(Equal(1)) 17 | }) 18 | }) 19 | }) 20 | 21 | }) 22 | -------------------------------------------------------------------------------- /core/portfolio_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Portfolio", func() { 9 | 10 | var () 11 | 12 | BeforeEach(func() { 13 | }) 14 | 15 | Describe("Getting started with ginko", func() { 16 | Context("With 101 test", func() { 17 | It("should work", func() { 18 | Expect(1).To(Equal(1)) 19 | }) 20 | }) 21 | }) 22 | 23 | }) 24 | -------------------------------------------------------------------------------- /core/currency_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Currency", func() { 9 | 10 | var ( 11 | currency Currency 12 | ) 13 | 14 | BeforeEach(func() { 15 | currency = "BTC" 16 | }) 17 | 18 | Describe("Getting started with ginko", func() { 19 | Context("With 101 test", func() { 20 | It("should work", func() { 21 | Expect(1).To(Equal(1)) 22 | }) 23 | }) 24 | }) 25 | 26 | }) 27 | -------------------------------------------------------------------------------- /core/exchange_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Exchange", func() { 9 | 10 | var ( 11 | currency Currency 12 | ) 13 | 14 | BeforeEach(func() { 15 | currency = "BTC" 16 | }) 17 | 18 | Describe("Getting started with ginko", func() { 19 | Context("With 101 test", func() { 20 | It("should work", func() { 21 | Expect(1).To(Equal(1)) 22 | }) 23 | }) 24 | }) 25 | 26 | }) 27 | -------------------------------------------------------------------------------- /exchanges/binance/binance_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Binance", func() { 9 | 10 | // var ( 11 | // currency Currency 12 | // ) 13 | 14 | // BeforeEach(func() { 15 | // }) 16 | 17 | Describe("Getting started with ginko", func() { 18 | Context("With 101 test", func() { 19 | It("should work", func() { 20 | Expect(1).To(Equal(1)) 21 | }) 22 | }) 23 | }) 24 | 25 | }) 26 | -------------------------------------------------------------------------------- /core/orderbook_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Orderbook", func() { 9 | 10 | var ( 11 | orderbook Orderbook 12 | ) 13 | 14 | BeforeEach(func() { 15 | orderbook = Orderbook{} 16 | }) 17 | 18 | Describe("Getting started with ginko", func() { 19 | Context("With 101 test", func() { 20 | It("should work", func() { 21 | Expect(1).To(Equal(1)) 22 | }) 23 | }) 24 | }) 25 | 26 | }) 27 | -------------------------------------------------------------------------------- /core/currency_pair_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("CurrencyPair", func() { 9 | 10 | var ( 11 | currencyPair CurrencyPair 12 | ) 13 | 14 | BeforeEach(func() { 15 | currencyPair = CurrencyPair{} 16 | }) 17 | 18 | Describe("Getting started with ginko", func() { 19 | Context("With 101 test", func() { 20 | It("should work", func() { 21 | Expect(1).To(Equal(1)) 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /core/exchange_mashup_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("ExchangeMashup", func() { 9 | 10 | var ( 11 | mashup ExchangeMashup 12 | ) 13 | 14 | BeforeEach(func() { 15 | mashup = ExchangeMashup{} 16 | }) 17 | 18 | Describe("Getting started with ginko", func() { 19 | Context("With 101 test", func() { 20 | It("should work", func() { 21 | Expect(1).To(Equal(1)) 22 | }) 23 | }) 24 | }) 25 | 26 | }) 27 | -------------------------------------------------------------------------------- /core/currency_pair.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // CurrencyPair represents a pair of currencies. 4 | type CurrencyPair struct { 5 | Base Currency `json:"base"` 6 | Quote Currency `json:"quote"` 7 | } 8 | 9 | type CurrencyPairSettings struct { 10 | BasePrecision int `json:"basePrecision"` 11 | QuotePrecision int `json:"quotePrecision"` 12 | MinPrice float64 `json:"minPrice"` 13 | MaxPrice float64 `json:"maxPrice"` 14 | MinAmount float64 `json:"minAmount"` 15 | MaxAmount float64 `json:"maxAmount"` 16 | MinTotal float64 `json:"minTotal"` 17 | Fee float64 `json:"fee"` 18 | } 19 | -------------------------------------------------------------------------------- /core/order_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Order", func() { 9 | 10 | var ( 11 | order Order 12 | ) 13 | 14 | BeforeEach(func() { 15 | order = Order{} 16 | }) 17 | 18 | Describe("Testing Order", func() { 19 | Context("Considering orders with fees", func() { 20 | It("should return the correct volume out", func() { 21 | o := Order{} 22 | o.InitBid(0.00000561, 23.73927493) 23 | o.updateVolumesInOut() 24 | Expect(o.BaseVolumeOut).To(Equal(23.679926742675)) 25 | }) 26 | }) 27 | }) 28 | 29 | }) 30 | -------------------------------------------------------------------------------- /strategies/arbitrage/validation.go: -------------------------------------------------------------------------------- 1 | package arbitrage 2 | 3 | import ( 4 | "gotrading/core" 5 | "time" 6 | ) 7 | 8 | type Validation struct { 9 | Report Report 10 | } 11 | 12 | func (v *Validation) Init(exec Execution) { 13 | v.Report = exec.Report 14 | } 15 | 16 | func (v *Validation) Run() { 17 | m := core.SharedPortfolioManager() 18 | 19 | r := &v.Report 20 | r.ValidationStartedAt = time.Now() 21 | 22 | firstHit := r.Orders[0].Hit 23 | lastHit := r.Orders[2].Hit 24 | execIn := m.Position(r.PreExecutionPortfolioStateID, firstHit.Endpoint.Exchange.Name, core.Currency("BTC")) 25 | execOut := m.Position(r.PostExecutionPortfolioStateID, lastHit.Endpoint.Exchange.Name, core.Currency("BTC")) 26 | r.Profit = core.Trunc8(execOut - execIn) 27 | r.SimulationMinusExecution = r.SimulatedProfit - r.Profit 28 | r.ValidationEndedAt = time.Now() 29 | } 30 | -------------------------------------------------------------------------------- /graph/path.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "gotrading/core" 9 | ) 10 | 11 | type Path struct { 12 | Hits []*core.Hit `json:"hits"` 13 | Id *string `json:"id"` 14 | Name *string `json:"description"` 15 | USD float64 16 | } 17 | 18 | func (p *Path) Encode() { 19 | desc := p.Description() 20 | p.Name = &desc 21 | 22 | h := sha1.New() 23 | h.Write([]byte(desc)) 24 | enc := hex.EncodeToString(h.Sum(nil)) 25 | p.Id = &enc 26 | } 27 | 28 | func (p Path) contains(h core.Hit) bool { 29 | found := false 30 | for _, m := range p.Hits { 31 | found = h.IsEqual(*m) 32 | } 33 | return found 34 | } 35 | 36 | func (p Path) Description() string { 37 | str := "" 38 | for _, n := range p.Hits { 39 | str += n.Description() + " -> " 40 | } 41 | return str 42 | } 43 | 44 | func (p Path) Display() { 45 | fmt.Println(p.Description()) 46 | } 47 | -------------------------------------------------------------------------------- /core/hit.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Hit struct { 4 | Endpoint *Endpoint `json:"endpoint"` 5 | IsBaseToQuote bool `json:"isBaseToQuote"` 6 | SoldCurrency Currency `json:"soldCurrency"` 7 | BoughtCurrency Currency `json:"boughtCurrency"` 8 | } 9 | 10 | func (h Hit) IsEqual(hit Hit) bool { 11 | return h.Endpoint.IsEqual(*hit.Endpoint) 12 | } 13 | 14 | func (h Hit) Description() string { 15 | var str string 16 | if h.IsBaseToQuote { 17 | str = "[" + string(h.Endpoint.From) + "-/+" + string(h.Endpoint.To) + "]@" + h.Endpoint.Exchange.Name 18 | } else { 19 | str = "[" + string(h.Endpoint.From) + "+/-" + string(h.Endpoint.To) + "]@" + h.Endpoint.Exchange.Name 20 | } 21 | return str 22 | } 23 | 24 | func (h Hit) ID() string { 25 | var str string 26 | if h.IsBaseToQuote { 27 | str = string(h.Endpoint.From) + "-" + string(h.Endpoint.To) + "@" + h.Endpoint.Exchange.Name 28 | } else { 29 | str = string(h.Endpoint.To) + "-" + string(h.Endpoint.From) + "@" + h.Endpoint.Exchange.Name 30 | } 31 | return str 32 | } 33 | -------------------------------------------------------------------------------- /core/endpoint.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Endpoint struct { 9 | From Currency `json:"from"` 10 | To Currency `json:"to"` 11 | Exchange Exchange `json:"exchange"` 12 | Orderbook *Orderbook `json:"orderbook"` 13 | } 14 | 15 | type EndpointLookup struct { 16 | Endpoint *Endpoint 17 | PathsCount int 18 | } 19 | 20 | func (e Endpoint) display() { 21 | fmt.Println(e.Description()) 22 | } 23 | 24 | func (e Endpoint) IsEqual(m Endpoint) bool { 25 | f := (strings.Compare(string(e.From), string(m.From)) == 0) 26 | t := (strings.Compare(string(e.To), string(m.To)) == 0) 27 | fi := (strings.Compare(string(e.To), string(m.From)) == 0) 28 | ti := (strings.Compare(string(e.From), string(m.To)) == 0) 29 | exch := (strings.Compare(e.Exchange.Name, m.Exchange.Name) == 0) 30 | return f && t && exch || fi && ti && exch 31 | } 32 | 33 | func (e Endpoint) Description() string { 34 | var str string 35 | str = string(e.From) + " / " + string(e.To) + " (" + e.Exchange.Name + ")" 36 | return str 37 | } 38 | 39 | func (e Endpoint) ID() string { 40 | var str string 41 | str = string(e.From) + "+" + string(e.To) + "@" + e.Exchange.Name 42 | return str 43 | } 44 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/json-iterator/go" 26 | version = "1.0.4" 27 | 28 | [[constraint]] 29 | name = "github.com/onsi/ginkgo" 30 | version = "1.4.0" 31 | 32 | [[constraint]] 33 | name = "github.com/onsi/gomega" 34 | version = "1.3.0" 35 | 36 | [[constraint]] 37 | name = "github.com/satori/go.uuid" 38 | version = "1.1.0" 39 | 40 | [[constraint]] 41 | name = "github.com/spf13/viper" 42 | version = "1.0.0" 43 | 44 | [[constraint]] 45 | branch = "master" 46 | name = "github.com/streadway/amqp" 47 | 48 | [[constraint]] 49 | branch = "master" 50 | name = "golang.org/x/crypto" 51 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exchanges": { 3 | "Liqui": { 4 | "api_key": "TAU3HM65-M30J2QRG-9I8OQ3AA-MOA47E5L-S8TWMS9B", 5 | "api_secret": "604b4cca615b6510ffbff19b427d3de20e018f47186e6e30bff1828d3c46a9a9", 6 | "verbose": false, 7 | "max_requests_per_sec": 4, 8 | "pairs_enabled": "ETH_BTC,TRX_ETH,TRX_BTC" 9 | }, 10 | "Binance": { 11 | "api_key": "lF3Mp2Vo2D7Xf3Rl5oT7VAiLMrzrJbgatoaRVLdx5FSaShEbQKpx7g52CEaCQpLv", 12 | "api_secret": "BIa7PhhcCJvAIskPWBUXFaDHC6EpHVxjfoEGDcnrLV9P7k4JnKflTW9Z2YbskDnR", 13 | "verbose": false, 14 | "max_requests_per_sec": 2, 15 | "pairs_enabled": "ETH_BTC,TRX_ETH,TRX_BTC" 16 | } 17 | }, 18 | "strategies": { 19 | "arbitrage": { 20 | "forceExecution": false, 21 | "hits_per_sec": 4, 22 | "from_currency": "BTC", 23 | "to_currency": "BTC", 24 | "shifts_count": 3, 25 | "exchanges_enabled": "Binance", 26 | "exchange_crossing_enabled": false, 27 | "exchanges": { 28 | "Liqui": { 29 | "pairs_enabled": "ETH_BTC,TRX_ETH,TRX_BTC" 30 | }, 31 | "Binance": { 32 | "pairs_enabled": "ETH_BTC,TRX_ETH,TRX_BTC" 33 | } 34 | }, 35 | "reporting": { 36 | "publisher": { 37 | "url": "amqp://yqkpiqzz:aew9v2ZoAprCB339ZAu_TlVmjRlzJryL@spider.rmq.cloudamqp.com/yqkpiqzz", 38 | "exchange_name": "arbitrage.routing", 39 | "routing_key": "usd.btc" 40 | } 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /strategies/arbitrage/execution.go: -------------------------------------------------------------------------------- 1 | package arbitrage 2 | 3 | import ( 4 | "gotrading/core" 5 | "gotrading/networking" 6 | "math" 7 | "time" 8 | ) 9 | 10 | type Execution struct { 11 | Report Report 12 | } 13 | 14 | func round(num float64) int { 15 | return int(num + math.Copysign(0.5, num)) 16 | } 17 | 18 | func toFixed(num float64, precision int) float64 { 19 | output := math.Pow(10, float64(precision)) 20 | return float64(round(num*output)) / output 21 | } 22 | 23 | func (exec *Execution) Init(sim Simulation) { 24 | exec.Report = sim.Report 25 | } 26 | 27 | func (exec *Execution) Run() { 28 | m := core.SharedPortfolioManager() 29 | 30 | r := &exec.Report 31 | r.IsExecutionSuccessful = true 32 | r.ExecutionStartedAt = time.Now() 33 | r.PreExecutionPortfolioStateID = m.LastStateID 34 | 35 | batch := networking.Batch{} 36 | batch.PostOrders(r.Orders, func(dispatched []core.OrderDispatched) { 37 | 38 | r.DispatchedOrders = dispatched 39 | r.PostExecutionPortfolioStateID = m.LastStateID 40 | r.ExecutionEndedAt = time.Now() 41 | }) 42 | 43 | // for i, o := range r.Orders { 44 | // exchange := o.Hit.Endpoint.Exchange 45 | // exchange.PostOrder(o) 46 | 47 | // if error != nil { 48 | // r.Results[i] = error.Error() 49 | // r.ExecutionEndedAt = time.Now() 50 | // r.IsExecutionSuccessful = false 51 | // return 52 | // } else { 53 | // r.Results[i] = "hit" // Taker? Maker? 54 | // } 55 | // } 56 | } 57 | 58 | func (exec *Execution) IsSuccessful() bool { 59 | return exec.Report.IsExecutionSuccessful 60 | } 61 | -------------------------------------------------------------------------------- /reporting/publisher.go: -------------------------------------------------------------------------------- 1 | package reporting 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/streadway/amqp" 9 | ) 10 | 11 | type Publisher struct { 12 | channel *amqp.Channel 13 | connexion *amqp.Connection 14 | exchangeName string 15 | routingKey string 16 | } 17 | 18 | func (pub *Publisher) Init(params map[string]string) { 19 | var err error 20 | 21 | pub.connexion, err = amqp.Dial(params["url"]) 22 | failOnError(err, "Failed to connect to RabbitMQ") 23 | 24 | pub.channel, err = pub.connexion.Channel() 25 | failOnError(err, "Failed to open a channel") 26 | 27 | pub.exchangeName = params["exchange_name"] 28 | pub.routingKey = params["routing_key"] 29 | 30 | err = pub.channel.ExchangeDeclare( 31 | pub.exchangeName, // name 32 | "topic", // type 33 | true, // durable 34 | false, // auto-deleted 35 | false, // internal 36 | false, // no-wait 37 | nil, // arguments 38 | ) 39 | failOnError(err, "Failed to declare an exchange") 40 | } 41 | 42 | func (pub *Publisher) Close() { 43 | pub.connexion.Close() 44 | pub.channel.Close() 45 | } 46 | 47 | func (pub *Publisher) Send(report Report) { 48 | marshal, err := report.Encode() 49 | if err != nil { 50 | fmt.Println("Error encoding report", err.Error()) 51 | return 52 | } 53 | pub.channel.Publish( 54 | pub.exchangeName, // exchange 55 | pub.routingKey, // routing key 56 | false, // mandatory 57 | false, // immediate 58 | amqp.Publishing{ 59 | ContentType: "application/json", 60 | Body: []byte(marshal), 61 | }) 62 | } 63 | 64 | func failOnError(err error, msg string) { 65 | if err != nil { 66 | log.Fatalf("%s: %s", msg, err) 67 | time.Sleep(20 * time.Second) 68 | panic(fmt.Sprintf("%s: %s", msg, err)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /networking/batch.go: -------------------------------------------------------------------------------- 1 | package networking 2 | 3 | import ( 4 | "gotrading/core" 5 | ) 6 | 7 | type Batch struct { 8 | } 9 | 10 | type orderbooksFetched func(orderbooks []*core.Orderbook) 11 | 12 | type ordersPosted func(orders []core.OrderDispatched) 13 | 14 | type sortedOrderbook struct { 15 | Index int 16 | Orderbook *core.Orderbook 17 | } 18 | 19 | type sortedOrder struct { 20 | Index int 21 | OrderDispatched core.OrderDispatched 22 | } 23 | 24 | func (b *Batch) GetOrderbooks(hits []*core.Hit, fn orderbooksFetched) { 25 | g := SharedGatling() 26 | 27 | orderbooks := make([]*core.Orderbook, len(hits)) 28 | c := make(chan sortedOrderbook, len(hits)) 29 | 30 | for i, h := range hits { 31 | if len(g.Clients) > 1 { 32 | go b.GetOrderbook(h, i, c) 33 | } else { 34 | b.GetOrderbook(h, i, c) 35 | } 36 | } 37 | 38 | for range hits { 39 | elem := <-c 40 | orderbooks[elem.Index] = elem.Orderbook 41 | } 42 | close(c) 43 | fn(orderbooks) 44 | } 45 | 46 | func (b *Batch) GetOrderbook(hit *core.Hit, i int, c chan sortedOrderbook) { 47 | exchange := hit.Endpoint.Exchange 48 | 49 | o, _ := exchange.GetOrderbook(*hit) 50 | c <- sortedOrderbook{i, &o} 51 | } 52 | 53 | func (b *Batch) PostOrders(orders []core.Order, fn ordersPosted) { 54 | g := SharedGatling() 55 | 56 | dispOrders := make([]core.OrderDispatched, len(orders)) 57 | c := make(chan sortedOrder, len(orders)) 58 | 59 | for i, o := range orders { 60 | if len(g.Clients) > 1 { 61 | go b.PostOrder(o, i, c) 62 | } else { 63 | b.PostOrder(o, i, c) 64 | } 65 | } 66 | 67 | for range orders { 68 | elem := <-c 69 | dispOrders[elem.Index] = elem.OrderDispatched 70 | } 71 | close(c) 72 | fn(dispOrders) 73 | } 74 | 75 | func (b *Batch) PostOrder(order core.Order, i int, c chan sortedOrder) { 76 | exchange := order.Hit.Endpoint.Exchange 77 | 78 | od, _ := exchange.PostOrder(order) 79 | c <- sortedOrder{i, od} 80 | } 81 | -------------------------------------------------------------------------------- /core/exchange_mashup.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // ExchangeMashup allowing to identify available bridges between currencies accross exchanges 4 | type ExchangeMashup struct { 5 | Currencies []Currency 6 | Exchanges []Exchange 7 | CurrenciesLookup map[Currency]int 8 | ExchangesLookup map[string]int 9 | Links [][][]bool 10 | } 11 | 12 | // Init initializes a mashup 13 | func (m *ExchangeMashup) Init(exchanges []Exchange) { 14 | m.CurrenciesLookup = make(map[Currency]int) 15 | m.Currencies = make([]Currency, 0) 16 | m.Exchanges = exchanges 17 | m.ExchangesLookup = make(map[string]int, len(exchanges)) 18 | 19 | for i, exch := range exchanges { 20 | for _, pair := range exch.PairsEnabled { 21 | 22 | _, ok := m.CurrenciesLookup[pair.Base] 23 | if !ok { 24 | m.Currencies = append(m.Currencies, pair.Base) 25 | m.CurrenciesLookup[pair.Base] = len(m.Currencies) - 1 26 | } 27 | _, ok = m.CurrenciesLookup[pair.Quote] 28 | if !ok { 29 | m.Currencies = append(m.Currencies, pair.Quote) 30 | m.CurrenciesLookup[pair.Quote] = len(m.Currencies) - 1 31 | 32 | } 33 | } 34 | m.ExchangesLookup[exch.Name] = i 35 | } 36 | 37 | m.Links = make([][][]bool, len(m.Currencies)) 38 | for i := range m.Currencies { 39 | m.Links[i] = make([][]bool, len(m.Currencies)) 40 | for j := range m.Currencies { 41 | m.Links[i][j] = make([]bool, len(exchanges)) 42 | for z := range exchanges { 43 | m.Links[i][j][z] = false 44 | } 45 | } 46 | } 47 | 48 | for _, exch := range exchanges { 49 | for _, pair := range exch.PairsEnabled { 50 | m.Links[m.CurrenciesLookup[pair.Base]][m.CurrenciesLookup[pair.Quote]][m.ExchangesLookup[exch.Name]] = true 51 | } 52 | } 53 | } 54 | 55 | // LinkExist returns true if a currency pair exists for a given exchange 56 | func (m *ExchangeMashup) LinkExist(base Currency, quote Currency, exch Exchange) bool { 57 | ok := m.Links[m.CurrenciesLookup[base]][m.CurrenciesLookup[quote]][m.ExchangesLookup[exch.Name]] 58 | return ok 59 | } 60 | -------------------------------------------------------------------------------- /core/nonce.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Nonce struct holds the nonce value 10 | type Nonce struct { 11 | // Standard nonce 12 | n int64 13 | mtx sync.Mutex 14 | // Hash table exclusive exchange specific nonce values 15 | boundedCall map[string]int64 16 | boundedMtx sync.Mutex 17 | } 18 | 19 | // Inc increments the nonce value 20 | func (n *Nonce) Inc() { 21 | n.mtx.Lock() 22 | n.n++ 23 | n.mtx.Unlock() 24 | } 25 | 26 | // Get retrives the nonce value 27 | func (n *Nonce) Get() int64 { 28 | n.mtx.Lock() 29 | defer n.mtx.Unlock() 30 | return n.n 31 | } 32 | 33 | // GetInc increments and returns the value of the nonce 34 | func (n *Nonce) GetInc() int64 { 35 | n.mtx.Lock() 36 | defer n.mtx.Unlock() 37 | n.n++ 38 | return n.n 39 | } 40 | 41 | // Set sets the nonce value 42 | func (n *Nonce) Set(val int64) { 43 | n.mtx.Lock() 44 | n.n = val 45 | n.mtx.Unlock() 46 | } 47 | 48 | // Returns a string version of the nonce 49 | func (n *Nonce) String() string { 50 | n.mtx.Lock() 51 | result := strconv.FormatInt(n.n, 10) 52 | n.mtx.Unlock() 53 | return result 54 | } 55 | 56 | // Value is a return type for GetValue 57 | type Value int64 58 | 59 | // GetValue returns a nonce value and can be set as a higher precision. Values 60 | // stored in an exchange specific hash table using a single locked call. 61 | func (n *Nonce) GetValue(exchName string, nanoPrecision bool) Value { 62 | n.boundedMtx.Lock() 63 | defer n.boundedMtx.Unlock() 64 | 65 | if n.boundedCall == nil { 66 | n.boundedCall = make(map[string]int64) 67 | } 68 | 69 | if n.boundedCall[exchName] == 0 { 70 | if nanoPrecision { 71 | n.boundedCall[exchName] = time.Now().UnixNano() 72 | return Value(n.boundedCall[exchName]) 73 | } 74 | n.boundedCall[exchName] = time.Now().Unix() 75 | return Value(n.boundedCall[exchName]) 76 | } 77 | n.boundedCall[exchName]++ 78 | return Value(n.boundedCall[exchName]) 79 | } 80 | 81 | // String is a Value method that changes format to a string 82 | func (v Value) String() string { 83 | return strconv.FormatInt(int64(v), 10) 84 | } 85 | -------------------------------------------------------------------------------- /exchanges/factory.go: -------------------------------------------------------------------------------- 1 | package exchanges 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "gotrading/core" 8 | "gotrading/exchanges/binance" 9 | "gotrading/exchanges/liqui" 10 | 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type Factory struct { 15 | } 16 | 17 | // standardizedExchange enforces standard functions for all supported exchanges 18 | type standardizedExchange interface { 19 | GetSettings() func() (core.ExchangeSettings, error) 20 | GetOrderbook() func(hit core.Hit) (core.Orderbook, error) 21 | GetPortfolio() func(core.ExchangeSettings) (core.Portfolio, error) 22 | PostOrder() func(core.Order, core.ExchangeSettings) (core.OrderDispatched, error) 23 | // Deposit(client http.Client) (bool, error) 24 | // Withdraw(client http.Client) (bool, error) 25 | } 26 | 27 | func (f *Factory) BuildExchange(name string) core.Exchange { 28 | key := strings.Join([]string{"exchanges", name}, ".") 29 | config := viper.GetStringMapString(key) 30 | fmt.Println("Building", name, config) 31 | 32 | exchange := core.Exchange{} 33 | exchange.Name = name 34 | exchange.LoadPairsEnabled(config["pairs_enabled"]) 35 | switch name { 36 | case "Binance": 37 | injectStandardizedMethods(&exchange, binance.Binance{}) 38 | case "Liqui": 39 | injectStandardizedMethods(&exchange, liqui.Liqui{}) 40 | default: 41 | } 42 | exchange.LoadSettings() 43 | exchange.ExchangeSettings.APIKey = config["api_key"] 44 | exchange.ExchangeSettings.APISecret = config["api_secret"] 45 | portfolio, err := exchange.GetPortfolio() 46 | if err == nil { 47 | manager := core.SharedPortfolioManager() 48 | state := core.NewPortfolioStateFromPositions(portfolio.Positions) 49 | manager.UpdateWithNewState(state, false) 50 | } else { 51 | fmt.Println("Error while fetching portfolio", err) 52 | } 53 | return exchange 54 | } 55 | 56 | func injectStandardizedMethods(b *core.Exchange, exch standardizedExchange) { 57 | b.FuncGetSettings = exch.GetSettings() 58 | b.FuncGetOrderbook = exch.GetOrderbook() 59 | b.FuncGetPortfolio = exch.GetPortfolio() 60 | b.FuncPostOrder = exch.PostOrder() 61 | // b.fnDeposit = exch.Deposit 62 | // b.fnWithdraw = exch.Withdraw 63 | } 64 | -------------------------------------------------------------------------------- /core/exchange.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | type Exchange struct { 9 | Name string `json:"name"` 10 | PairsEnabled []CurrencyPair `json:"-"` 11 | ExchangeSettings ExchangeSettings `json:"-"` 12 | 13 | FuncGetSettings func() (ExchangeSettings, error) `json:"-"` 14 | FuncGetOrderbook func(hit Hit) (Orderbook, error) `json:"-"` 15 | FuncGetPortfolio func(settings ExchangeSettings) (Portfolio, error) `json:"-"` 16 | FuncPostOrder func(order Order, settings ExchangeSettings) (OrderDispatched, error) `json:"-"` 17 | // fnDeposit func(client http.Client) (bool, error) 18 | // fnWithdraw func(client http.Client) (bool, error) 19 | } 20 | 21 | type ExchangeSettings struct { 22 | Name string `json:"-"` 23 | APIKey string `json:"-"` 24 | APISecret string `json:"-"` 25 | AvailablePairs []CurrencyPair `json:"-"` 26 | PairsSettings map[CurrencyPair]CurrencyPairSettings `json:"-"` 27 | IsCurrencyPairNormalized bool `json:"-"` 28 | Nonce Nonce `json:"-"` 29 | } 30 | 31 | func (e *Exchange) LoadSettings() { 32 | settings, err := e.FuncGetSettings() 33 | if err == nil { 34 | nonce := Nonce{} 35 | nonce.Set(time.Now().Unix()) 36 | settings.Nonce = nonce 37 | settings.Name = e.Name 38 | e.ExchangeSettings = settings 39 | } 40 | } 41 | 42 | func (e *Exchange) GetOrderbook(hit Hit) (Orderbook, error) { 43 | return e.FuncGetOrderbook(hit) 44 | } 45 | 46 | func (e *Exchange) GetPortfolio() (Portfolio, error) { 47 | return e.FuncGetPortfolio(e.ExchangeSettings) 48 | } 49 | 50 | func (e *Exchange) PostOrder(order Order) (OrderDispatched, error) { 51 | return e.FuncPostOrder(order, e.ExchangeSettings) 52 | } 53 | 54 | func (e *Exchange) LoadPairsEnabled(joinedPairs string) { 55 | pairs := strings.Split(joinedPairs, ",") 56 | e.PairsEnabled = []CurrencyPair{} 57 | for _, pair := range pairs { 58 | c := strings.Split(pair, "_") 59 | e.PairsEnabled = append( 60 | e.PairsEnabled, 61 | CurrencyPair{Currency(c[0]), Currency(c[1])}) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "gotrading/core" 10 | "gotrading/exchanges" 11 | "gotrading/graph" 12 | "gotrading/reporting" 13 | "gotrading/strategies/arbitrage" 14 | 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | func main() { 19 | 20 | viper.SetConfigName("config") 21 | viper.AddConfigPath(".") 22 | 23 | err := viper.ReadInConfig() 24 | if err != nil { 25 | panic(fmt.Errorf("Fatal error config file: %s \n", err)) 26 | } 27 | 28 | factory := exchanges.Factory{} 29 | exchanges := []core.Exchange{} 30 | 31 | arbitrageSettings := viper.GetStringMapString("strategies.arbitrage") 32 | exchangesEnabled := strings.Split(arbitrageSettings["exchanges_enabled"], ",") 33 | 34 | for _, name := range exchangesEnabled { 35 | exch := factory.BuildExchange(name) 36 | exchanges = append(exchanges, exch) 37 | } 38 | 39 | mashup := core.ExchangeMashup{} 40 | mashup.Init(exchanges) 41 | 42 | from := core.Currency(arbitrageSettings["from_currency"]) 43 | to := core.Currency(arbitrageSettings["to_currency"]) 44 | depth, _ := strconv.Atoi(arbitrageSettings["shifts_count"]) 45 | treeOfPossibles, _, _, _ := graph.PathFinder(mashup, from, to, depth) 46 | 47 | publisher := reporting.Publisher{} 48 | publisher.Init(viper.GetStringMapString("strategies.arbitrage.reporting.publisher")) 49 | defer publisher.Close() 50 | 51 | manager := core.SharedPortfolioManager() 52 | i := manager.CurrentPosition("Liqui", core.Currency("BTC")) 53 | fmt.Println(i) 54 | fmt.Println("----") 55 | forceExecution := viper.GetBool("strategies.arbitrage.forceExecution") 56 | 57 | for { 58 | treeOfPossibles.DepthTraversing(func(hits []*core.Hit) { 59 | 60 | sim := arbitrage.Simulation{} 61 | sim.Init(hits) 62 | sim.Run() 63 | 64 | if forceExecution == true { 65 | if sim.IsExecutable() == false { 66 | return 67 | } 68 | 69 | exec := arbitrage.Execution{} 70 | exec.Init(sim) 71 | exec.Run() 72 | 73 | valid := arbitrage.Validation{} 74 | valid.Init(exec) 75 | valid.Run() 76 | publisher.Send(valid.Report) 77 | 78 | os.Exit(3) 79 | } else { 80 | if sim.IsExecutable() == false { 81 | return 82 | } 83 | go publisher.Send(sim.Report) 84 | 85 | exec := arbitrage.Execution{} 86 | exec.Init(sim) 87 | exec.Run() 88 | if exec.IsSuccessful() == false { 89 | go publisher.Send(exec.Report) 90 | // Recovery? Rollback? 91 | return 92 | } 93 | 94 | valid := arbitrage.Validation{} 95 | valid.Init(exec) 96 | valid.Run() 97 | publisher.Send(valid.Report) 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /graph/tree.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "gotrading/core" 5 | "strconv" 6 | ) 7 | 8 | type Tree struct { 9 | Roots []*Vertice 10 | Name string 11 | } 12 | 13 | type Vertice struct { 14 | Hit *core.Hit 15 | Children []*Vertice 16 | Ancestor *Vertice 17 | IsRoot bool 18 | } 19 | 20 | type depthTraversing func(hits []*core.Hit) 21 | 22 | func (t *Tree) InsertPath(p Path) { 23 | var v *Vertice 24 | for i, n := range p.Hits { 25 | if i == 0 { 26 | v = t.FindOrCreateRoot(n) 27 | } else { 28 | newNode := v.FindOrCreateChild(n) 29 | v = newNode 30 | } 31 | } 32 | } 33 | 34 | func (t *Tree) FindOrCreateRoot(h *core.Hit) *Vertice { 35 | for _, r := range t.Roots { 36 | if h.IsEqual(*r.Hit) { 37 | return r 38 | } 39 | } 40 | r := createVertice(h) 41 | r.IsRoot = true 42 | r.Ancestor = nil 43 | t.Roots = append(t.Roots, r) 44 | return r 45 | } 46 | 47 | func createVertice(h *core.Hit) *Vertice { 48 | childs := make([]*Vertice, 0) 49 | v := Vertice{h, childs, nil, false} 50 | return &v 51 | } 52 | 53 | func (v *Vertice) RightSibling() *Vertice { 54 | if v.IsRoot == false { 55 | siblings := v.Ancestor.Children 56 | for i, s := range siblings { 57 | if v.Hit.IsEqual(*s.Hit) { 58 | if i+1 < len(siblings) { 59 | return siblings[i+1] 60 | } 61 | } 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | func (v *Vertice) LeftSibling() *Vertice { 68 | if v.IsRoot == false { 69 | siblings := v.Ancestor.Children 70 | for i, s := range siblings { 71 | if v.Hit.IsEqual(*s.Hit) { 72 | if i+1 < len(siblings) { 73 | return siblings[i+1] 74 | } 75 | } 76 | } 77 | } 78 | return nil 79 | } 80 | 81 | func (v *Vertice) FindOrCreateChild(h *core.Hit) *Vertice { 82 | for _, c := range v.Children { 83 | if h.IsEqual(*c.Hit) { 84 | return c 85 | } 86 | } 87 | c := createVertice(h) 88 | c.IsRoot = false 89 | c.Ancestor = v 90 | v.Children = append(v.Children, c) 91 | return c 92 | } 93 | 94 | func (t Tree) DepthTraversing(fn depthTraversing) { 95 | for _, r := range t.Roots { 96 | ancestors := []*Vertice{r} 97 | hits := []*core.Hit{r.Hit} 98 | if r.IsLeaf() { 99 | fn(hits) 100 | } else { 101 | r.depthTraversing(ancestors, hits, fn) 102 | } 103 | } 104 | } 105 | 106 | func (v Vertice) IsLeaf() bool { 107 | return len(v.Children) == 0 108 | } 109 | 110 | func (v Vertice) depthTraversing(ancestors []*Vertice, hits []*core.Hit, fn depthTraversing) { 111 | for _, c := range v.Children { 112 | vertices := append(ancestors, c) 113 | contents := append(hits, c.Hit) 114 | if c.IsLeaf() { 115 | fn(contents) 116 | } else { 117 | c.depthTraversing(vertices, contents, fn) 118 | } 119 | } 120 | } 121 | 122 | func (t Tree) Description() string { 123 | str := strconv.Itoa(len(t.Roots)) 124 | return str 125 | } 126 | -------------------------------------------------------------------------------- /graph/path_finder_test.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "gotrading/core" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Finding path between pairs, within and accross exchanges", func() { 11 | 12 | var () 13 | 14 | BeforeEach(func() { 15 | }) 16 | 17 | Describe(` 18 | Considering the 3 pairs: [ABC/DEF DEF/XYZ XYZ/ABC] available on Exhange1`, func() { 19 | Context(` 20 | When looking for the available paths within Exhange1, starting and ending with ABC 21 | `, func() { 22 | var ( 23 | exchanges []core.Exchange 24 | mashup core.ExchangeMashup 25 | from core.Currency 26 | to core.Currency 27 | hits []*Hit 28 | lookups map[string][]Path 29 | paths []Path 30 | ) 31 | 32 | BeforeEach(func() { 33 | abc := core.Currency("ABC") 34 | def := core.Currency("DEF") 35 | xyz := core.Currency("XYZ") 36 | 37 | p1 := core.CurrencyPair{abc, def} 38 | p2 := core.CurrencyPair{def, xyz} 39 | p3 := core.CurrencyPair{xyz, abc} 40 | 41 | exchange1 := core.Exchange{"Exchange1", []core.CurrencyPair{p1, p2, p3}, nil} 42 | 43 | exchanges = []core.Exchange{exchange1} 44 | mashup = core.ExchangeMashup{} 45 | mashup.Init(exchanges) 46 | from = abc 47 | to = from 48 | depth := 3 49 | hits, lookups, paths = PathFinder(mashup, from, to, depth) 50 | }) 51 | 52 | It("should return 2 paths", func() { 53 | // [ABC-/+DEF] -> [DEF-/+XYZ] -> [XYZ-/+ABC] 54 | // [XYZ+/-ABC] -> [DEF+/-XYZ] -> [ABC+/-DEF] 55 | Expect(len(paths)).To(Equal(2)) 56 | }) 57 | }) 58 | }) 59 | 60 | Describe(` 61 | Considering the 3 pairs: [BTC/EUR XBT/EUR BTC/XBT] available on Exhange1`, func() { 62 | Context(` 63 | When looking for the available paths within Exhange1, starting and ending with BTC 64 | `, func() { 65 | var ( 66 | exchanges []core.Exchange 67 | mashup core.ExchangeMashup 68 | from core.Currency 69 | to core.Currency 70 | hits []*Hit 71 | lookups map[string][]Path 72 | paths []Path 73 | ) 74 | 75 | BeforeEach(func() { 76 | btc := core.Currency("BTC") 77 | eur := core.Currency("EUR") 78 | xbt := core.Currency("XBT") 79 | 80 | p1 := core.CurrencyPair{btc, eur} 81 | p2 := core.CurrencyPair{xbt, eur} 82 | p3 := core.CurrencyPair{btc, xbt} 83 | 84 | exchange1 := core.Exchange{"Exchange1", []core.CurrencyPair{p1, p2, p3}, nil} 85 | 86 | exchanges = []core.Exchange{exchange1} 87 | mashup = core.ExchangeMashup{} 88 | mashup.Init(exchanges) 89 | from = btc 90 | to = from 91 | depth := 3 92 | hits, lookups, paths = PathFinder(mashup, from, to, depth) 93 | }) 94 | 95 | It("should return 2 paths", func() { 96 | // [BTC-/+XBT] -> [XBT-/+EUR] -> [BTC+/-EUR] 97 | // [BTC-/+EUR] -> [XBT+/-EUR] -> [BTC+/-XBT] 98 | Expect(len(paths)).To(Equal(2)) 99 | }) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Gotrading logo](./assets/gotrading.png) 3 | 4 | 5 | 6 | ## Getting Started 7 | 8 | I started this side project on my spare time a few months ago. The idea was too build a library that would help getting started with algotrading, and give me the ability to pass orders on any exchange. 9 | 10 | #### Features 11 | 12 | - [x] Sending orders to Liqui 13 | - [x] Sending orders to Binance 14 | - [x] Working around API rate limits 😎 15 | 16 | I spent some time writing and re-writing the abstraction for adding more exchanges, it should now take 3 to 4 hours (testing not included) to add a new exchange. 17 | 18 | #### Status 19 | 20 | I need a few days/weeks to clean the project, write some documentation, consolidate and write some tests, before being able to have this bot running 24/7. 21 | I recently shifted my spare time on playing with machine learning, I'll probably go back to this project at some point (depending on how is the community answering), but can't give an ETA at this point. 22 | 23 | 24 | ## Demo 25 | 26 | Short video of the UI I've been developing for visualizing and debugging my trades (built with React + D3, listening to RabbitMQ events sent by the bot). 27 | 28 | [![Gotrading demo](https://img.youtube.com/vi/P-G78LB2LfU/0.jpg)](http://www.youtube.com/watch?v=P-G78LB2LfU) 29 | 30 | This dashboard is a separate project that I can also open source, just open an issue on this repo if you're interested. 31 | 32 | 33 | ## Architecture 34 | 35 | #### Worker 36 | First, we need to instanciate the EC2 or `gatling`, that will be responsible for fetching the quotes and sending orders (ideally one per exchange). This node needs to be in the same / nearest datacenter hosting the exchange (`ap-northeast-1a` for Binance), in order to limit latency. 37 | 38 | The configuration of this node is a bit special: we will be attaching as many Virtual Network Interfaces as possible (2 for a t2-micro), and attaching as many Elastic IP as possible (2 EIP / VNI for t2-micro). 39 | 40 | This part of the project is managed with Terraform (also in a separate repo, open a new issue if you're interested): 41 | 42 | ``` 43 | module "binance" { 44 | source = "../modules/instances/worker" 45 | 46 | instance_type = "t2.micro" 47 | ami = "ami-12572374" 48 | 49 | network_interfaces_count = 2 50 | ips_per_network_interface = 2 51 | 52 | availability_zone = "ap-northeast-1a" 53 | } 54 | 55 | ``` 56 | 57 | Thanks to this tuning, instead of having our worker being limited to **5** req/sec, we can have **20** req/sec, and we could theoritically scale this limit to **3,750** req/sec with a `c5d.18xlarge`. 58 | 59 | This parameter is important, since the first strategy implemented is an arbitrage using 3 quotes on one exchange (ex: BTC→ETH, ETH→TRX, TRX→BTC). 60 | By fetching the 3 quotes 6 times per seconds, we are more reactive than the users getting the quotes using the websocket API. 61 | 62 | 63 | ## How to use 64 | 65 | This project is designed in layers free of inter-dependencies, and you should be able to be used as a library. 66 | 67 | - core: types required for modelizing the space (Currency, Pair, Orderbook, etc) 68 | - networking: mainly abstracting the concept of `gatling` 69 | - graph: utils for manipulating the graph of pairs available on one exchange. 70 | - exchanges: where new exchanges should be added 71 | 72 | You can also definitely have a look at the code available in `./strategies`, to have a better of how everything can be orchestrated. 73 | 74 | 75 | ## License 76 | 77 | Author: Ludovic Galabru 78 | 79 | This project is licensed under the MIT License. 80 | 81 | ## Credits 82 | 83 | [@egonelbre](https://github.com/egonelbre/gophers) for the gopher :) 84 | 85 | -------------------------------------------------------------------------------- /strategies/arbitrage/report.go: -------------------------------------------------------------------------------- 1 | package arbitrage 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "time" 9 | 10 | "gotrading/core" 11 | ) 12 | 13 | type Report struct { 14 | ID *string `json:"id"` 15 | Title string `json:"title"` 16 | Orders []core.Order `json:"orders"` 17 | DispatchedOrders []core.OrderDispatched `json:"dispatchedOrders"` 18 | Performance float64 `json:"performance"` 19 | SimulatedProfit float64 `json:"simulatedProfit"` 20 | Profit float64 `json:"profit"` 21 | Rates []float64 `json:"rates"` 22 | AdjustedVolumes []float64 `json:"volumes"` 23 | VolumeToEngage float64 `json:"volumeToEngage"` 24 | VolumeIn float64 `json:"volumeIn"` 25 | VolumeOut float64 `json:"volumeOut"` 26 | Cost float64 `json:"cost"` 27 | IsTradedVolumeEnough bool `json:"isTradedVolumeEnough"` 28 | SimulationStartedAt time.Time `json:"simulationStartedAt"` 29 | SimulationFetchingStartedAt time.Time `json:"simulationFetchingStartedAt"` 30 | SimulationComputingStartedAt time.Time `json:"simulationComputingStartedAt"` 31 | SimulationEndedAt time.Time `json:"simulationEndedAt"` 32 | IsSimulationIncomplete bool `json:"isSimulationIncomplete"` 33 | IsSimulationSuccessful bool `json:"isSimulationSuccessful"` 34 | ExecutionStartedAt time.Time `json:"executionStartedAt"` 35 | ExecutionEndedAt time.Time `json:"executionEndedAt"` 36 | IsExecutionSuccessful bool `json:"isExecutionSuccessful"` 37 | ValidationStartedAt time.Time `json:"validationStartedAt"` 38 | ValidationEndedAt time.Time `json:"validationEndedAt"` 39 | SimulationMinusExecution float64 `json:"simulationMinusExecution"` 40 | PreExecutionPortfolioStateID string `json:"-"` 41 | PostExecutionPortfolioStateID string `json:"-"` 42 | PreExecutionPortfolioState core.PortfolioState `json:"statePreExecution"` 43 | PostExecutionPortfolioState core.PortfolioState `json:"statePostExecution"` 44 | } 45 | 46 | func (r Report) Encode() ([]byte, error) { 47 | desc := "Report" 48 | r.SimulationFetchingStartedAt = r.SimulationComputingStartedAt 49 | for _, o := range r.Orders { 50 | if o.Hit == nil { 51 | return nil, fmt.Errorf("Order incomplete") 52 | } 53 | desc = desc + " / " + o.Hit.Endpoint.Description() 54 | if o.Hit.Endpoint.Orderbook.StartedLastUpdateAt.Before(r.SimulationFetchingStartedAt) { 55 | r.SimulationFetchingStartedAt = o.Hit.Endpoint.Orderbook.StartedLastUpdateAt 56 | } 57 | } 58 | h := sha1.New() 59 | h.Write([]byte(desc)) 60 | enc := hex.EncodeToString(h.Sum(nil)) 61 | r.ID = &enc 62 | r.Title = desc 63 | r.PreExecutionPortfolioState = core.SharedPortfolioManager().States[r.PreExecutionPortfolioStateID] 64 | r.PostExecutionPortfolioState = core.SharedPortfolioManager().States[r.PostExecutionPortfolioStateID] 65 | 66 | return json.Marshal(r) 67 | } 68 | 69 | func (r Report) Description() string { 70 | desc := "Report" 71 | for _, o := range r.Orders { 72 | var link string 73 | if o.Hit == nil { 74 | link = "Missing link" 75 | } else { 76 | link = o.Hit.Endpoint.Description() 77 | } 78 | desc = desc + " -> " + link 79 | } 80 | return desc 81 | } 82 | -------------------------------------------------------------------------------- /core/order.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // OrderTransactionType describes the transaction type: Bid / Ask 8 | type OrderTransactionType uint 9 | 10 | const ( 11 | // Bid - we are buying the base of a currency pair, or selling the quote 12 | Bid OrderTransactionType = iota 13 | // Ask - we are selling the base of a currency pair, or buying the quote 14 | Ask 15 | ) 16 | 17 | // Order represents an order 18 | type Order struct { 19 | Hit *Hit `json:"hit,omitempty"` 20 | Price float64 `json:"price"` 21 | PriceOfQuoteToBase float64 `json:"quoteToBasePrice"` 22 | BaseVolume float64 `json:"baseVolume"` 23 | QuoteVolume float64 `json:"quoteVolume"` 24 | TransactionType OrderTransactionType `json:"transactionType"` 25 | Fee float64 `json:"fee"` 26 | TakerFee float64 `json:"takerFee"` 27 | BaseVolumeIn float64 `json:"baseVolumeIn"` 28 | BaseVolumeOut float64 `json:"baseVolumeOut"` 29 | QuoteVolumeIn float64 `json:"quoteVolumeIn"` 30 | QuoteVolumeOut float64 `json:"quoteVolumeOut"` 31 | Progress float64 `json:"progress"` 32 | } 33 | 34 | // InitAsk initialize an Order, setting the transactionType to Ask 35 | func (o *Order) InitAsk(price float64, baseVolume float64) { 36 | o.TransactionType = Ask 37 | o.Init(price, baseVolume) 38 | } 39 | 40 | // InitBid initialize an Order, setting the transactionType to Bid 41 | func (o *Order) InitBid(price float64, baseVolume float64) { 42 | o.TransactionType = Bid 43 | o.Init(price, baseVolume) 44 | } 45 | 46 | // NewAsk initialize an Order, setting the transactionType to Ask 47 | func NewAsk(price float64, baseVolume float64) Order { 48 | o := Order{} 49 | o.InitAsk(price, baseVolume) 50 | return o 51 | } 52 | 53 | // NewBid returns an Order, setting the transactionType to Bid 54 | func NewBid(price float64, baseVolume float64) Order { 55 | o := Order{} 56 | o.InitBid(price, baseVolume) 57 | return o 58 | } 59 | 60 | // Init initialize an Order 61 | func (o *Order) Init(price float64, baseVolume float64) { 62 | o.Price = price 63 | o.PriceOfQuoteToBase = 1 / price 64 | o.TakerFee = 0.10 / 100 65 | o.UpdateBaseVolume(baseVolume) 66 | } 67 | 68 | // UpdateBaseVolume cascade update on BaseVolume and QuoteVolume 69 | func (o *Order) UpdateBaseVolume(baseVolume float64) { 70 | o.BaseVolume = baseVolume 71 | o.QuoteVolume = o.Price * o.BaseVolume 72 | o.Fee = o.BaseVolume * o.TakerFee 73 | o.updateVolumesInOut() 74 | } 75 | 76 | // UpdateQuoteVolume cascade update on BaseVolume and QuoteVolume 77 | func (o *Order) UpdateQuoteVolume(quoteVolume float64) { 78 | o.QuoteVolume = quoteVolume 79 | o.BaseVolume = o.QuoteVolume / o.Price 80 | o.Fee = o.BaseVolume * o.TakerFee 81 | o.updateVolumesInOut() 82 | } 83 | 84 | func (o *Order) updateVolumesInOut() { 85 | if o.TransactionType == Bid { 86 | o.BaseVolumeIn = 0 87 | o.QuoteVolumeIn = Trunc8(o.QuoteVolume) 88 | o.BaseVolumeOut = Trunc8(o.BaseVolume - o.BaseVolume*o.TakerFee) 89 | o.QuoteVolumeOut = 0 90 | } else if o.TransactionType == Ask { 91 | o.BaseVolumeIn = Trunc8(o.BaseVolume) 92 | o.QuoteVolumeIn = 0 93 | o.BaseVolumeOut = 0 94 | o.QuoteVolumeOut = Trunc8(o.QuoteVolume - o.QuoteVolume*o.TakerFee) 95 | } 96 | } 97 | 98 | // CreateMatchingAsk returns an Ask order matching the current Bid (crossing ths spread) 99 | func (o *Order) CreateMatchingAsk() (*Order, error) { 100 | if o.TransactionType != Bid { 101 | return nil, errors.New("order: not a bid") 102 | } 103 | m := *o 104 | m.TransactionType = Ask 105 | return &m, nil 106 | } 107 | 108 | // CreateMatchingBid returns a Bid order matching the current Ask (crossing ths spread) 109 | func (o *Order) CreateMatchingBid() (*Order, error) { 110 | if o.TransactionType != Ask { 111 | return nil, errors.New("order: not a ask") 112 | } 113 | m := *o 114 | m.TransactionType = Bid 115 | return &m, nil 116 | } 117 | -------------------------------------------------------------------------------- /core/portfolio.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "sync" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | var instance *PortfolioStateManager 10 | var once sync.Once 11 | 12 | func SharedPortfolioManager() *PortfolioStateManager { 13 | once.Do(func() { 14 | instance = &PortfolioStateManager{} 15 | instance.States = make(map[string]PortfolioState) 16 | }) 17 | return instance 18 | } 19 | 20 | type PortfolioStateManager struct { 21 | States map[string]PortfolioState 22 | LastStateID string 23 | } 24 | 25 | // Portfolio wraps all your positions 26 | type PortfolioState struct { 27 | StateID string `json:"stateID"` 28 | Positions map[string]map[Currency]float64 `json:"positions"` 29 | } 30 | 31 | // Portfolio wraps all your positions 32 | type PortfolioStateSlice struct { 33 | Exch string 34 | Positions map[Currency]float64 35 | } 36 | 37 | // Portfolio wraps all your positions 38 | type Portfolio struct { 39 | Positions map[string]map[Currency]float64 40 | } 41 | 42 | // Update position 43 | func (m *PortfolioStateManager) PushState(state PortfolioState) { 44 | m.LastStateID = state.StateID 45 | m.States[state.StateID] = state 46 | } 47 | 48 | func NewPortfolioStateFromPositions(positions map[string]map[Currency]float64) PortfolioState { 49 | state := NewPortfolioState() 50 | state.Positions = positions 51 | return state 52 | } 53 | 54 | func NewPortfolioState() PortfolioState { 55 | state := PortfolioState{} 56 | uuid := (uuid.NewV4()).String() 57 | state.StateID = uuid 58 | state.Positions = make(map[string]map[Currency]float64) 59 | return state 60 | } 61 | 62 | func (m *PortfolioStateManager) LastPositions() map[string]map[Currency]float64 { 63 | return m.States[m.LastStateID].Positions 64 | } 65 | 66 | func (m *PortfolioStateManager) Position(stateID, exch string, curr Currency) float64 { 67 | return m.States[stateID].Positions[exch][curr] 68 | } 69 | 70 | func (m *PortfolioStateManager) CurrentPosition(exch string, curr Currency) float64 { 71 | return m.States[m.LastStateID].Positions[exch][curr] 72 | } 73 | 74 | // Update position 75 | func (m *PortfolioStateManager) UpdateWithNewState(state PortfolioState, override bool) { 76 | if override || len(m.States) == 0 { 77 | m.PushState(state) 78 | } else { 79 | last := m.States[m.LastStateID] 80 | new := NewPortfolioState() 81 | new.StateID = (uuid.NewV4()).String() 82 | 83 | for exch := range last.Positions { 84 | new.Positions[exch] = make(map[Currency]float64) 85 | for currency := range state.Positions[exch] { 86 | new.Positions[exch][currency] = last.Positions[exch][currency] 87 | } 88 | } 89 | for exch := range state.Positions { 90 | for currency := range state.Positions[exch] { 91 | new.Positions[exch][currency] = state.Positions[exch][currency] 92 | } 93 | } 94 | m.PushState(new) 95 | } 96 | } 97 | 98 | // Update position 99 | func (m *PortfolioStateManager) UpdateWithNewPosition(exch string, c Currency, amount float64) { 100 | current := m.States[m.LastStateID] 101 | next := current.Copy() 102 | next.UpdatePosition(exch, c, amount) 103 | uuid := (uuid.NewV4()).String() 104 | next.StateID = uuid 105 | m.PushState(next) 106 | } 107 | 108 | // Fork current state 109 | func (m *PortfolioStateManager) ForkCurrentState() PortfolioState { 110 | current := m.States[m.LastStateID] 111 | fork := current.Copy() 112 | uuid := (uuid.NewV4()).String() 113 | fork.StateID = uuid 114 | return fork 115 | } 116 | 117 | // Update position 118 | func (s *PortfolioState) UpdatePosition(exch string, c Currency, amount float64) { 119 | if s.Positions == nil { 120 | s.Positions = make(map[string]map[Currency]float64) 121 | } 122 | if s.Positions[exch] == nil { 123 | s.Positions[exch] = make(map[Currency]float64) 124 | } 125 | s.Positions[exch][c] = Trunc8(amount) 126 | } 127 | 128 | // Copy state 129 | func (s *PortfolioState) Copy() PortfolioState { 130 | copy := PortfolioState{} 131 | copy.Positions = make(map[string]map[Currency]float64) 132 | 133 | for exch := range s.Positions { 134 | copy.Positions[exch] = make(map[Currency]float64) 135 | for currency := range s.Positions[exch] { 136 | copy.Positions[exch][currency] = s.Positions[exch][currency] 137 | } 138 | } 139 | return copy 140 | } 141 | 142 | // Update position 143 | func (s *Portfolio) UpdatePosition(exch string, c Currency, amount float64) { 144 | if s.Positions == nil { 145 | s.Positions = make(map[string]map[Currency]float64) 146 | } 147 | if s.Positions[exch] == nil { 148 | s.Positions[exch] = make(map[Currency]float64) 149 | } 150 | s.Positions[exch][c] = Trunc8(amount) 151 | } 152 | -------------------------------------------------------------------------------- /graph/path_finder.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "fmt" 5 | "gotrading/core" 6 | ) 7 | 8 | func PathFinder(mashup core.ExchangeMashup, from core.Currency, to core.Currency, depth int) (Tree, []*core.Endpoint, map[string][]Path, []Path) { 9 | var rawPaths []Path 10 | endpointLookup := make(map[string]*core.Endpoint) 11 | nodeLookup := make(map[string]*core.Hit) 12 | 13 | for _, to := range mashup.Currencies { 14 | if to != from { 15 | for _, exch := range mashup.Exchanges { 16 | n, endpointLookup, nodeLookup := nodeFromMashup(from, to, exch, mashup, endpointLookup, nodeLookup) 17 | if n != nil { 18 | rawPaths = append(findPaths(mashup, depth, Path{[]*core.Hit{n}, nil, nil, 0}, endpointLookup, nodeLookup), rawPaths...) 19 | } 20 | } 21 | } 22 | } 23 | 24 | // arbitrage := arbitrage.From(endpoint1).To(endpoint2).To(endpoint3).To(endpoint4) 25 | // result := arbitrage.Run() 26 | 27 | // Behind the scene, arbitrage, is going to deal with the fetching. 28 | // vertices := treeOfPossibles.Roots() 29 | // treeNodes := vertice.Children() 30 | 31 | treeOfPossibles := Tree{} 32 | endpoints := make([]*core.Endpoint, 0) 33 | paths := make(map[string][]Path) 34 | 35 | for _, path := range rawPaths { 36 | treeOfPossibles.InsertPath(path) 37 | 38 | for _, n := range path.Hits { 39 | 40 | p, ok := paths[n.Endpoint.ID()] 41 | if !ok { 42 | endpoints = append(endpoints, n.Endpoint) 43 | p = make([]Path, 0) 44 | } 45 | path.Encode() 46 | paths[n.Endpoint.ID()] = append(p, path) 47 | } 48 | } 49 | fmt.Println("Observing", len(rawPaths), "paths") 50 | return treeOfPossibles, endpoints, paths, rawPaths 51 | } 52 | 53 | func findPaths(m core.ExchangeMashup, depth int, p Path, endpointLookup map[string]*core.Endpoint, nodeLookup map[string]*core.Hit) []Path { 54 | var paths []Path 55 | firstNode := p.Hits[0] 56 | lastNode := p.Hits[len(p.Hits)-1] 57 | if len(p.Hits) == depth { 58 | from := firstNode.SoldCurrency 59 | to := lastNode.BoughtCurrency 60 | if from == to { 61 | paths = []Path{p} 62 | } 63 | } else if len(p.Hits) < depth { 64 | from := lastNode.BoughtCurrency 65 | for _, to := range m.Currencies { 66 | if to != from { 67 | for _, exch := range m.Exchanges { 68 | n, endpointLookup, nodeLookup := nodeFromMashup(from, to, exch, m, endpointLookup, nodeLookup) 69 | if n != nil { 70 | firstFrom := firstNode.SoldCurrency 71 | nextTo := n.BoughtCurrency 72 | if (nextTo == firstFrom) && p.contains(*n) == false { 73 | pathToEvaluate := Path{append(p.Hits, n), nil, nil, 0} 74 | candidates := findPaths(m, depth, pathToEvaluate, endpointLookup, nodeLookup) 75 | if len(candidates) > 0 { 76 | paths = append(paths, candidates...) 77 | } 78 | } else if len(p.Hits) < depth-1 { 79 | if p.contains(*n) == false { 80 | pathToEvaluate := Path{append(p.Hits, n), nil, nil, 0} 81 | candidates := findPaths(m, depth, pathToEvaluate, endpointLookup, nodeLookup) 82 | if len(candidates) > 0 { 83 | paths = append(paths, candidates...) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | return paths 93 | } 94 | 95 | func nodeFromMashup(from core.Currency, to core.Currency, exchange core.Exchange, mashup core.ExchangeMashup, endpointLookup map[string]*core.Endpoint, nodeLookup map[string]*core.Hit) (*core.Hit, map[string]*core.Endpoint, map[string]*core.Hit) { 96 | var n *core.Hit 97 | ok := mashup.LinkExist(from, to, exchange) 98 | if ok { 99 | var base, quote core.Currency 100 | base = from 101 | quote = to 102 | proto := core.Endpoint{base, quote, exchange, nil} 103 | endpoint, ok := endpointLookup[proto.ID()] 104 | if !ok { 105 | endpointLookup[proto.ID()] = &proto 106 | endpoint = &proto 107 | } 108 | cproto := core.Hit{endpoint, true, from, to} 109 | node, ok := nodeLookup[cproto.ID()] 110 | if !ok { 111 | nodeLookup[cproto.ID()] = &cproto 112 | node = &cproto 113 | } 114 | n = node 115 | } else { 116 | ok := mashup.LinkExist(to, from, exchange) 117 | if ok { 118 | var base, quote core.Currency 119 | base = to 120 | quote = from 121 | proto := core.Endpoint{base, quote, exchange, nil} 122 | endpoint, ok := endpointLookup[proto.ID()] 123 | if !ok { 124 | endpointLookup[proto.ID()] = &proto 125 | endpoint = &proto 126 | } 127 | cproto := core.Hit{endpoint, false, from, to} 128 | node, ok := nodeLookup[cproto.ID()] 129 | if !ok { 130 | nodeLookup[cproto.ID()] = &cproto 131 | node = &cproto 132 | } 133 | n = node 134 | } 135 | } 136 | return n, endpointLookup, nodeLookup 137 | } 138 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/fsnotify/fsnotify" 6 | packages = ["."] 7 | revision = "629574ca2a5df945712d3079857300b5e4da0236" 8 | version = "v1.4.2" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/hashicorp/hcl" 13 | packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] 14 | revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" 15 | 16 | [[projects]] 17 | name = "github.com/json-iterator/go" 18 | packages = ["."] 19 | revision = "f7279a603edee96fe7764d3de9c6ff8cf9970994" 20 | version = "1.0.4" 21 | 22 | [[projects]] 23 | name = "github.com/magiconair/properties" 24 | packages = ["."] 25 | revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73" 26 | version = "v1.7.4" 27 | 28 | [[projects]] 29 | branch = "master" 30 | name = "github.com/mitchellh/mapstructure" 31 | packages = ["."] 32 | revision = "06020f85339e21b2478f756a78e295255ffa4d6a" 33 | 34 | [[projects]] 35 | name = "github.com/onsi/ginkgo" 36 | packages = [".","config","internal/codelocation","internal/containernode","internal/failer","internal/leafnodes","internal/remote","internal/spec","internal/spec_iterator","internal/specrunner","internal/suite","internal/testingtproxy","internal/writer","reporters","reporters/stenographer","reporters/stenographer/support/go-colorable","reporters/stenographer/support/go-isatty","types"] 37 | revision = "9eda700730cba42af70d53180f9dcce9266bc2bc" 38 | version = "v1.4.0" 39 | 40 | [[projects]] 41 | name = "github.com/onsi/gomega" 42 | packages = [".","format","internal/assertion","internal/asyncassertion","internal/oraclematcher","internal/testingtsupport","matchers","matchers/support/goraph/bipartitegraph","matchers/support/goraph/edge","matchers/support/goraph/node","matchers/support/goraph/util","types"] 43 | revision = "003f63b7f4cff3fc95357005358af2de0f5fe152" 44 | version = "v1.3.0" 45 | 46 | [[projects]] 47 | name = "github.com/pelletier/go-toml" 48 | packages = ["."] 49 | revision = "16398bac157da96aa88f98a2df640c7f32af1da2" 50 | version = "v1.0.1" 51 | 52 | [[projects]] 53 | name = "github.com/satori/go.uuid" 54 | packages = ["."] 55 | revision = "879c5887cd475cd7864858769793b2ceb0d44feb" 56 | version = "v1.1.0" 57 | 58 | [[projects]] 59 | name = "github.com/spf13/afero" 60 | packages = [".","mem"] 61 | revision = "ec3a3111d1e1bdff38a61e09d5a5f5e974905611" 62 | version = "v1.0.1" 63 | 64 | [[projects]] 65 | name = "github.com/spf13/cast" 66 | packages = ["."] 67 | revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" 68 | version = "v1.1.0" 69 | 70 | [[projects]] 71 | branch = "master" 72 | name = "github.com/spf13/jwalterweatherman" 73 | packages = ["."] 74 | revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" 75 | 76 | [[projects]] 77 | name = "github.com/spf13/pflag" 78 | packages = ["."] 79 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 80 | version = "v1.0.0" 81 | 82 | [[projects]] 83 | name = "github.com/spf13/viper" 84 | packages = ["."] 85 | revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" 86 | version = "v1.0.0" 87 | 88 | [[projects]] 89 | branch = "master" 90 | name = "github.com/streadway/amqp" 91 | packages = ["."] 92 | revision = "ff34ec9cc65c2a23db5126d962c431018f65af59" 93 | 94 | [[projects]] 95 | branch = "master" 96 | name = "golang.org/x/crypto" 97 | packages = ["bcrypt","blowfish"] 98 | revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8" 99 | 100 | [[projects]] 101 | branch = "master" 102 | name = "golang.org/x/net" 103 | packages = ["html","html/atom","html/charset"] 104 | revision = "434ec0c7fe3742c984919a691b2018a6e9694425" 105 | 106 | [[projects]] 107 | branch = "master" 108 | name = "golang.org/x/sys" 109 | packages = ["unix"] 110 | revision = "b9cf5f96b68d9eaa53d5db5fef235718767f416a" 111 | 112 | [[projects]] 113 | branch = "master" 114 | name = "golang.org/x/text" 115 | packages = ["encoding","encoding/charmap","encoding/htmlindex","encoding/internal","encoding/internal/identifier","encoding/japanese","encoding/korean","encoding/simplifiedchinese","encoding/traditionalchinese","encoding/unicode","internal/gen","internal/tag","internal/triegen","internal/ucd","internal/utf8internal","language","runes","transform","unicode/cldr","unicode/norm"] 116 | revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" 117 | 118 | [[projects]] 119 | branch = "v2" 120 | name = "gopkg.in/yaml.v2" 121 | packages = ["."] 122 | revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" 123 | 124 | [solve-meta] 125 | analyzer-name = "dep" 126 | analyzer-version = 1 127 | inputs-digest = "4f033fa3c4598255dc3b66b6d0208da1e5805293fdd1f4889a578c43632665c6" 128 | solver-name = "gps-cdcl" 129 | solver-version = 1 130 | -------------------------------------------------------------------------------- /networking/gatling.go: -------------------------------------------------------------------------------- 1 | package networking 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | type Gatling struct { 16 | Clients []*http.Client 17 | MaxRequestsPerSecondsForHost map[string]int 18 | LastRequestFromClientToHostOccuredAt map[*http.Client]map[string]time.Time 19 | DefaultMaxRequestsPerSecondsForHost int 20 | Mutexes map[*http.Client]sync.RWMutex 21 | IsVerbose bool 22 | RoundRobin int 23 | } 24 | 25 | var instance *Gatling 26 | var once sync.Once 27 | 28 | func SharedGatling() *Gatling { 29 | once.Do(func() { 30 | instance = &Gatling{} 31 | instance.warmUp() 32 | }) 33 | return instance 34 | } 35 | 36 | func (g *Gatling) warmUp() { 37 | 38 | g.LastRequestFromClientToHostOccuredAt = make(map[*http.Client]map[string]time.Time) 39 | g.DefaultMaxRequestsPerSecondsForHost = 10 40 | g.Mutexes = make(map[*http.Client]sync.RWMutex) 41 | 42 | addrs, _ := net.InterfaceAddrs() 43 | eligibleAddrs := []net.Addr{} 44 | for _, addr := range addrs { 45 | if strings.HasPrefix(addr.String(), "10.0.") { 46 | eligibleAddrs = append(eligibleAddrs, addr) 47 | } 48 | } 49 | 50 | fmt.Println(eligibleAddrs) 51 | if len(eligibleAddrs) > 0 { 52 | g.Clients = make([]*http.Client, len(eligibleAddrs)) 53 | for i, addr := range eligibleAddrs { 54 | 55 | tr := &http.Transport{ 56 | MaxIdleConns: 10, 57 | IdleConnTimeout: 30 * time.Second, 58 | DisableCompression: true, 59 | } 60 | 61 | tcpAddr := &net.TCPAddr{ 62 | IP: addr.(*net.IPNet).IP, 63 | } 64 | 65 | d := net.Dialer{LocalAddr: tcpAddr} 66 | 67 | tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { 68 | fmt.Println(d.LocalAddr, addr) 69 | return d.DialContext(ctx, network, addr) 70 | } 71 | g.Clients[i] = &http.Client{Transport: tr} 72 | } 73 | } else { 74 | g.Clients = make([]*http.Client, 1) 75 | 76 | tr := &http.Transport{ 77 | MaxIdleConns: 10, 78 | IdleConnTimeout: 30 * time.Second, 79 | DisableCompression: true, 80 | } 81 | 82 | ief, err := net.InterfaceByName("en0") 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | addrs, err := ief.Addrs() 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | tcpAddr := &net.TCPAddr{ 92 | IP: addrs[1].(*net.IPNet).IP, 93 | } 94 | 95 | d := net.Dialer{LocalAddr: tcpAddr} 96 | 97 | tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { 98 | return d.DialContext(ctx, network, addr) 99 | } 100 | g.Clients[0] = &http.Client{Transport: tr} 101 | } 102 | } 103 | 104 | func (g *Gatling) Send(request *http.Request) ([]byte, error, time.Time, time.Time) { 105 | 106 | if g.IsVerbose { 107 | log.Println("Gatling> Preparing interface", request.URL) 108 | } 109 | 110 | if len(g.Clients) > 1 { 111 | g.RoundRobin = (g.RoundRobin + 1) % 3 112 | } else { 113 | g.RoundRobin = 0 114 | } 115 | client := g.Clients[g.RoundRobin] 116 | 117 | var contents []byte 118 | var err error 119 | 120 | URL := request.URL 121 | hostname := URL.Hostname() 122 | maxRequestsPerSeconds := g.MaxRequestsPerSecondsForHost[hostname] 123 | if maxRequestsPerSeconds == 0 { 124 | maxRequestsPerSeconds = g.DefaultMaxRequestsPerSecondsForHost 125 | } 126 | delayBetweenRequests := 1.0 / float64(maxRequestsPerSeconds) 127 | lastOccurence, ok := g.LastRequestFromClientToHostOccuredAt[client][hostname] 128 | 129 | t := delayBetweenRequests - time.Since(lastOccurence).Seconds() 130 | if ok && t > 0 { 131 | time.Sleep(time.Duration(t*1000) * time.Millisecond) 132 | } 133 | 134 | if _, ok := g.LastRequestFromClientToHostOccuredAt[client]; ok { 135 | g.LastRequestFromClientToHostOccuredAt[client][hostname] = time.Now() 136 | } else { 137 | g.LastRequestFromClientToHostOccuredAt[client] = make(map[string]time.Time) 138 | g.LastRequestFromClientToHostOccuredAt[client][hostname] = time.Now() 139 | } 140 | 141 | start := time.Now() 142 | res, err := client.Do(request) 143 | if err != nil { 144 | return nil, err, start, time.Now() 145 | } 146 | 147 | // resDump, err := httputil.DumpResponse(res, true) 148 | // if err != nil { 149 | // fmt.Println(err) 150 | // } 151 | // reqDump, _ := httputil.DumpRequest(request, true) 152 | 153 | // fmt.Println(string(reqDump)) 154 | // fmt.Println("==========") 155 | // fmt.Println(string(resDump)) 156 | 157 | if res.StatusCode != 200 { 158 | return nil, fmt.Errorf("common.SendHTTPGetRequest() error: HTTP status code %d", res.StatusCode), start, time.Now() 159 | } 160 | 161 | contents, err = ioutil.ReadAll(res.Body) 162 | if err != nil { 163 | return nil, err, start, time.Now() 164 | } 165 | 166 | defer res.Body.Close() 167 | 168 | return contents, err, start, time.Now() 169 | } 170 | 171 | func (g *Gatling) GET(url string) ([]byte, error, time.Time, time.Time) { 172 | req, _ := http.NewRequest("GET", url, nil) 173 | return g.Send(req) 174 | } 175 | -------------------------------------------------------------------------------- /strategies/arbitrage/simulation.go: -------------------------------------------------------------------------------- 1 | package arbitrage 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "gotrading/core" 9 | "gotrading/networking" 10 | ) 11 | 12 | type Simulation struct { 13 | hits []*core.Hit 14 | Report Report 15 | } 16 | 17 | func (sim *Simulation) Init(hits []*core.Hit) { 18 | sim.hits = make([]*core.Hit, len(hits)) 19 | for i, h := range hits { 20 | copy := *h 21 | sim.hits[i] = © 22 | } 23 | sim.Report = Report{} 24 | } 25 | 26 | func (sim *Simulation) Run() { 27 | r := &sim.Report 28 | r.SimulationStartedAt = time.Now() 29 | r.IsSimulationIncomplete = false 30 | batch := networking.Batch{} 31 | 32 | batch.GetOrderbooks(sim.hits, func(orderbooks []*core.Orderbook) { 33 | 34 | r.SimulationComputingStartedAt = time.Now() 35 | r.Cost = 0 36 | r.Rates = make([]float64, len(sim.hits)) 37 | r.AdjustedVolumes = make([]float64, len(sim.hits)) 38 | r.IsSimulationSuccessful = true 39 | r.Orders = make([]core.Order, len(sim.hits)) 40 | fromInitialToCurrent := float64(1) 41 | // rateForInitialCurrency := float64(1) // How many INITIAL_CURRENCY are we getting for 1 CURRENT_CURRENCY 42 | 43 | m := core.SharedPortfolioManager() 44 | 45 | for i, n := range sim.hits { 46 | orderbook := orderbooks[i] 47 | n.Endpoint.Orderbook = orderbook 48 | if orderbook == nil { 49 | sim.Abort() 50 | return 51 | } 52 | 53 | var priceOfCurrencyToSell float64 54 | var volumeOfCurrencyToSell float64 55 | var order core.Order 56 | 57 | if n.IsBaseToQuote { 58 | // We are selling the base -> we match the Bid. 59 | if len(orderbook.Bids) > 0 { 60 | bestBid := orderbook.Bids[0] 61 | o, err := bestBid.CreateMatchingAsk() 62 | if err == nil { 63 | order = *o 64 | priceOfCurrencyToSell = order.Price 65 | volumeOfCurrencyToSell = order.BaseVolume 66 | } else { 67 | sim.Abort() 68 | return 69 | } 70 | } else { 71 | sim.Abort() 72 | return 73 | } 74 | } else { 75 | // We are selling the quote <=> we are buying the base -> we match the Ask. 76 | if len(orderbook.Asks) > 0 { 77 | bestAsk := orderbook.Asks[0] 78 | o, err := bestAsk.CreateMatchingBid() 79 | if err == nil { 80 | order = *o 81 | priceOfCurrencyToSell = order.PriceOfQuoteToBase 82 | volumeOfCurrencyToSell = order.QuoteVolume 83 | } else { 84 | sim.Abort() 85 | return 86 | } 87 | } else { 88 | sim.Abort() 89 | return 90 | } 91 | } 92 | 93 | fromInitialToCurrent = fromInitialToCurrent * priceOfCurrencyToSell 94 | r.Rates[i] = fromInitialToCurrent 95 | r.Performance = fromInitialToCurrent 96 | decimals := n.Endpoint.Exchange.ExchangeSettings.PairsSettings[orderbook.CurrencyPair].BasePrecision 97 | 98 | if i == 0 { 99 | initialBalance := m.CurrentPosition(n.Endpoint.Exchange.Name, n.SoldCurrency) 100 | initialBalance = initialBalance - (initialBalance * 0.25) 101 | volumeToCeil := math.Min(initialBalance, volumeOfCurrencyToSell) 102 | r.VolumeToEngage = ceilf(volumeToCeil, decimals) 103 | } else { 104 | limitingAmount := r.VolumeToEngage * fromInitialToCurrent 105 | currentAmount := volumeOfCurrencyToSell * priceOfCurrencyToSell 106 | newLimitingAmount := math.Min(limitingAmount, currentAmount) 107 | r.VolumeToEngage = newLimitingAmount / fromInitialToCurrent 108 | } 109 | order.Hit = n 110 | r.Orders[i] = order 111 | } 112 | 113 | for i, n := range sim.hits { 114 | var currentVolumeToEngage float64 115 | if i == 0 { 116 | currentVolumeToEngage = r.VolumeToEngage 117 | } else { 118 | if sim.hits[i-1].IsBaseToQuote { 119 | currentVolumeToEngage = r.Orders[i-1].QuoteVolumeOut 120 | } else if r.Orders[i-1].TransactionType == core.Bid { 121 | currentVolumeToEngage = r.Orders[i-1].BaseVolumeOut 122 | } 123 | } 124 | if n.IsBaseToQuote { 125 | r.AdjustedVolumes[i] = currentVolumeToEngage * r.Orders[i].Price 126 | } else { 127 | r.AdjustedVolumes[i] = currentVolumeToEngage * r.Orders[i].PriceOfQuoteToBase 128 | } 129 | if n.IsBaseToQuote { 130 | r.Orders[i].UpdateQuoteVolume(r.AdjustedVolumes[i]) 131 | } else { 132 | r.Orders[i].UpdateBaseVolume(r.AdjustedVolumes[i]) 133 | } 134 | r.Cost = r.Cost + r.Orders[i].Fee*r.Rates[i] 135 | } 136 | 137 | firstOrder := r.Orders[0] 138 | if firstOrder.TransactionType == core.Bid { 139 | r.VolumeIn = firstOrder.QuoteVolumeIn 140 | } else { 141 | r.VolumeIn = firstOrder.BaseVolumeIn 142 | } 143 | 144 | lastOrder := r.Orders[len(r.Orders)-1] 145 | if lastOrder.TransactionType == core.Bid { 146 | r.VolumeOut = lastOrder.BaseVolumeOut 147 | } else { 148 | r.VolumeOut = lastOrder.QuoteVolumeOut 149 | } 150 | 151 | fmt.Println(r.VolumeIn, r.VolumeOut) 152 | r.SimulatedProfit = core.Trunc8(r.VolumeOut - r.VolumeIn) 153 | 154 | if r.VolumeIn < 0.0001 || r.VolumeOut < 0.0001 { 155 | fmt.Println("Traded volume under threshold") 156 | r.IsTradedVolumeEnough = false 157 | r.IsSimulationSuccessful = false 158 | r.SimulationEndedAt = time.Now() 159 | return 160 | } 161 | r.IsTradedVolumeEnough = true 162 | r.Performance = r.VolumeOut / r.VolumeIn 163 | r.SimulationEndedAt = time.Now() 164 | r.IsSimulationSuccessful = r.SimulatedProfit > 0.0 165 | }) 166 | } 167 | 168 | func ceilf(v float64, d int) float64 { 169 | df := float64(d) 170 | return math.Ceil(v*math.Pow(10, df)) / math.Pow(10, df) 171 | } 172 | 173 | func (sim *Simulation) Abort() { 174 | r := &sim.Report 175 | r.IsSimulationSuccessful = false 176 | r.SimulationEndedAt = time.Now() 177 | r.IsSimulationIncomplete = true 178 | } 179 | 180 | func (sim *Simulation) IsSuccessful() bool { 181 | return sim.Report.IsSimulationSuccessful && sim.Report.IsTradedVolumeEnough 182 | } 183 | 184 | func (sim *Simulation) IsExecutable() bool { 185 | return sim.Report.IsSimulationIncomplete == false && sim.Report.IsTradedVolumeEnough 186 | } 187 | -------------------------------------------------------------------------------- /exchanges/binance/binance.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "gotrading/core" 10 | "gotrading/networking" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | jsoniter "github.com/json-iterator/go" 19 | ) 20 | 21 | const ( 22 | hostURL = "https://api.binance.com/api" 23 | exchangeInfo = "exchangeInfo" 24 | depth = "depth" 25 | account = "account" 26 | ) 27 | 28 | type Binance struct { 29 | } 30 | 31 | func (b Binance) GetSettings() func() (core.ExchangeSettings, error) { 32 | return func() (core.ExchangeSettings, error) { 33 | 34 | type pairSettings struct { 35 | Symbol string `json:"symbol"` 36 | Status string `json:"status"` 37 | BaseAsset string `json:"baseAsset"` 38 | QuoteAsset string `json:"quoteAsset"` 39 | BaseAssetPrecision int `json:"baseAssetPrecision"` 40 | QuoteAssetPrecision int `json:"quoteAssetPrecision"` 41 | Filters []map[string]string `json:"filters"` 42 | } 43 | 44 | type Response struct { 45 | ServerTime int64 `json:"serverTime"` 46 | Symbols []pairSettings `json:"symbols"` 47 | } 48 | 49 | response := Response{} 50 | settings := core.ExchangeSettings{} 51 | gatling := networking.SharedGatling() 52 | 53 | url := fmt.Sprintf("%s/v1/%s", hostURL, exchangeInfo) 54 | 55 | contents, err, _, _ := gatling.GET(url) 56 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 57 | err = json.Unmarshal(contents[:], &response) 58 | 59 | if len(response.Symbols) == 0 { 60 | return settings, errors.New("info empty") 61 | } 62 | 63 | settings.IsCurrencyPairNormalized = true 64 | settings.AvailablePairs = make([]core.CurrencyPair, len(response.Symbols)) 65 | settings.PairsSettings = make(map[core.CurrencyPair]core.CurrencyPairSettings, len(response.Symbols)) 66 | 67 | for i, sym := range response.Symbols { 68 | base := core.Currency(sym.BaseAsset) 69 | quote := core.Currency(sym.QuoteAsset) 70 | pair := core.CurrencyPair{Base: base, Quote: quote} 71 | settings.AvailablePairs[i] = pair 72 | 73 | cps := core.CurrencyPairSettings{} 74 | cps.BasePrecision = sym.BaseAssetPrecision 75 | cps.QuotePrecision = sym.QuoteAssetPrecision 76 | cps.MinAmount, _ = strconv.ParseFloat(sym.Filters[1]["minQty"], 64) 77 | cps.MaxAmount, _ = strconv.ParseFloat(sym.Filters[1]["maxQty"], 64) 78 | cps.MinPrice, _ = strconv.ParseFloat(sym.Filters[0]["minPrice"], 64) 79 | cps.MaxPrice, _ = strconv.ParseFloat(sym.Filters[0]["maxPrice"], 64) 80 | settings.PairsSettings[pair] = cps 81 | } 82 | return settings, err 83 | } 84 | } 85 | 86 | func (b Binance) GetOrderbook() func(hit core.Hit) (core.Orderbook, error) { 87 | return func(hit core.Hit) (core.Orderbook, error) { 88 | var err error 89 | 90 | type Response struct { 91 | Asks [][2]string `json:"asks"` 92 | Bids [][2]string `json:"bids"` 93 | } 94 | 95 | response := Response{} 96 | endpoint := hit.Endpoint 97 | dst := &core.Orderbook{} 98 | dst.CurrencyPair = core.CurrencyPair{Base: endpoint.From, Quote: endpoint.To} 99 | curr := strings.ToUpper(fmt.Sprintf("%s%s", endpoint.From, endpoint.To)) 100 | 101 | depthValue := 5 102 | req := fmt.Sprintf("%s/v1/%s?symbol=%s&limit=%d", hostURL, depth, curr, depthValue) 103 | 104 | gatling := networking.SharedGatling() 105 | contents, err, start, end := gatling.GET(req) 106 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 107 | err = json.Unmarshal(contents, &response) 108 | if err != nil { 109 | log.Println(string(contents[:])) 110 | } 111 | 112 | if err == nil { 113 | dst.Bids = make([]core.Order, depthValue) 114 | dst.Asks = make([]core.Order, depthValue) 115 | dst.StartedLastUpdateAt = start 116 | dst.EndedLastUpdateAt = end 117 | 118 | for i, ask := range response.Asks { 119 | p, _ := strconv.ParseFloat(ask[0], 64) 120 | v, _ := strconv.ParseFloat(ask[1], 64) 121 | dst.Asks[i] = core.NewAsk(p, v) 122 | } 123 | for i, bid := range response.Bids { 124 | p, _ := strconv.ParseFloat(bid[0], 64) 125 | v, _ := strconv.ParseFloat(bid[1], 64) 126 | dst.Bids[i] = core.NewBid(p, v) 127 | } 128 | } else { 129 | fmt.Println("Error", endpoint.Description(), err) 130 | } 131 | return *dst, err 132 | } 133 | } 134 | 135 | func (b Binance) GetPortfolio() func(settings core.ExchangeSettings) (core.Portfolio, error) { 136 | return func(settings core.ExchangeSettings) (core.Portfolio, error) { 137 | portfolio := core.Portfolio{} 138 | 139 | timestamp := time.Now().Unix() * 1000 140 | 141 | values := url.Values{} 142 | values.Set("timestamp", fmt.Sprintf("%d", timestamp)) 143 | 144 | mac := hmac.New(sha256.New, []byte(settings.APISecret)) 145 | _, err := mac.Write([]byte(values.Encode())) 146 | if err != nil { 147 | return portfolio, err 148 | } 149 | signature := hex.EncodeToString(mac.Sum(nil)) 150 | 151 | url := fmt.Sprintf("%s/v3/%s?%s&signature=%s", hostURL, account, values.Encode(), signature) 152 | req, err := http.NewRequest("GET", url, nil) 153 | 154 | if err != nil { 155 | return portfolio, err 156 | } 157 | 158 | req.Header.Add("X-MBX-APIKEY", settings.APIKey) 159 | 160 | gatling := networking.SharedGatling() 161 | contents, err, _, _ := gatling.Send(req) 162 | 163 | if err != nil { 164 | log.Println(err) 165 | } 166 | 167 | type Balamce struct { 168 | Asset string `json:"asset"` 169 | Free string `json:"free"` 170 | Locked string `json:"locked"` 171 | } 172 | 173 | type Response struct { 174 | MakerCommission int `json:"makerCommission"` 175 | TakerCommission int `json:"takerCommission"` 176 | BuyerCommission int `json:"buyerCommission"` 177 | SellerCommission int `json:"sellerCommission"` 178 | CanTrade bool `json:"canTrade"` 179 | CanWithdraw bool `json:"canWithdraw"` 180 | UpdateTime int `json:"updateTime"` 181 | Balances []Balamce `json:"balances"` 182 | } 183 | 184 | response := Response{} 185 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 186 | err = json.Unmarshal(contents, &response) 187 | if err != nil { 188 | log.Println(err) 189 | } else { 190 | log.Println(response) 191 | } 192 | 193 | balances := response.Balances 194 | for _, balance := range balances { 195 | curr := core.Currency(balance.Asset) 196 | position, _ := strconv.ParseFloat(balance.Free, 64) 197 | portfolio.UpdatePosition(settings.Name, curr, position) 198 | } 199 | 200 | curr := core.Currency("BTC") 201 | position := 100.0 202 | portfolio.UpdatePosition(settings.Name, curr, position) 203 | 204 | return portfolio, err 205 | } 206 | } 207 | 208 | func (b Binance) PostOrder() func(order core.Order, settings core.ExchangeSettings) (core.OrderDispatched, error) { 209 | return func(order core.Order, settings core.ExchangeSettings) (core.OrderDispatched, error) { 210 | var o core.OrderDispatched 211 | var err error 212 | fmt.Println("Posting Order on Binance") 213 | return o, err 214 | } 215 | } 216 | 217 | // func (b *Binance) Deposit(client http.Client) (bool, error) { 218 | // var err error 219 | // return true, err 220 | // } 221 | 222 | // func (b *Binance) Withdraw(client http.Client) (bool, error) { 223 | // var err error 224 | // return true, err 225 | // } 226 | -------------------------------------------------------------------------------- /exchanges/liqui/liqui.go: -------------------------------------------------------------------------------- 1 | package liqui 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha512" 6 | "encoding/hex" 7 | "math" 8 | 9 | "errors" 10 | "fmt" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "strconv" 15 | "strings" 16 | 17 | "gotrading/core" 18 | "gotrading/networking" 19 | 20 | "github.com/json-iterator/go" 21 | ) 22 | 23 | const ( 24 | liquiAPIPublicURL = "https://api.Liqui.io/api/3" 25 | liquiAPIPrivateURL = "https://api.Liqui.io/tapi" 26 | liquiInfo = "info" 27 | liquiTicker = "ticker" 28 | liquiDepth = "depth" 29 | liquiTrades = "trades" 30 | liquiGetInfo = "getInfo" 31 | liquiTrade = "Trade" 32 | liquiActiveOrders = "ActiveOrders" 33 | liquiOrderInfo = "OrderInfo" 34 | liquiCancelOrder = "CancelOrder" 35 | liquiTradeHistory = "TradeHistory" 36 | liquiWithdrawCoin = "WithdrawCoin" 37 | ) 38 | 39 | type Liqui struct { 40 | } 41 | 42 | func (b Liqui) GetSettings() func() (core.ExchangeSettings, error) { 43 | return func() (core.ExchangeSettings, error) { 44 | 45 | type pairsSettings struct { 46 | DecimalPlaces int `json:"decimal_places"` 47 | MinPrice float64 `json:"min_price"` 48 | MaxPrice float64 `json:"max_price"` 49 | MinAmount float64 `json:"min_amount"` 50 | MaxAmount float64 `json:"max_amount"` 51 | MinTotal float64 `json:"min_total"` 52 | Fee float64 `json:"fee"` 53 | } 54 | 55 | type Response struct { 56 | ServerTime int `json:"server_time"` 57 | PairsSettings map[string]pairsSettings `json:"pairs"` 58 | } 59 | 60 | response := Response{} 61 | settings := core.ExchangeSettings{} 62 | gatling := networking.SharedGatling() 63 | 64 | url := fmt.Sprintf("%s/%s", liquiAPIPublicURL, liquiInfo) 65 | 66 | contents, err, _, _ := gatling.GET(url) 67 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 68 | err = json.Unmarshal(contents[:], &response) 69 | 70 | if len(response.PairsSettings) == 0 { 71 | return settings, errors.New("info empty") 72 | } 73 | 74 | settings.IsCurrencyPairNormalized = true 75 | settings.AvailablePairs = make([]core.CurrencyPair, len(response.PairsSettings)) 76 | settings.PairsSettings = make(map[core.CurrencyPair]core.CurrencyPairSettings, len(response.PairsSettings)) 77 | 78 | i := 0 79 | for key := range response.PairsSettings { 80 | currs := strings.Split(strings.ToUpper(key), "_") 81 | base := core.Currency(currs[0]) 82 | quote := core.Currency(currs[1]) 83 | pair := core.CurrencyPair{Base: base, Quote: quote} 84 | settings.AvailablePairs[i] = pair 85 | 86 | cps := core.CurrencyPairSettings{} 87 | cps.BasePrecision = response.PairsSettings[key].DecimalPlaces 88 | cps.QuotePrecision = response.PairsSettings[key].DecimalPlaces 89 | cps.MinAmount = response.PairsSettings[key].MinAmount 90 | cps.MaxAmount = response.PairsSettings[key].MaxAmount 91 | cps.MinPrice = response.PairsSettings[key].MinPrice 92 | cps.MaxPrice = response.PairsSettings[key].MaxPrice 93 | settings.PairsSettings[pair] = cps 94 | i++ 95 | } 96 | return settings, err 97 | } 98 | } 99 | 100 | func (b Liqui) GetOrderbook() func(hit core.Hit) (core.Orderbook, error) { 101 | return func(hit core.Hit) (core.Orderbook, error) { 102 | 103 | type Response struct { 104 | Orderbook map[string]struct { 105 | Asks [][]float64 `json:"asks"` 106 | Bids [][]float64 `json:"bids"` 107 | } 108 | } 109 | 110 | response := Response{} 111 | endpoint := hit.Endpoint 112 | dst := &core.Orderbook{} 113 | dst.CurrencyPair = core.CurrencyPair{Base: endpoint.From, Quote: endpoint.To} 114 | curr := strings.ToLower(fmt.Sprintf("%s_%s", endpoint.From, endpoint.To)) 115 | 116 | depth := 3 117 | req := fmt.Sprintf("%s/%s/%s?limit=%d", liquiAPIPublicURL, liquiDepth, curr, depth) 118 | 119 | gatling := networking.SharedGatling() 120 | contents, err, start, end := gatling.GET(req) 121 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 122 | err = json.Unmarshal(contents, &response.Orderbook) 123 | if err != nil { 124 | log.Println(string(contents[:])) 125 | } 126 | src := response.Orderbook[curr] 127 | 128 | if err == nil { 129 | dst.Bids = make([]core.Order, depth) 130 | dst.Asks = make([]core.Order, depth) 131 | dst.StartedLastUpdateAt = start 132 | dst.EndedLastUpdateAt = end 133 | 134 | for i, ask := range src.Asks { 135 | dst.Asks[i] = core.NewAsk(ask[0], ask[1]) 136 | } 137 | for i, bid := range src.Bids { 138 | dst.Bids[i] = core.NewBid(bid[0], bid[1]) 139 | } 140 | } else { 141 | fmt.Println("Error", endpoint.Description(), err) 142 | } 143 | return *dst, err 144 | } 145 | } 146 | 147 | func (b Liqui) PostOrder() func(order core.Order, settings core.ExchangeSettings) (core.OrderDispatched, error) { 148 | return func(order core.Order, settings core.ExchangeSettings) (core.OrderDispatched, error) { 149 | var err error 150 | 151 | do := core.OrderDispatched{} 152 | do.Order = &order 153 | 154 | endpoint := order.Hit.Endpoint 155 | from := string(endpoint.From) 156 | to := string(endpoint.To) 157 | remotePair := strings.ToLower(from + "_" + to) 158 | pair := core.CurrencyPair{endpoint.From, endpoint.To} 159 | 160 | var orderType string 161 | var amount float64 162 | decimals := float64(settings.PairsSettings[pair].BasePrecision) 163 | 164 | if order.TransactionType == core.Ask { 165 | orderType = "sell" 166 | amount = order.BaseVolumeIn 167 | } else { 168 | orderType = "buy" 169 | amount = order.QuoteVolumeIn / order.Price 170 | } 171 | 172 | rate := order.Price 173 | amount = math.Trunc(amount*math.Pow(10, decimals)) / math.Pow(10, decimals) 174 | 175 | nonce := int(settings.Nonce.GetInc()) 176 | 177 | values := url.Values{} 178 | values.Set("method", "Trade") 179 | values.Set("nonce", strconv.Itoa(nonce)) 180 | values.Set("pair", remotePair) 181 | values.Set("type", orderType) 182 | values.Set("rate", strconv.FormatFloat(rate, 'f', -1, 64)) 183 | values.Set("amount", strconv.FormatFloat(amount, 'f', int(decimals), 64)) 184 | encoded := values.Encode() 185 | fmt.Println("Executing order:", encoded) 186 | 187 | h := hmac.New(sha512.New, []byte(settings.APISecret)) 188 | h.Write([]byte(encoded)) 189 | hmac := hex.EncodeToString(h.Sum(nil)) 190 | 191 | headers := make(map[string]string) 192 | headers["Key"] = settings.APIKey 193 | headers["Sign"] = hmac 194 | headers["Content-Type"] = "application/x-www-form-urlencoded" 195 | 196 | req, err := http.NewRequest("POST", liquiAPIPrivateURL, strings.NewReader(encoded)) 197 | 198 | if err != nil { 199 | return do, err 200 | } 201 | for k, v := range headers { 202 | req.Header.Add(k, v) 203 | } 204 | gatling := networking.SharedGatling() 205 | contents, err, start, end := gatling.Send(req) 206 | 207 | do.SentAt = start 208 | do.ConfirmedAt = end 209 | 210 | type Return struct { 211 | Received float64 `json:"received"` 212 | Remains float64 `json:"remains"` 213 | OrderID int `json:"order_id"` 214 | InitOrderID int `json:"init_order_id"` 215 | Funds map[string]float64 `json:"funds"` 216 | } 217 | 218 | type Response struct { 219 | Return Return `json:"return"` 220 | } 221 | 222 | response := Response{} 223 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 224 | err = json.Unmarshal(contents, &response) 225 | if err != nil { 226 | log.Println(string(contents[:])) 227 | } 228 | fmt.Println(string(contents[:])) 229 | 230 | order.Progress = response.Return.Received / amount 231 | funds := response.Return.Funds 232 | 233 | manager := core.SharedPortfolioManager() 234 | state := manager.ForkCurrentState() 235 | for curr := range funds { 236 | state.UpdatePosition(settings.Name, core.Currency(strings.ToUpper(curr)), funds[curr]) 237 | } 238 | manager.PushState(state) 239 | 240 | return do, err 241 | } 242 | } 243 | 244 | func (b Liqui) GetPortfolio() func(settings core.ExchangeSettings) (core.Portfolio, error) { 245 | return func(settings core.ExchangeSettings) (core.Portfolio, error) { 246 | portfolio := core.Portfolio{} 247 | var err error 248 | fmt.Println("Getting Portfolio from Liqui") 249 | 250 | nonce := int(settings.Nonce.GetInc()) 251 | 252 | values := url.Values{} 253 | values.Set("method", liquiGetInfo) 254 | values.Set("nonce", strconv.Itoa(nonce)) 255 | encoded := values.Encode() 256 | 257 | h := hmac.New(sha512.New, []byte(settings.APISecret)) 258 | h.Write([]byte(encoded)) 259 | hmac := hex.EncodeToString(h.Sum(nil)) 260 | 261 | headers := make(map[string]string) 262 | headers["Key"] = settings.APIKey 263 | headers["Sign"] = hmac 264 | headers["Content-Type"] = "application/x-www-form-urlencoded" 265 | 266 | req, err := http.NewRequest("POST", liquiAPIPrivateURL, strings.NewReader(encoded)) 267 | 268 | if err != nil { 269 | return portfolio, err 270 | } 271 | for k, v := range headers { 272 | req.Header.Add(k, v) 273 | } 274 | gatling := networking.SharedGatling() 275 | contents, err, _, _ := gatling.Send(req) 276 | 277 | if err != nil { 278 | log.Println(err) 279 | } else { 280 | log.Println(contents) 281 | } 282 | 283 | type Return struct { 284 | Funds map[string]float64 `json:"funds"` 285 | } 286 | 287 | type Response struct { 288 | Sucess int `json:"success"` 289 | Error string `json:"error"` 290 | Return Return `json:"return"` 291 | } 292 | 293 | response := Response{} 294 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 295 | err = json.Unmarshal(contents, &response) 296 | if err != nil { 297 | log.Println(err) 298 | } else { 299 | log.Println(response) 300 | } 301 | 302 | funds := response.Return.Funds 303 | fmt.Println("->", funds) 304 | for curr := range funds { 305 | portfolio.UpdatePosition(settings.Name, core.Currency(strings.ToUpper(curr)), funds[curr]) 306 | } 307 | 308 | return portfolio, err 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /strategies/arbitrage/simulation_test.go: -------------------------------------------------------------------------------- 1 | package arbitrage 2 | 3 | import ( 4 | "fmt" 5 | "gotrading/core" 6 | "gotrading/graph" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("Arbitrage in 3 steps, starting and finishing with ABC", func() { 14 | 15 | var ( 16 | arbitrage Arbitrage 17 | ) 18 | 19 | BeforeEach(func() { 20 | arbitrage = Arbitrage{} 21 | }) 22 | 23 | Describe(` 24 | Considering the combination: [ABC/DEF]@Exhange1 -> [XYZ/DEF]@Exhange1 and the orderbooks: 25 | [ABC/DEF]@Exhange1 -> Best Bid: 1ABC = 2DEF, Best Ask: 1ABC = 2DEF 26 | [XYZ/DEF]@Exhange1 -> Best Bid: 1XYZ = 3DEF, Best Ask: 1XYZ = 3DEF`, func() { 27 | Context(` 28 | When I fulfill all the orders, running the arbitrage`, func() { 29 | var ( 30 | chains []ArbitrageChain 31 | ob1 core.Orderbook 32 | ob2 core.Orderbook 33 | // ob3 core.Orderbook 34 | paths []graph.Path 35 | ) 36 | 37 | BeforeEach(func() { 38 | exchange1 := core.Exchange{"Exchange1", make([]core.CurrencyPair, 0), nil} 39 | 40 | abc := core.Currency("ABC") 41 | def := core.Currency("DEF") 42 | xyz := core.Currency("XYZ") 43 | 44 | pair1 := core.CurrencyPair{abc, def} 45 | pair2 := core.CurrencyPair{xyz, def} 46 | 47 | bids1 := append([]core.Order{}, core.NewBid(pair1, 2, 6)) 48 | asks1 := append([]core.Order{}, core.NewAsk(pair1, 2, 6)) 49 | bids2 := append([]core.Order{}, core.NewBid(pair2, 3, 2)) 50 | asks2 := append([]core.Order{}, core.NewAsk(pair2, 3, 2)) 51 | 52 | ob1 = core.Orderbook{pair1, bids1, asks1, time.Now()} 53 | ob2 = core.Orderbook{pair2, bids2, asks2, time.Now()} 54 | 55 | endpoint1 := graph.Endpoint{abc, def, exchange1, &ob1} 56 | endpoint2 := graph.Endpoint{xyz, def, exchange1, &ob2} 57 | 58 | hits := make([]*graph.Hit, 2) 59 | hits[0] = &(graph.Hit{&endpoint1, true, &abc, &def}) 60 | hits[1] = &(graph.Hit{&endpoint2, false, &xyz, &def}) 61 | 62 | paths = make([]graph.Path, 1) 63 | paths[0] = graph.Path{hits, nil, nil, 0} 64 | 65 | chains = arbitrage.Run(paths) 66 | }) 67 | 68 | It("should return one chain", func() { 69 | Expect(len(chains)).To(Equal(1)) 70 | }) 71 | 72 | It("should return one chain enforcing the initial volume to 3", func() { 73 | c := chains[0] 74 | fmt.Println(c) 75 | Expect(c.VolumeToEngage).To(Equal(3.0)) 76 | }) 77 | 78 | It("should return one chain announcing a performance equal to 1x", func() { 79 | c := chains[0] 80 | Expect(c.Performance).To(Equal(2. / 3.)) 81 | }) 82 | 83 | It("should return 2 as a rate for the node #1", func() { 84 | c := chains[0] 85 | Expect(c.Rates[0]).To(Equal(2.0)) 86 | }) 87 | 88 | It("should return 2./3. as an adjusted volume for the node #2", func() { 89 | c := chains[0] 90 | Expect(c.Rates[1]).To(Equal(2. / 3.)) 91 | }) 92 | 93 | It("should return 6 as an adjusted volume for the node #1", func() { 94 | c := chains[0] 95 | Expect(c.AdjustedVolumes[0]).To(Equal(6.)) 96 | }) 97 | 98 | It("should return 2 as an adjusted volume for the node #2", func() { 99 | c := chains[0] 100 | Expect(c.AdjustedVolumes[1]).To(Equal(2.)) 101 | }) 102 | 103 | It("should return 3 for the volume of the order corresponding to node #1", func() { 104 | c := chains[0] 105 | Expect(c.Orders[0].BaseVolume).To(Equal(3.)) 106 | }) 107 | 108 | It("should return 2 for the volume of the order corresponding to node #2", func() { 109 | c := chains[0] 110 | Expect(c.Orders[1].BaseVolume).To(Equal(2.)) 111 | }) 112 | 113 | }) 114 | }) 115 | 116 | // Describe(` 117 | // Considering the combination: [ABC/DEF]@Exhange1 -> [DEF/XYZ]@Exhange1 -> [XYZ/ABC]@Exhange1, and the orderbooks: 118 | // [ABC/DEF]@Exhange1 -> Best Bid: 1ABC = 10DEF, Best Ask: 1ABC = 10DEF #ABC=0, DEF=10, XYZ=0 119 | // [DEF/XYZ]@Exhange1 -> Best Bid: 1DEF = 10XYZ, Best Ask: 1DEF = 10XYZ #ABC=0, DEF=0, XYZ=100 120 | // [XYZ/ABC]@Exhange1 -> Best Bid: 1XYZ = 0.01ABC, Best Ask: 1XYZ = 0.01ABC`, func() { 121 | // Context(` 122 | // When I fulfill all the orders, running the arbitrage`, func() { 123 | // var ( 124 | // chains []ArbitrageChain 125 | // ob1 core.Orderbook 126 | // ob2 core.Orderbook 127 | // ob3 core.Orderbook 128 | // paths []graph.Path 129 | // ) 130 | 131 | // BeforeEach(func() { 132 | // exchange1 := core.Exchange{"Exchange1", make([]core.CurrencyPair, 0), nil} 133 | 134 | // abc := core.Currency("ABC") 135 | // def := core.Currency("DEF") 136 | // xyz := core.Currency("XYZ") 137 | 138 | // pair1 := core.CurrencyPair{abc, def} 139 | // pair2 := core.CurrencyPair{def, xyz} 140 | // pair3 := core.CurrencyPair{xyz, abc} 141 | 142 | // bids1 := append([]core.Order{}, core.NewBid(pair1, 10, 1)) 143 | // asks1 := append([]core.Order{}, core.NewAsk(pair1, 10, 1)) 144 | // bids2 := append([]core.Order{}, core.NewBid(pair2, 10, 10)) 145 | // asks2 := append([]core.Order{}, core.NewAsk(pair2, 10, 1)) 146 | // bids3 := append([]core.Order{}, core.NewBid(pair3, 0.01, 100)) 147 | // asks3 := append([]core.Order{}, core.NewAsk(pair3, 0.01, 1)) 148 | 149 | // ob1 = core.Orderbook{pair1, bids1, asks1, time.Now()} 150 | // ob2 = core.Orderbook{pair2, bids2, asks2, time.Now()} 151 | // ob3 = core.Orderbook{pair3, bids3, asks3, time.Now()} 152 | 153 | // endpoint1 := graph.Endpoint{abc, def, exchange1, &ob1} 154 | // endpoint2 := graph.Endpoint{def, xyz, exchange1, &ob2} 155 | // endpoint3 := graph.Endpoint{xyz, abc, exchange1, &ob3} 156 | 157 | // hits := make([]*graph.Hit, 3) 158 | // hits[0] = &(graph.Hit{&endpoint1, true, &abc, &def}) 159 | // hits[1] = &(graph.Hit{&endpoint2, true, &def, &xyz}) 160 | // hits[2] = &(graph.Hit{&endpoint3, true, &xyz, &abc}) 161 | 162 | // paths = make([]graph.Path, 1) 163 | // paths[0] = graph.Path{hits, nil, nil} 164 | 165 | // chains = arbitrage.Run(paths) 166 | // }) 167 | 168 | // It("should return one chain", func() { 169 | // Expect(len(chains)).To(Equal(1)) 170 | // }) 171 | 172 | // It("should return one chain enforcing the initial volume to 1", func() { 173 | // c := chains[0] 174 | // fmt.Println(c) 175 | // Expect(c.VolumeToEngage).To(Equal(1.0)) 176 | // }) 177 | 178 | // It("should return one chain announcing a performance equal to 1x", func() { 179 | // c := chains[0] 180 | // Expect(c.Performance).To(Equal(1.0)) 181 | // }) 182 | 183 | // It("should return one chain announcing a performance equal to 10x if 1XYZ = 0.10ABC instead of 1XYZ = 0.01ABC", func() { 184 | // ob3.Bids[0].Price = 0.10 185 | // chains = arbitrage.Run(paths) 186 | // c := chains[0] 187 | // Expect(c.Performance).To(Equal(10.0)) 188 | // }) 189 | 190 | // It("should return one chain announcing a performance equal to 10x if 1XYZ = 0.10ABC instead of 1XYZ = 0.01ABC", func() { 191 | // ob3.Bids[0].Price = 0.10 192 | // chains = arbitrage.Run(paths) 193 | // c := chains[0] 194 | // Expect(c.Performance).To(Equal(10.0)) 195 | // }) 196 | 197 | // It("should return one chain enforcing the initial volume to 0.1 if only 10 XYZ are available", func() { 198 | // ob3.Bids[0].BaseVolume = 10 199 | // chains = arbitrage.Run(paths) 200 | // c := chains[0] 201 | // Expect(c.VolumeToEngage).To(Equal(0.1)) 202 | // }) 203 | // }) 204 | // }) 205 | 206 | // Describe(` 207 | // Considering the combination: [ABC/DEF]@Exhange1 -> [XYZ/DEF]@Exhange1 -> [XYZ/ABC]@Exhange1, and the orderbooks: 208 | // [ABC/DEF]@Exhange1 -> Best Bid: 1ABC = 10DEF, Best Ask: 1ABC = 10DEF #ABC=0, DEF=10, XYZ=0 209 | // [XYZ/DEF]@Exhange1 -> Best Bid: 1XYZ = 0.01DEF, Best Ask: 1DEF = 0.1XYZ #ABC=0, DEF=0, XYZ=100 210 | // [XYZ/ABC]@Exhange1 -> Best Bid: 1XYZ = 0.1ABC, Best Ask: 1XYZ = 0.01ABC`, func() { 211 | // Context(` 212 | // When I fulfill all the orders, running the arbitrage`, func() { 213 | // var ( 214 | // chains []ArbitrageChain 215 | // ob1 core.Orderbook 216 | // ob2 core.Orderbook 217 | // ob3 core.Orderbook 218 | // paths []graph.Path 219 | // ) 220 | 221 | // BeforeEach(func() { 222 | // exchange1 := core.Exchange{"Exchange1", make([]core.CurrencyPair, 0), nil} 223 | 224 | // abc := core.Currency("ABC") 225 | // def := core.Currency("DEF") 226 | // xyz := core.Currency("XYZ") 227 | 228 | // pair1 := core.CurrencyPair{abc, def} 229 | // pair2 := core.CurrencyPair{xyz, def} 230 | // pair3 := core.CurrencyPair{xyz, abc} 231 | 232 | // bids1 := append([]core.Order{}, core.NewBid(pair1, 10, 1)) 233 | // asks1 := append([]core.Order{}, core.NewAsk(pair1, 10, 1)) 234 | // bids2 := append([]core.Order{}, core.NewBid(pair2, 0.01, 1000)) 235 | // asks2 := append([]core.Order{}, core.NewAsk(pair2, 0.01, 1000)) 236 | // bids3 := append([]core.Order{}, core.NewBid(pair3, 0.001, 1000)) 237 | // asks3 := append([]core.Order{}, core.NewAsk(pair3, 0.001, 1000)) 238 | 239 | // ob1 = core.Orderbook{pair1, bids1, asks1, time.Now()} 240 | // ob2 = core.Orderbook{pair2, bids2, asks2, time.Now()} 241 | // ob3 = core.Orderbook{pair3, bids3, asks3, time.Now()} 242 | 243 | // endpoint1 := graph.Endpoint{abc, def, exchange1, &ob1} 244 | // endpoint2 := graph.Endpoint{xyz, def, exchange1, &ob2} 245 | // endpoint3 := graph.Endpoint{xyz, abc, exchange1, &ob3} 246 | 247 | // hits := make([]*graph.Hit, 3) 248 | // hits[0] = &(graph.Hit{&endpoint1, true, &abc, &def}) 249 | // hits[1] = &(graph.Hit{&endpoint2, false, &xyz, &def}) 250 | // hits[2] = &(graph.Hit{&endpoint3, true, &xyz, &abc}) 251 | 252 | // paths = make([]graph.Path, 1) 253 | // paths[0] = graph.Path{hits, nil, nil} 254 | 255 | // chains = arbitrage.Run(paths) 256 | // }) 257 | 258 | // It("should return one chain", func() { 259 | // Expect(len(chains)).To(Equal(1)) 260 | // }) 261 | 262 | // It("should return one chain enforcing the initial volume to 1", func() { 263 | // c := chains[0] 264 | // fmt.Println(c) 265 | // Expect(c.VolumeToEngage).To(Equal(1.0)) 266 | // }) 267 | 268 | // It("should return one chain announcing a performance equal to 1x", func() { 269 | // c := chains[0] 270 | // Expect(c.Performance).To(Equal(1.0)) 271 | // }) 272 | 273 | // It("should return one chain announcing a performance equal to 10x if 1XYZ = 0.10ABC instead of 1XYZ = 0.01ABC", func() { 274 | // ob3.Bids[0].Price = 0.10 275 | // chains = arbitrage.Run(paths) 276 | // c := chains[0] 277 | // Expect(c.Performance).To(Equal(10.0)) 278 | // }) 279 | 280 | // It("should return one chain announcing a performance equal to 10x if 1XYZ = 0.10ABC instead of 1XYZ = 0.01ABC", func() { 281 | // ob3.Bids[0].Price = 0.10 282 | // chains = arbitrage.Run(paths) 283 | // c := chains[0] 284 | // Expect(c.Performance).To(Equal(10.0)) 285 | // }) 286 | 287 | // It("should return one chain enforcing the initial volume to 0.1 if only 10 XYZ are available", func() { 288 | // ob3.Bids[0].BaseVolume = 10 289 | // chains = arbitrage.Run(paths) 290 | // c := chains[0] 291 | // Expect(c.VolumeToEngage).To(Equal(0.1)) 292 | // }) 293 | // }) 294 | // }) 295 | }) 296 | --------------------------------------------------------------------------------