├── assets └── gopher.png ├── .gitignore ├── go.mod ├── MakeFile ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── go.sum ├── examples ├── logic_example_go └── martingale_go ├── .travis.yml ├── configs ├── config.yml ├── config-testnet.yml └── config-test.yml ├── docker-compose.yml ├── internal ├── convert │ ├── convert.go │ └── convert_test.go ├── api │ ├── client_test.go │ └── client.go ├── central │ ├── colun_test.go │ └── colun.go ├── display │ ├── message.go │ └── message_test.go ├── reader │ ├── config_test.go │ └── config.go └── logic │ └── logic.go ├── cmd └── main.go ├── Dockerfile ├── LICENSE └── README.md /assets/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiago-scherrer/gotrader/HEAD/assets/gopher.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Output of the go coverage tool, specifically when used with LiteIDE 2 | *.out 3 | 4 | # vim tmp files 5 | *.swp 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thiago-scherrer/gotrader 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-redis/redis v6.15.7+incompatible 7 | gopkg.in/yaml.v2 v2.2.8 8 | ) 9 | -------------------------------------------------------------------------------- /MakeFile: -------------------------------------------------------------------------------- 1 | redis: 2 | docker-compose up -d redis 3 | 4 | tdd: 5 | 6 | go mod download 7 | ENV="test" go test -cover ./... 8 | go vet ./... 9 | 10 | clean: 11 | docker-compose down -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= 2 | github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 3 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 4 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 5 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 6 | -------------------------------------------------------------------------------- /examples/logic_example_go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | // Path from api to view the orderbook 4 | const orb string = "/api/v1/orderBook/L2?" 5 | 6 | // CandleRunner verify the api and start the logic system 7 | func CandleRunner() string { 8 | return "statusLogic" 9 | } 10 | 11 | func returnDepth() string { 12 | return "Buy/Sell/Draw" 13 | } 14 | 15 | // ClosePositionProfitBuy the Buy position 16 | func ClosePositionProfitBuy() { 17 | 18 | } 19 | 20 | // ClosePositionProfitSell close the Sell position 21 | func ClosePositionProfitSell() { 22 | } 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change. 4 | 5 | ## Type of change 6 | 7 | Select what your PR intends to do: 8 | 9 | - [ ] Bug fix. 10 | - [ ] New feature. 11 | - [ ] This change requires a documentation update. 12 | 13 | # Checklist: 14 | 15 | - [ ] My code follows the [golang-standards](https://github.com/golang-standards/project-layout) 16 | - [ ] I have performed a gofmt, go vet, gocyclo, golint 17 | - [ ] I agree to the project license 18 | - [ ] My changes generate no new warnings 19 | - [ ] I have added tests 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: true 3 | dist: xenial 4 | 5 | language: go 6 | 7 | env: 8 | - GO111MODULE=on 9 | - GOFLAGS='-mod vendor' 10 | 11 | go: 12 | - 1.14.2 13 | 14 | env: 15 | - DOCKER_COMPOSE_VERSION=1.23.2 16 | 17 | addons: 18 | hosts: 19 | - redis 20 | 21 | before_install: 22 | - sudo rm /usr/local/bin/docker-compose 23 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 24 | - chmod +x docker-compose 25 | - sudo mv docker-compose /usr/local/bin 26 | 27 | script: 28 | - make -f MakeFile redis tdd clean 29 | - docker-compose build 30 | -------------------------------------------------------------------------------- /configs/config.yml: -------------------------------------------------------------------------------- 1 | # User id to use API (required) 2 | userid: "" 3 | 4 | # Password/Secret key to use API (required) 5 | secret: "" 6 | 7 | # API endpoint/url. Aways use HTTPS! (required) 8 | endpoint: "https://www.bitmex.com" 9 | 10 | # Amount of contracts for trade. Only integers (required) 11 | hand: 50 12 | 13 | # Asset to trade (required) 14 | asset: "XBTUSD" 15 | 16 | # Cadle time (minutes). The minimum is 1 (required) 17 | candle: 5 18 | 19 | # threshold to trigger the rule (required) 20 | threshold: 5 21 | 22 | # Leverage value. Set a number between 0.01 and 100. 23 | # Send 0 to enable cross margin. (required) 24 | leverage: "10" 25 | 26 | # Profit value to close (percentage) 27 | profit: 0.1 28 | 29 | # Stop Loss trigger (percentage) 30 | stoploss: 6.0 31 | -------------------------------------------------------------------------------- /configs/config-testnet.yml: -------------------------------------------------------------------------------- 1 | # User id to use API (bitmex example) 2 | userid: "" 3 | 4 | # Password to use API (bitmex example) 5 | secret: "" 6 | 7 | # API endpoint/url. Aways use HTTPS! 8 | endpoint: "https://testnet.bitmex.com" 9 | 10 | # Amount of contracts for trade. Only integers (required) 11 | hand: 50 12 | 13 | # Asset to trade (required) 14 | asset: "XBTUSD" 15 | 16 | # Cadle time (minutes). The minimum is 1 (required) 17 | candle: 5 18 | 19 | # threshold to trigger the rule (required) 20 | threshold: 5 21 | 22 | # Leverage value. Set a number between 0.01 and 100. 23 | # Send 0 to enable cross margin. (required) 24 | leverage: "10" 25 | 26 | # Profit value to close (percentage) 27 | profit: 0.1 28 | 29 | # Stop Loss trigger (percentage) 30 | stoploss: 6.0 31 | -------------------------------------------------------------------------------- /configs/config-test.yml: -------------------------------------------------------------------------------- 1 | # User id to use API (bitmex example) 2 | userid: "LAqUlngMIQkIUjXMUreyu3qn" 3 | 4 | # Password to use API (bitmex example) 5 | secret: "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO" 6 | 7 | # API endpoint/url. Aways use HTTPS! 8 | endpoint: "https://testnet.bitmex.com" 9 | 10 | # Amount of contracts for trade. Only integers (required) 11 | hand: 2 12 | 13 | # Asset to trade 14 | asset: "XBTUSD" 15 | 16 | # Cadle time (minutes) 17 | candle: 1 18 | 19 | # threshold to trigger the rule. 20 | threshold: 1 21 | 22 | # Leverage value. Set a number between 0.01 and 100. 23 | # Send 0 to enable cross margin. 24 | leverage: "0.1" 25 | 26 | # Profit value to close (percentage) 27 | profit: 0.1 28 | 29 | # Stop Loss trigger (percentage) 30 | stoploss: 1.0 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | redis: 5 | container_name: redis 6 | image: redis 7 | command: redis-server 8 | ports: 9 | - "6379:6379" 10 | builder: 11 | container_name: gotrader-builder 12 | build: 13 | context: . 14 | target: builder 15 | runner: 16 | container_name: gotrader 17 | build: 18 | context: . 19 | target: runner 20 | depends_on: 21 | - redis 22 | links: 23 | - redis 24 | environment: 25 | - GOTRADER_MODE=prod 26 | - REDIS_URL=redis://redis 27 | volumes: 28 | - type: bind 29 | source: ./configs/config-testnet.yml 30 | target: /opt/config-testnet.yml 31 | - type: bind 32 | source: ./configs/config.yml 33 | target: /opt/config.yml 34 | -------------------------------------------------------------------------------- /internal/convert/convert.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // BytesToString convert bytes to string 9 | func BytesToString(data []byte) string { 10 | return string(data[:]) 11 | } 12 | 13 | // StringToBytes convert string to byte 14 | func StringToBytes(data string) []byte { 15 | return []byte(data[:]) 16 | } 17 | 18 | // StringToInt convert string to int64 19 | func StringToInt(data string) int64 { 20 | result, _ := strconv.ParseInt(data, 10, 64) 21 | return result 22 | } 23 | 24 | // IntToString convert string to int64 25 | func IntToString(data int64) string { 26 | return strconv.FormatInt(int64(data), 10) 27 | } 28 | 29 | // FloatToString convert float64 to string 30 | func FloatToString(data float64) string { 31 | return fmt.Sprintf("%f", data) 32 | } 33 | 34 | // StringToFloat64 convert string to float64 35 | func StringToFloat64(data string) float64 { 36 | result, _ := strconv.ParseFloat(data, 64) 37 | return result 38 | } 39 | 40 | // FloatToInt convert int to float64 41 | func FloatToInt(data float64) int64 { 42 | return int64(data) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/thiago-scherrer/gotrader/internal/central" 7 | "github.com/thiago-scherrer/gotrader/internal/display" 8 | "github.com/thiago-scherrer/gotrader/internal/logic" 9 | "github.com/thiago-scherrer/gotrader/internal/reader" 10 | ) 11 | 12 | func main() { 13 | for { 14 | reader.Boot() 15 | daemonize() 16 | } 17 | } 18 | 19 | func daemonize() { 20 | for { 21 | reader.ConfigPath() 22 | 23 | log.Println( 24 | display.HelloMsg(reader.Asset()), 25 | ) 26 | 27 | trd := logic.CandleRunner() 28 | if trd == "Draw" { 29 | log.Println( 30 | display.DrawMode(), 31 | ) 32 | break 33 | } 34 | 35 | hand := reader.Hand() 36 | 37 | if central.CreateOrder(trd, hand) == false { 38 | trd = "Error" 39 | } 40 | 41 | log.Println( 42 | display.OrderPrice( 43 | reader.Asset(), 44 | central.GetPosition(), 45 | ), 46 | ) 47 | 48 | if trd == "Buy" { 49 | logic.ClosePositionProfitBuy() 50 | } else if trd == "Sell" { 51 | logic.ClosePositionProfitSell() 52 | } else { 53 | display.OrderCancelMsg() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # build 3 | # ------------------------------------------------------------------------------ 4 | FROM golang:1.14.2 AS builder 5 | 6 | LABEL NAME golang 7 | LABEL VERSION 2.0 8 | 9 | ENV APP /src/gotrader 10 | 11 | WORKDIR ${APP}/src/gotrader 12 | 13 | RUN mkdir -p ${APP}/src/gotrader 14 | 15 | COPY . ${APP}/src/gotrader 16 | 17 | RUN cd ${APP}/src/gotrader \ 18 | && go mod download 19 | 20 | RUN mv configs/*.yml /opt/ \ 21 | && cd ${APP}/src/gotrader \ 22 | && cd cmd \ 23 | && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /bin/gotrader \ 24 | && useradd gotrader 25 | 26 | # ------------------------------------------------------------------------------ 27 | # daemon image 28 | # ------------------------------------------------------------------------------ 29 | FROM scratch AS runner 30 | 31 | LABEL NAME scratch 32 | LABEL VERSION 2.0 33 | 34 | USER gotrader 35 | 36 | ENV GOTRADER_MODE testnet 37 | 38 | COPY --from=builder /etc/ssl /etc/ssl 39 | COPY --from=builder /etc/group /etc/group 40 | COPY --from=builder /etc/passwd /etc/passwd 41 | COPY --from=builder /bin/gotrader /bin/gotrader 42 | 43 | CMD ["gotrader"] 44 | -------------------------------------------------------------------------------- /internal/api/client_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/thiago-scherrer/gotrader/internal/reader" 7 | ) 8 | 9 | func TestHmac(t *testing.T) { 10 | reader.ConfigPath() 11 | 12 | expired := "1518064236" 13 | path := "/api/v1/instrument" 14 | requestTipe := "GET" 15 | secretQuery := "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO" 16 | hexExpected := "9c37199dd75f47b63774ddbb5e2851998848d5ec62b9a2bbc380a48f620b305e" 17 | hexResult := hexCreator(secretQuery, requestTipe, path, expired, "data") 18 | 19 | if hexExpected != hexResult { 20 | t.Error("GET hex not match, got: ", hexResult, "need: ", hexExpected) 21 | } 22 | } 23 | 24 | func Test_hexCreator(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | want string 28 | }{ 29 | {"Test", "4210b09a7ec51b6399c8b32284925bce0c28156b4800d97f4cf5815ab059fd4b"}, 30 | } 31 | secret := "42" 32 | requestTipe := "42" 33 | path := "42" 34 | expired := "never" 35 | data := "42" 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := hexCreator(secret, requestTipe, path, 39 | expired, data); got != tt.want { 40 | t.Errorf("hexCreator() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /internal/central/colun_test.go: -------------------------------------------------------------------------------- 1 | package central 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/thiago-scherrer/gotrader/internal/convert" 7 | ) 8 | 9 | func TestParserAmount(t *testing.T) { 10 | mock := `{ "Amount": 10 }` 11 | getResult := parserAmount(convert.StringToBytes(mock)) 12 | 13 | if getResult != 10 { 14 | t.Error("json parser not working, got:", getResult) 15 | } 16 | } 17 | 18 | func TestLastPriceJson(t *testing.T) { 19 | mock := `[{ "LastPrice": 10.1 }]` 20 | getResult := lastPrice(convert.StringToBytes(mock)) 21 | 22 | if getResult != 10.1 { 23 | t.Error("LastPrice json parser not working, got:", getResult) 24 | } 25 | } 26 | 27 | func Test_lastPrice(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | want float64 31 | }{ 32 | {"Test", 1}, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := lastPrice(convert.StringToBytes(`[{"lastPrice":1}]`)); got != tt.want { 37 | t.Errorf("lastPrice() = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func Test_parserAmount(t *testing.T) { 44 | tests := []struct { 45 | name string 46 | want int 47 | }{ 48 | {"Test", 1}, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | if got := parserAmount(convert.StringToBytes(`{"amount":1}`)); got != tt.want { 53 | t.Errorf("parserAmount() = %v, want %v", got, tt.want) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/display/message.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "github.com/thiago-scherrer/gotrader/internal/convert" 5 | ) 6 | 7 | // HelloMsg will be see when the bot start a new trade 8 | func HelloMsg(a string) string { 9 | return " " + a + " - Starting a new round" 10 | } 11 | 12 | // UsageMsg display a basic msg when not found the config file 13 | func UsageMsg() string { 14 | return "Config not found! Usage: gotrader config some_config.yml" 15 | } 16 | 17 | // SetleverageMsg display set the l 18 | func SetleverageMsg(a, l string) string { 19 | return " " + a + " - Setting leverage: " + l 20 | } 21 | 22 | // OrderCreatedMsg display the type order to the user 23 | func OrderCreatedMsg(a, t string) string { 24 | return " " + a + " - A new order type: " + t + " as been created! " 25 | } 26 | 27 | // OrderCancelMsg display cancell msg 28 | func OrderCancelMsg() string { 29 | return "Canceling trade, order not executed" 30 | } 31 | 32 | // OrderDoneMsg display a msg when order fulfilled 33 | func OrderDoneMsg(a string) string { 34 | return " " + a + " - Order fulfilled!" 35 | } 36 | 37 | // OrdertriggerMsg display a msg when order trigged 38 | func OrdertriggerMsg(a string) string { 39 | return " " + a + " - Profit target trigged" 40 | } 41 | 42 | // OrderWaintMsg display a msg when will waint 43 | func OrderWaintMsg(a string) string { 44 | return " " + a + " - Waiting to get order fulfilled..." 45 | } 46 | 47 | //StopLossMsg show stop loss msg 48 | func StopLossMsg(a string) string { 49 | return " " + a + " - Stop Loss trigged!" 50 | } 51 | 52 | // ProfitMsg display msg when the trade get profit 53 | func ProfitMsg(a string) string { 54 | return " " + a + " - Round done." 55 | } 56 | 57 | // OrderPrice display the current price 58 | func OrderPrice(asset string, price float64) string { 59 | return " " + asset + " - Price now: " + convert.FloatToString(price) 60 | } 61 | 62 | // DrawMode display the draw msg 63 | func DrawMode() string { 64 | return "The number of buy and sell orders are equal, leaving." 65 | } 66 | -------------------------------------------------------------------------------- /internal/api/client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/thiago-scherrer/gotrader/internal/convert" 14 | "github.com/thiago-scherrer/gotrader/internal/reader" 15 | ) 16 | 17 | // hexCreator encode a string to a sha256, this is needed in bitme API 18 | func hexCreator(Secret, requestTipe, path, expired, data string) string { 19 | concat := requestTipe + path + expired + data 20 | 21 | h := hmac.New(sha256.New, []byte(Secret)) 22 | h.Write([]byte(concat)) 23 | 24 | hexResult := hex.EncodeToString( 25 | h.Sum(nil), 26 | ) 27 | 28 | return hexResult 29 | } 30 | 31 | // timeExpired create a time to expire a session API. 32 | func timeExpired() int64 { 33 | timeExpired := timeStamp() + 60 34 | 35 | return timeExpired 36 | } 37 | 38 | // timeStamp create a timestamp to be used in a session. 39 | func timeStamp() int64 { 40 | now := time.Now() 41 | 42 | timestamp := now.Unix() 43 | 44 | return timestamp 45 | } 46 | 47 | // ClientRobot make all request to the bitmex API 48 | func ClientRobot(requestType, path string, data []byte) ([]byte, int) { 49 | cl := &http.Client{} 50 | ep := reader.Endpoint() 51 | sq := reader.Secret() 52 | uid := reader.Userid() 53 | exp := convert.IntToString( 54 | (timeExpired()), 55 | ) 56 | hex := hexCreator( 57 | sq, 58 | requestType, 59 | path, 60 | exp, 61 | convert.BytesToString(data), 62 | ) 63 | url := ep + path 64 | 65 | req, err := http.NewRequest(requestType, url, bytes.NewBuffer(data)) 66 | if err != nil { 67 | log.Println("Error create a request, got: ", err) 68 | } 69 | 70 | req.Header.Set("api-signature", hex) 71 | req.Header.Set("api-expires", exp) 72 | req.Header.Set("api-key", uid) 73 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 74 | req.Header.Set("Accept", "application/json") 75 | req.Header.Set("User-Agent", "gotrader") 76 | rsp, err := cl.Do(req) 77 | if err != nil { 78 | log.Println("Error to send the request to the API bitmex, got: ", err) 79 | return []byte(""), 500 80 | } 81 | body, _ := ioutil.ReadAll(rsp.Body) 82 | 83 | return body, rsp.StatusCode 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoTrader bot 2 | 3 | Project to create a trade bot for bitmex trade platform. 4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/thiago-scherrer/gotrader)](https://goreportcard.com/report/github.com/thiago-scherrer/gotrader) [![Build Status](https://travis-ci.org/thiago-scherrer/gotrader.svg?branch=master)](https://travis-ci.org/thiago-scherrer/gotrader) 6 | [![GoDoc](https://godoc.org/github.com/thiago-scherrer/gotrader?status.svg)](https://godoc.org/github.com/thiago-scherrer/gotrader) 7 | 8 | ![gopher](assets/gopher.png) 9 | 10 | ## Requirements 11 | 12 | - docker => 19.03.8 13 | - docker-compose => 1.25.4 14 | - bitmex account 15 | 16 | ## How it works 17 | 18 | Its purpose is to automate a rule created by the trader. 19 | 20 | ## Caution 21 | 22 | This bot still under construction and does not guarantee anything, it may not even work properly. You can lose money with it! **Test the gotrader bot in the test network first!** 23 | But if you have good results, PR the logic =) 24 | 25 | Don't panic! 26 | 27 | ## Enabled 28 | 29 | - New and close order, buy or sell 30 | - leverage 31 | - Able to use custom logic 32 | 33 | ## Supported Contracts 34 | 35 | - XBTUSD 36 | - ETHUSD 37 | 38 | ## Runing 39 | 40 | Add your settings to the file **configs/config.yml** 41 | 42 | Go back to the root dir and follow the steps below: 43 | 44 | ```sh 45 | docker-compose build 46 | ``` 47 | 48 | After *test* and *build*, run the bot (background): 49 | 50 | ```sh 51 | docker-compose up -d 52 | ``` 53 | 54 | You can see the logs with *docker logs* command, like: 55 | 56 | ```sh 57 | docker logs -f gotrader 58 | ``` 59 | 60 | To stop the bot, run: 61 | 62 | ```sh 63 | docker-compose down 64 | ``` 65 | 66 | ## Using other logic 67 | 68 | The acual logic can be changed on *internal/logic/*. Some examples can be found on *examples/*. 69 | 70 | Go to the **example/** folder and then, choose or create a strategy. Copy the file like **martingale_go** to the **internal/logic/logic.go**. 71 | 72 | ## ENVs 73 | 74 | - GOTRADER_MODE 75 | - `prod` (default): load `config.yml` 76 | - `testnet`: load `config-testnet.yml` 77 | - REDISURL 78 | - `redis:6379` (default): endpoint to use redis 79 | 80 | ## References 81 | 82 | - [bitmex api](https://www.bitmex.com/api/explorer/) 83 | - [docker-compose install](https://docs.docker.com/compose/install/) 84 | - [gopherize](https://gopherize.me) 85 | - [project-layout](https://github.com/golang-standards/project-layout) 86 | -------------------------------------------------------------------------------- /internal/convert/convert_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestBytesToString(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | want string 12 | }{ 13 | {"Test", "42"}, 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | if got := BytesToString(StringToBytes("42")); got != tt.want { 18 | t.Errorf("BytesToString() = %v, want %v", got, tt.want) 19 | } 20 | }) 21 | } 22 | } 23 | 24 | func TestStringToBytes(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | want []byte 28 | }{ 29 | {"Teste", StringToBytes("42")}, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | if got := StringToBytes("42"); !reflect.DeepEqual(got, tt.want) { 34 | t.Errorf("StringToBytes() = %v, want %v", got, tt.want) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func TestStringToInt(t *testing.T) { 41 | tests := []struct { 42 | name string 43 | want int64 44 | }{ 45 | {"Test", 42}, 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | if got := StringToInt("42"); got != tt.want { 50 | t.Errorf("StringToInt() = %v, want %v", got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestIntToString(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | want string 60 | }{ 61 | {"Test", "42"}, 62 | } 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | if got := IntToString(42); got != tt.want { 66 | t.Errorf("IntToString() = %v, want %v", got, tt.want) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | func TestFloatToString(t *testing.T) { 73 | tests := []struct { 74 | name string 75 | want string 76 | }{ 77 | {"Test", "42.000000"}, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | if got := FloatToString(42.0); got != tt.want { 82 | t.Errorf("FloatToString() = %v, want %v", got, tt.want) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestFloatToInt(t *testing.T) { 89 | tests := []struct { 90 | name string 91 | want int64 92 | }{ 93 | {"Test", 42}, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | if got := FloatToInt(42); got != tt.want { 98 | t.Errorf("FloatToInt() = %v, want %v", got, tt.want) 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /internal/display/message_test.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUsageMsg(t *testing.T) { 8 | if UsageMsg() != "Config not found! Usage: gotrader config some_config.yml" { 9 | t.Error("error to get usage msg, got:", UsageMsg()) 10 | } 11 | } 12 | 13 | func Test_setleverageMsg(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | want string 17 | }{ 18 | {"test: ", " " + "BTC" + " - Setting leverage: 0.1"}, 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | if got := SetleverageMsg("BTC", "0.1"); got != tt.want { 23 | t.Errorf("setlavarageMsg() = %v, want %v", got, tt.want) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func Test_orderCreatedMsg(t *testing.T) { 30 | tests := []struct { 31 | name string 32 | want string 33 | }{ 34 | {"Test", " " + "BTC" + " - A new order type: Sell as been created! "}, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := OrderCreatedMsg("BTC", "Sell"); got != tt.want { 39 | t.Errorf("orderCreatedMsg() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func Test_orderDoneMsg(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | want string 49 | }{ 50 | {"Test", " " + "BTC" + " - Order fulfilled!"}, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | if got := OrderDoneMsg("BTC"); got != tt.want { 55 | t.Errorf("orderDoneMsg() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func Test_ordertriggerMsg(t *testing.T) { 62 | tests := []struct { 63 | name string 64 | want string 65 | }{ 66 | {"Test", " " + "BTC" + " - Profit target trigged"}, 67 | } 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | if got := OrdertriggerMsg("BTC"); got != tt.want { 71 | t.Errorf("ordertriggerMsg() = %v, want %v", got, tt.want) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func Test_orderWaintMsg(t *testing.T) { 78 | tests := []struct { 79 | name string 80 | want string 81 | }{ 82 | {"Test", " " + "BTC" + " - Waiting to get order fulfilled..."}, 83 | } 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | if got := OrderWaintMsg("BTC"); got != tt.want { 87 | t.Errorf("orderWaintMsg() = %v, want %v", got, tt.want) 88 | } 89 | }) 90 | } 91 | } 92 | 93 | func TestDrawMode(t *testing.T) { 94 | want := "The number of buy and sell orders are equal, leaving." 95 | if DrawMode() != want { 96 | t.Error("error to get draw msg, WANT: ", want, " GOT: ", DrawMode()) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/reader/config_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestFlag(t *testing.T) { 9 | getResult := ConfigPath() 10 | if len(getResult) <= 1 { 11 | t.Error("init flag not working, got: ", getResult) 12 | } 13 | } 14 | 15 | func TestReader(t *testing.T) { 16 | Boot() 17 | 18 | asset := Asset() 19 | 20 | if asset != "XBTUSD" { 21 | t.Error("error to read config file, got:", asset) 22 | } 23 | } 24 | 25 | func Test_configReader(t *testing.T) { 26 | Boot() 27 | tests := []struct { 28 | name string 29 | want *Conf 30 | }{ 31 | {"Test", configReader()}, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | if got := configReader(); !reflect.DeepEqual(got, tt.want) { 36 | t.Errorf("configReader() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func Test_asset(t *testing.T) { 43 | Boot() 44 | tests := []struct { 45 | name string 46 | want string 47 | }{ 48 | {"Test", "XBTUSD"}, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | if got := Asset(); got != tt.want { 53 | t.Errorf("Asset() = %v, want %v", got, tt.want) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func Test_Candle(t *testing.T) { 60 | Boot() 61 | tests := []struct { 62 | name string 63 | want int64 64 | }{ 65 | {"Test", 1}, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | if got := Candle(); got != tt.want { 70 | t.Errorf("Candle() = %v, want %v", got, tt.want) 71 | } 72 | }) 73 | } 74 | } 75 | 76 | func Test_endpoint(t *testing.T) { 77 | Boot() 78 | tests := []struct { 79 | name string 80 | want string 81 | }{ 82 | {"Test", "https://testnet.bitmex.com"}, 83 | } 84 | 85 | for _, tt := range tests { 86 | t.Run(tt.name, func(t *testing.T) { 87 | if got := Endpoint(); got != tt.want { 88 | t.Errorf("Endpoint() = %v, want %v", got, tt.want) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func Test_leverage(t *testing.T) { 95 | Boot() 96 | tests := []struct { 97 | name string 98 | want string 99 | }{ 100 | {"Test", "0.1"}, 101 | } 102 | 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | if got := Leverage(); got != tt.want { 106 | t.Errorf("leverage() = %v, want %v", got, tt.want) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func Test_secret(t *testing.T) { 113 | Boot() 114 | tests := []struct { 115 | name string 116 | want string 117 | }{ 118 | {"Test", "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO"}, 119 | } 120 | for _, tt := range tests { 121 | t.Run(tt.name, func(t *testing.T) { 122 | if got := Secret(); got != tt.want { 123 | t.Errorf("secret() = %v, want %v", got, tt.want) 124 | } 125 | }) 126 | } 127 | } 128 | 129 | func Test_Threshold(t *testing.T) { 130 | Boot() 131 | tests := []struct { 132 | name string 133 | want int64 134 | }{ 135 | {"Test", 1}, 136 | } 137 | for _, tt := range tests { 138 | t.Run(tt.name, func(t *testing.T) { 139 | if got := Threshold(); got != tt.want { 140 | t.Errorf("Threshold() = %v, want %v", got, tt.want) 141 | } 142 | }) 143 | } 144 | } 145 | 146 | func Test_userid(t *testing.T) { 147 | Boot() 148 | tests := []struct { 149 | name string 150 | want string 151 | }{ 152 | {"Test", "LAqUlngMIQkIUjXMUreyu3qn"}, 153 | } 154 | for _, tt := range tests { 155 | t.Run(tt.name, func(t *testing.T) { 156 | if got := Userid(); got != tt.want { 157 | t.Errorf("userid() = %v, want %v", got, tt.want) 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/logic/logic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/thiago-scherrer/gotrader/internal/api" 12 | "github.com/thiago-scherrer/gotrader/internal/central" 13 | "github.com/thiago-scherrer/gotrader/internal/convert" 14 | "github.com/thiago-scherrer/gotrader/internal/display" 15 | rd "github.com/thiago-scherrer/gotrader/internal/reader" 16 | ) 17 | 18 | // Path from api to view the orderbook 19 | const orb string = "/api/v1/orderBook/L2?" 20 | 21 | // CandleRunner verify the api and start the logic system 22 | func CandleRunner() string { 23 | t := rd.Threshold() 24 | c := rd.Candle() 25 | var tsl int 26 | var cby int 27 | 28 | for i := int64(0); i < t; i++ { 29 | for ii := int64(0); ii < c; ii++ { 30 | res := returnDepth() 31 | if res == "Buy" { 32 | cby++ 33 | } else if res == "Sell" { 34 | tsl++ 35 | } else { 36 | i = -1 37 | } 38 | } 39 | } 40 | 41 | if cby > tsl { 42 | return "Buy" 43 | } else if tsl > cby { 44 | return "Sell" 45 | } 46 | 47 | return "Draw" 48 | } 49 | 50 | func returnDepth() string { 51 | var sell int 52 | var buy int 53 | 54 | poh := "/api/v1/trade?" 55 | ap := rd.APIArray() 56 | t := timeStamp() 57 | d := rd.Data() 58 | 59 | u := url.Values{} 60 | u.Set("symbol", rd.Asset()) 61 | u.Add("partial", "false") 62 | u.Add("count", "500") 63 | u.Add("reverse", "false") 64 | u.Add("filter", t) 65 | 66 | for index := 1; index <= 60; index++ { 67 | u.Set("start", strconv.Itoa(index)) 68 | 69 | p := poh + u.Encode() 70 | res, code := api.ClientRobot("GET", p, d) 71 | 72 | if code != 200 { 73 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(res)) 74 | time.Sleep(time.Duration(2) * time.Second) 75 | } 76 | 77 | err := json.Unmarshal(res, &ap) 78 | if err != nil { 79 | break 80 | } 81 | if len(res) <= 5 { 82 | break 83 | } 84 | 85 | for _, v := range ap[:] { 86 | if v.Side == "Sell" { 87 | sell = sell + v.Size 88 | } else if v.Side == "Buy" { 89 | buy = buy + v.Size 90 | } 91 | 92 | } 93 | time.Sleep(time.Duration(1) * time.Second) 94 | 95 | } 96 | 97 | return logicSystem(buy, sell) 98 | } 99 | 100 | func timeStamp() string { 101 | ctm := rd.Candle() 102 | 103 | t := time.Now().UTC().Add(time.Duration(-ctm) * time.Minute) 104 | 105 | date := t.Format("2006-01-02") 106 | 107 | time := t.Format("15:04") 108 | 109 | return `{"timestamp.date": "` + date + `", "timestamp.minute": "` + time + `" }` 110 | } 111 | 112 | func logicSystem(buy, sell int) string { 113 | if buy > sell { 114 | return "Buy" 115 | } else if sell > buy { 116 | return "Sell" 117 | } 118 | 119 | return "Draw" 120 | } 121 | 122 | func stopLossBuy(pst float64, price float64) bool { 123 | return price <= (pst - ((pst / 100) * rd.StopLoss())) 124 | } 125 | 126 | func stopLossSell(pst float64, price float64) bool { 127 | return price >= (pst + ((pst / 100) * rd.StopLoss())) 128 | } 129 | 130 | func closePositionBuy(pst float64, price float64) bool { 131 | return price >= (pst + ((pst / 100) * rd.Profit())) 132 | } 133 | 134 | func closePositionSell(pst float64, price float64) bool { 135 | return price <= (pst - ((pst / 100) * rd.Profit())) 136 | } 137 | 138 | func priceCloseBuy(price float64) string { 139 | priceClose := fmt.Sprintf("%2.f", 140 | (price + 0.5), 141 | ) 142 | 143 | return priceClose 144 | } 145 | 146 | func priceCloseSell(price float64) string { 147 | priceClose := fmt.Sprintf("%2.f", 148 | (price - 0.5), 149 | ) 150 | 151 | return priceClose 152 | } 153 | 154 | func lossTargetBuy(price float64) string { 155 | priceClose := fmt.Sprintf("%2.f", 156 | (price - 0.5), 157 | ) 158 | 159 | return priceClose 160 | } 161 | 162 | func lossTargetSell(price float64) string { 163 | priceClose := fmt.Sprintf("%2.f", 164 | (price + 0.5), 165 | ) 166 | 167 | return priceClose 168 | } 169 | 170 | // ClosePositionProfitBuy is responsible for closing a Buy order 171 | func ClosePositionProfitBuy() { 172 | pst := central.GetPosition() 173 | 174 | for { 175 | price := central.Price() 176 | 177 | if closePositionBuy(pst, price) { 178 | profitTarget := priceCloseBuy(price) 179 | 180 | log.Println(display.OrdertriggerMsg(rd.Asset())) 181 | 182 | central.ClosePosition(profitTarget) 183 | if central.GetProfit() { 184 | break 185 | } 186 | } else if stopLossBuy(pst, price) { 187 | log.Println(display.StopLossMsg(rd.Asset())) 188 | 189 | central.ClosePosition(lossTargetBuy(price)) 190 | if central.GetProfit() { 191 | break 192 | } 193 | } 194 | time.Sleep(time.Duration(10) * time.Second) 195 | } 196 | } 197 | 198 | // ClosePositionProfitSell is responsible for closing a Sell position 199 | func ClosePositionProfitSell() { 200 | pst := central.GetPosition() 201 | 202 | for { 203 | price := central.Price() 204 | 205 | if closePositionSell(pst, price) { 206 | profitTarget := priceCloseSell(price) 207 | 208 | log.Println(display.OrdertriggerMsg(rd.Asset())) 209 | 210 | central.ClosePosition(profitTarget) 211 | if central.GetProfit() { 212 | break 213 | } 214 | } else if stopLossSell(pst, price) { 215 | log.Println(display.StopLossMsg(rd.Asset())) 216 | 217 | central.ClosePosition(lossTargetSell(price)) 218 | if central.GetProfit() { 219 | break 220 | } 221 | } 222 | time.Sleep(time.Duration(10) * time.Second) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /internal/reader/config.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "sync" 7 | 8 | "github.com/go-redis/redis" 9 | "github.com/thiago-scherrer/gotrader/internal/convert" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | // APIResponseComplex used to struct data from API response, 14 | // thanks https://mholt.github.io/json-to-go/ 15 | type APIResponseComplex struct { 16 | Amount int `json:"amount"` 17 | AvgEntryPrice float64 `json:"avgEntryPrice"` 18 | ChannelID int `json:"channelID"` 19 | IsOpen bool `json:"isOpen"` 20 | ID int64 `json:"id"` 21 | LastPrice float64 `json:"lastPrice"` 22 | OrderID string `json:"orderID"` 23 | OrderQty int `json:"orderQty"` 24 | Price float64 `json:"price"` 25 | Side string `json:"side"` 26 | Size int `json:"size"` 27 | Trades int64 `json:"trades"` 28 | } 29 | 30 | // Conf instruction are the file yaml on disc 31 | type Conf struct { 32 | Asset string `yaml:"asset"` 33 | Candle int `yaml:"candle"` 34 | Endpoint string `yaml:"endpoint"` 35 | Hand int64 `yaml:"hand"` 36 | Leverage string `yaml:"leverage"` 37 | Profit float64 `yaml:"profit"` 38 | Secret string `yaml:"secret"` 39 | StopLoss float64 `yaml:"stoploss"` 40 | Threshold int `yaml:"threshold"` 41 | Userid string `yaml:"userid"` 42 | } 43 | 44 | // Use to get the right time of the candle time 45 | const fixtime int = 6 46 | 47 | // ConfigPath verify where is config file 48 | func ConfigPath() string { 49 | var config string 50 | 51 | if os.Getenv("GOTRADER_MODE") == "prod" { 52 | config = "/opt/config.yml" 53 | } else if os.Getenv("GOTRADER_MODE") == "testnet" { 54 | config = "./opt/config-testnet.yml" 55 | } else { 56 | config = "../../configs/config-test.yml" 57 | } 58 | 59 | return config 60 | } 61 | 62 | // ConfigReader - read the file from PC 63 | func configReader() *Conf { 64 | confFile := ConfigPath() 65 | conf := Conf{} 66 | 67 | var once sync.Once 68 | 69 | onceReader := func() { 70 | config, _ := ioutil.ReadFile(confFile) 71 | yaml.Unmarshal(config, &conf) 72 | } 73 | 74 | once.Do(onceReader) 75 | return &conf 76 | 77 | } 78 | 79 | // RDclient create a client to the redis container 80 | func RDclient() *redis.Client { 81 | 82 | redisurl := os.Getenv("REDISURL") 83 | 84 | if redisurl == "" { 85 | redisurl = "redis:6379" 86 | } 87 | 88 | return redis.NewClient(&redis.Options{ 89 | Addr: redisurl, 90 | Password: "", 91 | DB: 0, 92 | }) 93 | } 94 | 95 | // Boot create the initial Bootstrap config 96 | func Boot() { 97 | conf := configReader() 98 | db := RDclient() 99 | defer db.Close() 100 | bootStatus, _ := db.Get("reload").Result() 101 | 102 | if bootStatus != "true" { 103 | db.Set("hand", conf.Hand, 0).Err() 104 | db.Set("asset", conf.Asset, 0).Err() 105 | db.Set("candle", conf.Candle, 0).Err() 106 | db.Set("threshold", conf.Threshold, 0).Err() 107 | db.Set("leverage", conf.Leverage, 0).Err() 108 | db.Set("profit", conf.Profit, 0).Err() 109 | db.Set("stoploss", conf.StopLoss, 0).Err() 110 | } 111 | } 112 | 113 | // Asset set the contract type to trade 114 | func Asset() string { 115 | db := RDclient() 116 | defer db.Close() 117 | result, _ := db.Get("asset").Result() 118 | return result 119 | } 120 | 121 | // Candle return the time of candle setting 122 | func Candle() int64 { 123 | db := RDclient() 124 | defer db.Close() 125 | result, _ := db.Get("candle").Result() 126 | return convert.StringToInt(result) 127 | } 128 | 129 | // Endpoint return url from bitmex 130 | func Endpoint() string { 131 | conf := configReader() 132 | return conf.Endpoint 133 | } 134 | 135 | // Hand return value to trade 136 | func Hand() string { 137 | db := RDclient() 138 | defer db.Close() 139 | result, _ := db.Get("hand").Result() 140 | return result 141 | } 142 | 143 | // Leverage return the value to set on laverage trading 144 | func Leverage() string { 145 | db := RDclient() 146 | defer db.Close() 147 | result, _ := db.Get("leverage").Result() 148 | return result 149 | } 150 | 151 | // Secret return API password 152 | func Secret() string { 153 | conf := configReader() 154 | return conf.Secret 155 | } 156 | 157 | // Threshold return the the value from config file 158 | func Threshold() int64 { 159 | db := RDclient() 160 | defer db.Close() 161 | result, _ := db.Get("threshold").Result() 162 | return convert.StringToInt(result) 163 | } 164 | 165 | // Userid return user identify from bitmex 166 | func Userid() string { 167 | conf := configReader() 168 | return conf.Userid 169 | } 170 | 171 | // Profit return the profit percentage 172 | func Profit() float64 { 173 | db := RDclient() 174 | defer db.Close() 175 | result, _ := db.Get("profit").Result() 176 | return convert.StringToFloat64(result) 177 | } 178 | 179 | // StopLoss return the StopLoss percentage 180 | func StopLoss() float64 { 181 | db := RDclient() 182 | defer db.Close() 183 | result, _ := db.Get("stoploss").Result() 184 | return convert.StringToFloat64(result) 185 | } 186 | 187 | //APISimple return JSON 188 | func APISimple() APIResponseComplex { 189 | var ar APIResponseComplex 190 | return ar 191 | } 192 | 193 | //APIArray return JSON Array 194 | func APIArray() []APIResponseComplex { 195 | var ar []APIResponseComplex 196 | return ar 197 | } 198 | 199 | // Data return a default data to the bitmet api 200 | func Data() []byte { 201 | return convert.StringToBytes("message=GoTrader bot&channelID=1") 202 | } 203 | -------------------------------------------------------------------------------- /examples/martingale_go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/thiago-scherrer/gotrader/internal/api" 12 | "github.com/thiago-scherrer/gotrader/internal/central" 13 | "github.com/thiago-scherrer/gotrader/internal/convert" 14 | "github.com/thiago-scherrer/gotrader/internal/display" 15 | "github.com/thiago-scherrer/gotrader/internal/reader" 16 | rd "github.com/thiago-scherrer/gotrader/internal/reader" 17 | ) 18 | 19 | // Path from api to view the orderbook 20 | const orb string = "/api/v1/orderBook/L2?" 21 | 22 | // CandleRunner verify the api and start the logic system 23 | func CandleRunner() string { 24 | t := rd.Threshold() 25 | c := rd.Candle() 26 | var tsl int 27 | var cby int 28 | 29 | for i := int64(0); i < t; i++ { 30 | for ii := int64(0); ii < c; ii++ { 31 | res := returnDepth() 32 | if res == "Buy" { 33 | cby++ 34 | } else if res == "Sell" { 35 | tsl++ 36 | } else { 37 | i = -1 38 | } 39 | } 40 | } 41 | 42 | if cby > tsl { 43 | return "Buy" 44 | } else if tsl > cby { 45 | return "Sell" 46 | } 47 | 48 | return "Draw" 49 | } 50 | 51 | func returnDepth() string { 52 | var sell int 53 | var buy int 54 | 55 | poh := "/api/v1/trade?" 56 | ap := rd.APIArray() 57 | t := timeStamp() 58 | d := rd.Data() 59 | 60 | u := url.Values{} 61 | u.Set("symbol", rd.Asset()) 62 | u.Add("partial", "false") 63 | u.Add("count", "500") 64 | u.Add("reverse", "false") 65 | u.Add("filter", t) 66 | 67 | for index := 1; index <= 60; index++ { 68 | u.Set("start", strconv.Itoa(index)) 69 | 70 | p := poh + u.Encode() 71 | res, code := api.ClientRobot("GET", p, d) 72 | 73 | if code != 200 { 74 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(res)) 75 | time.Sleep(time.Duration(2) * time.Second) 76 | } 77 | 78 | err := json.Unmarshal(res, &ap) 79 | if err != nil { 80 | break 81 | } 82 | 83 | if len(res) <= 5 { 84 | break 85 | } 86 | 87 | for _, v := range ap[:] { 88 | if v.Side == "Sell" { 89 | sell = sell + v.Size 90 | } else if v.Side == "Buy" { 91 | buy = buy + v.Size 92 | } 93 | 94 | } 95 | time.Sleep(time.Duration(1) * time.Second) 96 | 97 | } 98 | return logicSystem(buy, sell) 99 | } 100 | 101 | func timeStamp() string { 102 | ctm := rd.Candle() 103 | t := time.Now().UTC().Add(time.Duration(-ctm) * time.Minute) 104 | date := t.Format("2006-01-02") 105 | time := t.Format("15:04") 106 | return `{"timestamp.date": "` + date + `", "timestamp.minute": "` + time + `" }` 107 | } 108 | 109 | func logicSystem(buy, sell int) string { 110 | if buy > sell { 111 | return "Buy" 112 | } else if sell > buy { 113 | return "Sell" 114 | } 115 | return "Draw" 116 | } 117 | 118 | func stopLossBuy(pst float64, price float64) bool { 119 | return price <= (pst - ((pst / 100) * rd.StopLoss())) 120 | } 121 | 122 | func stopLossSell(pst float64, price float64) bool { 123 | return price >= (pst + ((pst / 100) * rd.StopLoss())) 124 | } 125 | 126 | func closePositionBuy(pst float64, price float64) bool { 127 | return price >= (pst + ((pst / 100) * rd.Profit())) 128 | } 129 | 130 | func closePositionSell(pst float64, price float64) bool { 131 | return price <= (pst - ((pst / 100) * rd.Profit())) 132 | } 133 | 134 | func priceCloseBuy(price float64) string { 135 | priceClose := fmt.Sprintf("%2.f", 136 | (price + 0.5), 137 | ) 138 | return priceClose 139 | } 140 | 141 | func priceCloseSell(price float64) string { 142 | priceClose := fmt.Sprintf("%2.f", 143 | (price - 0.5), 144 | ) 145 | return priceClose 146 | } 147 | 148 | func lossTargetBuy(price float64) string { 149 | priceClose := fmt.Sprintf("%2.f", 150 | (price - 0.5), 151 | ) 152 | return priceClose 153 | } 154 | 155 | func lossTargetSell(price float64) string { 156 | priceClose := fmt.Sprintf("%2.f", 157 | (price + 0.5), 158 | ) 159 | return priceClose 160 | } 161 | 162 | // ClosePositionProfitBuy the Buy position 163 | func ClosePositionProfitBuy() { 164 | pst := central.GetPosition() 165 | 166 | for { 167 | price := central.Price() 168 | 169 | if closePositionBuy(pst, price) { 170 | profitTarget := priceCloseBuy(price) 171 | 172 | log.Println(display.OrdertriggerMsg(rd.Asset())) 173 | 174 | central.ClosePosition(profitTarget) 175 | if central.GetProfit() { 176 | martingaleDecrease() 177 | break 178 | } 179 | } else if stopLossBuy(pst, price) { 180 | log.Println(display.StopLossMsg(rd.Asset())) 181 | 182 | central.ClosePosition(lossTargetBuy(price)) 183 | if central.GetProfit() { 184 | martingaleIncrease() 185 | break 186 | } 187 | } 188 | time.Sleep(time.Duration(10) * time.Second) 189 | } 190 | } 191 | 192 | // ClosePositionProfitSell close the Sell position 193 | func ClosePositionProfitSell() { 194 | pst := central.GetPosition() 195 | 196 | for { 197 | price := central.Price() 198 | if closePositionSell(pst, price) { 199 | profitTarget := priceCloseSell(price) 200 | 201 | log.Println(display.OrdertriggerMsg(rd.Asset())) 202 | 203 | central.ClosePosition(profitTarget) 204 | if central.GetProfit() { 205 | martingaleDecrease() 206 | break 207 | } 208 | } else if stopLossSell(pst, price) { 209 | log.Println(display.StopLossMsg(rd.Asset())) 210 | 211 | central.ClosePosition(lossTargetSell(price)) 212 | if central.GetProfit() { 213 | martingaleIncrease() 214 | break 215 | } 216 | } 217 | time.Sleep(time.Duration(10) * time.Second) 218 | } 219 | } 220 | 221 | func martingaleIncrease() { 222 | db := reader.RDclient() 223 | defer db.Close() 224 | 225 | err := db.Set("reload", "true", 0).Err() 226 | if err != nil { 227 | log.Println("Error to save on redis: ", err) 228 | } 229 | 230 | mvalue, _ := db.Get("mvalue").Result() 231 | 232 | if convert.StringToInt(mvalue) < 1 { 233 | db.IncrBy("mvalue", 2).Err() 234 | } else { 235 | db.IncrBy("mvalue", 1).Err() 236 | } 237 | 238 | setHand() 239 | } 240 | 241 | func martingaleDecrease() { 242 | db := reader.RDclient() 243 | defer db.Close() 244 | 245 | err := db.Set("reload", "false", 0).Err() 246 | if err != nil { 247 | log.Println("Error to save on redis: ", err) 248 | } 249 | 250 | err = db.Set("mvalue", 0, 0).Err() 251 | if err != nil { 252 | log.Println("Error to save on redis: ", err) 253 | } 254 | } 255 | 256 | func setHand() { 257 | db := reader.RDclient() 258 | mvalue, _ := db.Get("mvalue").Result() 259 | defer db.Close() 260 | 261 | mval := convert.StringToInt(mvalue) 262 | hand := convert.StringToInt(reader.Hand()) 263 | result := convert.IntToString(hand * mval) 264 | 265 | err := db.Set("hand", result, 0).Err() 266 | if err != nil { 267 | log.Println("Error to save on redis hand: ", err) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /internal/central/colun.go: -------------------------------------------------------------------------------- 1 | package central 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/thiago-scherrer/gotrader/internal/api" 10 | "github.com/thiago-scherrer/gotrader/internal/convert" 11 | "github.com/thiago-scherrer/gotrader/internal/display" 12 | rd "github.com/thiago-scherrer/gotrader/internal/reader" 13 | ) 14 | 15 | // Order path to use on API Request 16 | const oph string = "/api/v1/order" 17 | 18 | // Position path to use on API Request 19 | const poh string = "/api/v1/position?" 20 | 21 | // Basic path to use on API Request 22 | const ith string = "/api/v1/instrument?" 23 | 24 | // Laverage path to use on API Request 25 | const lth = "/api/v1/position/leverage" 26 | 27 | // A random number to make a sleep before starting a new round 28 | const tlp = 50 29 | 30 | // A random number to make a sleep before starting a new request after a error 31 | const elp = 50 32 | 33 | // A simple order timout to auto cancel if not executed in ms (3min) 34 | const timeoutOrd = "60000" 35 | 36 | // parserAmount unmarshal a r API to return the wallet amount 37 | func parserAmount(data []byte) int { 38 | ap := rd.APISimple() 39 | err := json.Unmarshal(data, &ap) 40 | if err != nil { 41 | log.Println("Error to get Amount: ", err) 42 | } 43 | return ap.Amount 44 | } 45 | 46 | // lastPrice unmarshal a r API to return the last price 47 | func lastPrice(d []byte) float64 { 48 | ap := rd.APIArray() 49 | var r float64 50 | 51 | err := json.Unmarshal(d, &ap) 52 | if err != nil { 53 | log.Println("Error to get last price: ", err) 54 | } 55 | for _, v := range ap[:] { 56 | r = v.LastPrice 57 | } 58 | 59 | return r 60 | } 61 | 62 | func makeOrder(orderType, hand string) bool { 63 | for { 64 | orderTimeOut() 65 | if statusOrder() == true { 66 | return true 67 | } 68 | hand := hand 69 | ast := rd.Asset() 70 | prc := convert.FloatToString( 71 | Price(), 72 | ) 73 | rtp := "POST" 74 | 75 | u := url.Values{} 76 | u.Set("symbol", ast) 77 | u.Add("side", orderType) 78 | u.Add("orderQty", hand) 79 | u.Add("price", prc) 80 | u.Add("ordType", "Limit") 81 | u.Add("execInst", "ParticipateDoNotInitiate") 82 | data := convert.StringToBytes(u.Encode()) 83 | 84 | glt, code := api.ClientRobot(rtp, oph, data) 85 | if code != 200 { 86 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(glt)) 87 | } else { 88 | if statusOrder() == true { 89 | return true 90 | } 91 | } 92 | 93 | time.Sleep(time.Duration(65) * time.Second) 94 | } 95 | } 96 | 97 | func statusOrder() bool { 98 | path := poh + `filter={"symbol":"` + rd.Asset() + `"}&count=1` 99 | data := convert.StringToBytes("message=GoTrader bot&channelID=1") 100 | glt, code := api.ClientRobot("GET", path, data) 101 | ap := rd.APIArray() 102 | var r bool 103 | 104 | if code != 200 { 105 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(glt)) 106 | return false 107 | } 108 | 109 | err := json.Unmarshal(glt, &ap) 110 | if err != nil { 111 | log.Println("json open error:", err) 112 | } 113 | for _, v := range ap[:] { 114 | r = v.IsOpen 115 | } 116 | return r 117 | } 118 | 119 | // GetPosition get the actual open possitions 120 | func GetPosition() float64 { 121 | ap := rd.APIArray() 122 | var r float64 123 | pth := poh + `filter={"symbol":"` + rd.Asset() + `"}&count=1` 124 | rtp := "GET" 125 | dt := convert.StringToBytes("message=GoTrader bot&channelID=1") 126 | 127 | for { 128 | glt, code := api.ClientRobot(rtp, pth, dt) 129 | if code == 200 { 130 | err := json.Unmarshal(glt, &ap) 131 | if err != nil { 132 | log.Println("Error to get position:", err) 133 | time.Sleep(time.Duration(elp) * time.Second) 134 | } else { 135 | break 136 | } 137 | } else { 138 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(glt)) 139 | time.Sleep(time.Duration(elp) * time.Second) 140 | } 141 | } 142 | 143 | for _, v := range ap[:] { 144 | r = v.AvgEntryPrice 145 | } 146 | return r 147 | } 148 | 149 | // Price return the actual asset value 150 | func Price() float64 { 151 | ast := rd.Asset() 152 | var g []byte 153 | var code int 154 | u := url.Values{} 155 | u.Set("symbol", ast) 156 | u.Add("count", "100") 157 | u.Add("reverse", "false") 158 | u.Add("columns", "lastPrice") 159 | 160 | p := ith + u.Encode() 161 | d := convert.StringToBytes("message=GoTrader bot&channelID=1") 162 | for { 163 | g, code = api.ClientRobot("GET", p, d) 164 | if code == 200 { 165 | break 166 | } else { 167 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(g)) 168 | time.Sleep(time.Duration(elp) * time.Second) 169 | } 170 | } 171 | return lastPrice(g) 172 | } 173 | 174 | func setLeverge() { 175 | ast := rd.Asset() 176 | path := lth 177 | rtp := "POST" 178 | l := rd.Leverage() 179 | u := url.Values{} 180 | u.Set("symbol", ast) 181 | u.Add("leverage", l) 182 | data := convert.StringToBytes(u.Encode()) 183 | 184 | api.ClientRobot(rtp, path, data) 185 | log.Println(display.SetleverageMsg(rd.Asset(), l)) 186 | 187 | } 188 | 189 | // CreateOrder create the order on bitmex 190 | func CreateOrder(typeOrder, hand string) bool { 191 | setLeverge() 192 | makeOrder(typeOrder, hand) 193 | if statusOrder() { 194 | log.Println(display.OrderDoneMsg(rd.Asset())) 195 | log.Println(display.OrderCreatedMsg(rd.Asset(), typeOrder)) 196 | return true 197 | } 198 | return false 199 | } 200 | 201 | func orderTimeOut() { 202 | poh := "/api/v1/order/cancelAllAfter?" 203 | data := convert.StringToBytes("message=GoTrader bot&channelID=1") 204 | u := url.Values{} 205 | u.Set("timeout", timeoutOrd) 206 | 207 | p := poh + u.Encode() 208 | 209 | for { 210 | res, code := api.ClientRobot("POST", p, data) 211 | if code == 200 { 212 | break 213 | } 214 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(res)) 215 | time.Sleep(time.Duration(5) * time.Second) 216 | } 217 | } 218 | 219 | // ClosePosition close all opened position 220 | func ClosePosition(priceClose string) { 221 | ast := rd.Asset() 222 | path := oph 223 | rtp := "POST" 224 | 225 | u := url.Values{} 226 | u.Set("symbol", ast) 227 | u.Add("execInst", "Close") 228 | u.Add("price", priceClose) 229 | u.Add("ordType", "Limit") 230 | u.Add("execInst", "ParticipateDoNotInitiate") 231 | 232 | data := convert.StringToBytes(u.Encode()) 233 | 234 | for { 235 | orderTimeOut() 236 | g, code := api.ClientRobot(rtp, path, data) 237 | if code == 200 { 238 | break 239 | } else { 240 | log.Println("Something wrong with api:", code, "Response: ", convert.BytesToString(g)) 241 | time.Sleep(time.Duration(elp) * time.Second) 242 | } 243 | } 244 | } 245 | 246 | // GetProfit waint to start a new trade round 247 | func GetProfit() bool { 248 | log.Println(display.OrderWaintMsg(rd.Asset())) 249 | 250 | for index := 0; index < 4; index++ { 251 | if statusOrder() == false { 252 | log.Println(display.ProfitMsg(rd.Asset())) 253 | return true 254 | } 255 | time.Sleep(time.Duration(15) * time.Second) 256 | } 257 | return false 258 | } 259 | --------------------------------------------------------------------------------