(Exchange) Add FTX exchange support with implementation tutorial (#495)

* initial

* wip

* brokenwip

* broken wipzzzz

* more functions

brokenwip

NO API KEYS

* broken wip

* WIP

* wip

* WIP

work in progress

* WIP

* WIP

* wip

* more wip

* wip

* ws wip

* broken wip

* adding new functions for websocket to work

* trying to fix websocket issues

* websocket bug fix wip

* broken websocket implementation

* WS unauth functions + brokenWS auth func

* authentication problems

* authentication problems fixed

* data handling for websocket

* websocket completed

* remove verbose

* minor error fix changes and testing

* reorganising variable declarations and minor errors fixed

* enabled exchanges updated

* enabled exchanges fixed

* remove keys

* glorious nits

* xdta n shazzy nitzzz

* shazzy n thrasher nitz

* nitz wip

* broken wip

* apichecker donee n make code better

* apichecker donee n make code better

* OB update

* wip

* wip

* all nitz done

* merge conflicts

* go mod tidy

* merge conflicts

* PLEASE merge conflicts

* new funcs added n binanceapi check update

NO APIKEYS

* basic tests

* linter fixs

* linter fixs

* remove verbose

* test errors fixed

* remove comented code

* minor changes

* some tests fixed

no apikeys

* documentation work

* documentation

* wip

* ryan nitz

* nits addressed

* unnecessary conversion

* no fail

* remove verbose

* type field checking

* broken

* websocket nits fixed

* some thangs

* remove verbose

* fix function

* linter issues

* test error fixed

* nits

* bumperino fixed

* very small change

* nits

* errors fixing

* errors fixing retry

* linters

* thrasher glorious nits

* more changes

* changes

* 2 more changes to be addressed

* 2 more changes to be addressed

* issues addressed

* whip

* changes

* missed change

* changes

* currency issues

* changes

* unsaved

* int64

* HUGE

* HUGE

* NO NITS PLS

* no more

* YES

* :

* changes

* PLEASE

* n another one

* thanks guys

* ill believe in god if this ever ends

* :D
This commit is contained in:
Adam
2020-07-02 10:38:50 +10:00
committed by GitHub
parent 2351d89b77
commit c2c200cd1b
66 changed files with 6720 additions and 282 deletions

View File

@@ -30,6 +30,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| BTSE | Yes | Yes | NA |
| COINUT | Yes | Yes | NA |
| Exmo | Yes | NA | NA |
| FTX | Yes | Yes | No |
| CoinbasePro | Yes | Yes | No|
| Coinbene | Yes | No | No |
| GateIO | Yes | Yes | NA |
@@ -139,10 +140,10 @@ Binaries will be published once the codebase reaches a stable condition.
|User|Contribution Amount|
|--|--|
| [thrasher-](https://github.com/thrasher-) | 639 |
| [thrasher-](https://github.com/thrasher-) | 640 |
| [shazbert](https://github.com/shazbert) | 191 |
| [gloriousCode](https://github.com/gloriousCode) | 169 |
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 46 |
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 48 |
| [xtda](https://github.com/xtda) | 43 |
| [ermalguni](https://github.com/ermalguni) | 14 |
| [vadimzhukck](https://github.com/vadimzhukck) | 10 |

View File

@@ -31,8 +31,10 @@ const (
backupFile = "backup.json"
github = "GitHub Sha Check"
htmlScrape = "HTML String Check"
pathBinance = "https://binance-docs.github.io/apidocs/spot/en/#change-log"
pathOkCoin = "https://www.okcoin.com/docs/en/#change-change"
pathOkex = "https://www.okex.com/docs/en/#change-change"
pathFTX = "https://github.com/ftexchange/ftx"
pathBTSE = "https://www.btse.com/apiexplorer/spot/#btse-spot-api"
pathBitfinex = "https://docs.bitfinex.com/docs/changelog"
pathBitmex = "https://www.bitmex.com/static/md/en-US/apiChangelog"
@@ -448,8 +450,12 @@ func checkChangeLog(htmlData *HTMLScrapingData) (string, error) {
var dataStrings []string
var err error
switch htmlData.Path {
case pathBinance:
dataStrings, err = htmlScrapeBinance(htmlData)
case pathBTSE:
dataStrings, err = htmlScrapeBTSE(htmlData)
case pathFTX:
dataStrings, err = htmlScrapeFTX(htmlData)
case pathBitfinex:
dataStrings, err = htmlScrapeBitfinex(htmlData)
case pathBitmex:
@@ -637,8 +643,7 @@ loop:
if err != nil {
return resp, err
}
result := r.MatchString(tempStr)
if result {
if r.MatchString(tempStr) {
appendStr := r.FindString(tempStr)
resp = append(resp, appendStr)
}
@@ -686,39 +691,6 @@ loop:
return resp, nil
}
// htmlScrapeBitfinex gets the check string for Bitfinex exchange
func htmlScrapeBitfinex(htmlData *HTMLScrapingData) ([]string, error) {
temp, err := http.Get(htmlData.Path)
if err != nil {
return nil, err
}
defer temp.Body.Close()
a, err := ioutil.ReadAll(temp.Body)
if err != nil {
return nil, err
}
r, err := regexp.Compile(htmlData.RegExp)
if err != nil {
return nil, err
}
str := r.FindAllString(string(a), -1)
var resp []string
for x := range str {
tempStr := strings.Replace(str[x], "section-v-", "", 1)
var repeat bool
for y := range resp {
if tempStr == resp[y] {
repeat = true
break
}
}
if !repeat {
resp = append(resp, tempStr)
}
}
return resp, nil
}
// htmlScrapeBitmex gets the check string for Bitmex exchange
func htmlScrapeBitmex(htmlData *HTMLScrapingData) ([]string, error) {
var resp []string
@@ -813,59 +785,6 @@ func htmlScrapeBTCMarkets(htmlData *HTMLScrapingData) ([]string, error) {
return resp, nil
}
// htmlScrapeBitflyer gets the check string for BTCMarkets exchange
func htmlScrapeBitflyer(htmlData *HTMLScrapingData) ([]string, error) {
var resp []string
var tempArray []string
temp, err := http.Get(htmlData.Path)
if err != nil {
return resp, err
}
defer temp.Body.Close()
tokenizer := html.NewTokenizer(temp.Body)
loop:
for {
next := tokenizer.Next()
switch next {
case html.ErrorToken:
break loop
case html.StartTagToken:
token := tokenizer.Token()
if token.Data == htmlData.TokenData {
for {
nextToken := tokenizer.Next()
switch nextToken {
case html.EndTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TokenDataEnd {
break loop
}
case html.StartTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TextTokenData {
inner := tokenizer.Next()
if inner == html.TextToken {
tempStr := string(tokenizer.Text())
r, err := regexp.Compile(htmlData.RegExp)
if err != nil {
return resp, err
}
result := r.MatchString(tempStr)
if result {
appendStr := r.FindString(tempStr)
tempArray = append(tempArray, appendStr)
}
}
}
}
}
}
}
}
resp = append(resp, tempArray[1])
return resp, nil
}
// htmlScrapeOk gets the check string for Okex
func htmlScrapeOk(htmlData *HTMLScrapingData) ([]string, error) {
var resp []string
@@ -1111,62 +1030,6 @@ func htmlScrapeBitstamp(htmlData *HTMLScrapingData) ([]string, error) {
return resp, nil
}
// htmlScrapeKraken gets the check string for Kraken Exchange
func htmlScrapeKraken(htmlData *HTMLScrapingData) ([]string, error) {
var resp []string
temp, err := http.Get(htmlData.Path)
if err != nil {
return resp, err
}
defer temp.Body.Close()
tokenizer := html.NewTokenizer(temp.Body)
loop:
for {
next := tokenizer.Next()
switch next {
case html.ErrorToken:
break loop
case html.StartTagToken:
token := tokenizer.Token()
if token.Data == htmlData.TokenData {
inner := tokenizer.Next()
if inner == html.TextToken {
if string(tokenizer.Text()) == "Get account balance" {
loop2:
for {
next := tokenizer.Next()
switch next {
case html.EndTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TokenDataEnd {
break loop2
}
case html.StartTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TextTokenData {
inside := tokenizer.Next()
if inside == html.TextToken {
tempStr := string(tokenizer.Text())
r, err := regexp.Compile(htmlData.RegExp)
if err != nil {
return resp, err
}
result := r.MatchString(tempStr)
if result {
resp = append(resp, tempStr)
}
}
}
}
}
}
}
}
}
}
return resp, nil
}
// htmlScrapeAlphaPoint gets the check string for Kraken Exchange
func htmlScrapeAlphaPoint(htmlData *HTMLScrapingData) ([]string, error) {
var resp []string
@@ -1610,3 +1473,288 @@ func trelloCreateNewChecklist() error {
}
return errors.New("checklist id and name not found, checklist creation failed")
}
// htmlScrapeKraken gets the check string for Kraken Exchange
func htmlScrapeKraken(htmlData *HTMLScrapingData) ([]string, error) {
var resp []string
temp, err := http.Get(htmlData.Path)
if err != nil {
return resp, err
}
defer temp.Body.Close()
tokenizer := html.NewTokenizer(temp.Body)
loop:
for {
next := tokenizer.Next()
switch next {
case html.ErrorToken:
break loop
case html.StartTagToken:
token := tokenizer.Token()
if token.Data == htmlData.TokenData {
inner := tokenizer.Next()
if inner == html.TextToken {
if string(tokenizer.Text()) == "Get account balance" {
loop2:
for {
next := tokenizer.Next()
switch next {
case html.EndTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TokenDataEnd {
break loop2
}
case html.StartTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TextTokenData {
inside := tokenizer.Next()
if inside == html.TextToken {
tempStr := string(tokenizer.Text())
r, err := regexp.Compile(htmlData.RegExp)
if err != nil {
return resp, err
}
result := r.MatchString(tempStr)
if result {
resp = append(resp, tempStr)
}
}
}
}
}
}
}
}
}
}
return resp, nil
}
// htmlScrapeBitflyer gets the check string for BTCMarkets exchange
func htmlScrapeBitflyer(htmlData *HTMLScrapingData) ([]string, error) {
var resp []string
var tempArray []string
temp, err := http.Get(htmlData.Path)
if err != nil {
return resp, err
}
defer temp.Body.Close()
tokenizer := html.NewTokenizer(temp.Body)
loop:
for {
next := tokenizer.Next()
switch next {
case html.ErrorToken:
break loop
case html.StartTagToken:
token := tokenizer.Token()
if token.Data == htmlData.TokenData {
for {
nextToken := tokenizer.Next()
switch nextToken {
case html.EndTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TokenDataEnd {
break loop
}
case html.StartTagToken:
t := tokenizer.Token()
if t.Data == htmlData.TextTokenData {
inner := tokenizer.Next()
if inner == html.TextToken {
tempStr := string(tokenizer.Text())
r, err := regexp.Compile(htmlData.RegExp)
if err != nil {
return resp, err
}
result := r.MatchString(tempStr)
if result {
appendStr := r.FindString(tempStr)
tempArray = append(tempArray, appendStr)
}
}
}
}
}
}
}
}
resp = append(resp, tempArray[1])
return resp, nil
}
// htmlScrapeFTX gets the check string for FTX exchange
func htmlScrapeFTX(htmlData *HTMLScrapingData) ([]string, error) {
temp, err := http.Get(htmlData.Path)
if err != nil {
return nil, err
}
defer temp.Body.Close()
a := temp.Body
tokenizer := html.NewTokenizer(a)
var respStr string
loop:
for {
next := tokenizer.Next()
switch next {
case html.ErrorToken:
break loop
case html.StartTagToken:
token := tokenizer.Token()
if token.Data == htmlData.TokenData {
for _, a := range token.Attr {
if a.Key == htmlData.Key && a.Val == htmlData.Val {
loop2:
for {
anotherToken := tokenizer.Next()
switch anotherToken {
case html.StartTagToken:
z := tokenizer.Token()
if z.Data == "a" {
for _, m := range z.Attr {
if m.Key == "title" {
switch m.Val {
case "rest":
loop3:
for {
nextToken := tokenizer.Next()
switch nextToken {
case html.StartTagToken:
f := tokenizer.Token()
if f.Data == "time-ago" {
for _, b := range f.Attr {
if b.Key == "datetime" {
respStr += b.Val
}
}
}
case html.EndTagToken:
tk := tokenizer.Token()
if tk.Data == htmlData.TokenDataEnd {
break loop3
}
}
}
case "websocket":
loop4:
for {
nextToken := tokenizer.Next()
switch nextToken {
case html.StartTagToken:
f := tokenizer.Token()
if f.Data == "time-ago" {
for _, b := range f.Attr {
if b.Key == "datetime" {
respStr += b.Val
}
}
}
case html.EndTagToken:
tk := tokenizer.Token()
if tk.Data == htmlData.TokenDataEnd {
break loop4
}
}
}
}
}
}
}
case html.ErrorToken:
break loop2
}
}
}
}
}
}
}
return []string{respStr}, nil
}
// htmlScrapeBitfinex gets the check string for Bitfinex exchange
func htmlScrapeBitfinex(htmlData *HTMLScrapingData) ([]string, error) {
temp, err := http.Get(htmlData.Path)
if err != nil {
return nil, err
}
defer temp.Body.Close()
a, err := ioutil.ReadAll(temp.Body)
if err != nil {
return nil, err
}
r, err := regexp.Compile(htmlData.RegExp)
if err != nil {
return nil, err
}
str := r.FindAllString(string(a), -1)
var resp []string
for x := range str {
tempStr := strings.Replace(str[x], "section-v-", "", 1)
var repeat bool
for y := range resp {
if tempStr == resp[y] {
repeat = true
break
}
}
if !repeat {
resp = append(resp, tempStr)
}
}
return resp, nil
}
// htmlScrapeBinance gets checkstring for binance exchange
func htmlScrapeBinance(htmlData *HTMLScrapingData) ([]string, error) {
temp, err := http.Get(htmlData.Path)
if err != nil {
return nil, err
}
defer temp.Body.Close()
tokenizer := html.NewTokenizer(temp.Body)
var resp []string
loop:
for {
next := tokenizer.Next()
switch next {
case html.ErrorToken:
break loop
case html.StartTagToken:
token := tokenizer.Token()
if token.Data == htmlData.TokenData {
for _, a := range token.Attr {
if a.Key == htmlData.Key && a.Val == htmlData.Val {
loop2:
for {
nextToken := tokenizer.Next()
switch nextToken {
case html.EndTagToken:
nt := tokenizer.Token()
if nt.Data == htmlData.TokenDataEnd {
break loop2
}
case html.StartTagToken:
tk := tokenizer.Token()
if tk.Data == htmlData.TextTokenData {
inner := tokenizer.Next()
if inner == html.TextToken {
tempStr := string(tokenizer.Text())
r, err := regexp.Compile(htmlData.RegExp)
if err != nil {
return resp, err
}
if r.MatchString(tempStr) {
resp = append(resp, tempStr)
}
}
}
}
}
}
}
}
}
}
return resp, nil
}

View File

@@ -148,13 +148,15 @@ func TestCheckChangeLog(t *testing.T) {
func TestAdd(t *testing.T) {
t.Parallel()
data2 := HTMLScrapingData{TokenData: "a",
Key: "href",
Val: "./#change-change",
TokenDataEnd: "./#change-",
RegExp: `./#change-\d{8}`,
Path: "wrongpath"}
err := addExch("WrongExch", htmlScrape, data2, false)
data2 := HTMLScrapingData{
TokenData: "h1",
Key: "id",
Val: "change-log",
TextTokenData: "strong",
TokenDataEnd: "p",
Path: "incorrectpath",
}
err := addExch("FalseName", htmlScrape, data2, false)
if err == nil {
t.Log("expected an error due to invalid path being parsed in")
}
@@ -418,8 +420,6 @@ func TestHTMLYobit(t *testing.T) {
func TestHTMLScrapeLocalBitcoins(t *testing.T) {
t.Parallel()
data := HTMLScrapingData{TokenData: "div",
Key: "class",
Val: "col-md-12",
RegExp: `col-md-12([\s\S]*?)clearfix`,
Path: "https://localbitcoins.com/api-docs/"}
_, err := htmlScrapeLocalBitcoins(&data)
@@ -701,3 +701,31 @@ func TestTrelloDeleteCheckItems(t *testing.T) {
t.Error(err)
}
}
func TestHTMLScrapeFTX(t *testing.T) {
data := HTMLScrapingData{
TokenData: "span",
Key: "class",
Val: "css-truncate css-truncate-target d-block width-fit",
TokenDataEnd: "svg",
Path: "https://github.com/ftexchange/ftx"}
a, err := htmlScrapeFTX(&data)
if err != nil || len(a) != 1 {
t.Error(err)
}
}
func TestHTMLScrapeBinance(t *testing.T) {
data := HTMLScrapingData{
TokenData: "h1",
Key: "id",
Val: "change-log",
TextTokenData: "strong",
TokenDataEnd: "p",
Path: "https://binance-docs.github.io/apidocs/spot/en/#change-log",
}
_, err := htmlScrapeBinance(&data)
if err != nil {
t.Error(err)
}
}

430
cmd/apichecker/backup.json Normal file
View File

@@ -0,0 +1,430 @@
{
"CardID": "",
"ChecklistID": "",
"ListID": "",
"BoardID": "",
"Key": "",
"Token": "",
"CreateCardName": "",
"CreateListName": "",
"CreateChecklistName": "",
"Exchanges": [
{
"Name": "Huobi",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "change-log",
"TokenDataEnd": "h2",
"TextTokenData": "td",
"DateFormat": "2006.01.02 15:04",
"RegExp": "^20(\\d){2}.(\\d){2}.(\\d){2} (\\d){2}:(\\d){2}$",
"CheckString": "2019.12.27 19:00",
"Path": "https://huobiapi.github.io/docs/spot/v1/en/#change-log"
}
},
"Disabled": false
},
{
"Name": "Coinbasepro",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "changelog",
"TokenDataEnd": "ul",
"TextTokenData": "strong",
"DateFormat": "01/02/06",
"RegExp": "^(\\d){1,2}/(\\d){1,2}/(\\d){2}$",
"CheckString": "2/20/20",
"Path": "https://docs.pro.coinbase.com/#changelog"
}
},
"Disabled": false
},
{
"Name": "Binance",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "binance-exchange/binance-official-api-docs",
"Sha": "4878d48adc0075669ba85033b0e2d40c2876cf56"
}
},
"Disabled": false
},
{
"Name": "Bithumb",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "bithumb-pro/bithumb.pro-official-api-docs",
"Sha": "6293502c7736ab4971491978225ef4d104bdff31"
}
},
"Disabled": false
},
{
"Name": "Bittrex",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "Bittrex/bittrex.github.io",
"Sha": "fc1ea9c10c48aa82c4dc2c6be74887ef61b5b31b"
}
},
"Disabled": false
},
{
"Name": "CoinbeneSpot",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "Coinbene/API-SPOT-v2-Documents",
"Sha": "e9135a782ba6016bcf008778be368882ad7c784d"
}
},
"Disabled": false
},
{
"Name": "CoinbeneSwap",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "Coinbene/API-SWAP-Documents",
"Sha": "6b7871dae4d2af028a33dde956fbce101e2f9acd"
}
},
"Disabled": false
},
{
"Name": "Coinut",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "coinut/api",
"Sha": "6936dab4d2beba3c8245a603aebf3f545ebcf3f9"
}
},
"Disabled": false
},
{
"Name": "Gateio",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "gateio/gateapi-go",
"Sha": "81e2f1bb92c5406853139ae054bbda599483e127"
}
},
"Disabled": false
},
{
"Name": "Lbank",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "LBank-exchange/lbank-official-api-docs",
"Sha": "85e7bb83f03d4239c3aad26cabb997a1a2bbb3d1"
}
},
"Disabled": false
},
{
"Name": "BTSE",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "btse-spot-api",
"TokenDataEnd": "blockquote",
"TextTokenData": "h1",
"RegExp": "^BTSE Spot API v(\\d){1}.(\\d){1}$",
"CheckString": "BTSE Spot API v3.0.2",
"Path": "https://www.btse.com/apiexplorer/spot/#btse-spot-api"
}
},
"Disabled": false
},
{
"Name": "Bitfinex",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "class",
"Val": "header-scroll",
"TokenDataEnd": "p",
"DateFormat": "2006-01-02",
"RegExp": "section-v-(2\\d{3}-\\d{1,2}-\\d{1,2})",
"CheckString": "2019-08-19",
"Path": "https://docs.bitfinex.com/docs/changelog"
}
},
"Disabled": false
},
{
"Name": "ANX",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"RegExp": "ANX Exchange API v\\d{1}",
"CheckString": "ANX Exchange API v3",
"Path": "https://anxv3.docs.apiary.io/"
}
},
"Disabled": false
},
{
"Name": "Poloniex",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "changelog",
"TokenDataEnd": "div",
"TextTokenData": "h2",
"DateFormat": "2006-01-02",
"RegExp": "(2\\d{3}-\\d{1,2}-\\d{1,2})",
"CheckString": "2020-03-24",
"Path": "https://docs.poloniex.com/#changelog"
}
},
"Disabled": false
},
{
"Name": "ItBit",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "a",
"Key": "href",
"RegExp": "^https://api.itbit.com/v\\d{1}/$",
"CheckString": "https://api.itbit.com/v1/",
"Path": "https://api.itbit.com/docs"
}
},
"Disabled": false
},
{
"Name": "Bitmex",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h4",
"Key": "id",
"DateFormat": "Jan-2-2006",
"RegExp": "([A-Z]{1}[a-z]{2}-\\d{1,2}-2\\d{3})",
"CheckString": "Dec-16-2019",
"Path": "https://www.bitmex.com/static/md/en-US/apiChangelog"
}
},
"Disabled": false
},
{
"Name": "HitBTC",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "about-companyname-api",
"TokenDataEnd": "h2",
"TextTokenData": "p",
"RegExp": "newest version \\d{1}.\\d{1}",
"CheckString": "newest version 2.0",
"Path": "https://api.hitbtc.com/"
}
},
"Disabled": false
},
{
"Name": "BTC Markets",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"RegExp": "^version: \\d{1}.\\d{1}.\\d{1}",
"CheckString": "version: 3.0.0",
"Path": "https://api.btcmarkets.net/openapi/info/index.yaml"
}
},
"Disabled": false
},
{
"Name": "Bitflyer",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "p",
"TokenDataEnd": "h3",
"TextTokenData": "code",
"RegExp": "^https://api.bitflyer.com/v\\d{1}/$",
"CheckString": "https://api.bitflyer.com/v1/",
"Path": "https://lightning.bitflyer.com/docs?lang=en"
}
},
"Disabled": false
},
{
"Name": "LakeBTC",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "div",
"Key": "class",
"Val": "flash-message",
"TokenDataEnd": "h2",
"TextTokenData": "h1",
"RegExp": "APIv\\d{1}",
"CheckString": "de2491b95ef1f6ea334247b13f0f14f6816fb5961cc63acc0542b07fc0336dd8",
"Path": "https://www.lakebtc.com/s/api_v2"
}
},
"Disabled": false
},
{
"Name": "Exmo",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"RegExp": "Last updated on [\\s\\S]*, 20\\d{2}",
"CheckString": "Last updated on December, 16th, 2019",
"Path": "https://exmo.com/en/api/"
}
},
"Disabled": false
},
{
"Name": "Kraken",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h3",
"TokenDataEnd": "p",
"TextTokenData": "p",
"RegExp": "URL: https://api.kraken.com/\\d{1}/private/Balance",
"CheckString": "URL: https://api.kraken.com/0/private/Balance",
"Path": "https://www.kraken.com/features/api"
}
},
"Disabled": false
},
{
"Name": "Bitstamp",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h2",
"Key": "class",
"Val": "text-subtitle mt48",
"TokenDataEnd": "h4",
"TextTokenData": "p",
"RegExp": "refer to the v\\d{1} API for future references.",
"CheckString": "refer to the v2 API for future references.",
"Path": "https://www.bitstamp.net/api/"
}
},
"Disabled": false
},
{
"Name": "AlphaPoint",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "introduction",
"TokenDataEnd": "blockquote",
"TextTokenData": "h3",
"RegExp": "revised-calls-\\d{1}-\\d{1}-\\d{1}-gt-\\d{1}-\\d{1}-\\d{1}",
"CheckString": "revised-calls-3-3-2-gt-3-3-3",
"Path": "https://alphapoint.github.io/slate/#introduction"
}
},
"Disabled": false
},
{
"Name": "Yobit",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h2",
"Key": "id",
"CheckString": "v3",
"Path": "https://www.yobit.net/en/api/"
}
},
"Disabled": false
},
{
"Name": "LocalBitcoins",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "div",
"Key": "class",
"Val": "col-md-12",
"RegExp": "col-md-12([\\s\\S]*?)clearfix",
"CheckString": "37a144dc619776b87c098da5a88bef7fed6c8a7cea2d4b9a38c96750726c93ff",
"Path": "https://localbitcoins.com/api-docs/"
}
},
"Disabled": false
},
{
"Name": "OkCoin International",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "a",
"Key": "href",
"Val": "./#change-change",
"TokenDataEnd": "./#change-",
"RegExp": "./#change-\\d{8}",
"CheckString": "20200229",
"Path": "https://www.okcoin.com/docs/en/#change-change"
}
},
"Disabled": false
},
{
"Name": "Okex",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "a",
"Key": "href",
"Val": "./#change-change",
"TokenDataEnd": "./#change-",
"RegExp": "./#change-\\d{8}",
"CheckString": "20200331",
"Path": "https://www.okex.com/docs/en/#change-change"
}
},
"Disabled": false
},
{
"Name": "Gemini",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "revision-history",
"TokenDataEnd": "table",
"TextTokenData": "td",
"DateFormat": "2006/01/02",
"RegExp": "^20(\\d){2}/(\\d){2}/(\\d){2}$",
"CheckString": "2020/03/05",
"Path": "https://docs.gemini.com/rest-api/#revision-history"
}
},
"Disabled": false
}
]
}

View File

@@ -47,11 +47,16 @@
},
{
"Name": "Binance",
"CheckType": "GitHub Sha Check",
"CheckType": "HTML String Check",
"Data": {
"GitHubData": {
"Repo": "binance-exchange/binance-official-api-docs",
"Sha": "4878d48adc0075669ba85033b0e2d40c2876cf56"
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "change-log",
"TokenDataEnd": "p",
"TextTokenData": "strong",
"CheckString": "2020-05-06",
"Path": "https://binance-docs.github.io/apidocs/spot/en/#change-log"
}
},
"Disabled": false
@@ -425,6 +430,20 @@
}
},
"Disabled": false
},
{
"Name": "FTX",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"Key": "class",
"Val": "css-truncate css-truncate-target",
"TokenDataEnd": "td",
"CheckString": "65e8800b5c6800aad896f888b2a62afc-d29c4f140f6ca068db9970054076ba63e51a9e96bf56d570e62cdb573c86b18526296117-1c388b9ec24e52ca4537240db3f48025ec9fca7a",
"Path": "https://github.com/ftexchange/ftx"
}
},
"Disabled": false
}
]
}

View File

@@ -46,15 +46,20 @@
"Disabled": false
},
{
"Name": "Binance",
"CheckType": "GitHub Sha Check",
"Data": {
"GitHubData": {
"Repo": "binance-exchange/binance-official-api-docs",
"Sha": "4878d48adc0075669ba85033b0e2d40c2876cf56"
}
},
"Disabled": false
"Name": "Binance",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"TokenData": "h1",
"Key": "id",
"Val": "change-log",
"TokenDataEnd": "p",
"TextTokenData": "strong",
"CheckString": "2020-05-06",
"Path": "https://binance-docs.github.io/apidocs/spot/en/#change-log"
}
},
"Disabled": false
},
{
"Name": "Bithumb",
@@ -425,6 +430,20 @@
}
},
"Disabled": false
},
{
"Name": "FTX",
"CheckType": "HTML String Check",
"Data": {
"HTMLData": {
"Key": "class",
"Val": "css-truncate css-truncate-target",
"TokenDataEnd": "td",
"CheckString": "65e8800b5c6800aad896f888b2a62afc-d29c4f140f6ca068db9970054076ba63e51a9e96bf56d570e62cdb573c86b18526296117-1c388b9ec24e52ca4537240db3f48025ec9fca7a",
"Path": "https://github.com/ftexchange/ftx"
}
},
"Disabled": false
}
]
}

View File

@@ -7,6 +7,10 @@
+ Please checkout individual exchange README for more information on
implementation
## Guide for adding a new exchange
+ A guide on implementing API support for a new exchange can be found [here](../docs/ADD_NEW_EXCHANGE.md)
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}

View File

@@ -0,0 +1,99 @@
{{define "exchanges ftx" -}}
{{template "header" .}}
## FTX Exchange
### Current Features
+ REST Support
+ Websocket Support
### How to enable
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
+ Individual package example below:
```go
// Exchanges will be abstracted out in further updates and examples will be
// supplied then
```
### How to do REST public/private calls
+ If enabled via "configuration".json file the exchange will be added to the
IBotExchange array in the ```go var bot Bot``` and you will only be able to use
the wrapper interface functions for accessing exchange data. View routines.go
for an example of integration usage with GoCryptoTrader. Rudimentary example
below:
main.go
```go
var f exchange.IBotExchange
for i := range bot.Exchanges {
if bot.Exchanges[i].GetName() == "FTX" {
f = bot.Exchanges[i]
}
}
// Public calls - wrapper functions
// Fetches current ticker information
tick, err := f.FetchTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := f.FetchOrderbook()
if err != nil {
// Handle error
}
// Private calls - wrapper functions - make sure your APIKEY and APISECRET are
// set and AuthenticatedAPISupport is set to true
// Fetches current account information
accountInfo, err := f.GetAccountInfo()
if err != nil {
// Handle error
}
```
+ If enabled via individually importing package, rudimentary example below:
```go
// Public calls
// Fetches current ticker information
ticker, err := f.GetTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := f.GetOrderBook()
if err != nil {
// Handle error
}
// Private calls - make sure your APIKEY and APISECRET are set and
// AuthenticatedAPISupport is set to true
// GetUserInfo returns account info
accountInfo, err := f.GetUserInfo(...)
if err != nil {
// Handle error
}
// Submits an order and the exchange and returns its tradeID
tradeID, err := f.Trade(...)
if err != nil {
// Handle error
}
```
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -31,6 +31,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| BTSE | Yes | Yes | NA |
| COINUT | Yes | Yes | NA |
| Exmo | Yes | NA | NA |
| FTX | Yes | Yes | No |
| CoinbasePro | Yes | Yes | No|
| Coinbene | Yes | No | No |
| GateIO | Yes | Yes | NA |

View File

View File

@@ -298,8 +298,8 @@ func ({{.Variable}} *{{.CapitalName}}) GetFundingHistory() ([]exchange.FundHisto
return nil, common.ErrNotYetImplemented
}
// GetExchangeHistory returns historic trade data since exchange opening.
func ({{.Variable}} *{{.CapitalName}}) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func ({{.Variable}} *{{.CapitalName}}) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -117,7 +117,7 @@ func testWrappers(e exchange.IBotExchange) []string {
funcs = append(funcs, "GetAccountInfo")
}
_, err = e.GetExchangeHistory(p, assetType)
_, err = e.GetExchangeHistory(p, assetType, time.Time{}, time.Time{})
if err == common.ErrNotYetImplemented {
funcs = append(funcs, "GetExchangeHistory")
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
"sync"
"text/template"
"time"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/config"
@@ -415,14 +416,14 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config)
})
var r8 []exchange.TradeHistory
r8, err = e.GetExchangeHistory(p, assetTypes[i])
r8, err = e.GetExchangeHistory(p, assetTypes[i], time.Now().Add(-time.Minute), time.Now())
msg = ""
if err != nil {
msg = err.Error()
responseContainer.ErrorCount++
}
responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{
SentParams: jsonifyInterface([]interface{}{p, assetTypes[i]}),
SentParams: jsonifyInterface([]interface{}{p, assetTypes[i], time.Now().Add(-time.Minute), time.Now()}),
Function: "GetExchangeHistory",
Error: msg,
Response: jsonifyInterface([]interface{}{r8}),
@@ -506,9 +507,10 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config)
})
// r13
cancelRequest := order.Cancel{
Side: testOrderSide,
Pair: p,
ID: config.OrderSubmission.OrderID,
Side: testOrderSide,
Pair: p,
ID: config.OrderSubmission.OrderID,
AssetType: assetTypes[i],
}
err = e.CancelOrder(&cancelRequest)
msg = ""

View File

@@ -18,7 +18,7 @@ import (
const (
// Default number of enabled exchanges. Modify this whenever an exchange is
// added or removed
defaultEnabledExchanges = 27
defaultEnabledExchanges = 28
testFakeExchangeName = "Stampbit"
testPair = "BTC-USD"
)

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
"unicode"
"github.com/thrasher-corp/gocryptotrader/common"
)
@@ -268,7 +269,17 @@ func (b *BaseCodes) UpdateContract(fullName, symbol, assocExchange string) error
// Register registers a currency from a string and returns a currency code
func (b *BaseCodes) Register(c string) Code {
NewUpperCode := strings.ToUpper(c)
NewUpperCode := c
lower := true
for _, r := range c {
if !unicode.IsLower(r) {
lower = false
break
}
}
if lower {
NewUpperCode = strings.ToUpper(c)
}
format := strings.Contains(c, NewUpperCode)
b.mtx.Lock()

View File

@@ -10,6 +10,9 @@ import (
// a Pair struct
func NewPairDelimiter(currencyPair, delimiter string) Pair {
result := strings.Split(currencyPair, delimiter)
if len(result) > 2 {
result[1] = strings.Join(result[1:], delimiter)
}
return Pair{
Delimiter: delimiter,
Base: NewCode(result[0]),

View File

@@ -337,6 +337,26 @@ func TestNewPairDelimiter(t *testing.T) {
actual, expected,
)
}
pair = NewPairDelimiter("BTC-MOVE-0626", "-")
actual = pair.String()
expected = "BTC-MOVE-0626"
if actual != expected {
t.Errorf(
"Pair(): %s was not equal to expected value: %s",
actual, expected,
)
}
pair = NewPairDelimiter("fBTC-USDT", "-")
actual = pair.String()
expected = "fBTC-USDT"
if actual != expected {
t.Errorf(
"Pair(): %s was not equal to expected value: %s",
actual, expected,
)
}
}
// TestNewPairFromIndex returns a CurrencyPair via a currency string and

1084
docs/ADD_NEW_EXCHANGE.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbene"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinut"
"github.com/thrasher-corp/gocryptotrader/exchanges/exmo"
"github.com/thrasher-corp/gocryptotrader/exchanges/ftx"
"github.com/thrasher-corp/gocryptotrader/exchanges/gateio"
"github.com/thrasher-corp/gocryptotrader/exchanges/gemini"
"github.com/thrasher-corp/gocryptotrader/exchanges/hitbtc"
@@ -187,6 +188,8 @@ func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error {
exch = new(exmo.EXMO)
case "coinbasepro":
exch = new(coinbasepro.CoinbasePro)
case "ftx":
exch = new(ftx.FTX)
case "gateio":
exch = new(gateio.Gateio)
case "gemini":

View File

@@ -107,7 +107,7 @@ func (h *FakePassingExchange) SetPairs(_ currency.Pairs, _ asset.Item, _ bool) e
return nil
}
func (h *FakePassingExchange) GetAssetTypes() asset.Items { return asset.Items{asset.Spot} }
func (h *FakePassingExchange) GetExchangeHistory(_ currency.Pair, _ asset.Item) ([]exchange.TradeHistory, error) {
func (h *FakePassingExchange) GetExchangeHistory(_ currency.Pair, _ asset.Item, _, _ time.Time) ([]exchange.TradeHistory, error) {
return nil, nil
}
func (h *FakePassingExchange) SupportsAutoPairUpdates() bool { return true }

View File

@@ -25,6 +25,10 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
+ Please checkout individual exchange README for more information on
implementation
## Guide for adding a new exchange
+ A guide on implementing API support for a new exchange can be found [here](../docs/ADD_NEW_EXCHANGE.md)
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution

View File

@@ -210,8 +210,8 @@ func (a *Alphapoint) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrNotYetImplemented
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (a *Alphapoint) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (a *Alphapoint) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -409,8 +409,8 @@ func (b *Binance) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *Binance) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *Binance) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -424,8 +424,8 @@ func (b *Bitfinex) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *Bitfinex) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *Bitfinex) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -280,8 +280,8 @@ func (b *Bitflyer) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *Bitflyer) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *Bitflyer) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -311,8 +311,8 @@ func (b *Bithumb) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *Bithumb) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *Bithumb) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -421,8 +421,8 @@ func (b *Bitmex) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrNotYetImplemented
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *Bitmex) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *Bitmex) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -362,8 +362,8 @@ func (b *Bitstamp) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *Bitstamp) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *Bitstamp) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -339,8 +339,8 @@ func (b *Bittrex) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *Bittrex) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *Bittrex) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -369,8 +369,8 @@ func (b *BTCMarkets) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *BTCMarkets) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *BTCMarkets) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -348,8 +348,8 @@ func (b *BTSE) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (b *BTSE) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (b *BTSE) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -385,8 +385,8 @@ func (c *CoinbasePro) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (c *CoinbasePro) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (c *CoinbasePro) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -461,8 +461,8 @@ func (c *Coinbene) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (c *Coinbene) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (c *Coinbene) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrFunctionNotSupported
}

View File

@@ -462,8 +462,8 @@ func (c *COINUT) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (c *COINUT) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (c *COINUT) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -211,10 +211,12 @@ func (e *Base) GetAssetTypes() asset.Items {
}
// GetPairAssetType returns the associated asset type for the currency pair
// This method is only useful for exchanges that have pair names with multiple delimiters (BTC-USD-0626)
// Helpful if the exchange has only a single asset type but in that case the asset type can be hard coded
func (e *Base) GetPairAssetType(c currency.Pair) (asset.Item, error) {
assetTypes := e.GetAssetTypes()
for i := range assetTypes {
if e.GetEnabledPairs(assetTypes[i]).Contains(c, true) {
if e.GetAvailablePairs(assetTypes[i]).Contains(c, true) {
return assetTypes[i], nil
}
}

View File

@@ -1428,7 +1428,7 @@ func TestGetAssetType(t *testing.T) {
b.CurrencyPairs.AssetTypes = asset.Items{asset.Spot}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Enabled: currency.Pairs{
Available: currency.Pairs{
currency.NewPair(currency.BTC, currency.USD),
},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},

View File

@@ -115,6 +115,7 @@ type TradeHistory struct {
Amount float64
Exchange string
Type string
Side string
Fee float64
Description string
}

View File

@@ -348,8 +348,8 @@ func (e *EXMO) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (e *EXMO) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (e *EXMO) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

133
exchanges/ftx/README.md Normal file
View File

@@ -0,0 +1,133 @@
# GoCryptoTrader package Ftx
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/page-logo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/ftx)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This ftx package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## FTX Exchange
### Current Features
+ REST Support
+ Websocket Support
### How to enable
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
+ Individual package example below:
```go
// Exchanges will be abstracted out in further updates and examples will be
// supplied then
```
### How to do REST public/private calls
+ If enabled via "configuration".json file the exchange will be added to the
IBotExchange array in the ```go var bot Bot``` and you will only be able to use
the wrapper interface functions for accessing exchange data. View routines.go
for an example of integration usage with GoCryptoTrader. Rudimentary example
below:
main.go
```go
var f exchange.IBotExchange
for i := range bot.Exchanges {
if bot.Exchanges[i].GetName() == "FTX" {
f = bot.Exchanges[i]
}
}
// Public calls - wrapper functions
// Fetches current ticker information
tick, err := f.FetchTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := f.FetchOrderbook()
if err != nil {
// Handle error
}
// Private calls - wrapper functions - make sure your APIKEY and APISECRET are
// set and AuthenticatedAPISupport is set to true
// Fetches current account information
accountInfo, err := f.GetAccountInfo()
if err != nil {
// Handle error
}
```
+ If enabled via individually importing package, rudimentary example below:
```go
// Public calls
// Fetches current ticker information
ticker, err := f.GetTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := f.GetOrderBook()
if err != nil {
// Handle error
}
// Private calls - make sure your APIKEY and APISECRET are set and
// AuthenticatedAPISupport is set to true
// GetUserInfo returns account info
accountInfo, err := f.GetUserInfo(...)
if err != nil {
// Handle error
}
// Submits an order and the exchange and returns its tradeID
tradeID, err := f.Trade(...)
if err != nil {
// Handle error
}
```
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

959
exchanges/ftx/ftx.go Normal file
View File

@@ -0,0 +1,959 @@
package ftx
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
)
// FTX is the overarching type across this package
type FTX struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
}
const (
ftxAPIURL = "https://ftx.com/api"
// Public endpoints
getMarkets = "/markets"
getMarket = "/markets/"
getOrderbook = "/markets/%s/orderbook?depth=%s"
getTrades = "/markets/%s/trades?"
getHistoricalData = "/markets/%s/candles?"
getFutures = "/futures"
getFuture = "/futures/"
getFutureStats = "/futures/%s/stats"
getFundingRates = "/funding_rates"
getIndexWeights = "/indexes/%s/weights"
getAllWalletBalances = "/wallet/all_balances"
// Authenticated endpoints
getAccountInfo = "/account"
getPositions = "/positions"
setLeverage = "/account/leverage"
getCoins = "/wallet/coins"
getBalances = "/wallet/balances"
getDepositAddress = "/wallet/deposit_address/"
getDepositHistory = "/wallet/deposits"
getWithdrawalHistory = "/wallet/withdrawals"
withdrawRequest = "/wallet/withdrawals"
getOpenOrders = "/orders?"
getOrderHistory = "/orders/history?"
getOpenTriggerOrders = "/conditional_orders?"
getTriggerOrderTriggers = "/conditional_orders/%s/triggers"
getTriggerOrderHistory = "/conditional_orders/history?"
placeOrder = "/orders"
placeTriggerOrder = "/conditional_orders"
modifyOrder = "/orders/%s/modify"
modifyOrderByClientID = "/orders/by_client_id/%s/modify"
modifyTriggerOrder = "/conditional_orders/%s/modify"
getOrderStatus = "/orders/"
getOrderStatusByClientID = "/orders/by_client_id/"
deleteOrder = "/orders/"
deleteOrderByClientID = "/orders/by_client_id/"
cancelTriggerOrder = "/conditional_orders/"
getFills = "/fills?"
getFundingPayments = "/funding_payments?"
getLeveragedTokens = "/lt/tokens"
getTokenInfo = "/lt/"
getLTBalances = "/lt/balances"
getLTCreations = "/lt/creations"
requestLTCreation = "/lt/%s/create"
getLTRedemptions = "/lt/redemptions"
requestLTRedemption = "/lt/%s/redeem"
getListQuotes = "/options/requests"
getMyQuotesRequests = "/options/my_requests"
createQuoteRequest = "/options/requests"
deleteQuote = "/options/requests/"
endpointQuote = "/options/requests/%s/quotes"
getMyQuotes = "/options/my_quotes"
deleteMyQuote = "/options/quotes/"
acceptQuote = "/options/quotes/%s/accept"
getOptionsInfo = "/options/account_info"
getOptionsPositions = "/options/positions"
getPublicOptionsTrades = "/options/trades"
getOptionsFills = "/options/fills"
requestOTCQuote = "/otc/quotes"
getOTCQuoteStatus = "/otc/quotes/"
acceptOTCQuote = "/otc/quotes/%s/accept"
// Other Consts
trailingStopOrderType = "trailingStop"
takeProfitOrderType = "takeProfit"
closedStatus = "closed"
spotString = "spot"
futuresString = "future"
ratePeriod = time.Second
rateLimit = 30
)
// GetMarkets gets market data
func (f *FTX) GetMarkets() ([]MarketData, error) {
resp := struct {
Data []MarketData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(ftxAPIURL+getMarkets, &resp)
}
// GetMarket gets market data for a provided asset type
func (f *FTX) GetMarket(marketName string) (MarketData, error) {
resp := struct {
Data MarketData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(ftxAPIURL+getMarket+marketName,
&resp)
}
// GetOrderbook gets orderbook for a given market with a given depth (default depth 20)
func (f *FTX) GetOrderbook(marketName string, depth int64) (OrderbookData, error) {
result := struct {
Data TempOBData `json:"result"`
}{}
strDepth := strconv.FormatInt(depth, 10)
var resp OrderbookData
err := f.SendHTTPRequest(fmt.Sprintf(ftxAPIURL+getOrderbook, marketName, strDepth), &result)
if err != nil {
return resp, err
}
resp.MarketName = marketName
for x := range result.Data.Asks {
resp.Asks = append(resp.Asks, OData{Price: result.Data.Asks[x][0],
Size: result.Data.Asks[x][1],
})
}
for y := range result.Data.Bids {
resp.Bids = append(resp.Bids, OData{Price: result.Data.Bids[y][0],
Size: result.Data.Bids[y][1],
})
}
return resp, nil
}
// GetTrades gets trades based on the conditions specified
func (f *FTX) GetTrades(marketName string, startTime, endTime time.Time, limit int64) ([]TradeData, error) {
strLimit := strconv.FormatInt(limit, 10)
params := url.Values{}
params.Set("limit", strLimit)
resp := struct {
Data []TradeData `json:"result"`
}{}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
return resp.Data, f.SendHTTPRequest(fmt.Sprintf(ftxAPIURL+getTrades, marketName)+params.Encode(),
&resp)
}
// GetHistoricalData gets historical OHLCV data for a given market pair
func (f *FTX) GetHistoricalData(marketName, timeInterval, limit string, startTime, endTime time.Time) ([]OHLCVData, error) {
resp := struct {
Data []OHLCVData `json:"result"`
}{}
params := url.Values{}
params.Set("resolution", timeInterval)
if limit != "" {
params.Set("limit", limit)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
return resp.Data, f.SendHTTPRequest(fmt.Sprintf(ftxAPIURL+getHistoricalData, marketName)+params.Encode(), &resp)
}
// GetFutures gets data on futures
func (f *FTX) GetFutures() ([]FuturesData, error) {
resp := struct {
Data []FuturesData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(ftxAPIURL+getFutures, &resp)
}
// GetFuture gets data on a given future
func (f *FTX) GetFuture(futureName string) (FuturesData, error) {
resp := struct {
Data FuturesData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(ftxAPIURL+getFuture+futureName, &resp)
}
// GetFutureStats gets data on a given future's stats
func (f *FTX) GetFutureStats(futureName string) (FutureStatsData, error) {
resp := struct {
Data FutureStatsData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(fmt.Sprintf(ftxAPIURL+getFutureStats, futureName), &resp)
}
// GetFundingRates gets data on funding rates
func (f *FTX) GetFundingRates() ([]FundingRatesData, error) {
resp := struct {
Data []FundingRatesData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(ftxAPIURL+getFundingRates, &resp)
}
// GetIndexWeights gets index weights
func (f *FTX) GetIndexWeights(index string) (IndexWeights, error) {
var resp IndexWeights
return resp, f.SendHTTPRequest(ftxAPIURL+fmt.Sprintf(getIndexWeights, index), &resp)
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (f *FTX) SendHTTPRequest(path string, result interface{}) error {
return f.SendPayload(context.Background(), &request.Item{
Method: http.MethodGet,
Path: path,
Result: result,
Verbose: f.Verbose,
HTTPDebugging: f.HTTPDebugging,
HTTPRecording: f.HTTPRecording,
})
}
// GetAccountInfo gets account info
func (f *FTX) GetAccountInfo() (AccountInfoData, error) {
resp := struct {
Data AccountInfoData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getAccountInfo, nil, &resp)
}
// GetPositions gets the users positions
func (f *FTX) GetPositions() ([]PositionData, error) {
resp := struct {
Data []PositionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getPositions, nil, &resp)
}
// ChangeAccountLeverage changes default leverage used by account
func (f *FTX) ChangeAccountLeverage(leverage float64) error {
req := make(map[string]interface{})
req["leverage"] = leverage
return f.SendAuthHTTPRequest(http.MethodPost, setLeverage, req, nil)
}
// GetCoins gets coins' data in the account wallet
func (f *FTX) GetCoins() ([]WalletCoinsData, error) {
resp := struct {
Data []WalletCoinsData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getCoins, nil, &resp)
}
// GetBalances gets balances of the account
func (f *FTX) GetBalances() ([]BalancesData, error) {
resp := struct {
Data []BalancesData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getBalances, nil, &resp)
}
// GetAllWalletBalances gets all wallets' balances
func (f *FTX) GetAllWalletBalances() (AllWalletAccountData, error) {
resp := struct {
Data AllWalletAccountData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getAllWalletBalances, nil, &resp)
}
// FetchDepositAddress gets deposit address for a given coin
func (f *FTX) FetchDepositAddress(coin string) (DepositData, error) {
resp := struct {
Data DepositData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getDepositAddress+coin, nil, &resp)
}
// FetchDepositHistory gets deposit history
func (f *FTX) FetchDepositHistory() ([]TransactionData, error) {
resp := struct {
Data []TransactionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getDepositHistory, nil, &resp)
}
// FetchWithdrawalHistory gets withdrawal history
func (f *FTX) FetchWithdrawalHistory() ([]TransactionData, error) {
resp := struct {
Data []TransactionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getWithdrawalHistory, nil, &resp)
}
// Withdraw sends a withdrawal request
func (f *FTX) Withdraw(coin, address, tag, password, code string, size float64) (TransactionData, error) {
req := make(map[string]interface{})
req["coin"] = coin
req["address"] = address
req["size"] = size
if code != "" {
req["code"] = code
}
if tag != "" {
req["tag"] = tag
}
if password != "" {
req["password"] = password
}
resp := struct {
Data TransactionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, withdrawRequest, req, &resp)
}
// GetOpenOrders gets open orders
func (f *FTX) GetOpenOrders(marketName string) ([]OrderData, error) {
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
resp := struct {
Data []OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOpenOrders+params.Encode(), nil, &resp)
}
// FetchOrderHistory gets order history
func (f *FTX) FetchOrderHistory(marketName string, startTime, endTime time.Time, limit string) ([]OrderData, error) {
resp := struct {
Data []OrderData `json:"result"`
}{}
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if limit != "" {
params.Set("limit", limit)
}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOrderHistory+params.Encode(), nil, &resp)
}
// GetOpenTriggerOrders gets trigger orders that are currently open
func (f *FTX) GetOpenTriggerOrders(marketName, orderType string) ([]TriggerOrderData, error) {
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
if orderType != "" {
params.Set("type", orderType)
}
resp := struct {
Data []TriggerOrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOpenTriggerOrders+params.Encode(), nil, &resp)
}
// GetTriggerOrderTriggers gets trigger orders that are currently open
func (f *FTX) GetTriggerOrderTriggers(orderID string) ([]TriggerData, error) {
resp := struct {
Data []TriggerData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, fmt.Sprintf(getTriggerOrderTriggers, orderID), nil, &resp)
}
// GetTriggerOrderHistory gets trigger orders that are currently open
func (f *FTX) GetTriggerOrderHistory(marketName string, startTime, endTime time.Time, side, orderType, limit string) ([]TriggerOrderData, error) {
resp := struct {
Data []TriggerOrderData `json:"result"`
}{}
params := url.Values{}
if marketName != "" {
params.Set("market", marketName)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if side != "" {
params.Set("side", side)
}
if orderType != "" {
params.Set("type", orderType)
}
if limit != "" {
params.Set("limit", limit)
}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getTriggerOrderHistory+params.Encode(), nil, &resp)
}
// Order places an order
func (f *FTX) Order(marketName, side, orderType, reduceOnly, ioc, postOnly, clientID string, price, size float64) (OrderData, error) {
req := make(map[string]interface{})
req["market"] = marketName
req["side"] = side
req["price"] = price
req["type"] = orderType
req["size"] = size
if reduceOnly != "" {
req["reduceOnly"] = reduceOnly
}
if ioc != "" {
req["ioc"] = ioc
}
if postOnly != "" {
req["postOnly"] = postOnly
}
if clientID != "" {
req["clientID"] = clientID
}
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, placeOrder, req, &resp)
}
// TriggerOrder places an order
func (f *FTX) TriggerOrder(marketName, side, orderType, reduceOnly, retryUntilFilled string, size, triggerPrice, orderPrice, trailValue float64) (TriggerOrderData, error) {
req := make(map[string]interface{})
req["market"] = marketName
req["side"] = side
req["type"] = orderType
req["size"] = size
if reduceOnly != "" {
req["reduceOnly"] = reduceOnly
}
if retryUntilFilled != "" {
req["retryUntilFilled"] = retryUntilFilled
}
if orderType == order.Stop.Lower() || orderType == "" {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
if orderType == trailingStopOrderType {
req["trailValue"] = trailValue
}
if orderType == takeProfitOrderType {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
resp := struct {
Data TriggerOrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, placeTriggerOrder, req, &resp)
}
// ModifyPlacedOrder modifies a placed order
func (f *FTX) ModifyPlacedOrder(orderID, clientID string, price, size float64) (OrderData, error) {
req := make(map[string]interface{})
req["price"] = price
req["size"] = size
if clientID != "" {
req["clientID"] = clientID
}
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(modifyOrder, orderID), req, &resp)
}
// ModifyOrderByClientID modifies a placed order via clientOrderID
func (f *FTX) ModifyOrderByClientID(clientOrderID, clientID string, price, size float64) (OrderData, error) {
req := make(map[string]interface{})
req["price"] = price
req["size"] = size
if clientID != "" {
req["clientID"] = clientID
}
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(modifyOrderByClientID, clientOrderID), req, &resp)
}
// ModifyTriggerOrder modifies an existing trigger order
// Choices for ordertype include stop, trailingStop, takeProfit
func (f *FTX) ModifyTriggerOrder(orderID, orderType string, size, triggerPrice, orderPrice, trailValue float64) (TriggerOrderData, error) {
req := make(map[string]interface{})
req["size"] = size
if orderType == order.Stop.Lower() || orderType == "" {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
if orderType == trailingStopOrderType {
req["trailValue"] = trailValue
}
if orderType == takeProfitOrderType {
req["triggerPrice"] = triggerPrice
req["orderPrice"] = orderPrice
}
resp := struct {
Data TriggerOrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(modifyTriggerOrder, orderID), req, &resp)
}
// GetOrderStatus gets the order status of a given orderID
func (f *FTX) GetOrderStatus(orderID string) (OrderData, error) {
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOrderStatus+orderID, nil, &resp)
}
// GetOrderStatusByClientID gets the order status of a given clientOrderID
func (f *FTX) GetOrderStatusByClientID(clientOrderID string) (OrderData, error) {
resp := struct {
Data OrderData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOrderStatusByClientID+clientOrderID, nil, &resp)
}
// DeleteOrder deletes an order
func (f *FTX) DeleteOrder(orderID string) (string, error) {
resp := struct {
Data string `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, deleteOrder+orderID, nil, &resp)
}
// DeleteOrderByClientID deletes an order
func (f *FTX) DeleteOrderByClientID(clientID string) (string, error) {
resp := struct {
Data string `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, deleteOrderByClientID+clientID, nil, &resp)
}
// DeleteTriggerOrder deletes an order
func (f *FTX) DeleteTriggerOrder(orderID string) (string, error) {
resp := struct {
Data string `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodDelete, cancelTriggerOrder+orderID, nil, &resp)
}
// GetFills gets fills' data
func (f *FTX) GetFills(market, limit string, startTime, endTime time.Time) ([]FillsData, error) {
resp := struct {
Data []FillsData `json:"result"`
}{}
params := url.Values{}
if market != "" {
params.Set("market", market)
}
if limit != "" {
params.Set("limit", limit)
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getFills+params.Encode(), nil, &resp)
}
// GetFundingPayments gets funding payments
func (f *FTX) GetFundingPayments(startTime, endTime time.Time, future string) ([]FundingPaymentsData, error) {
resp := struct {
Data []FundingPaymentsData `json:"result"`
}{}
params := url.Values{}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if future != "" {
params.Set("future", future)
}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getFundingPayments+params.Encode(), nil, &resp)
}
// ListLeveragedTokens lists leveraged tokens
func (f *FTX) ListLeveragedTokens() ([]LeveragedTokensData, error) {
resp := struct {
Data []LeveragedTokensData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getLeveragedTokens, nil, &resp)
}
// GetTokenInfo gets token info
func (f *FTX) GetTokenInfo(tokenName string) ([]LeveragedTokensData, error) {
resp := struct {
Data []LeveragedTokensData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getTokenInfo+tokenName, nil, &resp)
}
// ListLTBalances gets leveraged tokens' balances
func (f *FTX) ListLTBalances() ([]LTBalanceData, error) {
resp := struct {
Data []LTBalanceData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getLTBalances, nil, &resp)
}
// ListLTCreations lists the leveraged tokens' creation requests
func (f *FTX) ListLTCreations() ([]LTCreationData, error) {
resp := struct {
Data []LTCreationData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getLTCreations, nil, &resp)
}
// RequestLTCreation sends a request to create a leveraged token
func (f *FTX) RequestLTCreation(tokenName string, size float64) (RequestTokenCreationData, error) {
req := make(map[string]interface{})
req["size"] = size
resp := struct {
Data RequestTokenCreationData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(requestLTCreation, tokenName), req, &resp)
}
// ListLTRedemptions lists the leveraged tokens' redemption requests
func (f *FTX) ListLTRedemptions() ([]LTRedemptionData, error) {
resp := struct {
Data []LTRedemptionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getLTRedemptions, nil, &resp)
}
// RequestLTRedemption sends a request to redeem a leveraged token
func (f *FTX) RequestLTRedemption(tokenName string, size float64) (LTRedemptionRequestData, error) {
req := make(map[string]interface{})
req["size"] = size
resp := struct {
Data LTRedemptionRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(requestLTRedemption, tokenName), req, &resp)
}
// GetQuoteRequests gets a list of quote requests
func (f *FTX) GetQuoteRequests() ([]QuoteRequestData, error) {
resp := struct {
Data []QuoteRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getListQuotes, nil, &resp)
}
// GetYourQuoteRequests gets a list of your quote requests
func (f *FTX) GetYourQuoteRequests() ([]PersonalQuotesData, error) {
resp := struct {
Data []PersonalQuotesData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getMyQuotesRequests, nil, &resp)
}
// CreateQuoteRequest sends a request to create a quote
func (f *FTX) CreateQuoteRequest(underlying, optionType, side string, expiry int64, requestExpiry string, strike, size, limitPrice, counterParyID float64, hideLimitPrice bool) (CreateQuoteRequestData, error) {
req := make(map[string]interface{})
req["underlying"] = underlying
req["type"] = optionType
req["side"] = side
req["strike"] = strike
req["expiry"] = expiry
req["size"] = size
if limitPrice != 0 {
req["limitPrice"] = limitPrice
}
if requestExpiry != "" {
req["requestExpiry"] = requestExpiry
}
if counterParyID != 0 {
req["counterParyID"] = counterParyID
}
req["hideLimitPrice"] = hideLimitPrice
resp := struct {
Data CreateQuoteRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, createQuoteRequest, req, &resp)
}
// DeleteQuote sends request to cancel a quote
func (f *FTX) DeleteQuote(requestID string) (CancelQuoteRequestData, error) {
resp := struct {
Data CancelQuoteRequestData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodDelete, deleteQuote+requestID, nil, &resp)
}
// GetQuotesForYourQuote gets a list of quotes for your quote
func (f *FTX) GetQuotesForYourQuote(requestID string) (QuoteForQuoteData, error) {
var resp QuoteForQuoteData
return resp, f.SendAuthHTTPRequest(http.MethodGet, fmt.Sprintf(endpointQuote, requestID), nil, &resp)
}
// MakeQuote makes a quote for a quote
func (f *FTX) MakeQuote(requestID, price string) ([]QuoteForQuoteData, error) {
params := url.Values{}
params.Set("price", price)
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(endpointQuote, requestID), nil, &resp)
}
// MyQuotes gets a list of my quotes for quotes
func (f *FTX) MyQuotes() ([]QuoteForQuoteData, error) {
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getMyQuotes, nil, &resp)
}
// DeleteMyQuote deletes my quote for quotes
func (f *FTX) DeleteMyQuote(quoteID string) ([]QuoteForQuoteData, error) {
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodDelete, deleteMyQuote+quoteID, nil, &resp)
}
// AcceptQuote accepts the quote for quote
func (f *FTX) AcceptQuote(quoteID string) ([]QuoteForQuoteData, error) {
resp := struct {
Data []QuoteForQuoteData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(acceptQuote, quoteID), nil, &resp)
}
// GetAccountOptionsInfo gets account's options' info
func (f *FTX) GetAccountOptionsInfo() (AccountOptionsInfoData, error) {
resp := struct {
Data AccountOptionsInfoData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOptionsInfo, nil, &resp)
}
// GetOptionsPositions gets options' positions
func (f *FTX) GetOptionsPositions() ([]OptionsPositionsData, error) {
resp := struct {
Data []OptionsPositionsData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOptionsPositions, nil, &resp)
}
// GetPublicOptionsTrades gets options' trades from public
func (f *FTX) GetPublicOptionsTrades(startTime, endTime time.Time, limit string) ([]OptionsTradesData, error) {
resp := struct {
Data []OptionsTradesData `json:"result"`
}{}
req := make(map[string]interface{})
if !startTime.IsZero() && !endTime.IsZero() {
req["start_time"] = strconv.FormatInt(startTime.Unix(), 10)
req["end_time"] = strconv.FormatInt(endTime.Unix(), 10)
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
}
if limit != "" {
req["limit"] = limit
}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getPublicOptionsTrades, req, &resp)
}
// GetOptionsFills gets fills data for options
func (f *FTX) GetOptionsFills(startTime, endTime time.Time, limit string) ([]OptionFillsData, error) {
resp := struct {
Data []OptionFillsData `json:"result"`
}{}
req := make(map[string]interface{})
if !startTime.IsZero() && !endTime.IsZero() {
req["start_time"] = strconv.FormatInt(startTime.Unix(), 10)
req["end_time"] = strconv.FormatInt(endTime.Unix(), 10)
if startTime.After(endTime) {
return resp.Data, errors.New("startTime cannot be after endTime")
}
}
if limit != "" {
req["limit"] = limit
}
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOptionsFills, req, &resp)
}
// SendAuthHTTPRequest sends an authenticated request
func (f *FTX) SendAuthHTTPRequest(method, path string, data, result interface{}) error {
ts := strconv.FormatInt(time.Now().UnixNano()/1000000, 10)
var body io.Reader
var hmac, payload []byte
var err error
if data != nil {
payload, err = json.Marshal(data)
if err != nil {
return err
}
body = bytes.NewBuffer(payload)
sigPayload := ts + method + "/api" + path + string(payload)
hmac = crypto.GetHMAC(crypto.HashSHA256, []byte(sigPayload), []byte(f.API.Credentials.Secret))
} else {
sigPayload := ts + method + "/api" + path
hmac = crypto.GetHMAC(crypto.HashSHA256, []byte(sigPayload), []byte(f.API.Credentials.Secret))
}
headers := make(map[string]string)
headers["FTX-KEY"] = f.API.Credentials.Key
headers["FTX-SIGN"] = crypto.HexEncodeToString(hmac)
headers["FTX-TS"] = ts
headers["Content-Type"] = "application/json"
return f.SendPayload(context.Background(), &request.Item{
Method: method,
Path: ftxAPIURL + path,
Headers: headers,
Body: body,
Result: result,
AuthRequest: true,
Verbose: f.Verbose,
HTTPDebugging: f.HTTPDebugging,
HTTPRecording: f.HTTPRecording,
})
}
// GetFee returns an estimate of fee based on type of transaction
func (f *FTX) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder)
default:
feeData, err := f.GetAccountInfo()
if err != nil {
return 0, err
}
switch feeBuilder.IsMaker {
case true:
fee = feeData.MakerFee * feeBuilder.Amount * feeBuilder.PurchasePrice
case false:
fee = feeData.TakerFee * feeBuilder.Amount * feeBuilder.PurchasePrice
}
if fee < 0 {
fee = 0
}
}
return fee, nil
}
// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 {
if feeBuilder.IsMaker {
return 0.0002 * feeBuilder.PurchasePrice * feeBuilder.Amount
}
return 0.0007 * feeBuilder.PurchasePrice * feeBuilder.Amount
}
func parseInterval(in time.Duration) (TimeInterval, error) {
switch in {
case kline.FifteenSecond:
return TimeIntervalFifteenSeconds, nil
case kline.OneMin:
return TimeIntervalMinute, nil
case kline.FiveMin:
return TimeIntervalFiveMinutes, nil
case kline.FifteenMin:
return TimeIntervalFifteenMinutes, nil
case kline.OneHour:
return TimeIntervalHour, nil
case kline.FourHour:
return TimeIntervalFourHours, nil
case kline.OneDay:
return TimeIntervalDay, nil
default:
return TimeIntervalMinute, errInvalidInterval
}
}
func (f *FTX) compatibleOrderVars(orderSide, orderStatus, orderType string, amount, filledAmount, avgFillPrice float64) (OrderVars, error) {
var resp OrderVars
switch orderSide {
case order.Buy.Lower():
resp.Side = order.Buy
case order.Sell.Lower():
resp.Side = order.Sell
}
switch orderStatus {
case strings.ToLower(order.New.String()):
resp.Status = order.New
case strings.ToLower(order.Open.String()):
resp.Status = order.Open
case closedStatus:
if filledAmount != 0 && filledAmount != amount {
resp.Status = order.PartiallyCancelled
}
if filledAmount == 0 {
resp.Status = order.Cancelled
}
if filledAmount == amount {
resp.Status = order.Filled
}
}
var feeBuilder exchange.FeeBuilder
feeBuilder.PurchasePrice = avgFillPrice
feeBuilder.Amount = amount
resp.OrderType = order.Market
if strings.EqualFold(orderType, order.Limit.String()) {
resp.OrderType = order.Limit
feeBuilder.IsMaker = true
}
fee, err := f.GetFee(&feeBuilder)
if err != nil {
return resp, err
}
resp.Fee = fee
return resp, nil
}
// RequestForQuotes requests for otc quotes
func (f *FTX) RequestForQuotes(base, quote string, amount float64) (RequestQuoteData, error) {
resp := struct {
Data RequestQuoteData `json:"result"`
}{}
req := make(map[string]interface{})
req["fromCoin"] = base
req["toCoin"] = quote
req["size"] = amount
return resp.Data, f.SendAuthHTTPRequest(http.MethodPost, requestOTCQuote, req, &resp)
}
// GetOTCQuoteStatus gets quote status of a quote
func (f *FTX) GetOTCQuoteStatus(marketName, quoteID string) ([]QuoteStatusData, error) {
resp := struct {
Data []QuoteStatusData `json:"result"`
}{}
params := url.Values{}
params.Set("market", marketName)
return resp.Data, f.SendAuthHTTPRequest(http.MethodGet, getOTCQuoteStatus+quoteID, params, &resp)
}
// AcceptOTCQuote requests for otc quotes
func (f *FTX) AcceptOTCQuote(quoteID string) error {
return f.SendAuthHTTPRequest(http.MethodPost, fmt.Sprintf(acceptOTCQuote, quoteID), nil, nil)
}

1188
exchanges/ftx/ftx_test.go Normal file

File diff suppressed because it is too large Load Diff

726
exchanges/ftx/ftx_types.go Normal file
View File

@@ -0,0 +1,726 @@
package ftx
import (
"errors"
"time"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// MarketData stores market data
type MarketData struct {
Name string `json:"name"`
BaseCurrency string `json:"baseCurrency"`
QuoteCurrency string `json:"quoteCurrency"`
MarketType string `json:"type"`
Underlying string `json:"underlying"`
Change1h float64 `json:"change1h"`
Change24h float64 `json:"change24h"`
ChangeBod float64 `json:"changeBod"`
QuoteVolume24h float64 `json:"quoteVolume24h"`
Enabled bool `json:"enabled"`
Ask float64 `json:"ask"`
Bid float64 `json:"bid"`
Last float64 `json:"last"`
USDVolume24h float64 `json:"volumeUSD24h"`
MinProvideSize float64 `json:"minProvideSize"`
PriceIncrement float64 `json:"priceIncrement"`
SizeIncrement float64 `json:"sizeIncrement"`
Restricted bool `json:"restricted"`
}
// OData stores orderdata in orderbook
type OData struct {
Price float64
Size float64
}
// OrderbookData stores orderbook data
type OrderbookData struct {
MarketName string
Asks []OData
Bids []OData
}
// TempOBData stores orderbook data temporarily
type TempOBData struct {
Asks [][2]float64 `json:"asks"`
Bids [][2]float64 `json:"bids"`
}
// TradeData stores data from trades
type TradeData struct {
ID int64 `json:"id"`
Liquidation bool `json:"liquidation"`
Price float64 `json:"price"`
Side string `json:"side"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
}
// OHLCVData stores historical OHLCV data
type OHLCVData struct {
Close float64 `json:"close"`
High float64 `json:"high"`
Low float64 `json:"low"`
Open float64 `json:"open"`
StartTime time.Time `json:"startTime"`
Time float64 `json:"time"`
Volume float64 `json:"volume"`
}
// FuturesData stores data for futures
type FuturesData struct {
Ask float64 `json:"ask"`
Bid float64 `json:"bid"`
Change1h float64 `json:"change1h"`
Change24h float64 `json:"change24h"`
ChangeBod float64 `json:"changeBod"`
VolumeUSD24h float64 `json:"volumeUsd24h"`
Volume float64 `json:"volume"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Expired bool `json:"expired"`
Expiry time.Time `json:"expiry"`
ExpiryDescription string `json:"expiryDescription"`
Group string `json:"group"`
Index float64 `json:"index"`
IMFFactor float64 `json:"imfFactor"`
Last float64 `json:"last"`
LowerBound float64 `json:"lowerBound"`
MarginPrice float64 `json:"marginPrice"`
Mark float64 `json:"mark"`
MoveStart interface{} `json:"moveStart"`
Name string `json:"name"`
Perpetual bool `json:"perpetual"`
PositionLimitWeight float64 `json:"positionLimitWeight"`
PostOnly bool `json:"postOnly"`
PriceIncrement float64 `json:"priceIncrement"`
SizeIncrement float64 `json:"sizeIncrement"`
Underlying string `json:"underlying"`
UpperBound float64 `json:"upperBound"`
FutureType string `json:"type"`
}
// FutureStatsData stores data on futures stats
type FutureStatsData struct {
Volume float64 `json:"volume"`
NextFundingRate float64 `json:"nextFundingRate"`
NextFundingTime time.Time `json:"nextFundingTime"`
ExpirationPrice float64 `json:"expirationPrice"`
PredictedExpirationPrice float64 `json:"predictedExpirationPrice"`
OpenInterest float64 `json:"openInterest"`
StrikePrice float64 `json:"strikePrice"`
}
// FundingRatesData stores data on funding rates
type FundingRatesData struct {
Future string `json:"future"`
Rate float64 `json:"rate"`
Time time.Time `json:"time"`
}
// IndexWeights stores index weights' data
type IndexWeights struct {
Result map[string]float64 `json:"result"`
}
// PositionData stores data of an open position
type PositionData struct {
Cost float64 `json:"cost"`
EntryPrice float64 `json:"entryPrice"`
Future string `json:"future"`
InitialMarginRequirement float64 `json:"initialMarginRequirement"`
LongOrderSize float64 `json:"longOrderSize"`
MaintenanceMarginRequirement float64 `json:"maintenanceMarginRequirement"`
NetSize float64 `json:"netSize"`
OpenSize float64 `json:"openSize"`
RealisedPnL float64 `json:"realisedPnL"`
ShortOrderSide float64 `json:"shortOrderSide"`
Side string `json:"side"`
Size float64 `json:"size"`
UnrealisedPnL float64 `json:"unrealisedPnL"`
}
// AccountInfoData stores account data
type AccountInfoData struct {
BackstopProvider bool `json:"backstopProvider"`
ChargeInterestOnNegativeUSD bool `json:"chargeInterestOnNegativeUsd"`
Collateral float64 `json:"collateral"`
FreeCollateral float64 `json:"freeCollateral"`
InitialMarginRequirement float64 `json:"initialMarginRequirement"`
Leverage float64 `json:"float64"`
Liquidating bool `json:"liquidating"`
MaintenanceMarginRequirement float64 `json:"maintenanceMarginRequirement"`
MakerFee float64 `json:"makerFee"`
MarginFraction float64 `json:"marginFraction"`
OpenMarginFraction float64 `json:"openMarginFraction"`
PositionLimit float64 `json:"positionLimit"`
PositionLimitUsed float64 `json:"positionLimitUsed"`
SpotLendingEnabled bool `json:"spotLendingEnabled"`
SpotMarginEnabled bool `json:"spotMarginEnabled"`
TakerFee float64 `json:"takerFee"`
TotalAccountValue float64 `json:"totalAccountValue"`
TotalPositionSize float64 `json:"totalPositionSize"`
UseFTTCollateral bool `json:"useFttCollateral"`
Username string `json:"username"`
Positions []PositionData `json:"positions"`
}
// WalletCoinsData stores data about wallet coins
type WalletCoinsData struct {
Bep2Asset interface{} `json:"bep2Asset"`
CanConvert bool `json:"canConvert"`
CanDeposit bool `json:"canDeposit"`
CanWithdraw bool `json:"canWithdraw"`
Collateral bool `json:"collateral"`
CollateralWeight float64 `json:"collateralWeight"`
CreditTo interface{} `json:"creditTo"`
ERC20Contract interface{} `json:"erc20Contract"`
Fiat bool `json:"fiat"`
HasTag bool `json:"hasTag"`
Hidden bool `json:"hidden"`
IsETF bool `json:"isEtf"`
IsToken bool `json:"isToken"`
Methods []interface{}
ID string `json:"id"`
Name string `json:"name"`
}
// BalancesData stores balances data
type BalancesData struct {
Coin string `json:"coin"`
Free float64 `json:"free"`
Total float64 `json:"total"`
}
// AllWalletAccountData stores account data on all WalletCoins
type AllWalletAccountData struct {
Main []BalancesData `json:"main"`
BattleRoyale []BalancesData `json:"Battle Royale"`
}
// DepositData stores deposit address data
type DepositData struct {
Address string `json:"address"`
Tag string `json:"tag"`
}
// TransactionData stores data about deposit history
type TransactionData struct {
Coin string `json:"coin"`
Confirmations int64 `json:"conformations"`
ConfirmedTime time.Time `json:"confirmedTime"`
Fee float64 `json:"fee"`
ID int64 `json:"id"`
SentTime time.Time `json:"sentTime"`
Size float64 `json:"size"`
Status string `json:"status"`
Time time.Time `json:"time"`
TxID string `json:"txid"`
}
// OrderData stores open order data
type OrderData struct {
CreatedAt time.Time `json:"createdAt"`
FilledSize float64 `json:"filledSize"`
Future string `json:"future"`
ID int64 `json:"id"`
Market string `json:"market"`
Price float64 `json:"price"`
AvgFillPrice float64 `json:"avgFillPrice"`
RemainingSize float64 `json:"remainingSize"`
Side string `json:"side"`
Size float64 `json:"size"`
Status string `json:"status"`
OrderType string `json:"type"`
ReduceOnly bool `json:"reduceOnly"`
IOC bool `json:"ioc"`
PostOnly bool `json:"postOnly"`
ClientID string `json:"clientId"`
}
// TriggerOrderData stores trigger order data
type TriggerOrderData struct {
CreatedAt time.Time `json:"createdAt"`
Error string `json:"error"`
Future string `json:"future"`
ID int64 `json:"id"`
Market string `json:"market"`
OrderID int64 `json:"orderId"`
OrderPrice float64 `json:"orderPrice"`
ReduceOnly bool `json:"reduceOnly"`
Side string `json:"side"`
Size float64 `json:"size"`
Status string `json:"status"`
TrailStart float64 `json:"trailStart"`
TrailValue float64 `json:"trailvalue"`
TriggerPrice float64 `json:"triggerPrice"`
TriggeredAt string `json:"triggeredAt"`
OrderType string `json:"type"`
MarketOrLimit string `json:"orderType"`
FilledSize float64 `json:"filledSize"`
AvgFillPrice float64 `json:"avgFillPrice"`
RetryUntilFilled bool `json:"retryUntilFilled"`
}
// TriggerData stores trigger orders' trigger data
type TriggerData struct {
Error string `json:"error"`
FilledSize float64 `json:"filledSize"`
OrderSize float64 `json:"orderSize"`
OrderID int64 `json:"orderId"`
Time time.Time `json:"time"`
}
// FillsData stores fills' data
type FillsData struct {
Fee float64 `json:"fee"`
FeeRate float64 `json:"feeRate"`
Future string `json:"future"`
ID string `json:"id"`
Liquidity string `json:"liquidity"`
Market string `json:"market"`
BaseCurrency string `json:"baseCurrency"`
QuoteCurrency string `json:"quoteCurrency"`
OrderID string `json:"orderID"`
TradeID string `json:"tradeID"`
Price float64 `json:"price"`
Side string `json:"side"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
OrderType string `json:"type"`
}
// FundingPaymentsData stores funding payments' data
type FundingPaymentsData struct {
Future string `json:"future"`
ID string `json:"id"`
Payment float64 `json:"payment"`
Time time.Time `json:"time"`
Rate float64 `json:"rate"`
}
// LeveragedTokensData stores data of leveraged tokens
type LeveragedTokensData struct {
Basket map[string]interface{} `json:"basket"`
Bep2AssetName string `json:"bep2AssetName"`
Name string `json:"name"`
Description string `json:"description"`
Underlying string `json:"underlying"`
Leverage float64 `json:"leverage"`
Outstanding float64 `json:"outstanding"`
PricePerShare float64 `json:"pricePerShare"`
PositionPerShare float64 `json:"positionPerShare"`
PositionsPerShare interface{} `json:"positionsPerShare"`
TargetComponents []string `json:"targetComponents"`
TotalCollateral float64 `json:"totalCollateral"`
TotalNav float64 `json:"totalNav"`
UnderlyingMark float64 `json:"underlyingMark"`
ContactAddress string `json:"contactAddress"`
Change1h float64 `json:"change1h"`
Change24h float64 `json:"change24h"`
ChangeBod float64 `json:"changeBod"`
}
// LTBalanceData stores balances of leveraged tokens
type LTBalanceData struct {
Token string `json:"token"`
Balance float64 `json:"balance"`
}
// LTCreationData stores token creation requests' data
type LTCreationData struct {
ID string `json:"id"`
Token string `json:"token"`
RequestedSize float64 `json:"requestedSize"`
Pending bool `json:"pending"`
CreatedSize float64 `json:"createdize"`
Price float64 `json:"price"`
Cost float64 `json:"cost"`
Fee float64 `json:"fee"`
RequestedAt time.Time `json:"requestedAt"`
FulfilledAt time.Time `json:"fulfilledAt"`
}
// RequestTokenCreationData stores data of the token creation requested
type RequestTokenCreationData struct {
ID string `json:"id"`
Token string `json:"token"`
RequestedSize float64 `json:"requestedSize"`
Cost float64 `json:"cost"`
Pending bool `json:"pending"`
RequestedAt time.Time `json:"requestedAt"`
}
// LTRedemptionData stores data of the token redemption request
type LTRedemptionData struct {
ID int64 `json:"id"`
Token string `json:"token"`
Size float64 `json:"size"`
Pending bool `json:"pending"`
Price float64 `json:"price"`
Proceeds float64 `json:"proceeds"`
Fee float64 `json:"fee"`
RequestedAt time.Time `json:"requestedAt"`
FulfilledAt time.Time `json:"fulfilledAt"`
}
// LTRedemptionRequestData stores redemption request data for a leveraged token
type LTRedemptionRequestData struct {
ID string `json:"id"`
Token string `json:"token"`
Size float64 `json:"size"`
ProjectedProceeds float64 `json:"projectedProceeds"`
Pending bool `json:"pending"`
RequestedAt time.Time `json:"requestedAt"`
}
// OptionData stores options' data
type OptionData struct {
Underlying string `json:"underlying"`
OptionType string `json:"type"`
Strike float64 `json:"strike"`
Expiry time.Time `json:"expiry"`
}
// QuoteRequestData stores option's quote request data
type QuoteRequestData struct {
ID int64 `json:"id"`
Option OptionData `json:"option"`
Side string `json:"side"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
RequestExpiry string `json:"requestExpiry"`
Status string `json:"status"`
}
// QuoteData stores quote's data
type QuoteData struct {
Collateral float64 `json:"collateral"`
ID int64 `json:"id"`
Price float64 `json:"price"`
QuoteExpiry string `json:"quoteExpiry"`
Status string `json:"status"`
Time time.Time `json:"time"`
}
// PersonalQuotesData stores data of your quotes
type PersonalQuotesData struct {
ID int64 `json:"id"`
Option OptionData `json:"option"`
Side string `json:"side"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
RequestExpiry string `json:"requestExpiry"`
Status string `json:"status"`
HideLimitPrice bool `json:"hideLimitPrice"`
LimitPrice float64 `json:"limitPrice"`
Quotes []QuoteData `json:"quotes"`
}
// CreateQuoteRequestData stores quote data of the request sent
type CreateQuoteRequestData struct {
ID int64 `json:"id"`
Expiry time.Time `json:"expiry"`
Strike float64 `json:"strike"`
OptionType string `json:"type"`
Underlying string `json:"underlying"`
RequestExpiry string `json:"requestExpiry"`
Side string `json:"side"`
Size float64 `json:"size"`
Status string `json:"status"`
Time time.Time `json:"time"`
}
// CancelQuoteRequestData stores cancel quote request data
type CancelQuoteRequestData struct {
ID int64 `json:"id"`
Option OptionData `json:"option"`
RequestExpiry string `json:"requestExpiry"`
Side string `json:"side"`
Size float64 `json:"size"`
Status string `json:"status"`
Time time.Time `json:"time"`
}
// QuoteForQuoteData gets quote data for your quote
type QuoteForQuoteData struct {
Collateral float64 `json:"collateral"`
ID int64 `json:"id"`
Option OptionData `json:"option"`
Price float64 `json:"price"`
QuoteExpiry string `json:"quoteExpiry"`
QuoterSide string `json:"quoterSide"`
RequestID int64 `json:"requestID"`
RequestSide string `json:"requestSide"`
Size float64 `json:"size"`
Status string `json:"status"`
Time time.Time `json:"time"`
}
// AccountOptionsInfoData stores account's options' info data
type AccountOptionsInfoData struct {
USDBalance float64 `json:"usdBalance"`
LiquidationPrice float64 `json:"liquidationPrice"`
Liquidating bool `json:"liquidating"`
}
// OptionsPositionsData stores options positions' data
type OptionsPositionsData struct {
EntryPrice float64 `json:"entryPrice"`
NetSize float64 `json:"netSize"`
Option OptionData `json:"option"`
Side string `json:"side"`
Size float64 `json:"size"`
PessimisticValuation float64 `json:"pessimisticValuation,omitempty"`
PessimisticIndexPrice float64 `json:"pessimisticIndexPrice,omitempty"`
}
// OptionsTradesData stores options' trades' data
type OptionsTradesData struct {
ID int64 `json:"id"`
Option OptionData `json:"option"`
Price float64 `json:"price"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
}
// OptionFillsData stores option's fills data
type OptionFillsData struct {
Fee float64 `json:"fee"`
FeeRate float64 `json:"feeRate"`
ID int64 `json:"id"`
Liquidity string `json:"liquidity"`
Option OptionData `json:"option"`
Price float64 `json:"price"`
QuoteID int64 `json:"quoteId"`
Side string `json:"side"`
Size float64 `json:"size"`
Time string `json:"time"`
}
// AuthenticationData stores authentication variables required
type AuthenticationData struct {
Key string `json:"key"`
Sign string `json:"sign"`
Time int64 `json:"time"`
}
// Authenticate stores authentication variables required
type Authenticate struct {
Args AuthenticationData `json:"args"`
Operation string `json:"op"`
}
// WsResponseData stores basic ws response data on being subscribed to a channel successfully
type WsResponseData struct {
ResponseType string `json:"type"`
Channel string `json:"channel"`
Market string `json:"market"`
Data interface{} `json:"data"`
}
// WsTickerData stores ws ticker data
type WsTickerData struct {
Bid float64 `json:"bid"`
Ask float64 `json:"ask"`
BidSize float64 `json:"bidSize"`
AskSize float64 `json:"askSize"`
Last float64 `json:"last"`
Time float64 `json:"time"`
}
// WsTradeData stores ws trade data
type WsTradeData struct {
ID int64 `json:"id"`
Price float64 `json:"price"`
Size float64 `json:"size"`
Side string `json:"side"`
Liquidation bool `json:"liquidation"`
Time time.Time `json:"time"`
}
// WsOrderbookData stores ws orderbook data
type WsOrderbookData struct {
Action string `json:"action"`
Bids [][2]float64 `json:"bids"`
Asks [][2]float64 `json:"asks"`
Time float64 `json:"time"`
Checksum int64 `json:"checksum"`
}
// WsOrders stores ws orders' data
type WsOrders struct {
ID int64 `json:"id"`
ClientID string `json:"clientId"`
Market string `json:"market"`
OrderType string `json:"type"`
Side string `json:"side"`
Size float64 `json:"size"`
Price float64 `json:"price"`
ReduceOnly bool `json:"reduceOnly"`
IOC bool `json:"ioc"`
PostOnly bool `json:"postOnly"`
Status string `json:"status"`
FilledSize float64 `json:"filedSize"`
RemainingSize float64 `json:"remainingSize"`
AvgFillPrice float64 `json:"avgFillPrice"`
}
// WsFills stores websocket fills' data
type WsFills struct {
Fee float64 `json:"fee"`
FeeRate float64 `json:"feeRate"`
Future string `json:"future"`
ID int64 `json:"id"`
Liquidity string `json:"liquidity"`
Market string `json:"market"`
OrderID int64 `json:"int64"`
TradeID int64 `json:"tradeID"`
Price float64 `json:"price"`
Side string `json:"side"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
OrderType string `json:"orderType"`
}
// WsSub has the data used to subscribe to a channel
type WsSub struct {
Channel string `json:"channel,omitempty"`
Market string `json:"market,omitempty"`
Operation string `json:"op,omitempty"`
}
// WsTickerDataStore stores ws ticker data
type WsTickerDataStore struct {
Channel string `json:"channel"`
Market string `json:"market"`
MessageType string `json:"type"`
Ticker WsTickerData `json:"data"`
}
// WsOrderbookDataStore stores ws orderbook data
type WsOrderbookDataStore struct {
Channel string `json:"channel"`
Market string `json:"market"`
MessageType string `json:"type"`
OBData WsOrderbookData `json:"data"`
}
// WsTradeDataStore stores ws trades' data
type WsTradeDataStore struct {
Channel string `json:"channel"`
Market string `json:"market"`
MessageType string `json:"type"`
TradeData []WsTradeData `json:"data"`
}
// WsOrderDataStore stores ws orders' data
type WsOrderDataStore struct {
Channel string `json:"channel"`
MessageType string `json:"type"`
OrderData WsOrders `json:"data"`
}
// WsFillsDataStore stores ws fills' data
type WsFillsDataStore struct {
Channel string `json:"channel"`
MessageType string `json:"type"`
FillsData WsFills `json:"fills"`
}
// TimeInterval represents interval enum.
type TimeInterval string
// Vars related to time intervals
var (
TimeIntervalFifteenSeconds = TimeInterval("15")
TimeIntervalMinute = TimeInterval("60")
TimeIntervalFiveMinutes = TimeInterval("300")
TimeIntervalFifteenMinutes = TimeInterval("900")
TimeIntervalHour = TimeInterval("3600")
TimeIntervalFourHours = TimeInterval("14400")
TimeIntervalDay = TimeInterval("86400")
)
var errInvalidInterval = errors.New("invalid interval")
// OrderVars stores side, status and type for any order/trade
type OrderVars struct {
Side order.Side
Status order.Status
OrderType order.Type
Fee float64
}
// WsMarketsData stores websocket markets data
type WsMarketsData struct {
Data map[string]WsMarketsDataStorage `json:"data"`
}
// WsMarketsDataStorage stores websocket markets data
type WsMarketsDataStorage struct {
Name string `json:"name,omitempty"`
Enabled bool `json:"enabled,omitempty"`
PriceIncrement float64 `json:"priceIncrement,omitempty"`
SizeIncrement float64 `json:"sizeIncrement,omitempty"`
MarketType string `json:"marketType,omitempty"`
BaseCurrency string `json:"baseCurrency,omitempty"`
QuoteCurrency string `json:"quoteCurrency,omitempty"`
Underlying string `json:"underlying,omitempty"`
Restricted bool `json:"restricted,omitempty"`
Future WsMarketsFutureData `json:"future,omitempty"`
}
// WsMarketsFutureData stores websocket markets' future data
type WsMarketsFutureData struct {
Name string `json:"name,omitempty"`
Underlying string `json:"underlying,omitempty"`
Description string `json:"description,omitempty"`
MarketType string `json:"type,omitempty"`
Expiry time.Time `json:"expiry,omitempty"`
Perpetual bool `json:"perpetual,omitempty"`
Expired bool `json:"expired,omitempty"`
Enabled bool `json:"enabled,omitempty"`
PostOnly bool `json:"postOnly,omitempty"`
IMFFactor float64 `json:"imfFactor,omitempty"`
UnderlyingDescription string `json:"underlyingDescription,omitempty"`
ExpiryDescription string `json:"expiryDescription,omitempty"`
MoveStart string `json:"moveStart,omitempty"`
PositionLimitWeight float64 `json:"positionLimitWeight,omitempty"`
Group string `json:"group,omitempty"`
}
// WSMarkets stores websocket markets data
type WSMarkets struct {
Channel string `json:"channel"`
MessageType string `json:"type"`
Data WsMarketsData `json:"data"`
Action string `json:"action"`
}
// RequestQuoteData stores data on the requested quote
type RequestQuoteData struct {
QuoteID int64 `json:"quoteId"`
}
// QuoteStatusData stores data of quotes' status
type QuoteStatusData struct {
BaseCoin string `json:"baseCoin"`
Cost float64 `json:"cost"`
Expired bool `json:"expired"`
Filled bool `json:"filled"`
FromCoin string `json:"fromCoin"`
ID int64 `json:"id"`
Price float64 `json:"price"`
Proceeds float64 `json:"proceeds"`
QuoteCoin string `json:"quoteCoin"`
Side string `json:"side"`
ToCoin string `json:"toCoin"`
}
// AcceptQuote stores data of accepted quote
type AcceptQuote struct {
Success bool `json:"success"`
}

View File

@@ -0,0 +1,511 @@
package ftx
import (
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
ftxWSURL = "wss://ftx.com/ws/"
ftxWebsocketTimer = 13 * time.Second
wsTicker = "ticker"
wsTrades = "trades"
wsOrderbook = "orderbook"
wsMarkets = "markets"
wsFills = "fills"
wsOrders = "orders"
wsUpdate = "update"
wsPartial = "partial"
subscribe = "subscribe"
unsubscribe = "unsubscribe"
)
var obSuccess = make(map[currency.Pair]bool)
// WsConnect connects to a websocket feed
func (f *FTX) WsConnect() error {
if !f.Websocket.IsEnabled() || !f.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := f.WebsocketConn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
f.WebsocketConn.SetupPingHandler(wshandler.WebsocketPingHandler{
MessageType: websocket.PingMessage,
Delay: ftxWebsocketTimer,
})
if f.Verbose {
log.Debugf(log.ExchangeSys, "%s Connected to Websocket.\n", f.Name)
}
f.GenerateDefaultSubscriptions()
go f.wsReadData()
if f.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
err := f.WsAuth()
if err != nil {
f.Websocket.DataHandler <- err
f.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
f.GenerateAuthSubscriptions()
}
return nil
}
// WsAuth sends an authentication message to receive auth data
func (f *FTX) WsAuth() error {
intNonce := time.Now().UnixNano() / 1000000
strNonce := strconv.FormatInt(intNonce, 10)
hmac := crypto.GetHMAC(
crypto.HashSHA256,
[]byte(strNonce+"websocket_login"),
[]byte(f.API.Credentials.Secret),
)
sign := crypto.HexEncodeToString(hmac)
req := Authenticate{Operation: "login",
Args: AuthenticationData{
Key: f.API.Credentials.Key,
Sign: sign,
Time: intNonce,
},
}
return f.WebsocketConn.SendJSONMessage(req)
}
// Subscribe sends a websocket message to receive data from the channel
func (f *FTX) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var sub WsSub
switch channelToSubscribe.Channel {
case wsFills, wsOrders, wsMarkets:
sub.Operation = subscribe
sub.Channel = channelToSubscribe.Channel
default:
a, err := f.GetPairAssetType(channelToSubscribe.Currency)
if err != nil {
return err
}
sub.Operation = subscribe
sub.Channel = channelToSubscribe.Channel
sub.Market = f.FormatExchangeCurrency(channelToSubscribe.Currency, a).String()
}
return f.WebsocketConn.SendJSONMessage(sub)
}
// GenerateDefaultSubscriptions generates default subscription
func (f *FTX) GenerateDefaultSubscriptions() {
var subscriptions []wshandler.WebsocketChannelSubscription
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: wsMarkets,
})
var channels = []string{wsTicker, wsTrades, wsOrderbook}
for a := range f.CurrencyPairs.AssetTypes {
pairs := f.GetEnabledPairs(f.CurrencyPairs.AssetTypes[a])
for z := range pairs {
newPair := currency.NewPairWithDelimiter(pairs[z].Base.String(), pairs[z].Quote.String(), "-")
for x := range channels {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channels[x],
Currency: newPair,
})
}
}
}
f.Websocket.SubscribeToChannels(subscriptions)
}
// GenerateAuthSubscriptions generates default subscription
func (f *FTX) GenerateAuthSubscriptions() {
var subscriptions []wshandler.WebsocketChannelSubscription
var channels = []string{wsOrders, wsFills}
for x := range channels {
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channels[x],
})
}
f.Websocket.SubscribeToChannels(subscriptions)
}
// wsReadData gets and passes on websocket messages for processing
func (f *FTX) wsReadData() {
f.Websocket.Wg.Add(1)
defer f.Websocket.Wg.Done()
for {
select {
case <-f.Websocket.ShutdownC:
return
default:
resp, err := f.WebsocketConn.ReadMessage()
if err != nil {
f.Websocket.ReadMessageErrors <- err
return
}
f.Websocket.TrafficAlert <- struct{}{}
err = f.wsHandleData(resp.Raw)
if err != nil {
f.Websocket.DataHandler <- err
}
}
}
}
func timestampFromFloat64(ts float64) time.Time {
secs := int64(ts)
nsecs := int64((ts - float64(secs)) * 1e9)
return time.Unix(secs, nsecs).UTC()
}
func (f *FTX) wsHandleData(respRaw []byte) error {
var result map[string]interface{}
err := json.Unmarshal(respRaw, &result)
if err != nil {
return err
}
switch result["type"] {
case wsUpdate:
var p currency.Pair
var a asset.Item
market, ok := result["market"]
if ok {
p = currency.NewPairFromString(market.(string))
a, err = f.GetPairAssetType(p)
if err != nil {
return err
}
}
switch result["channel"] {
case wsTicker:
var resultData WsTickerDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
f.Websocket.DataHandler <- &ticker.Price{
ExchangeName: f.Name,
Bid: resultData.Ticker.Bid,
Ask: resultData.Ticker.Ask,
Last: resultData.Ticker.Last,
LastUpdated: timestampFromFloat64(resultData.Ticker.Time),
Pair: p,
AssetType: a,
}
case wsOrderbook:
var resultData WsOrderbookDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
if len(resultData.OBData.Asks) == 0 && len(resultData.OBData.Bids) == 0 {
return nil
}
err = f.WsProcessUpdateOB(&resultData.OBData, p, a)
if err != nil {
f.wsResubToOB(p)
return err
}
case wsTrades:
var resultData WsTradeDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
for z := range resultData.TradeData {
var oSide order.Side
oSide, err = order.StringToOrderSide(resultData.TradeData[z].Side)
if err != nil {
f.Websocket.DataHandler <- order.ClassificationError{
Exchange: f.Name,
Err: err,
}
}
f.Websocket.DataHandler <- wshandler.TradeData{
Timestamp: resultData.TradeData[z].Time,
CurrencyPair: p,
AssetType: a,
Exchange: f.Name,
Price: resultData.TradeData[z].Price,
Amount: resultData.TradeData[z].Size,
Side: oSide,
}
}
case wsOrders:
var resultData WsOrderDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
pair := currency.NewPairFromString(resultData.OrderData.Market)
var assetType asset.Item
assetType, err = f.GetPairAssetType(pair)
if err != nil {
return err
}
var oSide order.Side
oSide, err = order.StringToOrderSide(resultData.OrderData.Side)
if err != nil {
f.Websocket.DataHandler <- order.ClassificationError{
Exchange: f.Name,
Err: err,
}
}
var resp order.Detail
resp.Side = oSide
resp.Amount = resultData.OrderData.Size
resp.AssetType = assetType
resp.ClientOrderID = resultData.OrderData.ClientID
resp.Exchange = f.Name
resp.ExecutedAmount = resultData.OrderData.FilledSize
resp.ID = strconv.FormatInt(resultData.OrderData.ID, 10)
resp.Pair = pair
resp.RemainingAmount = resultData.OrderData.Size - resultData.OrderData.FilledSize
var orderVars OrderVars
orderVars, err = f.compatibleOrderVars(resultData.OrderData.Side,
resultData.OrderData.Status,
resultData.OrderData.OrderType,
resultData.OrderData.FilledSize,
resultData.OrderData.Size,
resultData.OrderData.AvgFillPrice)
if err != nil {
return err
}
resp.Status = orderVars.Status
resp.Side = orderVars.Side
resp.Type = orderVars.OrderType
resp.Fee = orderVars.Fee
f.Websocket.DataHandler <- &resp
case wsFills:
var resultData WsFillsDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
f.Websocket.DataHandler <- resultData.FillsData
default:
f.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: f.Name + wshandler.UnhandledMessage + string(respRaw)}
}
case wsPartial:
switch result["channel"] {
case "orderbook":
var p currency.Pair
var a asset.Item
market, ok := result["market"]
if ok {
p = currency.NewPairFromString(market.(string))
a, err = f.GetPairAssetType(p)
if err != nil {
return err
}
}
var resultData WsOrderbookDataStore
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
err = f.WsProcessPartialOB(&resultData.OBData, p, a)
if err != nil {
f.wsResubToOB(p)
return err
}
// reset obchecksum failure blockage for pair
delete(obSuccess, p)
case wsMarkets:
var resultData WSMarkets
err = json.Unmarshal(respRaw, &resultData)
if err != nil {
return err
}
f.Websocket.DataHandler <- resultData.Data
}
case "error":
f.Websocket.DataHandler <- wshandler.UnhandledMessageWarning{Message: f.Name + wshandler.UnhandledMessage + string(respRaw)}
}
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (f *FTX) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var unSub WsSub
a, err := f.GetPairAssetType(channelToSubscribe.Currency)
if err != nil {
return err
}
unSub.Operation = unsubscribe
unSub.Channel = channelToSubscribe.Channel
unSub.Market = f.FormatExchangeCurrency(channelToSubscribe.Currency, a).String()
return f.WebsocketConn.SendJSONMessage(unSub)
}
// WsProcessUpdateOB processes an update on the orderbook
func (f *FTX) WsProcessUpdateOB(data *WsOrderbookData, p currency.Pair, a asset.Item) error {
update := wsorderbook.WebsocketOrderbookUpdate{
Asset: a,
Pair: p,
UpdateTime: timestampFromFloat64(data.Time),
}
var err error
for x := range data.Bids {
update.Bids = append(update.Bids, orderbook.Item{
Price: data.Bids[x][0],
Amount: data.Bids[x][1],
})
}
for x := range data.Asks {
update.Asks = append(update.Asks, orderbook.Item{
Price: data.Asks[x][0],
Amount: data.Asks[x][1],
})
}
err = f.Websocket.Orderbook.Update(&update)
if err != nil {
return err
}
updatedOb := f.Websocket.Orderbook.GetOrderbook(p, a)
checksum := f.CalcUpdateOBChecksum(updatedOb)
if checksum != data.Checksum {
log.Warnf(log.ExchangeSys, "%s checksum failure for item %s",
f.Name,
p)
return errors.New("checksum failed")
}
f.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: f.Name,
Asset: a,
Pair: p,
}
return nil
}
func (f *FTX) wsResubToOB(p currency.Pair) {
if ok := obSuccess[p]; ok {
return
}
obSuccess[p] = true
channelToResubscribe := wshandler.WebsocketChannelSubscription{
Channel: wsOrderbook,
Currency: p,
}
f.Websocket.ResubscribeToChannel(channelToResubscribe)
}
// WsProcessPartialOB creates an OB from websocket data
func (f *FTX) WsProcessPartialOB(data *WsOrderbookData, p currency.Pair, a asset.Item) error {
signedChecksum := f.CalcPartialOBChecksum(data)
if signedChecksum != data.Checksum {
return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid",
f.Name,
a,
p)
}
var bids, asks []orderbook.Item
for x := range data.Bids {
bids = append(bids, orderbook.Item{
Price: data.Bids[x][0],
Amount: data.Bids[x][1],
})
}
for x := range data.Asks {
asks = append(asks, orderbook.Item{
Price: data.Asks[x][0],
Amount: data.Asks[x][1],
})
}
newOrderBook := orderbook.Base{
Asks: asks,
Bids: bids,
AssetType: a,
LastUpdated: timestampFromFloat64(data.Time),
Pair: p,
ExchangeName: f.Name,
}
if err := f.Websocket.Orderbook.LoadSnapshot(&newOrderBook); err != nil {
return err
}
f.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: f.Name,
Asset: a,
Pair: p,
}
return nil
}
// CalcPartialOBChecksum calculates checksum of partial OB data received from WS
func (f *FTX) CalcPartialOBChecksum(data *WsOrderbookData) int64 {
var checksum strings.Builder
var price, amount string
for i := 0; i < 100; i++ {
if len(data.Bids)-1 >= i {
price = checksumParseNumber(data.Bids[i][0])
amount = checksumParseNumber(data.Bids[i][1])
checksum.WriteString(price + ":" + amount + ":")
}
if len(data.Asks)-1 >= i {
price = checksumParseNumber(data.Asks[i][0])
amount = checksumParseNumber(data.Asks[i][1])
checksum.WriteString(price + ":" + amount + ":")
}
}
checksumStr := strings.TrimSuffix(checksum.String(), ":")
return int64(crc32.ChecksumIEEE([]byte(checksumStr)))
}
// CalcUpdateOBChecksum calculates checksum of update OB data received from WS
func (f *FTX) CalcUpdateOBChecksum(data *orderbook.Base) int64 {
var checksum strings.Builder
var price, amount string
for i := 0; i < 100; i++ {
if len(data.Bids)-1 >= i {
price = checksumParseNumber(data.Bids[i].Price)
amount = checksumParseNumber(data.Bids[i].Amount)
checksum.WriteString(price + ":" + amount + ":")
}
if len(data.Asks)-1 >= i {
price = checksumParseNumber(data.Asks[i].Price)
amount = checksumParseNumber(data.Asks[i].Amount)
checksum.WriteString(price + ":" + amount + ":")
}
}
checksumStr := strings.TrimSuffix(checksum.String(), ":")
return int64(crc32.ChecksumIEEE([]byte(checksumStr)))
}
func checksumParseNumber(num float64) string {
modifier := byte('f')
if num < 0.0001 {
modifier = 'e'
}
r := strconv.FormatFloat(num, modifier, -1, 64)
if strings.IndexByte(r, '.') == -1 && modifier != 'e' {
r += ".0"
}
return r
}

View File

@@ -0,0 +1,873 @@
package ftx
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// GetDefaultConfig returns a default exchange config
func (f *FTX) GetDefaultConfig() (*config.ExchangeConfig, error) {
f.SetDefaults()
exchCfg := new(config.ExchangeConfig)
exchCfg.Name = f.Name
exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout
exchCfg.BaseCurrencies = f.BaseCurrencies
err := f.SetupDefaults(exchCfg)
if err != nil {
return nil, err
}
if f.Features.Supports.RESTCapabilities.AutoPairUpdates {
err = f.UpdateTradablePairs(true)
if err != nil {
return nil, err
}
}
return exchCfg, nil
}
// SetDefaults sets the basic defaults for FTX
func (f *FTX) SetDefaults() {
f.Name = "FTX"
f.Enabled = true
f.Verbose = true
f.API.CredentialsValidator.RequiresKey = true
f.API.CredentialsValidator.RequiresSecret = true
f.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
asset.Futures,
},
}
spot := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "/",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "/",
},
}
futures := currency.PairStore{
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "-",
},
}
f.CurrencyPairs.Store(asset.Spot, spot)
f.CurrencyPairs.Store(asset.Futures, futures)
f.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
RESTCapabilities: protocol.Features{
TickerFetching: true,
KlineFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
GetOrder: true,
GetOrders: true,
CancelOrders: true,
CancelOrder: true,
SubmitOrder: true,
TradeFee: true,
FiatDepositFee: true,
FiatWithdrawalFee: true,
CryptoWithdrawalFee: true,
},
WebsocketCapabilities: protocol.Features{
OrderbookFetching: true,
TradeFetching: true,
Subscribe: true,
Unsubscribe: true,
GetOrders: true,
GetOrder: true,
},
WithdrawPermissions: exchange.NoAPIWithdrawalMethods,
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
},
}
f.Requester = request.New(f.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(request.NewBasicRateLimit(ratePeriod, rateLimit)))
f.API.Endpoints.URLDefault = ftxAPIURL
f.API.Endpoints.URL = f.API.Endpoints.URLDefault
f.Websocket = wshandler.New()
f.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
f.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
f.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup takes in the supplied exchange configuration details and sets params
func (f *FTX) Setup(exch *config.ExchangeConfig) error {
if !exch.Enabled {
f.SetEnabled(false)
return nil
}
err := f.SetupDefaults(exch)
if err != nil {
return err
}
err = f.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: ftxWSURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: f.WsConnect,
Subscriber: f.Subscribe,
UnSubscriber: f.Unsubscribe,
Features: &f.Features.Supports.WebsocketCapabilities,
})
if err != nil {
return err
}
f.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: f.Name,
URL: f.Websocket.GetWebsocketURL(),
ProxyURL: f.Websocket.GetProxyAddress(),
Verbose: f.Verbose,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
f.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
false,
false,
false,
false,
exch.Name)
return nil
}
// Start starts the FTX go routine
func (f *FTX) Start(wg *sync.WaitGroup) {
wg.Add(1)
go func() {
f.Run()
wg.Done()
}()
}
// Run implements the FTX wrapper
func (f *FTX) Run() {
if f.Verbose {
log.Debugf(log.ExchangeSys,
"%s Websocket: %s.",
f.Name,
common.IsEnabled(f.Websocket.IsEnabled()))
f.PrintEnabledPairs()
}
if !f.GetEnabledFeatures().AutoPairUpdates {
return
}
err := f.UpdateTradablePairs(false)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s",
f.Name,
err)
}
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (f *FTX) FetchTradablePairs(a asset.Item) ([]string, error) {
if !f.SupportsAsset(a) {
return nil, fmt.Errorf("asset type of %s is not supported by %s", a, f.Name)
}
markets, err := f.GetMarkets()
if err != nil {
return nil, err
}
var pairs []string
switch a {
case asset.Spot:
for x := range markets {
if markets[x].MarketType == spotString {
pairs = append(pairs, markets[x].Name)
}
}
case asset.Futures:
for x := range markets {
if markets[x].MarketType == futuresString {
pairs = append(pairs, markets[x].Name)
}
}
}
return pairs, nil
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (f *FTX) UpdateTradablePairs(forceUpdate bool) error {
for x := range f.CurrencyPairs.AssetTypes {
pairs, err := f.FetchTradablePairs(f.CurrencyPairs.AssetTypes[x])
if err != nil {
return err
}
err = f.UpdatePairs(currency.NewPairsFromStrings(pairs),
f.CurrencyPairs.AssetTypes[x], false, forceUpdate)
if err != nil {
return err
}
}
return nil
}
// UpdateTicker updates and returns the ticker for a currency pair
func (f *FTX) UpdateTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
allPairs := f.GetEnabledPairs(assetType)
if !allPairs.Contains(p, true) {
allPairs = append(allPairs, p)
}
markets, err := f.GetMarkets()
if err != nil {
return nil, err
}
for a := range allPairs {
for x := range markets {
if markets[x].Name != f.FormatExchangeCurrency(allPairs[a], assetType).String() {
continue
}
var resp ticker.Price
resp.Pair = currency.NewPairFromString(markets[x].Name)
resp.Last = markets[x].Last
resp.Bid = markets[x].Bid
resp.Ask = markets[x].Ask
resp.LastUpdated = time.Now()
err = ticker.ProcessTicker(f.Name, &resp, assetType)
if err != nil {
return nil, err
}
}
}
return ticker.GetTicker(f.Name, p, assetType)
}
// FetchTicker returns the ticker for a currency pair
func (f *FTX) FetchTicker(p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerNew, err := ticker.GetTicker(f.Name, p, assetType)
if err != nil {
return f.UpdateTicker(p, assetType)
}
return tickerNew, nil
}
// FetchOrderbook returns orderbook base on the currency pair
func (f *FTX) FetchOrderbook(currency currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
ob, err := orderbook.Get(f.Name, currency, assetType)
if err != nil {
return f.UpdateOrderbook(currency, assetType)
}
return ob, nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (f *FTX) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
orderBook := new(orderbook.Base)
tempResp, err := f.GetOrderbook(f.FormatExchangeCurrency(p, assetType).String(), 0)
if err != nil {
return orderBook, err
}
for x := range tempResp.Bids {
orderBook.Bids = append(orderBook.Bids, orderbook.Item{
Amount: tempResp.Bids[x].Size,
Price: tempResp.Bids[x].Price})
}
for y := range tempResp.Asks {
orderBook.Asks = append(orderBook.Asks, orderbook.Item{
Amount: tempResp.Asks[y].Size,
Price: tempResp.Asks[y].Price})
}
orderBook.Pair = p
orderBook.ExchangeName = f.Name
orderBook.AssetType = assetType
err = orderBook.Process()
if err != nil {
return orderBook, err
}
return orderbook.Get(f.Name, p, assetType)
}
// UpdateAccountInfo retrieves balances for all enabled currencies
func (f *FTX) UpdateAccountInfo() (account.Holdings, error) {
var resp account.Holdings
data, err := f.GetBalances()
if err != nil {
return resp, err
}
var acc account.SubAccount
for i := range data {
c := currency.NewCode(data[i].Coin)
hold := data[i].Total - data[i].Free
total := data[i].Total
acc.Currencies = append(acc.Currencies,
account.Balance{CurrencyName: c,
TotalValue: total,
Hold: hold})
}
resp.Accounts = append(resp.Accounts, acc)
resp.Exchange = f.Name
err = account.Process(&resp)
if err != nil {
return account.Holdings{}, err
}
return resp, nil
}
// FetchAccountInfo retrieves balances for all enabled currencies
func (f *FTX) FetchAccountInfo() (account.Holdings, error) {
acc, err := account.GetHoldings(f.Name)
if err != nil {
return f.UpdateAccountInfo()
}
return acc, nil
}
// GetFundingHistory returns funding history, deposits and
// withdrawals
func (f *FTX) GetFundingHistory() ([]exchange.FundHistory, error) {
var resp []exchange.FundHistory
depositData, err := f.FetchDepositHistory()
if err != nil {
return resp, err
}
for x := range depositData {
var tempData exchange.FundHistory
tempData.Fee = depositData[x].Fee
tempData.Timestamp = depositData[x].Time
tempData.ExchangeName = f.Name
tempData.CryptoTxID = depositData[x].TxID
tempData.Status = depositData[x].Status
tempData.Amount = depositData[x].Size
tempData.Currency = depositData[x].Coin
tempData.TransferID = strconv.FormatInt(depositData[x].ID, 10)
resp = append(resp, tempData)
}
withdrawalData, err := f.FetchWithdrawalHistory()
if err != nil {
return resp, err
}
for y := range withdrawalData {
var tempData exchange.FundHistory
tempData.Fee = depositData[y].Fee
tempData.Timestamp = depositData[y].Time
tempData.ExchangeName = f.Name
tempData.CryptoTxID = depositData[y].TxID
tempData.Status = depositData[y].Status
tempData.Amount = depositData[y].Size
tempData.Currency = depositData[y].Coin
tempData.TransferID = strconv.FormatInt(depositData[y].ID, 10)
resp = append(resp, tempData)
}
return resp, nil
}
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (f *FTX) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
marketName := f.FormatExchangeCurrency(p, assetType).String()
var resp []exchange.TradeHistory
trades, err := f.GetTrades(marketName, time.Unix(timestampStart.Unix(), 0), time.Unix(timestampEnd.Unix(), 0), 100)
if err != nil {
return nil, err
}
for {
var tempResp exchange.TradeHistory
if len(trades) > 0 {
tempResp.Amount = trades[0].Size
tempResp.Price = trades[0].Price
tempResp.Exchange = f.Name
tempResp.Timestamp = trades[0].Time
tempResp.TID = strconv.FormatInt(trades[0].ID, 10)
tempResp.Side = trades[0].Side
resp = append(resp, tempResp)
}
for y := 1; y < len(trades); y++ {
tempResp.Amount = trades[y].Size
tempResp.Price = trades[y].Price
tempResp.Exchange = f.Name
tempResp.Timestamp = trades[y].Time
tempResp.TID = strconv.FormatInt(trades[y].ID, 10)
tempResp.Side = trades[y].Side
resp = append(resp, tempResp)
}
if len(trades) != 100 {
break
}
trades, err = f.GetTrades(marketName, time.Unix(timestampStart.Unix(), 0), time.Unix(trades[len(trades)-1].Time.Unix(), 0), 100)
if err != nil {
return resp, err
}
}
return resp, nil
}
// SubmitOrder submits a new order
func (f *FTX) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) {
var resp order.SubmitResponse
if err := s.Validate(); err != nil {
return resp, err
}
if s.Side == order.Sell {
s.Side = order.Ask
}
if s.Side == order.Buy {
s.Side = order.Bid
}
tempResp, err := f.Order(f.FormatExchangeCurrency(s.Pair, s.AssetType).String(),
s.Side.String(),
s.Type.String(),
"",
"",
"",
s.ClientOrderID,
s.Price,
s.Amount)
if err != nil {
return resp, err
}
resp.IsOrderPlaced = true
resp.OrderID = strconv.FormatInt(tempResp.ID, 10)
return resp, nil
}
// ModifyOrder will allow of changing orderbook placement and limit to
// market conversion
func (f *FTX) ModifyOrder(action *order.Modify) (string, error) {
if action.TriggerPrice != 0 {
a, err := f.ModifyTriggerOrder(action.ID,
action.Type.String(),
action.Amount,
action.TriggerPrice,
action.Price,
0)
if err != nil {
return "", err
}
return strconv.FormatInt(a.ID, 10), err
}
var o OrderData
var err error
switch action.ID {
case "":
o, err = f.ModifyOrderByClientID(action.ClientOrderID, action.ClientOrderID, action.Price, action.Amount)
if err != nil {
return "", err
}
default:
o, err = f.ModifyPlacedOrder(action.ID, action.ClientOrderID, action.Price, action.Amount)
if err != nil {
return "", err
}
}
return strconv.FormatInt(o.ID, 10), err
}
// CancelOrder cancels an order by its corresponding ID number
func (f *FTX) CancelOrder(order *order.Cancel) error {
_, err := f.DeleteOrder(order.ID)
return err
}
// CancelAllOrders cancels all orders associated with a currency pair
func (f *FTX) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
var resp order.CancelAllResponse
tempMap := make(map[string]string)
orders, err := f.GetOpenOrders(f.FormatExchangeCurrency(orderCancellation.Pair, orderCancellation.AssetType).String())
if err != nil {
return resp, err
}
for x := range orders {
_, err := f.DeleteOrder(strconv.FormatInt(orders[x].ID, 10))
if err != nil {
tempMap[strconv.FormatInt(orders[x].ID, 10)] = "Cancellation Failed"
continue
}
tempMap[strconv.FormatInt(orders[x].ID, 10)] = "Success"
}
resp.Status = tempMap
return resp, nil
}
// GetCompatible gets compatible variables for order vars
func (s *OrderData) GetCompatible(f *FTX) (OrderVars, error) {
var resp OrderVars
switch s.Side {
case order.Buy.Lower():
resp.Side = order.Buy
case order.Sell.Lower():
resp.Side = order.Sell
}
switch s.Status {
case strings.ToLower(order.New.String()):
resp.Status = order.New
case strings.ToLower(order.Open.String()):
resp.Status = order.Open
case closedStatus:
if s.FilledSize != 0 && s.FilledSize != s.Size {
resp.Status = order.PartiallyCancelled
}
if s.FilledSize == 0 {
resp.Status = order.Cancelled
}
if s.FilledSize == s.Size {
resp.Status = order.Filled
}
}
var feeBuilder exchange.FeeBuilder
feeBuilder.PurchasePrice = s.AvgFillPrice
feeBuilder.Amount = s.Size
resp.OrderType = order.Market
if strings.EqualFold(s.OrderType, order.Limit.String()) {
resp.OrderType = order.Limit
feeBuilder.IsMaker = true
}
fee, err := f.GetFee(&feeBuilder)
if err != nil {
return resp, err
}
resp.Fee = fee
return resp, nil
}
// GetOrderInfo returns information on a current open order
func (f *FTX) GetOrderInfo(orderID string) (order.Detail, error) {
var resp order.Detail
orderData, err := f.GetOrderStatus(orderID)
if err != nil {
return resp, err
}
p := currency.NewPairFromString(orderData.Market)
assetType, err := f.GetPairAssetType(p)
if err != nil {
return resp, err
}
resp.ID = strconv.FormatInt(orderData.ID, 10)
resp.Amount = orderData.Size
resp.ClientOrderID = orderData.ClientID
resp.Date = orderData.CreatedAt
resp.Exchange = f.Name
resp.ExecutedAmount = orderData.Size - orderData.RemainingSize
resp.Pair = p
resp.AssetType = assetType
resp.Price = orderData.Price
resp.RemainingAmount = orderData.RemainingSize
orderVars, err := orderData.GetCompatible(f)
if err != nil {
return resp, err
}
resp.Status = orderVars.Status
resp.Side = orderVars.Side
resp.Type = orderVars.OrderType
resp.Fee = orderVars.Fee
return resp, nil
}
// GetDepositAddress returns a deposit address for a specified currency
func (f *FTX) GetDepositAddress(cryptocurrency currency.Code, _ string) (string, error) {
a, err := f.FetchDepositAddress(cryptocurrency.String())
if err != nil {
return "", err
}
return a.Address, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func (f *FTX) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
var address, addressTag string
if withdrawRequest.Crypto != nil {
address = withdrawRequest.Crypto.Address
addressTag = withdrawRequest.Crypto.AddressTag
}
resp := withdraw.ExchangeResponse{}
a, err := f.Withdraw(withdrawRequest.Currency.String(),
address,
addressTag,
withdrawRequest.TradePassword,
strconv.FormatInt(withdrawRequest.OneTimePassword, 10),
withdrawRequest.Amount)
if err != nil {
return &resp, err
}
resp.ID = strconv.FormatInt(a.ID, 10)
resp.Status = a.Status
return &resp, nil
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is
// submitted
func (f *FTX) WithdrawFiatFunds(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a
// withdrawal is submitted
func (f *FTX) WithdrawFiatFundsToInternationalBank(_ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (f *FTX) GetWebsocket() (*wshandler.Websocket, error) {
return f.Websocket, nil
}
// GetActiveOrders retrieves any orders that are active/open
func (f *FTX) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) {
var resp []order.Detail
for x := range getOrdersRequest.Pairs {
assetType, err := f.GetPairAssetType(getOrdersRequest.Pairs[x])
if err != nil {
return resp, err
}
var tempResp order.Detail
orderData, err := f.GetOpenOrders(f.FormatExchangeCurrency(getOrdersRequest.Pairs[x], assetType).String())
if err != nil {
return resp, err
}
for y := range orderData {
tempResp.ID = strconv.FormatInt(orderData[y].ID, 10)
tempResp.Amount = orderData[y].Size
tempResp.AssetType = assetType
tempResp.ClientOrderID = orderData[y].ClientID
tempResp.Date = orderData[y].CreatedAt
tempResp.Exchange = f.Name
tempResp.ExecutedAmount = orderData[y].Size - orderData[y].RemainingSize
tempResp.Pair = currency.NewPairFromString(orderData[y].Market)
tempResp.Price = orderData[y].Price
tempResp.RemainingAmount = orderData[y].RemainingSize
var orderVars OrderVars
orderVars, err = f.compatibleOrderVars(orderData[y].Side,
orderData[y].Status,
orderData[y].OrderType,
orderData[y].FilledSize,
orderData[y].Size,
orderData[y].AvgFillPrice)
if err != nil {
return resp, err
}
tempResp.Status = orderVars.Status
tempResp.Side = orderVars.Side
tempResp.Type = orderVars.OrderType
tempResp.Fee = orderVars.Fee
resp = append(resp, tempResp)
}
triggerOrderData, err := f.GetOpenTriggerOrders(f.FormatExchangeCurrency(getOrdersRequest.Pairs[x], assetType).String(), getOrdersRequest.Type.String())
if err != nil {
return resp, err
}
for z := range triggerOrderData {
tempResp.ID = strconv.FormatInt(triggerOrderData[z].ID, 10)
tempResp.Amount = triggerOrderData[z].Size
tempResp.AssetType = assetType
tempResp.Date = triggerOrderData[z].CreatedAt
tempResp.Exchange = f.Name
tempResp.ExecutedAmount = triggerOrderData[z].FilledSize
tempResp.Pair = currency.NewPairFromString(triggerOrderData[z].Market)
tempResp.Price = triggerOrderData[z].AvgFillPrice
tempResp.RemainingAmount = triggerOrderData[z].Size - triggerOrderData[z].FilledSize
tempResp.TriggerPrice = triggerOrderData[z].TriggerPrice
orderVars, err := f.compatibleOrderVars(triggerOrderData[z].Side,
triggerOrderData[z].Status,
triggerOrderData[z].OrderType,
triggerOrderData[z].FilledSize,
triggerOrderData[z].Size,
triggerOrderData[z].AvgFillPrice)
if err != nil {
return resp, err
}
tempResp.Status = orderVars.Status
tempResp.Side = orderVars.Side
tempResp.Type = orderVars.OrderType
tempResp.Fee = orderVars.Fee
resp = append(resp, tempResp)
}
}
return resp, nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (f *FTX) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) {
var resp []order.Detail
for x := range getOrdersRequest.Pairs {
var tempResp order.Detail
assetType, err := f.GetPairAssetType(getOrdersRequest.Pairs[x])
if err != nil {
return resp, err
}
orderData, err := f.FetchOrderHistory(f.FormatExchangeCurrency(getOrdersRequest.Pairs[x], assetType).String(),
getOrdersRequest.StartTicks, getOrdersRequest.EndTicks, "")
if err != nil {
return resp, err
}
for y := range orderData {
tempResp.ID = strconv.FormatInt(orderData[y].ID, 10)
tempResp.Amount = orderData[y].Size
tempResp.AssetType = assetType
tempResp.ClientOrderID = orderData[y].ClientID
tempResp.Date = orderData[y].CreatedAt
tempResp.Exchange = f.Name
tempResp.ExecutedAmount = orderData[y].Size - orderData[y].RemainingSize
tempResp.Pair = currency.NewPairFromString(orderData[y].Market)
tempResp.Price = orderData[y].Price
tempResp.RemainingAmount = orderData[y].RemainingSize
var orderVars OrderVars
orderVars, err = f.compatibleOrderVars(orderData[y].Side,
orderData[y].Status,
orderData[y].OrderType,
orderData[y].FilledSize,
orderData[y].Size,
orderData[y].AvgFillPrice)
if err != nil {
return resp, err
}
tempResp.Status = orderVars.Status
tempResp.Side = orderVars.Side
tempResp.Type = orderVars.OrderType
tempResp.Fee = orderVars.Fee
resp = append(resp, tempResp)
}
triggerOrderData, err := f.GetTriggerOrderHistory(f.FormatExchangeCurrency(getOrdersRequest.Pairs[x], assetType).String(),
getOrdersRequest.StartTicks, getOrdersRequest.EndTicks, strings.ToLower(getOrdersRequest.Side.String()), strings.ToLower(getOrdersRequest.Type.String()), "")
if err != nil {
return resp, err
}
for z := range triggerOrderData {
tempResp.ID = strconv.FormatInt(triggerOrderData[z].ID, 10)
tempResp.Amount = triggerOrderData[z].Size
tempResp.AssetType = assetType
tempResp.Date = triggerOrderData[z].CreatedAt
tempResp.Exchange = f.Name
tempResp.ExecutedAmount = triggerOrderData[z].FilledSize
tempResp.Pair = currency.NewPairFromString(triggerOrderData[z].Market)
tempResp.Price = triggerOrderData[z].AvgFillPrice
tempResp.RemainingAmount = triggerOrderData[z].Size - triggerOrderData[z].FilledSize
tempResp.TriggerPrice = triggerOrderData[z].TriggerPrice
orderVars, err := f.compatibleOrderVars(triggerOrderData[z].Side,
triggerOrderData[z].Status,
triggerOrderData[z].OrderType,
triggerOrderData[z].FilledSize,
triggerOrderData[z].Size,
triggerOrderData[z].AvgFillPrice)
if err != nil {
return resp, err
}
tempResp.Status = orderVars.Status
tempResp.Side = orderVars.Side
tempResp.Type = orderVars.OrderType
tempResp.Fee = orderVars.Fee
resp = append(resp, tempResp)
}
}
return resp, nil
}
// GetFeeByType returns an estimate of fee based on the type of transaction
func (f *FTX) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
return f.GetFee(feeBuilder)
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (f *FTX) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
f.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (f *FTX) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
f.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (f *FTX) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return f.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (f *FTX) AuthenticateWebsocket() error {
return f.WsAuth()
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (f *FTX) ValidateCredentials() error {
_, err := f.UpdateAccountInfo()
return f.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (f *FTX) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
intervalToString, err := parseInterval(interval)
if err != nil {
return kline.Item{}, err
}
var resp kline.Item
ohlcData, err := f.GetHistoricalData(f.FormatExchangeCurrency(pair, a).String(),
string(intervalToString), "", start, end)
if err != nil {
return resp, err
}
resp.Exchange = f.Name
resp.Asset = a
resp.Pair = pair
for x := range ohlcData {
var tempData kline.Candle
tempData.Open = ohlcData[x].Open
tempData.High = ohlcData[x].High
tempData.Low = ohlcData[x].Low
tempData.Close = ohlcData[x].Close
tempData.Volume = ohlcData[x].Volume
tempData.Time = ohlcData[x].StartTime
resp.Candles = append(resp.Candles, tempData)
}
return resp, nil
}

View File

@@ -408,8 +408,8 @@ func (g *Gateio) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (g *Gateio) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (g *Gateio) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -330,8 +330,8 @@ func (g *Gemini) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (g *Gemini) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (g *Gemini) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -386,8 +386,8 @@ func (h *HitBTC) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (h *HitBTC) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (h *HitBTC) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -498,8 +498,8 @@ func (h *HUOBI) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (h *HUOBI) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (h *HUOBI) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -39,7 +39,7 @@ type IBotExchange interface {
GetAuthenticatedAPISupport(endpoint uint8) bool
SetPairs(pairs currency.Pairs, a asset.Item, enabled bool) error
GetAssetTypes() asset.Items
GetExchangeHistory(p currency.Pair, a asset.Item) ([]TradeHistory, error)
GetExchangeHistory(p currency.Pair, a asset.Item, startTime, endTime time.Time) ([]TradeHistory, error)
SupportsAutoPairUpdates() bool
SupportsRESTTickerBatchUpdates() bool
GetFeeByType(f *FeeBuilder) (float64, error)

View File

@@ -304,8 +304,8 @@ func (i *ItBit) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (i *ItBit) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (i *ItBit) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -9,19 +9,20 @@ import (
// Consts here define basic time intervals
const (
OneMin = time.Minute
ThreeMin = 3 * time.Minute
FiveMin = 5 * time.Minute
FifteenMin = 15 * time.Minute
ThirtyMin = 30 * time.Minute
OneHour = 1 * time.Hour
TwoHour = 2 * time.Hour
FourHour = 4 * time.Hour
SixHour = 6 * time.Hour
TwelveHour = 12 * time.Hour
OneDay = 24 * time.Hour
ThreeDay = 72 * time.Hour
OneWeek = 168 * time.Hour
FifteenSecond = 15 * time.Second
OneMin = time.Minute
ThreeMin = 3 * time.Minute
FiveMin = 5 * time.Minute
FifteenMin = 15 * time.Minute
ThirtyMin = 30 * time.Minute
OneHour = 1 * time.Hour
TwoHour = 2 * time.Hour
FourHour = 4 * time.Hour
SixHour = 6 * time.Hour
TwelveHour = 12 * time.Hour
OneDay = 24 * time.Hour
ThreeDay = 72 * time.Hour
OneWeek = 168 * time.Hour
)
// Item holds all the relevant information for internal kline elements

View File

@@ -450,8 +450,8 @@ func (k *Kraken) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (k *Kraken) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (k *Kraken) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -335,8 +335,8 @@ func (l *LakeBTC) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (l *LakeBTC) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (l *LakeBTC) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -304,8 +304,8 @@ func (l *Lbank) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (l *Lbank) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (l *Lbank) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrFunctionNotSupported
}

View File

@@ -285,8 +285,8 @@ func (l *LocalBitcoins) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (l *LocalBitcoins) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (l *LocalBitcoins) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
@@ -261,8 +262,8 @@ func (o *OKGroup) GetFundingHistory() (resp []exchange.FundHistory, err error) {
return resp, err
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (o *OKGroup) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (o *OKGroup) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -43,6 +43,7 @@ type Submit struct {
ID string
AccountID string
ClientID string
ClientOrderID string
WalletAddress string
Type Type
Side Side
@@ -83,6 +84,7 @@ type Modify struct {
Exchange string
InternalOrderID string
ID string
ClientOrderID string
AccountID string
ClientID string
WalletAddress string
@@ -122,6 +124,7 @@ type Detail struct {
Exchange string
InternalOrderID string
ID string
ClientOrderID string
AccountID string
ClientID string
WalletAddress string
@@ -145,6 +148,7 @@ type Cancel struct {
Amount float64
Exchange string
ID string
ClientOrderID string
AccountID string
ClientID string
WalletAddress string

View File

@@ -370,8 +370,8 @@ func (p *Poloniex) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (p *Poloniex) GetExchangeHistory(currencyPair currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (p *Poloniex) GetExchangeHistory(currencyPair currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -27,6 +27,7 @@ var Exchanges = []string{
"coinbene",
"coinut",
"exmo",
"ftx",
"gateio",
"gemini",
"hitbtc",

View File

@@ -321,8 +321,8 @@ func (y *Yobit) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (y *Yobit) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (y *Yobit) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -374,8 +374,8 @@ func (z *ZB) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (z *ZB) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) {
// GetExchangeHistory returns historic trade data within the timeframe provided.
func (z *ZB) GetExchangeHistory(p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]exchange.TradeHistory, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -58,8 +58,8 @@ func TestExchange_Exchanges(t *testing.T) {
t.Parallel()
x := exchangeTest.Exchanges(false)
y := len(x)
if y != 27 {
t.Fatalf("expected 27 received %v", y)
if y != 28 {
t.Fatalf("expected 28 received %v", y)
}
}

3
go.sum
View File

@@ -160,7 +160,9 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 h1:DQVOxR9qdYEybJUr/c7ku34r3PfajaMYXZwgDM7KuSk=
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12/go.mod h1:u9MdXq/QageOOSGp7qG4XAQsYUMP+V5zEel/Vrl6OOc=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -231,6 +233,7 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=

View File

@@ -2430,6 +2430,63 @@
"supportedCurrencies": ""
}
]
},
{
"name": "FTX",
"enabled": true,
"verbose": false,
"httpTimeout": 0,
"websocketResponseCheckTimeout": 0,
"websocketResponseMaxLimit": 0,
"websocketTrafficTimeout": 0,
"websocketOrderbookBufferLimit": 0,
"baseCurrencies": "USD",
"currencyPairs": {
"assetTypes": [
"spot",
"futures"
],
"pairs": {
"futures": {
"enabled": "DOGE-PERP",
"available": "ADA-PERP,ADA-0626,ALGO-PERP,ALGO-0626,ALT-PERP,ALT-0626,ATOM-PERP,ATOM-0626,BCH-PERP,BCH-0626,BNB-PERP,BNB-0626,BSV-PERP,BSV-0626,BTC-PERP,BTC-MOVE,BTC-MOVE,BTC-MOVE,BTC-MOVE,BTC-MOVE,BTC-MOVE,BTC-0626,BTC-MOVE,BTC-0925,BTC-MOVE,BTC-MOVE,BTMX-PERP,BTMX-0626,DOGE-PERP,DOGE-0626,DRGN-PERP,DRGN-0626,EOS-PERP,EOS-0626,ETC-PERP,ETC-0626,ETH-PERP,ETH-0626,EXCH-PERP,EXCH-0626,HT-PERP,HT-0626,LEO-PERP,LEO-0626,LINK-PERP,LINK-0626,LTC-PERP,LTC-0626,MATIC-PERP,MATIC-0626,MID-PERP,MID-0626,OIL100-0525,OKB-PERP,OKB-0626,PAXG-PERP,PAXG-0626,BERNIE,BIDEN,BLOOMBERG,PETE,TRUMP,WARREN,PRIV-PERP,PRIV-0626,SHIT-PERP,SHIT-0626,TOMO-PERP,TOMO-0626,TRX-PERP,TRX-0626,TRYB-PERP,TRYB-0626,USDT-PERP,USDT-0626,XAUT-PERP,XAUT-0626,XRP-PERP,XRP-0626,XTZ-PERP,XTZ-0626",
"requestFormat": {
"uppercase": true,
"delimiter": "-"
},
"configFormat": {
"uppercase": true,
"delimiter": "-"
}
},
"spot": {
"enabled": "BTC/USD",
"available": "BCH/USD,BCH/USDT,BNB/USD,BNB/USDT,BTC/USD,BTC/USDT,BTMX/USD,ETH/USD,ETH/USDT,FTT/BTC,FTT/USD,FTT/USDT,LINK/USD,LINK/USDT,LTC/USD,LTC/USDT,PAXG/USD,PAXG/USDT,TRX/USD,TRX/USDT,TRYB/USD,USDT/USD,XAUT/USD,XAUT/USDT,ADABEAR/USD,ADABULL/USD,ADAHALF/USD,ADAHALF/USDT,ADAHEDGE/USD,ALGOBEAR/USD,ALGOBULL/USD,ALGOHALF/USD,ALGOHALF/USDT,ALGOHEDGE/USD,ALTBEAR/USD,ALTBULL/USD,ALTHALF/USD,ALTHALF/USDT,ALTHEDGE/USD,ATOMBEAR/USD,ATOMBULL/USD,ATOMHALF/USD,ATOMHALF/USDT,ATOMHEDGE/USD,BCHBEAR/USD,BCHBEAR/USDT,BCHBULL/USD,BCHBULL/USDT,BCHHALF/USD,BCHHALF/USDT,BCHHEDGE/USD,BEAR/USD,BEAR/USDT,BEARSHIT/USD,BNBBEAR/USD,BNBBEAR/USDT,BNBBULL/USD,BNBBULL/USDT,BNBHALF/USD,BNBHALF/USDT,BNBHEDGE/USD,BSVBEAR/USD,BSVBEAR/USDT,BSVBULL/USD,BSVBULL/USDT,BSVHALF/USD,BSVHALF/USDT,BSVHEDGE/USD,BTMXBEAR/USD,BTMXBEAR/USDT,BTMXBULL/USD,BTMXBULL/USDT,BTMXHALF/USD,BTMXHALF/USDT,BTMXHEDGE/USD,BULL/USD,BULL/USDT,BULLSHIT/USD,BVOL/USD,BVOL/USDT,DOGEBEAR/USD,DOGEBULL/USD,DOGEHALF/USD,DOGEHALF/USDT,DOGEHEDGE/USD,DRGNBEAR/USD,DRGNBULL/USD,DRGNHALF/USD,DRGNHALF/USDT,DRGNHEDGE/USD,EOSBEAR/USD,EOSBEAR/USDT,EOSBULL/USD,EOSBULL/USDT,EOSHALF/USD,EOSHALF/USDT,EOSHEDGE/USD,ETCBEAR/USD,ETCBULL/USD,ETCHALF/USD,ETCHALF/USDT,ETCHEDGE/USD,ETHBEAR/USD,ETHBEAR/USDT,ETHBULL/USD,ETHBULL/USDT,ETHHALF/USD,ETHHALF/USDT,ETHHEDGE/USD,EXCHBEAR/USD,EXCHBULL/USD,EXCHHALF/USD,EXCHHALF/USDT,EXCHHEDGE/USD,HALF/USD,HALF/USDT,HALFSHIT/USD,HALFSHIT/USDT,HEDGE/USD,HEDGESHIT/USD,HTBEAR/USD,HTBULL/USD,HTHALF/USD,HTHALF/USDT,HTHEDGE/USD,IBVOL/USD,IBVOL/USDT,LEOBEAR/USD,LEOBULL/USD,LEOHALF/USD,LEOHALF/USDT,LEOHEDGE/USD,LINKBEAR/USD,LINKBEAR/USDT,LINKBULL/USD,LINKBULL/USDT,LINKHALF/USD,LINKHALF/USDT,LINKHEDGE/USD,LTCBEAR/USD,LTCBEAR/USDT,LTCBULL/USD,LTCBULL/USDT,LTCHALF/USD,LTCHALF/USDT,LTCHEDGE/USD,MATICBEAR/USD,MATICBULL/USD,MATICHALF/USD,MATICHALF/USDT,MATICHEDGE/USD,MIDBEAR/USD,MIDBULL/USD,MIDHALF/USD,MIDHALF/USDT,MIDHEDGE/USD,OKBBEAR/USD,OKBBULL/USD,OKBHALF/USD,OKBHALF/USDT,OKBHEDGE/USD,PAXGBEAR/USD,PAXGBULL/USD,PAXGHALF/USD,PAXGHALF/USDT,PAXGHEDGE/USD,PRIVBEAR/USD,PRIVBULL/USD,PRIVHALF/USD,PRIVHALF/USDT,PRIVHEDGE/USD,TOMOBEAR/USD,TOMOBULL/USD,TOMOHALF/USD,TOMOHALF/USDT,TOMOHEDGE/USD,TRXBEAR/USD,TRXBULL/USD,TRXHALF/USD,TRXHALF/USDT,TRXHEDGE/USD,TRYBBEAR/USD,TRYBBULL/USD,TRYBHALF/USD,TRYBHALF/USDT,TRYBHEDGE/USD,USDTBEAR/USD,USDTBULL/USD,USDTHALF/USD,USDTHALF/USDT,USDTHEDGE/USD,XAUTBEAR/USD,XAUTBULL/USD,XAUTHALF/USD,XAUTHALF/USDT,XAUTHEDGE/USD,XRPBEAR/USD,XRPBEAR/USDT,XRPBULL/USD,XRPBULL/USDT,XRPHALF/USD,XRPHALF/USDT,XRPHEDGE/USD,XTZBEAR/USD,XTZBEAR/USDT,XTZBULL/USD,XTZBULL/USDT,XTZHALF/USD,XTZHALF/USDT,XTZHEDGE/USD",
"requestFormat": {
"uppercase": true,
"delimiter": "/"
},
"configFormat": {
"uppercase": true,
"delimiter": "/"
}
}
}
},
"api": {
"authenticatedSupport": false,
"authenticatedWebsocketApiSupport": true,
"endpoints": {
"url": "",
"urlSecondary": "",
"websocketURL": ""
},
"credentials": {
"key": "Key",
"secret": "Secret"
}
},
"features": null
}
],
"bankAccounts": [