(Exchange) Add GetHistoricCandles() & GetHistoricCandlesEx() support to exchanges (#479)

* implemented binance and bitfinex GetHistoricCandles wrapper methods)

* coinbene supported added

* after and before clean up

* gateio wrapper completed

* merged upstream/master

* Added bsaic KlineIntervalSupported() method

* Converted binance fixed test

* WIP

* new KlineConvertToExchangeStandardString method added

* end of day WIP

* WIP

* end of day WIP started migration of trade history

* added kline support to hitbtc huobi lbank

* added exchangehistory to all supported exchanges started work on coinbase 300 candles/request method

* end of day WIP

* removed unused ta and misc changes to flag ready for review

* yobit cleanup

* revert coinbase changES

* general code clean up and added zb support

* poloniex support added

* renamed method to FormatExchangeKlineInterval other misc fixes

* linter fixes

* linter fixes

* removed verbose

* fixed poloniex test coverage

* revert poloniex mock data

* regenerated poloniex mock data

* a very verbose clean up

* binance mock clean up

* removed unneeded t.Log()

* setting verbose to true to debug CI issue

* first pass changes addressed

* common.ErrNotYetImplemented implemented :D

* comments added

* WIP-addressed exchange requests and reverted previous GetExchangeHistory changes

* WIP-addressed exchange requests and reverted previous GetExchangeHistory changes

* increased test coverage added kraken support

* OKGroup support completed started work on address GetExchangeHistory feedback and migrating to own PR under https://github.com/xtda/gocryptotrader/tree/exchange_history

* convert zb ratelimits

* gofmt run on okcoin

* increased delay on rate limit

* gofmt package

* fixed panic with coinbene and bithumb if conversion fails

* very broken end of day WIP

* added support for GetHistoricCandlesEx to coinbase and binance

* gofmt package

* coinbase, btcmarkets, zb ex wrapper function added

* added all exchange support for ex regenerated mock data

* update bithumb to return wrapper method

* gofmt package

* end of day started work on changes

* reworked test coverage added okgroup support general fixes/change requests addressed

* Added OneMonth

* limit checks on supportedexchanges

* reverted getexchangehistory

* reworked binance tesT

* added workaround for kraken panic

* renamed command to extended removed interval check on non-implemented commands

* added wrapperconfig back

* increased test coverage for FormatExchangeKlineInterval

* WIP

* increased test coverage for FormatExchangeKlineInterval bitfinex/gateio/huobi

* linter fixes

* zb kraken lbank coinbene btcmarkets support added

* removed verbose

* OK group support for other asset types added

* swapped margin to use spot endpoint

* index support added test coverage added for asset types

* added asset type to okcoin test

* gofmt

* add asset to extended method

* removed verbose

* add support for coinbene swap increase test coverage

* removed verbose

* small clean up of okgroup wrapper functions

* verbose to troubleshoot CI issues

* removed verbose

* added error check reverted coinbasechanges

* readme updated

* removed unused start/finish started work on decoupling api requests from kline package

* restructured coinbene, bithumb methods, added bitstamp support

* kraken time fix

* BTCMarkets restructure

* typo fix

* removed test for futures due to contact changing

* added start/end date to extended method over range

* converted to assettranslator

* removed verbose

* removed invalid char

* reverted incorrectly removed return

* added import

* further template updates

* macos hates my keyboard :D

* misc canges

* x -> i

* removed verbose

* updated fixCasing to allocate var before checks

* removed time conversion

* sort all outgoing kline candles

* fixCasing fix

* after/before checks added

* added parallel to test

* logic check on BTCmarkets

* removed unused param, used correct iterator

* converted HitBTC to use time.Time

* add iszero false check to candle times

* updated resultlimit to 5000

* new line added

* added comment to exported const

* use configured ratelimit

* fixed pair for test

* panic fixed WIP on fixCasing

* fixCasing rework, started work on readme docs

* enable rate limiter for wrapper issues tool

* docs updated

* removed err from return and formatted currency

* updated Yobit supported status

* Updated HitBTC to use onehour candles due to test exeuction times

* added further details to gctcli output

* added link to docs

* added link to tempalte

* disable FTX websocket in config_example

* fix poloneix

* regenerated poloniex mock data

* removed recording flag
This commit is contained in:
Andrew
2020-07-08 10:51:54 +10:00
committed by GitHub
parent c2c200cd1b
commit 4a736fb335
112 changed files with 52287 additions and 12550 deletions

View File

@@ -18,7 +18,6 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/log"
@@ -67,8 +66,7 @@ type Binance struct {
WebsocketConn *wshandler.WebsocketConnection
// Valid string list that is required by the exchange
validLimits []int
validIntervals []TimeInterval
validLimits []int
}
// GetExchangeInfo returns exchange information. Check binance_types for more
@@ -178,10 +176,11 @@ func (b *Binance) GetAggregatedTrades(symbol string, limit int) ([]AggregatedTra
params := url.Values{}
params.Set("symbol", strings.ToUpper(symbol))
params.Set("limit", strconv.Itoa(limit))
path := fmt.Sprintf("%s%s?%s", b.API.Endpoints.URL, aggregatedTrades, params.Encode())
if limit > 0 {
params.Set("limit", strconv.Itoa(limit))
}
path := b.API.Endpoints.URL + aggregatedTrades + "?" + params.Encode()
return resp, b.SendHTTPRequest(path, limitDefault, &resp)
}
@@ -199,7 +198,7 @@ func (b *Binance) GetSpotKline(arg KlinesRequestParams) ([]CandleStick, error) {
params := url.Values{}
params.Set("symbol", arg.Symbol)
params.Set("interval", string(arg.Interval))
params.Set("interval", arg.Interval)
if arg.Limit != 0 {
params.Set("limit", strconv.Itoa(arg.Limit))
}
@@ -581,36 +580,9 @@ func (b *Binance) CheckSymbol(symbol string, assetType asset.Item) error {
return errors.New("incorrect symbol values - please check available pairs in configuration")
}
// CheckIntervals checks value against a variable list
func (b *Binance) CheckIntervals(interval string) error {
for x := range b.validIntervals {
if TimeInterval(interval) == b.validIntervals[x] {
return nil
}
}
return errors.New(`incorrect interval values - valid values are "1m","3m","5m","15m","30m","1h","2h","4h","6h","8h","12h","1d","3d","1w","1M"`)
}
// SetValues sets the default valid values
func (b *Binance) SetValues() {
b.validLimits = []int{5, 10, 20, 50, 100, 500, 1000, 5000}
b.validIntervals = []TimeInterval{
TimeIntervalMinute,
TimeIntervalThreeMinutes,
TimeIntervalFiveMinutes,
TimeIntervalFifteenMinutes,
TimeIntervalThirtyMinutes,
TimeIntervalHour,
TimeIntervalTwoHours,
TimeIntervalFourHours,
TimeIntervalSixHours,
TimeIntervalEightHours,
TimeIntervalTwelveHours,
TimeIntervalDay,
TimeIntervalThreeDays,
TimeIntervalWeek,
TimeIntervalMonth,
}
}
// GetFee returns an estimate of fee based on type of transaction
@@ -757,38 +729,3 @@ func (b *Binance) MaintainWsAuthStreamKey() error {
HTTPRecording: b.HTTPRecording,
})
}
func parseInterval(in time.Duration) (TimeInterval, error) {
switch in {
case kline.OneMin:
return TimeIntervalMinute, nil
case kline.ThreeMin:
return TimeIntervalThreeMinutes, nil
case kline.FiveMin:
return TimeIntervalFiveMinutes, nil
case kline.FifteenMin:
return TimeIntervalFifteenMinutes, nil
case kline.ThirtyMin:
return TimeIntervalThirtyMinutes, nil
case kline.OneHour:
return TimeIntervalHour, nil
case kline.TwoHour:
return TimeIntervalTwoHours, nil
case kline.FourHour:
return TimeIntervalFourHours, nil
case kline.SixHour:
return TimeIntervalSixHours, nil
case kline.OneHour * 8:
return TimeIntervalEightHours, nil
case kline.TwelveHour:
return TimeIntervalTwelveHours, nil
case kline.OneDay:
return TimeIntervalDay, nil
case kline.ThreeDay:
return TimeIntervalThreeDays, nil
case kline.OneWeek:
return TimeIntervalWeek, nil
default:
return TimeIntervalMinute, errInvalidInterval
}
}

View File

@@ -93,7 +93,6 @@ func TestGetHistoricalTrades(t *testing.T) {
func TestGetAggregatedTrades(t *testing.T) {
t.Parallel()
_, err := b.GetAggregatedTrades("BTCUSDT", 5)
if err != nil {
t.Error("Binance GetAggregatedTrades() error", err)
@@ -102,11 +101,12 @@ func TestGetAggregatedTrades(t *testing.T) {
func TestGetSpotKline(t *testing.T) {
t.Parallel()
_, err := b.GetSpotKline(KlinesRequestParams{
Symbol: "BTCUSDT",
Interval: TimeIntervalFiveMinutes,
Limit: 24,
Symbol: "BTCUSDT",
Interval: kline.FiveMin.Short(),
Limit: 24,
StartTime: time.Unix(1577836800, 0).Unix() * 1000,
EndTime: time.Unix(1580515200, 0).Unix() * 1000,
})
if err != nil {
t.Error("Binance GetSpotKline() error", err)
@@ -501,7 +501,6 @@ func TestModifyOrder(t *testing.T) {
func TestWithdraw(t *testing.T) {
t.Parallel()
if areTestAPIKeysSet() && !canManipulateRealOrders && !mockTests {
t.Skip("API keys set, canManipulateRealOrders false, skipping test")
}
@@ -905,130 +904,65 @@ func TestExecutionTypeToOrderStatus(t *testing.T) {
}
func TestGetHistoricCandles(t *testing.T) {
if mockTests {
t.Skip("skipping test under mock as its covered by GetSpotKlines()")
}
currencyPair := currency.NewPairFromString("BTCUSDT")
start := time.Date(2017, 8, 18, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 6, 0)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, start, end, kline.OneDay)
startTime := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, startTime, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandles(currencyPair, asset.Spot, startTime, end, kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestParseInterval(t *testing.T) {
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
startTime := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, end, kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestBinance_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval time.Duration
expected TimeInterval
err error
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
TimeIntervalMinute,
nil,
},
{
"ThreeMin",
kline.ThreeMin,
TimeIntervalThreeMinutes,
nil,
},
{
"FiveMin",
kline.FiveMin,
TimeIntervalFiveMinutes,
nil,
},
{
"FifteenMin",
kline.FifteenMin,
TimeIntervalFifteenMinutes,
nil,
},
{
"ThirtyMin",
kline.ThirtyMin,
TimeIntervalThirtyMinutes,
nil,
},
{
"OneHour",
kline.OneHour,
TimeIntervalHour,
nil,
},
{
"TwoHour",
kline.TwoHour,
TimeIntervalTwoHours,
nil,
},
{
"FourHour",
kline.FourHour,
TimeIntervalFourHours,
nil,
},
{
"SixHour",
kline.SixHour,
TimeIntervalSixHours,
nil,
},
{
"EightHour",
kline.OneHour * 8,
TimeIntervalEightHours,
nil,
},
{
"TwelveHour",
kline.TwelveHour,
TimeIntervalTwelveHours,
nil,
"1m",
},
{
"OneDay",
kline.OneDay,
TimeIntervalDay,
nil,
"1d",
},
{
"ThreeDay",
kline.ThreeDay,
TimeIntervalThreeDays,
nil,
},
{
"OneWeek",
kline.OneWeek,
TimeIntervalWeek,
nil,
},
{
"default",
time.Hour * 1337,
TimeIntervalHour,
errInvalidInterval,
"OneMonth",
kline.OneMonth,
"1M",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
v, err := parseInterval(test.interval)
if err != nil {
if err != test.err {
t.Fatal(err)
}
} else {
if v != test.expected {
t.Fatalf("%v: received %v expected %v", test.name, v, test.expected)
}
ret := b.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}

View File

@@ -1,14 +1,11 @@
package binance
import (
"errors"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
)
var errInvalidInterval = errors.New("invalid interval")
// Response holds basic binance api response data
type Response struct {
Code int `json:"code"`
@@ -407,35 +404,13 @@ var (
// KlinesRequestParams represents Klines request data.
type KlinesRequestParams struct {
Symbol string // Required field; example LTCBTC, BTCUSDT
Interval TimeInterval // Time interval period
Limit int // Default 500; max 500.
Symbol string // Required field; example LTCBTC, BTCUSDT
Interval string // Time interval period
Limit int // Default 500; max 500.
StartTime int64
EndTime int64
}
// TimeInterval represents interval enum.
type TimeInterval string
// Vars related to time intervals
var (
TimeIntervalMinute = TimeInterval("1m")
TimeIntervalThreeMinutes = TimeInterval("3m")
TimeIntervalFiveMinutes = TimeInterval("5m")
TimeIntervalFifteenMinutes = TimeInterval("15m")
TimeIntervalThirtyMinutes = TimeInterval("30m")
TimeIntervalHour = TimeInterval("1h")
TimeIntervalTwoHours = TimeInterval("2h")
TimeIntervalFourHours = TimeInterval("4h")
TimeIntervalSixHours = TimeInterval("6h")
TimeIntervalEightHours = TimeInterval("8h")
TimeIntervalTwelveHours = TimeInterval("12h")
TimeIntervalDay = TimeInterval("1d")
TimeIntervalThreeDays = TimeInterval("3d")
TimeIntervalWeek = TimeInterval("1w")
TimeIntervalMonth = TimeInterval("1M")
)
// WithdrawalFees the large list of predefined withdrawal fees
// Prone to change
var WithdrawalFees = map[currency.Code]float64{

View File

@@ -110,9 +110,32 @@ func (b *Binance) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.ThreeDay.Word(): true,
kline.OneWeek.Word(): true,
kline.OneMonth.Word(): true,
},
ResultLimit: 1000,
},
},
}
@@ -672,22 +695,35 @@ func (b *Binance) ValidateCredentials() error {
return b.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Binance) 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
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (b *Binance) FormatExchangeKlineInterval(in kline.Interval) string {
if in == kline.OneDay {
return "1d"
}
klineParams := KlinesRequestParams{
Interval: intervalToString,
if in == kline.OneMonth {
return "1M"
}
return in.Short()
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Binance) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
if kline.TotalCandlesPerInterval(start, end, interval) > b.Features.Enabled.Kline.ResultLimit {
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
req := KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(interval),
Symbol: b.FormatExchangeCurrency(pair, a).String(),
StartTime: start.Unix() * 1000,
EndTime: end.Unix() * 1000,
}
candles, err := b.GetSpotKline(klineParams)
if err != nil {
return kline.Item{}, err
Limit: int(b.Features.Enabled.Kline.ResultLimit),
}
ret := kline.Item{
@@ -697,6 +733,11 @@ func (b *Binance) GetHistoricCandles(pair currency.Pair, a asset.Item, start, en
Interval: interval,
}
candles, err := b.GetSpotKline(req)
if err != nil {
return kline.Item{}, err
}
for x := range candles {
ret.Candles = append(ret.Candles, kline.Candle{
Time: candles[x].OpenTime,
@@ -707,5 +748,53 @@ func (b *Binance) GetHistoricCandles(pair currency.Pair, a asset.Item, start, en
Volume: candles[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *Binance) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: b.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, b.Features.Enabled.Kline.ResultLimit)
for x := range dates {
req := KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(interval),
Symbol: b.FormatExchangeCurrency(pair, a).String(),
StartTime: dates[x].Start.UTC().Unix() * 1000,
EndTime: dates[x].End.UTC().Unix() * 1000,
Limit: int(b.Features.Enabled.Kline.ResultLimit),
}
candles, err := b.GetSpotKline(req)
if err != nil {
return kline.Item{}, err
}
for i := range candles {
ret.Candles = append(ret.Candles, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].Close,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -240,7 +240,7 @@ func (b *Bitfinex) GetTrades(currencyPair string, limit, timestampStart, timesta
v.Encode()
var resp [][]interface{}
err := b.SendHTTPRequest(path, &resp, trade)
err := b.SendHTTPRequest(path, &resp, tradeRateLimit)
if err != nil {
return nil, err
}
@@ -408,7 +408,7 @@ func (b *Bitfinex) GetLends(symbol string, values url.Values) ([]Lends, error) {
// timeFrame values: '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D',
// '7D', '14D', '1M'
// section values: last or hist
func (b *Bitfinex) GetCandles(symbol, timeFrame string, start, end, limit int64, historic, ascending bool) ([]Candle, error) {
func (b *Bitfinex) GetCandles(symbol, timeFrame string, start, end int64, limit uint32, historic bool) ([]Candle, error) {
var fundingPeriod string
if symbol[0] == 'f' {
fundingPeriod = ":p30"
@@ -434,7 +434,7 @@ func (b *Bitfinex) GetCandles(symbol, timeFrame string, start, end, limit int64,
}
if limit > 0 {
v.Set("limit", strconv.FormatInt(limit, 10))
v.Set("limit", strconv.FormatInt(int64(limit), 10))
}
path += "/hist"
@@ -451,7 +451,7 @@ func (b *Bitfinex) GetCandles(symbol, timeFrame string, start, end, limit int64,
var c []Candle
for i := range response {
c = append(c, Candle{
Timestamp: int64(response[i][0].(float64)),
Timestamp: time.Unix(int64(response[i][0].(float64)/1000), 0),
Open: response[i][1].(float64),
Close: response[i][2].(float64),
High: response[i][3].(float64),
@@ -476,7 +476,7 @@ func (b *Bitfinex) GetCandles(symbol, timeFrame string, start, end, limit int64,
}
return []Candle{{
Timestamp: int64(response[0].(float64)),
Timestamp: time.Unix(int64(response[0].(float64))/1000, 0),
Open: response[1].(float64),
Close: response[2].(float64),
High: response[3].(float64),

View File

@@ -13,6 +13,7 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -153,7 +154,7 @@ func TestGetLends(t *testing.T) {
func TestGetCandles(t *testing.T) {
t.Parallel()
_, err := b.GetCandles("fUSD", "1m", 0, 0, 10, true, false)
_, err := b.GetCandles("fUSD", "1m", 0, 0, 10, true)
if err != nil {
t.Fatal(err)
}
@@ -1210,3 +1211,112 @@ func TestWsNotifications(t *testing.T) {
t.Error(err)
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSD")
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin*1337)
if err == nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("TBTCUSD")
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
_, err = b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin*1337)
if err == nil {
t.Fatal(err)
}
}
func TestFixCasing(t *testing.T) {
ret := b.fixCasing(currency.NewPairFromString("BTCUSD"), asset.Spot)
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("TBTCUSD"), asset.Spot)
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("tBTCUSD"), asset.Spot)
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("BTCUSD"), asset.Margin)
if ret != "fBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("BTCUSD"), asset.Spot)
if ret != "tBTCUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("FUNETH"), asset.Spot)
if ret != "tFUNETH" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("TNBUSD"), asset.Spot)
if ret != "tTNBUSD" {
t.Errorf("unexpected result: %v", ret)
}
ret = b.fixCasing(currency.NewPairFromString("tTNBUSD"), asset.Spot)
if ret != "tTNBUSD" {
t.Errorf("unexpected result: %v", ret)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"1m",
},
{
"OneDay",
kline.OneDay,
"1D",
},
{
"OneWeek",
kline.OneWeek,
"7D",
},
{
"TwoWeeks",
kline.OneWeek * 2,
"14D",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := b.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -1,6 +1,10 @@
package bitfinex
import "time"
import (
"time"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// AcceptedOrderType defines the accepted market types, exchange strings denote
// non-contract order types.
@@ -71,6 +75,7 @@ type Trade struct {
Rate float64
Period int64
Type string
Side order.Side
}
// Lendbook holds most recent funding data for a relevant currency
@@ -388,7 +393,7 @@ type WebsocketTrade struct {
// Candle holds OHLC data
type Candle struct {
Timestamp int64
Timestamp time.Time
Open float64
Close float64
High float64

View File

@@ -6,6 +6,7 @@ import (
"strings"
"sync"
"time"
"unicode"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
@@ -119,9 +120,31 @@ func (b *Bitfinex) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission |
exchange.AutoWithdrawFiatWithAPIPermission,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.OneWeek.Word(): true,
kline.TwoWeek.Word(): true,
},
ResultLimit: 10000,
},
},
}
@@ -773,7 +796,117 @@ func (b *Bitfinex) ValidateCredentials() error {
return b.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bitfinex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (b *Bitfinex) FormatExchangeKlineInterval(in kline.Interval) string {
switch in {
case kline.OneDay:
return "1D"
case kline.OneWeek:
return "7D"
case kline.OneWeek * 2:
return "14D"
default:
return in.Short()
}
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bitfinex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
if kline.TotalCandlesPerInterval(start, end, interval) > b.Features.Enabled.Kline.ResultLimit {
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
candles, err := b.GetCandles(b.fixCasing(pair, a), b.FormatExchangeKlineInterval(interval),
start.Unix()*1000, end.Unix()*1000,
b.Features.Enabled.Kline.ResultLimit, true)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: b.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
for x := range candles {
ret.Candles = append(ret.Candles, kline.Candle{
Time: candles[x].Timestamp,
Open: candles[x].Open,
High: candles[x].Close,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *Bitfinex) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: b.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, b.Features.Enabled.Kline.ResultLimit)
for x := range dates {
candles, err := b.GetCandles(b.fixCasing(pair, a), b.FormatExchangeKlineInterval(interval),
dates[x].Start.Unix()*1000, dates[x].End.Unix()*1000,
b.Features.Enabled.Kline.ResultLimit, true)
if err != nil {
return kline.Item{}, err
}
for i := range candles {
ret.Candles = append(ret.Candles, kline.Candle{
Time: candles[i].Timestamp,
Open: candles[i].Open,
High: candles[i].Close,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
func (b *Bitfinex) fixCasing(in currency.Pair, a asset.Item) string {
var checkString [2]byte
if a == asset.Spot {
checkString[0] = 't'
checkString[1] = 'T'
} else if a == asset.Margin {
checkString[0] = 'f'
checkString[1] = 'F'
}
y := in.Base.String()
if (y[0] != checkString[0] && y[0] != checkString[1]) ||
(y[0] == checkString[1] && y[1] == checkString[1]) || in.Base == currency.TNB {
return string(checkString[0]) + b.FormatExchangeCurrency(in, a).Upper().String()
}
runes := []rune(b.FormatExchangeCurrency(in, a).Upper().String())
runes[0] = unicode.ToLower(runes[0])
return string(runes)
}

View File

@@ -95,7 +95,7 @@ const (
platformStatus request.EndpointLimit = iota
tickerBatch
tickerFunction
trade
tradeRateLimit
orderbookFunction
stats
candle
@@ -262,7 +262,7 @@ func (r *RateLimit) Limit(f request.EndpointLimit) error {
time.Sleep(r.TickerBatch.Reserve().Delay())
case tickerFunction:
time.Sleep(r.Ticker.Reserve().Delay())
case trade:
case tradeRateLimit:
time.Sleep(r.Trade.Reserve().Delay())
case orderbookFunction:
time.Sleep(r.Orderbook.Reserve().Delay())

View File

@@ -392,6 +392,11 @@ func (b *Bitflyer) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bitflyer) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (b *Bitflyer) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *Bitflyer) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -26,6 +26,7 @@ const (
publicTicker = "/public/ticker/"
publicOrderBook = "/public/orderbook/"
publicTransactionHistory = "/public/transaction_history/"
publicCandleStick = "/public/candlestick/"
privateAccInfo = "/info/account"
privateAccBalance = "/info/balance"
@@ -599,3 +600,10 @@ var errCode = map[string]string{
"5600": "CUSTOM NOTICE (상황별 에러 메시지 출력) usually means transaction not allowed",
"5900": "Unknown Error",
}
// GetCandleStick returns candle stick data for requested pair
func (b *Bithumb) GetCandleStick(symbol, interval string) (resp OHLCVResponse, err error) {
path := b.API.Endpoints.URL + publicCandleStick + symbol + "/" + interval
err = b.SendHTTPRequest(path, &resp)
return
}

View File

@@ -4,12 +4,15 @@ import (
"log"
"os"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
@@ -523,3 +526,28 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
func TestGetCandleStick(t *testing.T) {
_, err := b.GetCandleStick("BTC_KRW", "1m")
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_KRW")
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneDay)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_KRW")
startTime := time.Now().Add(-time.Hour * 24)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneDay)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -284,3 +284,9 @@ type FullBalance struct {
Xcoin map[string]float64
Available map[string]float64
}
// OHLCVResponse holds returned kline data
type OHLCVResponse struct {
Status string `json:"status"`
Data [][6]interface{} `json:"data"`
}

View File

@@ -64,6 +64,7 @@ func (b *Bithumb) SetDefaults() {
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "_",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
@@ -95,15 +96,31 @@ func (b *Bithumb) SetDefaults() {
FiatWithdrawalFee: true,
CryptoDepositFee: true,
CryptoWithdrawalFee: true,
KlineFetching: true,
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.AutoWithdrawFiat,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.TenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
},
},
},
}
b.Requester = request.New(b.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(SetRateLimit()))
@@ -594,7 +611,94 @@ func (b *Bithumb) ValidateCredentials() error {
return b.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bithumb) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (b *Bithumb) FormatExchangeKlineInterval(in kline.Interval) string {
return in.Short()
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bithumb) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
candle, err := b.GetCandleStick(b.FormatExchangeCurrency(pair, a).String(), b.FormatExchangeKlineInterval(interval))
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: b.Name,
Pair: pair,
Interval: interval,
}
for x := range candle.Data {
var tempCandle kline.Candle
tempTime := candle.Data[x][0].(float64)
timestamp := time.Unix(0, int64(tempTime)*int64(time.Millisecond))
if timestamp.Before(start) {
continue
}
if timestamp.After(end) {
break
}
tempCandle.Time = timestamp
open, ok := candle.Data[x][1].(string)
if !ok {
return kline.Item{}, errors.New("open conversion failed")
}
tempCandle.Open, err = strconv.ParseFloat(open, 64)
if err != nil {
return kline.Item{}, err
}
high, ok := candle.Data[x][2].(string)
if !ok {
return kline.Item{}, errors.New("high conversion failed")
}
tempCandle.High, err = strconv.ParseFloat(high, 64)
if err != nil {
return kline.Item{}, err
}
low, ok := candle.Data[x][3].(string)
if !ok {
return kline.Item{}, errors.New("low conversion failed")
}
tempCandle.Low, err = strconv.ParseFloat(low, 64)
if err != nil {
return kline.Item{}, err
}
closeTemp, ok := candle.Data[x][4].(string)
if !ok {
return kline.Item{}, errors.New("close conversion failed")
}
tempCandle.Close, err = strconv.ParseFloat(closeTemp, 64)
if err != nil {
return kline.Item{}, err
}
vol, ok := candle.Data[x][5].(string)
if !ok {
return kline.Item{}, errors.New("vol conversion failed")
}
tempCandle.Volume, err = strconv.ParseFloat(vol, 64)
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempCandle)
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *Bithumb) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return b.GetHistoricCandles(pair, a, start, end, interval)
}

View File

@@ -371,7 +371,11 @@ func TestGetTrade(t *testing.T) {
}
func TestGetPreviousTrades(t *testing.T) {
_, err := b.GetPreviousTrades(&TradeGetBucketedParams{})
_, err := b.GetPreviousTrades(&TradeGetBucketedParams{
Symbol: "XBTBTC",
Start: int32(time.Now().Add(-time.Hour * 24).Unix()),
Columns: "open,high,low,close,volume",
})
if err == nil {
t.Error("GetPreviousTrades() Expected error")
}

View File

@@ -690,6 +690,11 @@ func (b *Bitmex) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bitmex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (b *Bitmex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *Bitmex) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -54,6 +54,7 @@ const (
bitstampAPIXrpDeposit = "xrp_address"
bitstampAPIReturnType = "string"
bitstampAPITradingPairsInfo = "trading-pairs-info"
bitstampOHLC = "ohlc"
bitstampRateInterval = time.Minute * 10
bitstampRequestRate = 8000
@@ -579,6 +580,24 @@ func (b *Bitstamp) GetUnconfirmedBitcoinDeposits() ([]UnconfirmedBTCTransactions
b.SendAuthenticatedHTTPRequest(bitstampAPIUnconfirmedBitcoin, false, nil, &response)
}
// OHLC returns OHLCV data for step (interval)
func (b *Bitstamp) OHLC(currency string, start, end time.Time, step, limit string) (resp OHLCResponse, err error) {
var v = url.Values{}
v.Add("limit", limit)
v.Add("step", step)
if start.After(end) && !end.IsZero() {
return resp, errors.New("start time cannot be after end time")
}
if !start.IsZero() {
v.Add("start", strconv.FormatInt(start.Unix(), 10))
}
if !end.IsZero() {
v.Add("end", strconv.FormatInt(end.Unix(), 10))
}
return resp, b.SendHTTPRequest(common.EncodeURLValues(b.API.Endpoints.URL+"/v"+bitstampAPIVersion+"/"+bitstampOHLC+"/"+currency, v), &resp)
}
// TransferAccountBalance transfers funds from either a main or sub account
// amount - to transfers
// currency - which currency to transfer

View File

@@ -3,10 +3,13 @@ package bitstamp
import (
"net/url"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
@@ -664,3 +667,33 @@ func TestWsRequestReconnect(t *testing.T) {
t.Error(err)
}
}
func TestBitstamp_OHLC(t *testing.T) {
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.OHLC("btcusd", start, end, "60", "10")
if err != nil {
t.Fatal(err)
}
}
func TestBitstamp_GetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("btcusd")
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandles(currencyPair, asset.Spot, start, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}
}
func TestBitstamp_GetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("btcusd")
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := b.GetHistoricCandlesExtended(currencyPair, asset.Spot, start, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -219,3 +219,18 @@ type websocketOrderBook struct {
Timestamp int64 `json:"timestamp,string"`
Microtimestamp string `json:"microtimestamp"`
}
// OHLCResponse holds returned candle data
type OHLCResponse struct {
Data struct {
Pair string `json:"pair"`
OHLCV []struct {
Timestamp int64 `json:"timestamp,string"`
Open float64 `json:"open,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Close float64 `json:"close,string"`
Volume float64 `json:"volume,string"`
} `json:"ohlc"`
} `json:"data"`
}

View File

@@ -103,9 +103,30 @@ func (b *Bitstamp) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.AutoWithdrawFiat,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
DateRanges: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.ThreeDay.Word(): true,
},
ResultLimit: 1000,
},
},
}
@@ -677,6 +698,95 @@ func (b *Bitstamp) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bitstamp) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (b *Bitstamp) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: b.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
candles, err := b.OHLC(
b.FormatExchangeCurrency(pair, a).Lower().String(),
start,
end,
b.FormatExchangeKlineInterval(interval),
strconv.FormatInt(int64(b.Features.Enabled.Kline.ResultLimit), 10),
)
if err != nil {
return kline.Item{}, err
}
for x := range candles.Data.OHLCV {
if time.Unix(candles.Data.OHLCV[x].Timestamp, 0).Before(start) ||
time.Unix(candles.Data.OHLCV[x].Timestamp, 0).After(end) {
continue
}
ret.Candles = append(ret.Candles, kline.Candle{
Time: time.Unix(candles.Data.OHLCV[x].Timestamp, 0),
Open: candles.Data.OHLCV[x].Open,
High: candles.Data.OHLCV[x].High,
Low: candles.Data.OHLCV[x].Low,
Close: candles.Data.OHLCV[x].Close,
Volume: candles.Data.OHLCV[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *Bitstamp) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: b.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, b.Features.Enabled.Kline.ResultLimit)
for x := range dates {
candles, err := b.OHLC(
b.FormatExchangeCurrency(pair, a).Lower().String(),
dates[x].Start,
dates[x].End,
b.FormatExchangeKlineInterval(interval),
strconv.FormatInt(int64(b.Features.Enabled.Kline.ResultLimit), 10),
)
if err != nil {
return kline.Item{}, err
}
for i := range candles.Data.OHLCV {
if time.Unix(candles.Data.OHLCV[i].Timestamp, 0).Before(start) ||
time.Unix(candles.Data.OHLCV[i].Timestamp, 0).After(end) {
continue
}
ret.Candles = append(ret.Candles, kline.Candle{
Time: time.Unix(candles.Data.OHLCV[i].Timestamp, 0),
Open: candles.Data.OHLCV[i].Open,
High: candles.Data.OHLCV[i].High,
Low: candles.Data.OHLCV[i].Low,
Close: candles.Data.OHLCV[i].Close,
Volume: candles.Data.OHLCV[i].Volume,
})
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -591,6 +591,11 @@ func (b *Bittrex) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *Bittrex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (b *Bittrex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *Bittrex) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -17,8 +17,6 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
)
@@ -68,13 +66,13 @@ const (
stop = "Stop"
takeProfit = "Take Profit"
subscribe = "subscribe"
fundChange = "fundChange"
orderChange = "orderChange"
heartbeat = "heartbeat"
tick = "tick"
wsOB = "orderbookUpdate"
trade = "trade"
subscribe = "subscribe"
fundChange = "fundChange"
orderChange = "orderChange"
heartbeat = "heartbeat"
tick = "tick"
wsOB = "orderbookUpdate"
tradeEndPoint = "tradeEndPoint"
)
// BTCMarkets is the overarching type across the BTCMarkets package
@@ -164,27 +162,15 @@ func (b *BTCMarkets) GetOrderbook(marketID string, level int64) (Orderbook, erro
}
// GetMarketCandles gets candles for specified currency pair
func (b *BTCMarkets) GetMarketCandles(marketID string, timeWindow time.Duration, from, to time.Time, before, after, limit int64) (kline.Item, error) {
func (b *BTCMarkets) GetMarketCandles(marketID, timeWindow string, from, to time.Time, before, after, limit int64) (out CandleResponse, err error) {
if (before > 0) && (after >= 0) {
return kline.Item{}, errors.New("BTCMarkets only supports either before or after, not both")
return out, errors.New("BTCMarkets only supports either before or after, not both")
}
var temp [][]string
params := url.Values{}
params.Set("timeWindow", timeWindow)
var intervalStr string
switch timeWindow {
case kline.OneMin:
intervalStr = "1m"
case kline.OneHour:
intervalStr = "1h"
case kline.OneDay:
intervalStr = "1d"
default:
return kline.Item{}, errInvalidTimeInterval
}
if timeWindow != 0 {
params.Set("timeWindow", intervalStr)
if from.After(to) && !to.IsZero() {
return out, errors.New("start time cannot be after end time")
}
if !from.IsZero() {
params.Set("from", from.UTC().Format(time.RFC3339))
@@ -201,49 +187,7 @@ func (b *BTCMarkets) GetMarketCandles(marketID string, timeWindow time.Duration,
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
err := b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsCandles+params.Encode(),
&temp)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: b.Name,
Pair: currency.NewPairFromString(marketID),
Asset: asset.Spot,
Interval: timeWindow,
}
var tempData kline.Candle
var tempTime time.Time
for x := range temp {
tempTime, err = time.Parse(time.RFC3339, temp[x][0])
if err != nil {
return kline.Item{}, err
}
tempData.Time = tempTime
tempData.Open, err = strconv.ParseFloat(temp[x][1], 64)
if err != nil {
return kline.Item{}, err
}
tempData.High, err = strconv.ParseFloat(temp[x][2], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Low, err = strconv.ParseFloat(temp[x][3], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Close, err = strconv.ParseFloat(temp[x][4], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Volume, err = strconv.ParseFloat(temp[x][5], 64)
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempData)
}
return ret, nil
return out, b.SendHTTPRequest(btcMarketsUnauthPath+marketID+btcMarketsCandles+params.Encode(), &out)
}
// GetTickers gets multiple tickers

View File

@@ -100,7 +100,7 @@ func TestGetOrderbook(t *testing.T) {
func TestGetMarketCandles(t *testing.T) {
t.Parallel()
_, err := b.GetMarketCandles(BTCAUD, kline.OneHour, time.Now().UTC().Add(-time.Hour*24), time.Now().UTC(), -1, -1, -1)
_, err := b.GetMarketCandles(BTCAUD, "1h", time.Now().UTC().Add(-time.Hour*24), time.Now().UTC(), -1, -1, -1)
if err != nil {
t.Error(err)
}
@@ -727,8 +727,49 @@ func TestBTCMarkets_GetHistoricCandles(t *testing.T) {
}
_, err = b.GetHistoricCandles(p, asset.Spot, time.Now().Add(-time.Hour*24).UTC(), time.Now().UTC(), kline.FifteenMin)
if err != nil {
if !errors.Is(err, errInvalidTimeInterval) {
if !errors.As(err, &kline.ErrorKline{}) {
t.Fatal(err)
}
}
}
func TestBTCMarkets_GetHistoricCandlesExtended(t *testing.T) {
start := time.Now().AddDate(0, 0, -1001)
end := time.Now()
p := currency.NewPairFromString(BTCAUD)
_, err := b.GetHistoricCandlesExtended(p, asset.Spot, start, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"1m",
},
{
"OneDay",
kline.OneDay,
"1d",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := b.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -37,6 +37,7 @@ type Trade struct {
Amount float64 `json:"amount,string"`
Price float64 `json:"price,string"`
Timestamp time.Time `json:"timestamp"`
Side string `json:"side"`
}
// tempOrderbook stores orderbook data
@@ -433,3 +434,6 @@ type WsError struct {
Code int64 `json:"code"`
Message string `json:"message"`
}
// CandleResponse holds OHLCV data for exchange
type CandleResponse [][6]string

View File

@@ -152,7 +152,7 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
Asset: asset.Spot,
Exchange: b.Name,
}
case trade:
case tradeEndPoint:
var trade WsTrade
err := json.Unmarshal(respRaw, &trade)
if err != nil {
@@ -283,7 +283,7 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
}
func (b *BTCMarkets) generateDefaultSubscriptions() {
var channels = []string{tick, trade, wsOB}
var channels = []string{tick, tradeEndPoint, wsOB}
enabledCurrencies := b.GetEnabledPairs(asset.Spot)
var subscriptions []wshandler.WebsocketChannelSubscription
for i := range channels {
@@ -299,7 +299,7 @@ func (b *BTCMarkets) generateDefaultSubscriptions() {
// Subscribe sends a websocket message to receive data from the channel
func (b *BTCMarkets) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
unauthChannels := []string{tick, trade, wsOB}
unauthChannels := []string{tick, tradeEndPoint, wsOB}
authChannels := []string{fundChange, heartbeat, orderChange}
switch {
case common.StringDataCompare(unauthChannels, channelToSubscribe.Channel):

View File

@@ -3,6 +3,7 @@ package btcmarkets
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
@@ -107,9 +108,21 @@ func (b *BTCMarkets) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.AutoWithdrawFiat,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.OneHour.Word(): true,
kline.OneDay.Word(): true,
},
ResultLimit: 1000,
},
},
}
@@ -743,7 +756,135 @@ func (b *BTCMarkets) ValidateCredentials() error {
return nil
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *BTCMarkets) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return b.GetMarketCandles(pair.String(), interval, start, end, -1, -1, 0)
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (b *BTCMarkets) FormatExchangeKlineInterval(in kline.Interval) string {
if in == kline.OneDay {
return "1d"
}
return in.Short()
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *BTCMarkets) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
if kline.TotalCandlesPerInterval(start, end, interval) > b.Features.Enabled.Kline.ResultLimit {
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
candles, err := b.GetMarketCandles(b.FormatExchangeCurrency(pair, a).String(),
b.FormatExchangeKlineInterval(interval),
start,
end,
-1,
-1,
-1)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: b.Name,
Pair: b.FormatExchangeCurrency(pair, a),
Asset: asset.Spot,
Interval: interval,
}
for x := range candles {
var tempTime time.Time
var tempData kline.Candle
tempTime, err = time.Parse(time.RFC3339, candles[x][0])
if err != nil {
return kline.Item{}, err
}
tempData.Time = tempTime
tempData.Open, err = strconv.ParseFloat(candles[x][1], 64)
if err != nil {
return kline.Item{}, err
}
tempData.High, err = strconv.ParseFloat(candles[x][2], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Low, err = strconv.ParseFloat(candles[x][3], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Close, err = strconv.ParseFloat(candles[x][4], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Volume, err = strconv.ParseFloat(candles[x][5], 64)
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempData)
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *BTCMarkets) GetHistoricCandlesExtended(p currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !b.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: b.Name,
Pair: p,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, b.Features.Enabled.Kline.ResultLimit)
for x := range dates {
candles, err := b.GetMarketCandles(p.String(),
b.FormatExchangeKlineInterval(interval),
dates[x].Start, dates[x].End, -1, -1, -1)
if err != nil {
return kline.Item{}, err
}
for i := range candles {
var tempTime time.Time
var tempData kline.Candle
tempTime, err = time.Parse(time.RFC3339, candles[i][0])
if err != nil {
return kline.Item{}, err
}
tempData.Time = tempTime
tempData.Open, err = strconv.ParseFloat(candles[i][1], 64)
if err != nil {
return kline.Item{}, err
}
tempData.High, err = strconv.ParseFloat(candles[i][2], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Low, err = strconv.ParseFloat(candles[i][3], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Close, err = strconv.ParseFloat(candles[i][4], 64)
if err != nil {
return kline.Item{}, err
}
tempData.Volume, err = strconv.ParseFloat(candles[i][5], 64)
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempData)
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -653,6 +653,11 @@ func (b *BTSE) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (b *BTSE) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (b *BTSE) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (b *BTSE) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -14,6 +14,7 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -79,11 +80,21 @@ func TestGetTrades(t *testing.T) {
}
func TestGetHistoricRatesGranularityCheck(t *testing.T) {
end := time.Now().UTC()
start := end.Add(-time.Hour * 24)
end := time.Now()
start := end.Add(-time.Hour * 2)
p := currency.NewPair(currency.BTC, currency.USD)
_, err := c.GetHistoricCandles(p, asset.Spot, start, end, kline.OneHour)
if err != nil {
t.Fatal(err)
}
}
_, err := c.GetHistoricCandles(p, asset.Spot, start, end, time.Hour)
func TestCoinbasePro_GetHistoricCandlesExtended(t *testing.T) {
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
p := currency.NewPair(currency.BTC, currency.USD)
_, err := c.GetHistoricCandlesExtended(p, asset.Spot, start, end, kline.OneDay)
if err != nil {
t.Fatal(err)
}

View File

@@ -3,6 +3,7 @@ package coinbasepro
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
@@ -112,9 +113,24 @@ func (c *CoinbasePro) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission |
exchange.AutoWithdrawFiatWithAPIPermission,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.OneHour.Word(): true,
kline.SixHour.Word(): true,
kline.OneDay.Word(): true,
},
ResultLimit: 300,
},
},
}
@@ -682,47 +698,38 @@ func (c *CoinbasePro) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}
// checkInterval checks allowable interval
func checkInterval(i time.Duration) (int64, error) {
switch i.Seconds() {
case 60:
return 60, nil
case 300:
return 300, nil
case 900:
return 900, nil
case 3600:
return 3600, nil
case 21600:
return 21600, nil
case 86400:
return 86400, nil
}
return 0, fmt.Errorf("interval not allowed %v", i.Seconds())
}
// GetHistoricCandles returns a set of candle between two time periods for a
// designated time period
func (c *CoinbasePro) GetHistoricCandles(p currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
i, err := checkInterval(interval)
func (c *CoinbasePro) GetHistoricCandles(p currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !c.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
if kline.TotalCandlesPerInterval(start, end, interval) > c.Features.Enabled.Kline.ResultLimit {
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
}
candles := kline.Item{
Exchange: c.Name,
Pair: p,
Asset: a,
Interval: interval,
}
gran, err := strconv.ParseInt(c.FormatExchangeKlineInterval(interval), 10, 64)
if err != nil {
return kline.Item{}, err
}
history, err := c.GetHistoricRates(c.FormatExchangeCurrency(p, a).String(),
start.Format(time.RFC3339),
end.Format(time.RFC3339),
i)
gran)
if err != nil {
return kline.Item{}, err
}
var candles kline.Item
candles.Asset = a
candles.Exchange = c.Name
candles.Interval = interval
candles.Pair = p
for x := range history {
candles.Candles = append(candles.Candles, kline.Candle{
Time: time.Unix(history[x].Time, 0),
@@ -733,9 +740,56 @@ func (c *CoinbasePro) GetHistoricCandles(p currency.Pair, a asset.Item, start, e
Volume: history[x].Volume,
})
}
candles.SortCandlesByTimestamp(false)
return candles, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (c *CoinbasePro) GetHistoricCandlesExtended(p currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !c.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: c.Name,
Pair: p,
Asset: a,
Interval: interval,
}
gran, err := strconv.ParseInt(c.FormatExchangeKlineInterval(interval), 10, 64)
if err != nil {
return kline.Item{}, err
}
dates := kline.CalcDateRanges(start, end, interval, c.Features.Enabled.Kline.ResultLimit)
for x := range dates {
history, err := c.GetHistoricRates(c.FormatExchangeCurrency(p, a).String(),
dates[x].Start.Format(time.RFC3339),
dates[x].End.Format(time.RFC3339),
gran)
if err != nil {
return kline.Item{}, err
}
for i := range history {
ret.Candles = append(ret.Candles, kline.Candle{
Time: time.Unix(history[i].Time, 0),
Low: history[i].Low,
High: history[i].High,
Open: history[i].Open,
Close: history[i].Close,
Volume: history[i].Volume,
})
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func (c *CoinbasePro) ValidateCredentials() error {

View File

@@ -613,74 +613,48 @@ func (c *Coinbene) GetSwapOrderbook(symbol string, size int64) (Orderbook, error
return s, nil
}
// GetKlines data returns the kline data for a specific symbol
func (c *Coinbene) GetKlines(pair string, start, end time.Time, period string) (resp CandleResponse, err error) {
v := url.Values{}
v.Add("symbol", pair)
if !start.IsZero() {
v.Add("start", strconv.FormatInt(start.Unix(), 10))
}
if !end.IsZero() {
v.Add("end", strconv.FormatInt(end.Unix(), 10))
}
v.Add("period", period)
path := common.EncodeURLValues(coinbeneAPIURL+coinbeneAPIVersion+coinbeneSpotKlines, v)
if err = c.SendHTTPRequest(path, contractKline, &resp); err != nil {
return
}
if resp.Code != 200 {
return resp, errors.New(resp.Message)
}
return
}
// GetSwapKlines data returns the kline data for a specific symbol
func (c *Coinbene) GetSwapKlines(symbol, startTime, endTime, resolution string) (SwapKlines, error) {
var s SwapKlines
func (c *Coinbene) GetSwapKlines(symbol string, start, end time.Time, resolution string) (resp CandleResponse, err error) {
v := url.Values{}
v.Set("symbol", symbol)
v.Set("startTime", startTime)
v.Set("endTime", endTime)
if !start.IsZero() {
v.Add("startTime", strconv.FormatInt(start.Unix(), 10))
}
if !end.IsZero() {
v.Add("endTime", strconv.FormatInt(end.Unix(), 10))
}
v.Set("resolution", resolution)
type resp struct {
Data [][]string `json:"data"`
}
var r resp
path := common.EncodeURLValues(coinbeneSwapAPIURL+coinbeneAPIVersion+coinbeneGetKlines, v)
if err := c.SendHTTPRequest(path, contractKline, &r); err != nil {
return nil, err
if err = c.SendHTTPRequest(path, contractKline, &resp); err != nil {
return
}
for x := range r.Data {
buyTurnover, err := strconv.ParseFloat(r.Data[x][8], 64)
if err != nil {
return nil, err
}
tm, err := strconv.ParseInt(r.Data[x][0], 10, 64)
if err != nil {
return nil, err
}
open, err := strconv.ParseFloat(r.Data[x][1], 64)
if err != nil {
return nil, err
}
closePrice, err := strconv.ParseFloat(r.Data[x][2], 64)
if err != nil {
return nil, err
}
high, err := strconv.ParseFloat(r.Data[x][3], 64)
if err != nil {
return nil, err
}
low, err := strconv.ParseFloat(r.Data[x][4], 64)
if err != nil {
return nil, err
}
volume, err := strconv.ParseFloat(r.Data[x][5], 64)
if err != nil {
return nil, err
}
turnover, err := strconv.ParseFloat(r.Data[x][6], 64)
if err != nil {
return nil, err
}
buyVolume, err := strconv.ParseFloat(r.Data[x][7], 64)
if err != nil {
return nil, err
}
s = append(s, SwapKlineItem{
Time: time.Unix(tm, 0),
Open: open,
Close: closePrice,
High: high,
Low: low,
Volume: volume,
Turnover: turnover,
BuyVolume: buyVolume,
BuyTurnover: buyTurnover,
})
}
return s, nil
return
}
// GetSwapTrades returns a list of trades for a swap symbol

View File

@@ -4,10 +4,12 @@ import (
"log"
"os"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"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/sharedtestvalues"
)
@@ -284,12 +286,19 @@ func TestGetSwapOrderbook(t *testing.T) {
}
}
func TestGetKlines(t *testing.T) {
t.Parallel()
_, err := c.GetKlines(currency.NewPairFromString(spotTestPair).String(),
time.Now().Add(-time.Hour*1), time.Now(), "1")
if err != nil {
t.Fatal(err)
}
}
func TestGetSwapKlines(t *testing.T) {
t.Parallel()
_, err := c.GetSwapKlines(swapTestPair,
"1573184608",
"1573184808",
"1")
_, err := c.GetSwapKlines(currency.NewPairFromString(swapTestPair).String(),
time.Now().Add(-time.Hour*1), time.Now(), "1")
if err != nil {
t.Error(err)
}
@@ -681,3 +690,78 @@ func TestWsUserOrder(t *testing.T) {
t.Error(err)
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString(spotTestPair)
startTime := time.Now().Add(-time.Hour * 24)
_, err := c.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
currencyPairSwap := currency.NewPairFromString(swapTestPair)
_, err = c.GetHistoricCandles(currencyPairSwap, asset.PerpetualSwap, startTime, time.Now(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString(spotTestPair)
startTime := time.Now().Add(-time.Hour * 24)
_, err := c.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"1",
},
{
"OneHour",
kline.OneHour,
"60",
},
{
"OneDay",
kline.OneDay,
"D",
},
{
"OneWeek",
kline.OneWeek,
"W",
},
{
"OneMonth",
kline.OneMonth,
"M",
},
{
"AllOther",
kline.TwoWeek,
"",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := c.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -354,3 +354,10 @@ type SwapFundingRate struct {
FeeRate float64 `json:"feeRate,string"`
Leverage int64 `json:"leverage"`
}
// CandleResponse stores returned kline data
type CandleResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data [][]interface{} `json:"data"`
}

View File

@@ -1,7 +1,9 @@
package coinbene
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
@@ -114,9 +116,30 @@ func (c *Coinbene) SetDefaults() {
},
WithdrawPermissions: exchange.NoFiatWithdrawals |
exchange.WithdrawCryptoViaWebsiteOnly,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.ThreeDay.Word(): true,
kline.OneWeek.Word(): true,
},
},
},
}
c.Requester = request.New(c.Name,
@@ -727,7 +750,111 @@ func (c *Coinbene) ValidateCredentials() error {
return c.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (c *Coinbene) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
// FormatExchangeKlineInterval returns Interval to string
func (c *Coinbene) FormatExchangeKlineInterval(in kline.Interval) string {
switch in {
case kline.OneMin, kline.ThreeMin, kline.FiveMin, kline.FifteenMin,
kline.ThirtyMin, kline.OneHour, kline.TwoHour, kline.FourHour, kline.SixHour, kline.TwelveHour:
return strconv.FormatFloat(in.Duration().Minutes(), 'f', 0, 64)
case kline.OneDay:
return "D"
case kline.OneWeek:
return "W"
case kline.OneMonth:
return "M"
}
return ""
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (c *Coinbene) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !c.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
var candles CandleResponse
var err error
if a == asset.PerpetualSwap {
candles, err = c.GetSwapKlines(c.FormatExchangeCurrency(pair, asset.PerpetualSwap).String(),
start, end,
c.FormatExchangeKlineInterval(interval))
} else {
candles, err = c.GetKlines(c.FormatExchangeCurrency(pair, asset.Spot).String(),
start, end,
c.FormatExchangeKlineInterval(interval))
}
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: c.Name,
Pair: pair,
Interval: interval,
}
for x := range candles.Data {
var tempCandle kline.Candle
tempTime := candles.Data[x][0].(string)
timestamp, err := time.Parse(time.RFC3339, tempTime)
if err != nil {
continue
}
tempCandle.Time = timestamp
open, ok := candles.Data[x][1].(string)
if !ok {
return kline.Item{}, errors.New("open conversion failed")
}
tempCandle.Open, err = strconv.ParseFloat(open, 64)
if err != nil {
return kline.Item{}, err
}
high, ok := candles.Data[x][2].(string)
if !ok {
return kline.Item{}, errors.New("high conversion failed")
}
tempCandle.High, err = strconv.ParseFloat(high, 64)
if err != nil {
return kline.Item{}, err
}
low, ok := candles.Data[x][3].(string)
if !ok {
return kline.Item{}, errors.New("low conversion failed")
}
tempCandle.Low, err = strconv.ParseFloat(low, 64)
if err != nil {
return kline.Item{}, err
}
closeTemp, ok := candles.Data[x][4].(string)
if !ok {
return kline.Item{}, errors.New("close conversion failed")
}
tempCandle.Close, err = strconv.ParseFloat(closeTemp, 64)
if err != nil {
return kline.Item{}, err
}
vol, ok := candles.Data[x][5].(string)
if !ok {
return kline.Item{}, errors.New("vol conversion failed")
}
tempCandle.Volume, err = strconv.ParseFloat(vol, 64)
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempCandle)
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (c *Coinbene) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return c.GetHistoricCandles(pair, a, start, end, interval)
}

View File

@@ -903,6 +903,11 @@ func (c *COINUT) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (c *COINUT) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (c *COINUT) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (c *COINUT) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -6,6 +6,7 @@ import (
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/log"
@@ -801,3 +803,14 @@ func (e *Base) DisableRateLimiter() error {
func (e *Base) EnableRateLimiter() error {
return e.Requester.EnableRateLimiter()
}
// KlineIntervalEnabled returns if requested interval is enabled on exchange
func (e *Base) KlineIntervalEnabled(in kline.Interval) bool {
return e.Features.Enabled.Kline.Intervals[in.Word()]
}
// FormatExchangeKlineInterval returns Interval to string
// Exchanges can override this if they require custom formatting
func (e *Base) FormatExchangeKlineInterval(in kline.Interval) string {
return strconv.FormatFloat(in.Duration().Seconds(), 'f', 0, 64)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -1479,3 +1480,35 @@ func TestGetFormattedPairAndAssetType(t *testing.T) {
t.Error("Expected error")
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"60",
},
{
"OneDay",
kline.OneDay,
"86400",
},
}
b := Base{}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := b.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -148,6 +149,7 @@ type Features struct {
// FeaturesEnabled stores the exchange enabled features
type FeaturesEnabled struct {
AutoPairUpdates bool
Kline kline.ExchangeCapabilitiesEnabled
}
// FeaturesSupported stores the exchanges supported features
@@ -157,6 +159,7 @@ type FeaturesSupported struct {
Websocket bool
WebsocketCapabilities protocol.Features
WithdrawPermissions uint32
Kline kline.ExchangeCapabilitiesSupported
}
// API stores the exchange API settings

View File

@@ -584,6 +584,11 @@ func (e *EXMO) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (e *EXMO) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (e *EXMO) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (e *EXMO) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -15,7 +15,6 @@ import (
"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"
@@ -870,27 +869,6 @@ func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 {
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 {

View File

@@ -864,6 +864,17 @@ func TestGetHistoricCandles(t *testing.T) {
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
t.Parallel()
currencyPair := currency.NewPairFromString(spotPair)
start := time.Date(2019, 11, 12, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 0, 5)
_, err := f.GetHistoricCandlesExtended(currencyPair, asset.Spot, start, end, kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func TestParsingWSFillData(t *testing.T) {
t.Parallel()
data := []byte(`{

View File

@@ -111,9 +111,25 @@ func (f *FTX) SetDefaults() {
GetOrder: true,
},
WithdrawPermissions: exchange.NoAPIWithdrawalMethods,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.FifteenSecond.Word(): true,
kline.OneMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.OneHour.Word(): true,
kline.FourHour.Word(): true,
kline.OneDay.Word(): true,
},
ResultLimit: 5000,
},
},
}
@@ -845,29 +861,76 @@ func (f *FTX) ValidateCredentials() error {
}
// 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)
func (f *FTX) GetHistoricCandles(p currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !f.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ohlcData, err := f.GetHistoricalData(f.FormatExchangeCurrency(p, a).String(),
f.FormatExchangeKlineInterval(interval),
strconv.FormatInt(int64(f.Features.Enabled.Kline.ResultLimit), 10),
start, end)
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
ret := kline.Item{
Exchange: f.Name,
Pair: p,
Asset: a,
Interval: interval,
}
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)
ret.Candles = append(ret.Candles, kline.Candle{
Time: ohlcData[x].StartTime,
Open: ohlcData[x].Open,
High: ohlcData[x].High,
Low: ohlcData[x].Low,
Close: ohlcData[x].Close,
Volume: ohlcData[x].Volume,
})
}
return resp, nil
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (f *FTX) GetHistoricCandlesExtended(p currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !f.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: f.Name,
Pair: p,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, f.Features.Enabled.Kline.ResultLimit)
for x := range dates {
ohlcData, err := f.GetHistoricalData(f.FormatExchangeCurrency(p, a).String(),
f.FormatExchangeKlineInterval(interval),
strconv.FormatInt(int64(f.Features.Enabled.Kline.ResultLimit), 10),
dates[x].Start, dates[x].End)
if err != nil {
return kline.Item{}, err
}
for i := range ohlcData {
ret.Candles = append(ret.Candles, kline.Candle{
Time: ohlcData[i].StartTime,
Open: ohlcData[i].Open,
High: ohlcData[i].High,
Low: ohlcData[i].Low,
Close: ohlcData[i].Close,
Volume: ohlcData[i].Volume,
})
}
}
return ret, nil
}

View File

@@ -13,6 +13,7 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
@@ -184,8 +185,8 @@ func (g *Gateio) GetOrderbook(symbol string) (Orderbook, error) {
}
// GetSpotKline returns kline data for the most recent time period
func (g *Gateio) GetSpotKline(arg KlinesRequestParams) ([]*KLineResponse, error) {
urlPath := fmt.Sprintf("%s/%s/%s/%s?group_sec=%d&range_hour=%d",
func (g *Gateio) GetSpotKline(arg KlinesRequestParams) (kline.Item, error) {
urlPath := fmt.Sprintf("%s/%s/%s/%s?group_sec=%s&range_hour=%d",
g.API.Endpoints.URLSecondary,
gateioAPIVersion,
gateioKline,
@@ -196,58 +197,59 @@ func (g *Gateio) GetSpotKline(arg KlinesRequestParams) ([]*KLineResponse, error)
var rawKlines map[string]interface{}
err := g.SendHTTPRequest(urlPath, &rawKlines)
if err != nil {
return nil, err
return kline.Item{}, err
}
result := kline.Item{
Exchange: g.Name,
}
var result []*KLineResponse
if rawKlines == nil || rawKlines["data"] == nil {
return nil, fmt.Errorf("rawKlines is nil. Err: %s", err)
return kline.Item{}, errors.New("rawKlines is nil")
}
rawKlineDatasString, _ := json.Marshal(rawKlines["data"].([]interface{}))
var rawKlineDatas [][]interface{}
if err := json.Unmarshal(rawKlineDatasString, &rawKlineDatas); err != nil {
return nil, fmt.Errorf("rawKlines unmarshal failed. Err: %s", err)
return kline.Item{}, fmt.Errorf("rawKlines unmarshal failed. Err: %s", err)
}
for _, k := range rawKlineDatas {
otString, _ := strconv.ParseFloat(k[0].(string), 64)
otString, err := strconv.ParseFloat(k[0].(string), 64)
if err != nil {
return kline.Item{}, err
}
ot, err := convert.TimeFromUnixTimestampFloat(otString)
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.OpenTime. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.OpenTime. Err: %s", err)
}
_vol, err := convert.FloatFromString(k[1])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Volume. Err: %s", err)
}
_id, err := convert.FloatFromString(k[0])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Id. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Volume. Err: %s", err)
}
_close, err := convert.FloatFromString(k[2])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Close. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Close. Err: %s", err)
}
_high, err := convert.FloatFromString(k[3])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.High. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.High. Err: %s", err)
}
_low, err := convert.FloatFromString(k[4])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Low. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Low. Err: %s", err)
}
_open, err := convert.FloatFromString(k[5])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Open. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Open. Err: %s", err)
}
result = append(result, &KLineResponse{
ID: _id,
KlineTime: ot,
Volume: _vol,
Close: _close,
High: _high,
Low: _low,
Open: _open,
result.Candles = append(result.Candles, kline.Candle{
Time: ot,
Volume: _vol,
Close: _close,
High: _high,
Low: _low,
Open: _open,
})
}
return result, nil

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"os"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -13,6 +14,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -148,11 +151,10 @@ func TestGetOrderbook(t *testing.T) {
func TestGetSpotKline(t *testing.T) {
t.Parallel()
_, err := g.GetSpotKline(KlinesRequestParams{
Symbol: "btc_usdt",
GroupSec: TimeIntervalFiveMinutes, // 5 minutes or less
HourSize: 1, // 1 hour data
GroupSec: "5", // 5 minutes or less
HourSize: 1, // 1 hour data
})
if err != nil {
@@ -578,7 +580,6 @@ func setupWSTestAuth(t *testing.T) {
// TestWsUnsubscribe dials websocket, sends an unsubscribe request.
func TestWsUnsubscribe(t *testing.T) {
setupWSTestAuth(t)
g.Verbose = true
err := g.Unsubscribe(wshandler.WebsocketChannelSubscription{
Channel: "ticker.subscribe",
Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"),
@@ -762,3 +763,52 @@ func TestParseTime(t *testing.T) {
t.Error("unexpected result")
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_USDT")
startTime := time.Now().Add(-time.Hour * 6)
_, err := g.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_USDT")
startTime := time.Now().Add(-time.Hour * 6)
_, err := g.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"60",
},
{
"OneDay",
kline.OneDay,
"86400",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := g.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -52,7 +52,7 @@ type BalancesResponse struct {
type KlinesRequestParams struct {
Symbol string // Required field; example LTCBTC,BTCUSDT
HourSize int // How many hours of data
GroupSec TimeInterval
GroupSec string
}
// KLineResponse holds the kline response data

View File

@@ -108,12 +108,29 @@ func (g *Gateio) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
},
},
},
}
g.Requester = request.New(g.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
@@ -710,7 +727,39 @@ func (g *Gateio) ValidateCredentials() error {
return g.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (g *Gateio) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (g *Gateio) FormatExchangeKlineInterval(in kline.Interval) string {
return strconv.FormatFloat(in.Duration().Seconds(), 'f', 0, 64)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (g *Gateio) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !g.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
hours := end.Sub(start).Hours()
params := KlinesRequestParams{
Symbol: g.FormatExchangeCurrency(pair, a).String(),
GroupSec: g.FormatExchangeKlineInterval(interval),
HourSize: int(hours),
}
klineData, err := g.GetSpotKline(params)
if err != nil {
return kline.Item{}, err
}
klineData.Interval = interval
klineData.Pair = pair
klineData.Asset = a
klineData.SortCandlesByTimestamp(false)
return klineData, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (g *Gateio) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return g.GetHistoricCandles(pair, a, start, end, interval)
}

View File

@@ -575,6 +575,11 @@ func (g *Gemini) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (g *Gemini) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
func (g *Gemini) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (g *Gemini) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"strconv"
"time"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
@@ -201,11 +202,10 @@ func (h *HitBTC) GetOrderbook(currencyPair string, limit int) (Orderbook, error)
// GetCandles returns candles which is used for OHLC a specific currency.
// Note: Result contain candles only with non zero volume.
func (h *HitBTC) GetCandles(currencyPair, limit, period string) ([]ChartData, error) {
func (h *HitBTC) GetCandles(currencyPair, limit, period string, start, end time.Time) ([]ChartData, error) {
// limit Limit of candles, default 100.
// period One of: M1 (one minute), M3, M5, M15, M30, H1, H4, D1, D7, 1M (one month). Default is M30 (30 minutes).
vals := url.Values{}
if limit != "" {
vals.Set("limit", limit)
}
@@ -214,6 +214,18 @@ func (h *HitBTC) GetCandles(currencyPair, limit, period string) ([]ChartData, er
vals.Set("period", period)
}
if !end.IsZero() && start.After(end) {
return nil, errors.New("start time cannot be after end time")
}
if !start.IsZero() {
vals.Set("from", start.Format(time.RFC3339))
}
if !end.IsZero() {
vals.Set("till", end.Format(time.RFC3339))
}
var resp []ChartData
path := fmt.Sprintf("%s/%s/%s?%s", h.API.Endpoints.URL, apiV2Candles, currencyPair, vals.Encode())
return resp, h.SendHTTPRequest(path, &resp)

View File

@@ -14,6 +14,7 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -71,12 +72,42 @@ func TestGetTrades(t *testing.T) {
}
func TestGetChartCandles(t *testing.T) {
_, err := h.GetCandles("BTCUSD", "", "")
_, err := h.GetCandles("BTCUSD", "", "D1", time.Now().Add(-24*time.Hour), time.Now())
if err != nil {
t.Error("Test faild - HitBTC GetChartData() error", err)
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSD")
startTime := time.Now().Add(-time.Hour * 24)
end := time.Now()
_, err := h.GetHistoricCandles(currencyPair, asset.Spot, startTime, end, kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = h.GetHistoricCandles(currencyPair, asset.Spot, startTime, end, kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSD")
startTime := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err := h.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, end, kline.OneHour)
if err != nil {
t.Fatal(err)
}
_, err = h.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, end, kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestGetCurrencies(t *testing.T) {
_, err := h.GetCurrencies()
if err != nil {
@@ -918,3 +949,44 @@ func TestWsTrades(t *testing.T) {
t.Error(err)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"M1",
},
{
"OneDay",
kline.OneDay,
"D1",
},
{
"SevenDay",
kline.SevenDay,
"D7",
},
{
"AllOther",
kline.OneMonth,
"",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := h.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -109,9 +109,26 @@ func (h *HitBTC) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
DateRanges: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.FourHour.Word(): true,
kline.OneDay.Word(): true,
kline.SevenDay.Word(): true,
},
ResultLimit: 1000,
},
},
}
@@ -628,7 +645,93 @@ func (h *HitBTC) ValidateCredentials() error {
return h.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (h *HitBTC) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (h *HitBTC) FormatExchangeKlineInterval(in kline.Interval) string {
switch in {
case kline.OneMin, kline.ThreeMin,
kline.FiveMin, kline.FifteenMin, kline.ThirtyMin:
return "M" + in.Short()[:len(in.Short())-1]
case kline.OneDay:
return "D1"
case kline.SevenDay:
return "D7"
}
return ""
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (h *HitBTC) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !h.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
data, err := h.GetCandles(h.FormatExchangeCurrency(pair, a).String(),
strconv.FormatInt(int64(h.Features.Enabled.Kline.ResultLimit), 10),
h.FormatExchangeKlineInterval(interval),
start, end)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: h.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
for x := range data {
ret.Candles = append(ret.Candles, kline.Candle{
Time: data[x].Timestamp,
Open: data[x].Open,
High: data[x].Max,
Low: data[x].Min,
Close: data[x].Close,
Volume: data[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (h *HitBTC) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !h.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: h.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, h.Features.Enabled.Kline.ResultLimit)
for y := range dates {
data, err := h.GetCandles(h.FormatExchangeCurrency(pair, a).String(),
strconv.FormatInt(int64(h.Features.Enabled.Kline.ResultLimit), 10),
h.FormatExchangeKlineInterval(interval),
dates[y].Start, dates[y].End)
if err != nil {
return kline.Item{}, err
}
for i := range data {
ret.Candles = append(ret.Candles, kline.Candle{
Time: data[i].Timestamp,
Open: data[i].Open,
High: data[i].Max,
Low: data[i].Min,
Close: data[i].Close,
Volume: data[i].Volume,
})
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -72,7 +72,7 @@ type HUOBI struct {
func (h *HUOBI) GetSpotKline(arg KlinesRequestParams) ([]KlineItem, error) {
vals := url.Values{}
vals.Set("symbol", arg.Symbol)
vals.Set("period", string(arg.Period))
vals.Set("period", arg.Period)
if arg.Size != 0 {
vals.Set("size", strconv.Itoa(arg.Size))

View File

@@ -5,6 +5,7 @@ import (
"os"
"strconv"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -12,6 +13,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -96,7 +99,7 @@ func TestGetSpotKline(t *testing.T) {
t.Parallel()
_, err := h.GetSpotKline(KlinesRequestParams{
Symbol: testSymbol,
Period: TimeIntervalHour,
Period: "1min",
Size: 0,
})
if err != nil {
@@ -104,6 +107,39 @@ func TestGetSpotKline(t *testing.T) {
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
startTime := time.Now().Add(-time.Hour * 1)
_, err := h.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = h.GetHistoricCandles(currencyPair, asset.Spot, startTime.AddDate(0, 0, -7), time.Now(), kline.OneDay)
if err != nil {
t.Fatal(err)
}
_, err = h.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
startTime := time.Now().Add(-time.Hour * 1)
_, err := h.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = h.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestGetMarketDetailMerged(t *testing.T) {
t.Parallel()
_, err := h.GetMarketDetailMerged(testSymbol)
@@ -1032,3 +1068,59 @@ func TestStringToOrderType(t *testing.T) {
}
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"1min",
},
{
"FourHour",
kline.FourHour,
"4hour",
},
{
"OneDay",
kline.OneDay,
"1day",
},
{
"OneWeek",
kline.OneWeek,
"1week",
},
{
"OneMonth",
kline.OneMonth,
"1mon",
},
{
"OneYear",
kline.OneYear,
"1year",
},
{
"AllOthers",
kline.TwoWeek,
"",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := h.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -299,27 +299,11 @@ var (
// KlinesRequestParams represents Klines request data.
type KlinesRequestParams struct {
Symbol string // Symbol to be used; example btcusdt, bccbtc......
Period TimeInterval // Kline time interval; 1min, 5min, 15min......
Size int // Size; [1-2000]
Symbol string // Symbol to be used; example btcusdt, bccbtc......
Period string // Kline time interval; 1min, 5min, 15min......
Size int // Size; [1-2000]
}
// TimeInterval base type
type TimeInterval string
// TimeInterval vars
var (
TimeIntervalMinute = TimeInterval("1min")
TimeIntervalFiveMinutes = TimeInterval("5min")
TimeIntervalFifteenMinutes = TimeInterval("15min")
TimeIntervalThirtyMinutes = TimeInterval("30min")
TimeIntervalHour = TimeInterval("60min")
TimeIntervalDay = TimeInterval("1day")
TimeIntervalWeek = TimeInterval("1week")
TimeIntervalMohth = TimeInterval("1mon")
TimeIntervalYear = TimeInterval("1year")
)
// WsRequest defines a request data structure
type WsRequest struct {
Topic string `json:"req,omitempty"`

View File

@@ -107,9 +107,27 @@ func (h *HUOBI) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.FourHour.Word(): true,
kline.OneDay.Word(): true,
kline.OneWeek.Word(): true,
kline.OneMonth.Word(): true,
kline.OneYear.Word(): true,
},
ResultLimit: 2000,
},
},
}
@@ -924,7 +942,70 @@ func (h *HUOBI) ValidateCredentials() error {
return h.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (h *HUOBI) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (h *HUOBI) FormatExchangeKlineInterval(in kline.Interval) string {
switch in {
case kline.OneMin, kline.FiveMin, kline.FifteenMin, kline.ThirtyMin:
return in.Short() + "in"
case kline.FourHour:
return "4hour"
case kline.OneDay:
return "1day"
case kline.OneMonth:
return "1mon"
case kline.OneWeek:
return "1week"
case kline.OneYear:
return "1year"
}
return ""
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (h *HUOBI) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !h.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
klineParams := KlinesRequestParams{
Period: h.FormatExchangeKlineInterval(interval),
Symbol: h.FormatExchangeCurrency(pair, a).String(),
}
candles, err := h.GetSpotKline(klineParams)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: h.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
for x := range candles {
if time.Unix(candles[x].ID, 0).Before(start) ||
time.Unix(candles[x].ID, 0).After(end) {
continue
}
ret.Candles = append(ret.Candles, kline.Candle{
Time: time.Unix(candles[x].ID, 0),
Open: candles[x].Open,
High: candles[x].Close,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (h *HUOBI) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return h.GetHistoricCandles(pair, a, start, end, interval)
}

View File

@@ -73,7 +73,8 @@ type IBotExchange interface {
GetDefaultConfig() (*config.ExchangeConfig, error)
GetBase() *Base
SupportsAsset(assetType asset.Item) bool
GetHistoricCandles(p currency.Pair, a asset.Item, timeStart, timeEnd time.Time, interval time.Duration) (kline.Item, error)
GetHistoricCandles(p currency.Pair, a asset.Item, timeStart, timeEnd time.Time, interval kline.Interval) (kline.Item, error)
GetHistoricCandlesExtended(p currency.Pair, a asset.Item, timeStart, timeEnd time.Time, interval kline.Interval) (kline.Item, error)
DisableRateLimiter() error
EnableRateLimiter() error
}

View File

@@ -61,9 +61,9 @@ func (i *ItBit) GetOrderbook(currencyPair string) (OrderbookResponse, error) {
//
// currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR"
// timestamp - matchNumber, only executions after this will be returned
func (i *ItBit) GetTradeHistory(currencyPair, timestamp string) (Trades, error) {
func (i *ItBit) GetTradeHistory(currencyPair, tradeID string) (Trades, error) {
response := Trades{}
req := "trades?since=" + timestamp
req := "trades?since=" + tradeID
path := fmt.Sprintf("%s/%s/%s/%s", i.API.Endpoints.URL, itbitMarkets, currencyPair, req)
return response, i.SendHTTPRequest(path, &response)

View File

@@ -571,6 +571,11 @@ func (i *ItBit) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (i *ItBit) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (i *ItBit) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (i *ItBit) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
@@ -12,8 +13,8 @@ import (
)
// CreateKline creates candles out of trade history data for a set time interval
func CreateKline(trades []order.TradeHistory, interval time.Duration, p currency.Pair, a asset.Item, exchange string) (Item, error) {
if interval < time.Minute {
func CreateKline(trades []order.TradeHistory, interval Interval, p currency.Pair, a asset.Item, exchange string) (Item, error) {
if interval.Duration() < time.Minute {
return Item{}, fmt.Errorf("invalid time interval: [%s]", interval)
}
@@ -22,15 +23,15 @@ func CreateKline(trades []order.TradeHistory, interval time.Duration, p currency
return Item{}, err
}
timeIntervalStart := trades[0].Timestamp.Truncate(interval)
timeIntervalStart := trades[0].Timestamp.Truncate(interval.Duration())
timeIntervalEnd := trades[len(trades)-1].Timestamp
// Adds time interval buffer zones
var timeIntervalCache [][]order.TradeHistory
var candleStart []time.Time
for t := timeIntervalStart; t.Before(timeIntervalEnd); t = t.Add(interval) {
timeBufferEnd := t.Add(interval)
for t := timeIntervalStart; t.Before(timeIntervalEnd); t = t.Add(interval.Duration()) {
timeBufferEnd := t.Add(interval.Duration())
insertionCount := 0
var zonedTradeHistory []order.TradeHistory
@@ -130,3 +131,165 @@ func validateData(trades []order.TradeHistory) error {
})
return nil
}
// String returns numeric string
func (i Interval) String() string {
return i.Duration().String()
}
// Word returns text version of Interval
func (i Interval) Word() string {
return durationToWord(i)
}
// Duration returns interval casted as time.Duration for compatibility
func (i Interval) Duration() time.Duration {
return time.Duration(i)
}
// Short returns short string version of interval
func (i Interval) Short() string {
s := i.String()
if strings.HasSuffix(s, "m0s") {
s = s[:len(s)-2]
}
if strings.HasSuffix(s, "h0m") {
s = s[:len(s)-2]
}
return s
}
// durationToWord returns english version of interval
func durationToWord(in Interval) string {
switch in {
case OneMin:
return "onemin"
case ThreeMin:
return "threemin"
case FiveMin:
return "fivemin"
case TenMin:
return "tenmin"
case FifteenMin:
return "fifteenmin"
case ThirtyMin:
return "thirtymin"
case OneHour:
return "onehour"
case TwoHour:
return "twohour"
case FourHour:
return "fourhour"
case SixHour:
return "sixhour"
case EightHour:
return "eighthour"
case TwelveHour:
return "twelvehour"
case OneDay:
return "oneday"
case ThreeDay:
return "threeday"
case FifteenDay:
return "fifteenday"
case OneWeek:
return "oneweek"
case TwoWeek:
return "twoweek"
case OneMonth:
return "onemonth"
case OneYear:
return "oneyear"
default:
return "notfound"
}
}
// TotalCandlesPerInterval turns total candles per period for interval
func TotalCandlesPerInterval(start, end time.Time, interval Interval) (out uint32) {
switch interval {
case OneMin:
out = uint32(end.Sub(start).Minutes())
case ThreeMin:
out = uint32(end.Sub(start).Minutes() / 3)
case FiveMin:
out = uint32(end.Sub(start).Minutes() / 5)
case TenMin:
out = uint32(end.Sub(start).Minutes() / 10)
case FifteenMin:
out = uint32(end.Sub(start).Minutes() / 15)
case ThirtyMin:
out = uint32(end.Sub(start).Minutes() / 30)
case OneHour:
out = uint32(end.Sub(start).Hours())
case TwoHour:
out = uint32(end.Sub(start).Hours() / 2)
case FourHour:
out = uint32(end.Sub(start).Hours() / 4)
case SixHour:
out = uint32(end.Sub(start).Hours() / 6)
case EightHour:
out = uint32(end.Sub(start).Hours() / 8)
case TwelveHour:
out = uint32(end.Sub(start).Hours() / 12)
case OneDay:
out = uint32(end.Sub(start).Hours() / 24)
case ThreeDay:
out = uint32(end.Sub(start).Hours() / 72)
case FifteenDay:
out = uint32(end.Sub(start).Hours() / (24 * 15))
case OneWeek:
out = uint32(end.Sub(start).Hours()) / (24 * 7)
case TwoWeek:
out = uint32(end.Sub(start).Hours() / (24 * 14))
case OneYear:
out = uint32(end.Sub(start).Hours() / 8760)
}
return out
}
// CalcDateRanges returns slice of start/end times based on start & end date
func CalcDateRanges(start, end time.Time, interval Interval, limit uint32) (out []DateRange) {
total := TotalCandlesPerInterval(start, end, interval)
if total < limit {
return []DateRange{{
Start: start,
End: end,
},
}
}
var allDateIntervals []time.Time
var y uint32
var lastNum int
for d := start; !d.After(end); d = d.Add(interval.Duration()) {
allDateIntervals = append(allDateIntervals, d)
}
for x := range allDateIntervals {
if y == limit {
out = append(out, DateRange{
allDateIntervals[x-int(limit)],
allDateIntervals[x],
})
y = 0
lastNum = x
}
y++
}
if allDateIntervals != nil {
out = append(out, DateRange{
Start: allDateIntervals[lastNum+1],
End: allDateIntervals[len(allDateIntervals)-1],
})
}
return out
}
// SortCandlesByTimestamp sorts candles by timestamp
func (k *Item) SortCandlesByTimestamp(asc bool) {
sort.Slice(k.Candles, func(i, j int) bool {
if asc {
return k.Candles[i].Time.After(k.Candles[j].Time)
}
return k.Candles[i].Time.Before(k.Candles[j].Time)
})
}

View File

@@ -2,6 +2,7 @@ package kline
import (
"math/rand"
"strings"
"testing"
"time"
@@ -71,7 +72,7 @@ func TestValidateData(t *testing.T) {
func TestCreateKline(t *testing.T) {
c, err := CreateKline(nil,
time.Minute,
OneMin,
currency.NewPair(currency.BTC, currency.USD),
asset.Spot,
"Binance")
@@ -113,3 +114,190 @@ func TestCreateKline(t *testing.T) {
t.Fatal("no data returned, expecting a lot.")
}
}
func TestKlineWord(t *testing.T) {
if OneDay.Word() != "oneday" {
t.Fatalf("unexpected result: %v", OneDay.Word())
}
}
func TestKlineDuration(t *testing.T) {
if OneDay.Duration() != time.Hour*24 {
t.Fatalf("unexpected result: %v", OneDay.Duration())
}
}
func TestKlineShort(t *testing.T) {
if OneDay.Short() != "24h" {
t.Fatalf("unexpected result: %v", OneDay.Short())
}
}
func TestDurationToWord(t *testing.T) {
testCases := []struct {
name string
interval Interval
}{
{
"OneMin",
OneMin,
},
{
"ThreeMin",
ThreeMin,
},
{
"FiveMin",
FiveMin,
},
{
"TenMin",
TenMin,
},
{
"FifteenMin",
FifteenMin,
},
{
"ThirtyMin",
ThirtyMin,
},
{
"OneHour",
OneHour,
},
{
"TwoHour",
TwoHour,
},
{
"FourHour",
FourHour,
},
{
"SixHour",
SixHour,
},
{
"EightHour",
OneHour * 8,
},
{
"TwelveHour",
TwelveHour,
},
{
"OneDay",
OneDay,
},
{
"ThreeDay",
ThreeDay,
},
{
"FifteenDay",
FifteenDay,
},
{
"OneWeek",
OneWeek,
},
{
"TwoWeek",
TwoWeek,
},
{
"OneMonth",
OneMonth,
},
{
"notfound",
Interval(time.Hour * 1337),
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
v := durationToWord(test.interval)
if !strings.EqualFold(v, test.name) {
t.Fatalf("%v: received %v expected %v", test.name, v, test.name)
}
})
}
}
func TestKlineErrors(t *testing.T) {
v := ErrorKline{
Interval: OneYear,
}
if v.Error() != "oneyear interval unsupported by exchange" {
t.Fatal("unexpected error returned")
}
if v.Unwrap().Error() != "8760h0m0s interval unsupported by exchange" {
t.Fatal("unexpected error returned")
}
}
func TestTotalCandlesPerInterval(t *testing.T) {
end := time.Now()
start := end.AddDate(-1, 0, 0)
v := TotalCandlesPerInterval(start, end, OneYear)
if v != 1 {
t.Fatalf("unexpected result expected 1 received %v", v)
}
v = TotalCandlesPerInterval(start, end, FifteenDay)
if v != 24 {
t.Fatalf("unexpected result expected 24 received %v", v)
}
}
func TestCalcDateRanges(t *testing.T) {
start := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
v := CalcDateRanges(start, end, OneMin, 300)
if v[0].Start.Unix() != time.Unix(1546300800, 0).Unix() {
t.Fatalf("unexpected result received %v", v[0].Start.Unix())
}
v = CalcDateRanges(time.Now(), time.Now().AddDate(0, 0, 1), OneDay, 100)
if len(v) != 1 {
t.Fatal("expected CalcDateRanges() with a Candle count lower than limit to return 1 result")
}
}
func TestItem_SortCandlesByTimestamp(t *testing.T) {
var tempKline = Item{
Exchange: "testExchange",
Pair: currency.NewPair(currency.BTC, currency.USDT),
Asset: asset.Spot,
Interval: OneDay,
}
for x := 0; x < 100; x++ {
y := rand.Float64()
tempKline.Candles = append(tempKline.Candles,
Candle{
Time: time.Now().AddDate(0, 0, -x),
Open: y,
High: y + float64(x),
Low: y - float64(x),
Close: y,
Volume: y,
})
}
tempKline.SortCandlesByTimestamp(false)
if tempKline.Candles[0].Time.After(tempKline.Candles[1].Time) {
t.Fatal("expected kline.Candles to be in descending order")
}
tempKline.SortCandlesByTimestamp(true)
if tempKline.Candles[0].Time.Before(tempKline.Candles[1].Time) {
t.Fatal("expected kline.Candles to be in ascending order")
}
}

View File

@@ -0,0 +1,96 @@
package kline
import (
"fmt"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// Consts here define basic time intervals
const (
FifteenSecond = Interval(15 * time.Second)
OneMin = Interval(time.Minute)
ThreeMin = 3 * OneMin
FiveMin = 5 * OneMin
TenMin = 10 * OneMin
FifteenMin = 15 * OneMin
ThirtyMin = 30 * OneMin
OneHour = Interval(time.Hour)
TwoHour = 2 * OneHour
FourHour = 4 * OneHour
SixHour = 6 * OneHour
EightHour = 8 * OneHour
TwelveHour = 12 * OneHour
OneDay = 24 * OneHour
ThreeDay = 3 * OneDay
SevenDay = 7 * OneDay
FifteenDay = 15 * OneDay
OneWeek = 7 * OneDay
TwoWeek = 2 * OneWeek
OneMonth = 31 * OneDay
OneYear = 365 * OneDay
)
const (
// ErrUnsupportedInterval locale for an unsupported interval
ErrUnsupportedInterval = "%s interval unsupported by exchange"
// ErrRequestExceedsExchangeLimits locale for exceeding rate limits message
ErrRequestExceedsExchangeLimits = "requested data would exceed exchange limits please lower range or use GetHistoricCandlesEx"
)
// Item holds all the relevant information for internal kline elements
type Item struct {
Exchange string
Pair currency.Pair
Asset asset.Item
Interval Interval
Candles []Candle
}
// Candle holds historic rate information.
type Candle struct {
Time time.Time
Open float64
High float64
Low float64
Close float64
Volume float64
}
// ExchangeCapabilitiesSupported all kline related exchange supported options
type ExchangeCapabilitiesSupported struct {
Intervals bool
DateRanges bool
}
// ExchangeCapabilitiesEnabled all kline related exchange enabled options
type ExchangeCapabilitiesEnabled struct {
Intervals map[string]bool `json:"intervals,omitempty"`
ResultLimit uint32
}
// Interval type for kline Interval usage
type Interval time.Duration
// ErrorKline struct to hold kline interval errors
type ErrorKline struct {
Interval Interval
}
// Error returns short interval unsupported message
func (k ErrorKline) Error() string {
return fmt.Sprintf(ErrUnsupportedInterval, k.Interval.Word())
}
// Unwrap returns interval unsupported message
func (k *ErrorKline) Unwrap() error {
return fmt.Errorf(ErrUnsupportedInterval, k.Interval)
}
// DateRange holds a start and end date for kline usage
type DateRange struct {
Start time.Time
End time.Time
}

View File

@@ -1,45 +0,0 @@
package kline
import (
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// Consts here define basic time intervals
const (
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
type Item struct {
Exchange string
Pair currency.Pair
Asset asset.Item
Interval time.Duration
Candles []Candle
}
// Candle holds historic rate information.
type Candle struct {
Time time.Time
Open float64
High float64
Low float64
Close float64
Volume float64
}

View File

@@ -216,10 +216,10 @@ func (k *Kraken) GetTickers(pairList string) (map[string]Ticker, error) {
}
// GetOHLC returns an array of open high low close values of a currency pair
func (k *Kraken) GetOHLC(symbol string) ([]OpenHighLowClose, error) {
func (k *Kraken) GetOHLC(symbol, interval string) ([]OpenHighLowClose, error) {
values := url.Values{}
values.Set("pair", symbol)
values.Set("interval", interval)
type Response struct {
Error []interface{} `json:"error"`
Data map[string]interface{} `json:"result"`
@@ -239,6 +239,11 @@ func (k *Kraken) GetOHLC(symbol string) ([]OpenHighLowClose, error) {
return OHLC, fmt.Errorf("getOHLC error: %s", result.Error)
}
_, ok := result.Data[symbol].([]interface{})
if !ok {
return nil, errors.New("invalid data returned")
}
for _, y := range result.Data[symbol].([]interface{}) {
o := OpenHighLowClose{}
for i, x := range y.([]interface{}) {
@@ -275,7 +280,6 @@ func (k *Kraken) GetDepth(symbol string) (Orderbook, error) {
var orderBook Orderbook
path := fmt.Sprintf("%s/%s/public/%s?%s", k.API.Endpoints.URL, krakenAPIVersion, krakenDepth, values.Encode())
err := k.SendHTTPRequest(path, &result)
if err != nil {
return orderBook, err

View File

@@ -6,6 +6,7 @@ import (
"os"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common/convert"
@@ -13,6 +14,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -160,7 +163,7 @@ func TestGetTickers(t *testing.T) {
// TestGetOHLC API endpoint test
func TestGetOHLC(t *testing.T) {
t.Parallel()
_, err := k.GetOHLC("BCHEUR")
_, err := k.GetOHLC("XXBTZUSD", "1440")
if err != nil {
t.Error("GetOHLC() error", err)
}
@@ -1421,3 +1424,60 @@ func TestParseTime(t *testing.T) {
t.Error("unexpected result")
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("XBTUSD")
_, err := k.GetHistoricCandles(currencyPair, asset.Spot, time.Now().AddDate(0, 0, -1), time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = k.GetHistoricCandles(currencyPair, asset.Spot, time.Now(), time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("XBTUSD")
_, err := k.GetHistoricCandlesExtended(currencyPair, asset.Spot, time.Now().AddDate(0, -6, 0), time.Now(), kline.OneDay)
if err != nil {
t.Fatal(err)
}
_, err = k.GetHistoricCandlesExtended(currencyPair, asset.Spot, time.Now(), time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"1",
},
{
"OneDay",
kline.OneDay,
"1440",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := k.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -119,9 +119,27 @@ func (k *Kraken) SetDefaults() {
exchange.WithdrawCryptoWith2FA |
exchange.AutoWithdrawFiatWithSetup |
exchange.WithdrawFiatWith2FA,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.FourHour.Word(): true,
kline.OneDay.Word(): true,
kline.FifteenDay.Word(): true,
kline.OneWeek.Word(): true,
},
},
},
}
@@ -770,7 +788,88 @@ func (k *Kraken) ValidateCredentials() error {
return k.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (k *Kraken) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (k *Kraken) FormatExchangeKlineInterval(in kline.Interval) string {
return strconv.FormatFloat(in.Duration().Minutes(), 'f', -1, 64)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (k *Kraken) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !k.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: k.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
candles, err := k.GetOHLC(assetTranslator.LookupCurrency(k.FormatExchangeCurrency(pair, a).Upper().String()), k.FormatExchangeKlineInterval(interval))
if err != nil {
return kline.Item{}, err
}
for x := range candles {
timeValue, err := convert.TimeFromUnixTimestampFloat(candles[x].Time * 1000)
if err != nil {
return kline.Item{}, err
}
if timeValue.Before(start) || timeValue.After(end) {
continue
}
ret.Candles = append(ret.Candles, kline.Candle{
Time: timeValue,
Open: candles[x].Open,
High: candles[x].Close,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (k *Kraken) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !k.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: k.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
candles, err := k.GetOHLC(assetTranslator.LookupCurrency(k.FormatExchangeCurrency(pair, a).Upper().String()), k.FormatExchangeKlineInterval(interval))
if err != nil {
return kline.Item{}, err
}
for i := range candles {
timeValue, err := convert.TimeFromUnixTimestampFloat(candles[i].Time * 1000)
if err != nil {
return kline.Item{}, err
}
if timeValue.Before(start) || timeValue.After(end) {
continue
}
ret.Candles = append(ret.Candles, kline.Candle{
Time: timeValue,
Open: candles[i].Open,
High: candles[i].Close,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
@@ -140,8 +141,13 @@ func (l *LakeBTC) GetOrderBook(currency string) (Orderbook, error) {
}
// GetTradeHistory returns the trade history for a given currency pair
func (l *LakeBTC) GetTradeHistory(currency string) ([]TradeHistory, error) {
path := fmt.Sprintf("%s/%s?symbol=%s", l.API.Endpoints.URL, lakeBTCTrades, strings.ToLower(currency))
func (l *LakeBTC) GetTradeHistory(currency string, start int64) ([]TradeHistory, error) {
v := url.Values{}
v.Set("symbol", strings.ToLower(currency))
if start != 0 {
v.Set("at", strconv.FormatInt(start, 10))
}
path := fmt.Sprintf("%s/%s?%s", l.API.Endpoints.URL, lakeBTCTrades, v.Encode())
var resp []TradeHistory
return resp, l.SendHTTPRequest(path, &resp)
}

View File

@@ -4,6 +4,7 @@ import (
"log"
"os"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
@@ -76,7 +77,7 @@ func TestGetOrderBook(t *testing.T) {
func TestGetTradeHistory(t *testing.T) {
t.Parallel()
_, err := l.GetTradeHistory("BTCUSD")
_, err := l.GetTradeHistory("BTCUSD", time.Now().Unix())
if err != nil {
t.Error("GetTradeHistory() error", err)
}

View File

@@ -560,6 +560,11 @@ func (l *LakeBTC) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (l *LakeBTC) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (l *LakeBTC) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (l *LakeBTC) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -11,6 +11,7 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -406,3 +407,75 @@ func TestGetOrderHistory(t *testing.T) {
t.Error(err)
}
}
func TestGetHistoricCandles(t *testing.T) {
t.Parallel()
pair := currency.NewPairFromString(testCurrencyPair)
_, err := l.GetHistoricCandles(pair, asset.Spot, time.Now().Add(-24*time.Hour), time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = l.GetHistoricCandles(pair, asset.Spot, time.Now().Add(-24*time.Hour), time.Now(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
t.Parallel()
startTime := time.Now().Add(-time.Hour)
end := time.Now()
pair := currency.NewPairFromString(testCurrencyPair)
_, err := l.GetHistoricCandlesExtended(pair, asset.Spot, startTime, end, kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"minute1",
},
{
"OneHour",
kline.OneHour,
"hour1",
},
{
"OneDay",
kline.OneDay,
"day1",
},
{
"OneWeek",
kline.OneWeek,
"week1",
},
{
"AllOther",
kline.FifteenDay,
"",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := l.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -95,6 +95,21 @@ func (l *Lbank) SetDefaults() {
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.FourHour.Word(): true,
kline.EightHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.OneWeek.Word(): true,
},
ResultLimit: 2880,
},
},
}
@@ -718,7 +733,99 @@ func (l *Lbank) ValidateCredentials() error {
return l.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (l *Lbank) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (l *Lbank) FormatExchangeKlineInterval(in kline.Interval) string {
switch in {
case kline.OneMin, kline.ThreeMin,
kline.FiveMin, kline.FifteenMin, kline.ThirtyMin:
return "minute" + in.Short()[:len(in.Short())-1]
case kline.OneHour, kline.FourHour,
kline.EightHour, kline.TwelveHour:
return "hour" + in.Short()[:len(in.Short())-1]
case kline.OneDay:
return "day1"
case kline.OneWeek:
return "week1"
}
return ""
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (l *Lbank) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !l.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
data, err := l.GetKlines(l.FormatExchangeCurrency(pair, a).String(),
strconv.FormatInt(int64(l.Features.Enabled.Kline.ResultLimit), 10),
l.FormatExchangeKlineInterval(interval),
strconv.FormatInt(start.Unix(), 10))
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: l.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
for x := range data {
ret.Candles = append(ret.Candles, kline.Candle{
Time: time.Unix(data[x].TimeStamp, 0),
Open: data[x].OpenPrice,
High: data[x].HigestPrice,
Low: data[x].LowestPrice,
Close: data[x].ClosePrice,
Volume: data[x].TradingVolume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (l *Lbank) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !l.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: l.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, l.Features.Enabled.Kline.ResultLimit)
for x := range dates {
data, err := l.GetKlines(l.FormatExchangeCurrency(pair, a).String(),
strconv.FormatInt(int64(l.Features.Enabled.Kline.ResultLimit), 10),
l.FormatExchangeKlineInterval(interval),
strconv.FormatInt(dates[x].Start.UTC().Unix(), 10))
if err != nil {
return kline.Item{}, err
}
for i := range data {
if time.Unix(data[i].TimeStamp, 0).UTC().Before(dates[x].Start.UTC()) || time.Unix(data[i].TimeStamp, 0).UTC().After(dates[x].End.UTC()) {
continue
}
ret.Candles = append(ret.Candles, kline.Candle{
Time: time.Unix(data[i].TimeStamp, 0).UTC(),
Open: data[i].OpenPrice,
High: data[i].HigestPrice,
Low: data[i].LowestPrice,
Close: data[i].ClosePrice,
Volume: data[i].TradingVolume,
})
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -599,6 +599,11 @@ func (l *LocalBitcoins) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (l *LocalBitcoins) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
func (l *LocalBitcoins) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (l *LocalBitcoins) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

View File

@@ -17,6 +17,7 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/okgroup"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
@@ -483,11 +484,12 @@ func TestGetSpotFilledOrdersInformation(t *testing.T) {
// TestGetSpotMarketData API endpoint test
func TestGetSpotMarketData(t *testing.T) {
request := okgroup.GetSpotMarketDataRequest{
request := &okgroup.GetMarketDataRequest{
Asset: asset.Spot,
InstrumentID: spotCurrency,
Granularity: 604800,
Granularity: "604800",
}
_, err := o.GetSpotMarketData(request)
_, err := o.GetMarketData(request)
if err != nil {
t.Error(err)
}
@@ -1095,3 +1097,26 @@ func TestGetOrderbook(t *testing.T) {
t.Error("error cannot be nil")
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
startTime := time.Unix(1588636800, 0)
_, err := o.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
startTime := time.Unix(1588636800, 0)
_, err := o.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = o.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}

View File

@@ -1,15 +1,18 @@
package okcoin
import (
"errors"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"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/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/okgroup"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
@@ -111,9 +114,31 @@ func (o *OKCoin) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.ThreeDay.Word(): true,
kline.OneWeek.Word(): true,
},
ResultLimit: 1440,
},
},
}
@@ -263,6 +288,141 @@ func (o *OKCoin) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (o *OKCoin) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
func (o *OKCoin) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !o.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
req := &okgroup.GetMarketDataRequest{
Asset: a,
Start: start.UTC().Format(time.RFC3339),
End: end.UTC().Format(time.RFC3339),
Granularity: o.FormatExchangeKlineInterval(interval),
InstrumentID: o.FormatExchangeCurrency(pair, a).String(),
}
candles, err := o.GetMarketData(req)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: o.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
for x := range candles {
t := candles[x].([]interface{})
tempCandle := kline.Candle{}
v, ok := t[0].(string)
if !ok {
return kline.Item{}, errors.New("unexpected value received")
}
tempCandle.Time, err = time.Parse(time.RFC3339, v)
if err != nil {
return kline.Item{}, err
}
tempCandle.Open, err = convert.FloatFromString(t[1])
if err != nil {
return kline.Item{}, err
}
tempCandle.High, err = convert.FloatFromString(t[2])
if err != nil {
return kline.Item{}, err
}
tempCandle.Low, err = convert.FloatFromString(t[3])
if err != nil {
return kline.Item{}, err
}
tempCandle.Close, err = convert.FloatFromString(t[4])
if err != nil {
return kline.Item{}, err
}
tempCandle.Volume, err = convert.FloatFromString(t[5])
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempCandle)
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (o *OKCoin) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !o.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: o.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, o.Features.Enabled.Kline.ResultLimit)
for x := range dates {
req := &okgroup.GetMarketDataRequest{
Asset: a,
Start: dates[x].Start.UTC().Format(time.RFC3339),
End: dates[x].End.UTC().Format(time.RFC3339),
Granularity: o.FormatExchangeKlineInterval(interval),
InstrumentID: o.FormatExchangeCurrency(pair, a).String(),
}
candles, err := o.GetMarketData(req)
if err != nil {
return kline.Item{}, err
}
for i := range candles {
t := candles[i].([]interface{})
tempCandle := kline.Candle{}
v, ok := t[0].(string)
if !ok {
return kline.Item{}, errors.New("unexpected value received")
}
tempCandle.Time, err = time.Parse(time.RFC3339, v)
if err != nil {
return kline.Item{}, err
}
tempCandle.Open, err = convert.FloatFromString(t[1])
if err != nil {
return kline.Item{}, err
}
tempCandle.High, err = convert.FloatFromString(t[2])
if err != nil {
return kline.Item{}, err
}
tempCandle.Low, err = convert.FloatFromString(t[3])
if err != nil {
return kline.Item{}, err
}
tempCandle.Close, err = convert.FloatFromString(t[4])
if err != nil {
return kline.Item{}, err
}
tempCandle.Volume, err = convert.FloatFromString(t[5])
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempCandle)
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -159,12 +159,6 @@ func (o *OKEX) GetFuturesFilledOrder(request okgroup.GetFuturesFilledOrderReques
return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &resp, true)
}
// GetFuturesMarketData Get the charts of the trading pairs. Charts are returned in grouped buckets based on requested granularity.
func (o *OKEX) GetFuturesMarketData(request okgroup.GetFuturesMarketDateRequest) (resp okgroup.GetFuturesMarketDataResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okgroup.OKGroupGetSpotMarketData, okgroup.FormatParameters(request))
return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &resp, true)
}
// GetFuturesHoldAmount Get the number of futures with hold.
func (o *OKEX) GetFuturesHoldAmount(instrumentID string) (resp okgroup.GetFuturesHoldAmountResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v/%v", okgroup.OKGroupAccounts, instrumentID, okGroupFutureHolds)
@@ -327,12 +321,6 @@ func (o *OKEX) GetSwapFilledOrdersData(request *okgroup.GetSwapFilledOrdersDataR
return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, requestURL, nil, &resp, false)
}
// GetSwapMarketData Get the charts of the trading pairs.
func (o *OKEX) GetSwapMarketData(request okgroup.GetSwapMarketDataRequest) (resp []okgroup.GetSwapMarketDataResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okgroup.OKGroupGetSpotMarketData, okgroup.FormatParameters(request))
return resp, o.SendHTTPRequest(http.MethodGet, okGroupSwapSubsection, requestURL, nil, &resp, false)
}
// GetSwapIndices Get Indices of tokens.
func (o *OKEX) GetSwapIndices(instrumentID string) (resp okgroup.GetSwapIndecesResponse, _ error) {
requestURL := fmt.Sprintf("%v/%v/%v", okgroup.OKGroupInstruments, instrumentID, okGroupIndices)

View File

@@ -19,6 +19,7 @@ import (
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/okgroup"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
@@ -519,16 +520,56 @@ func TestGetSpotFilledOrdersInformation(t *testing.T) {
// TestGetSpotMarketData API endpoint test
func TestGetSpotMarketData(t *testing.T) {
t.Parallel()
request := okgroup.GetSpotMarketDataRequest{
request := &okgroup.GetMarketDataRequest{
Asset: asset.Spot,
InstrumentID: spotCurrency,
Granularity: 604800,
Granularity: "604800",
}
_, err := o.GetSpotMarketData(request)
_, err := o.GetMarketData(request)
if err != nil {
t.Error(err)
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
startTime := time.Unix(1588636800, 0)
_, err := o.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = o.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
_, err = o.GetHistoricCandles(currencyPair, asset.Margin, startTime, time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
swapPair := currency.NewPairFromString("BTC-USD_SWAP")
_, err = o.GetHistoricCandles(swapPair, asset.PerpetualSwap, startTime, time.Now(), kline.OneDay)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCUSDT")
startTime := time.Unix(1588636800, 0)
_, err := o.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
_, err = o.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
// TestGetMarginTradingAccounts API endpoint test
func TestGetMarginTradingAccounts(t *testing.T) {
t.Parallel()
@@ -1240,19 +1281,6 @@ func TestGetSwapFilledOrdersData(t *testing.T) {
}
}
// TestGetSwapMarketData API endpoint test
func TestGetSwapMarketData(t *testing.T) {
t.Parallel()
request := okgroup.GetSwapMarketDataRequest{
InstrumentID: fmt.Sprintf("%v-%v-SWAP", currency.BTC, currency.USD),
Granularity: 604800,
}
_, err := o.GetSwapMarketData(request)
if err != nil {
t.Error(err)
}
}
// TestGetSwapIndeces API endpoint test
func TestGetSwapIndeces(t *testing.T) {
t.Parallel()

View File

@@ -8,11 +8,13 @@ import (
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"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/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/okgroup"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
@@ -145,9 +147,31 @@ func (o *OKEX) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.ThreeDay.Word(): true,
kline.OneWeek.Word(): true,
},
ResultLimit: 1440,
},
},
}
@@ -419,6 +443,142 @@ func (o *OKEX) FetchTicker(p currency.Pair, assetType asset.Item) (tickerData *t
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (o *OKEX) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
func (o *OKEX) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !o.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
req := &okgroup.GetMarketDataRequest{
Asset: a,
Start: start.UTC().Format(time.RFC3339),
End: end.UTC().Format(time.RFC3339),
Granularity: o.FormatExchangeKlineInterval(interval),
InstrumentID: o.FormatExchangeCurrency(pair, a).String(),
}
candles, err := o.GetMarketData(req)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: o.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
for x := range candles {
t := candles[x].([]interface{})
tempCandle := kline.Candle{}
v, ok := t[0].(string)
if !ok {
return kline.Item{}, errors.New("unexpected value received")
}
tempCandle.Time, err = time.Parse(time.RFC3339, v)
if err != nil {
return kline.Item{}, err
}
tempCandle.Open, err = convert.FloatFromString(t[1])
if err != nil {
return kline.Item{}, err
}
tempCandle.High, err = convert.FloatFromString(t[2])
if err != nil {
return kline.Item{}, err
}
tempCandle.Low, err = convert.FloatFromString(t[3])
if err != nil {
return kline.Item{}, err
}
tempCandle.Close, err = convert.FloatFromString(t[4])
if err != nil {
return kline.Item{}, err
}
tempCandle.Volume, err = convert.FloatFromString(t[5])
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempCandle)
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (o *OKEX) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !o.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
ret := kline.Item{
Exchange: o.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
dates := kline.CalcDateRanges(start, end, interval, o.Features.Enabled.Kline.ResultLimit)
for x := range dates {
req := &okgroup.GetMarketDataRequest{
Asset: a,
Start: dates[x].Start.UTC().Format(time.RFC3339),
End: dates[x].End.UTC().Format(time.RFC3339),
Granularity: o.FormatExchangeKlineInterval(interval),
InstrumentID: o.FormatExchangeCurrency(pair, a).String(),
}
candles, err := o.GetMarketData(req)
if err != nil {
return kline.Item{}, err
}
for i := range candles {
t := candles[i].([]interface{})
tempCandle := kline.Candle{}
v, ok := t[0].(string)
if !ok {
return kline.Item{}, errors.New("unexpected value received")
}
tempCandle.Time, err = time.Parse(time.RFC3339, v)
if err != nil {
return kline.Item{}, err
}
tempCandle.Open, err = convert.FloatFromString(t[1])
if err != nil {
return kline.Item{}, err
}
tempCandle.High, err = convert.FloatFromString(t[2])
if err != nil {
return kline.Item{}, err
}
tempCandle.Low, err = convert.FloatFromString(t[3])
if err != nil {
return kline.Item{}, err
}
tempCandle.Close, err = convert.FloatFromString(t[4])
if err != nil {
return kline.Item{}, err
}
tempCandle.Volume, err = convert.FloatFromString(t[5])
if err != nil {
return kline.Item{}, err
}
ret.Candles = append(ret.Candles, tempCandle)
}
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}

View File

@@ -28,9 +28,11 @@ const (
// OKGroupAPIPath const to help with api url formatting
OKGroupAPIPath = "api/"
// API subsections
okGroupAccountSubsection = "account"
okGroupTokenSubsection = "spot"
okGroupMarginTradingSubsection = "margin"
okGroupAccountSubsection = "account"
okGroupTokenSubsection = "spot"
okGroupMarginTradingSubsection = "margin"
okGroupFuturesTradingSubSection = "futures"
oKGroupSwapTradingSubSection = "swap"
// OKGroupAccounts common api endpoint
OKGroupAccounts = "accounts"
// OKGroupLedger common api endpoint
@@ -365,10 +367,21 @@ func (o *OKGroup) GetSpotFilledOrdersInformation(request GetSpotFilledOrdersInfo
return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false)
}
// GetSpotMarketData Get the charts of the trading pairs. Charts are returned in grouped buckets based on requested granularity.
func (o *OKGroup) GetSpotMarketData(request GetSpotMarketDataRequest) (resp GetSpotMarketDataResponse, _ error) {
// GetMarketData Get the charts of the trading pairs. Charts are returned in grouped buckets based on requested granularity.
func (o *OKGroup) GetMarketData(request *GetMarketDataRequest) (resp GetMarketDataResponse, err error) {
requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupInstruments, request.InstrumentID, OKGroupGetSpotMarketData, FormatParameters(request))
return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false)
var requestType string
switch request.Asset {
case asset.Spot, asset.Margin:
requestType = okGroupTokenSubsection
case asset.Futures:
requestType = okGroupFuturesTradingSubSection
case asset.PerpetualSwap:
requestType = oKGroupSwapTradingSubSection
default:
return nil, errors.New("asset not supported")
}
return resp, o.SendHTTPRequest(http.MethodGet, requestType, requestURL, nil, &resp, false)
}
// GetMarginTradingAccounts List all assets under token margin trading account, including information such as balance, amount on hold and more.

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// Order types
@@ -329,15 +330,16 @@ type GetSpotFilledOrdersInformationResponse struct {
TradeID string `json:"trade_id"`
}
// GetSpotMarketDataRequest request data for GetSpotMarketData
type GetSpotMarketDataRequest struct {
// GetMarketDataRequest request data for GetMarketData
type GetMarketDataRequest struct {
Asset asset.Item
Start string `url:"start,omitempty"` // [optional] start time in ISO 8601
End string `url:"end,omitempty"` // [optional] end time in ISO 8601
Granularity int64 `url:"granularity"` // The granularity field must be one of the following values: {60, 180, 300, 900, 1800, 3600, 7200, 14400, 43200, 86400, 604800}.
Granularity string `url:"granularity"` // The granularity field must be one of the following values: {60, 180, 300, 900, 1800, 3600, 7200, 14400, 43200, 86400, 604800}.
InstrumentID string `url:"-"` // [required] trading pairs
}
// GetSpotMarketDataResponse response data for GetSpotMarketData
// GetMarketDataResponse response data for GetMarketData
// Return Parameters
// time string Start time
// open string Open price
@@ -345,7 +347,7 @@ type GetSpotMarketDataRequest struct {
// low string Lowest price
// close string Close price
// volume string Trading volume
type GetSpotMarketDataResponse []interface{}
type GetMarketDataResponse []interface{}
// GetMarginAccountsResponse response data for GetMarginAccounts
type GetMarginAccountsResponse struct {
@@ -731,7 +733,7 @@ type GetFuturesMarketDateRequest struct {
InstrumentID string `url:"-"` // [required] trading pairs
}
// GetFuturesMarketDataResponse contains candle data from a GetSpotMarketDataRequest
// GetFuturesMarketDataResponse contains candle data from a GetMarketDataRequest
// Return Parameters
// time string Start time
// open string Open price

View File

@@ -177,30 +177,44 @@ func (p *Poloniex) GetTradeHistory(currencyPair, start, end string) ([]TradeHist
}
// GetChartData returns chart data for a specific currency pair
func (p *Poloniex) GetChartData(currencyPair, start, end, period string) ([]ChartData, error) {
func (p *Poloniex) GetChartData(currencyPair string, start, end time.Time, period string) ([]ChartData, error) {
vals := url.Values{}
vals.Set("currencyPair", currencyPair)
if start != "" {
vals.Set("start", start)
if !start.IsZero() {
vals.Set("start", strconv.FormatInt(start.Unix(), 10))
}
if end != "" {
vals.Set("end", end)
if !end.IsZero() {
vals.Set("end", strconv.FormatInt(end.Unix(), 10))
}
if period != "" {
vals.Set("period", period)
}
var temp json.RawMessage
var resp []ChartData
path := fmt.Sprintf("%s/public?command=returnChartData&%s", p.API.Endpoints.URL, vals.Encode())
err := p.SendHTTPRequest(path, &resp)
path := p.API.Endpoints.URL + "/public?command=returnChartData&" + vals.Encode()
err := p.SendHTTPRequest(path, &temp)
if err != nil {
return nil, err
}
tempUnmarshal := json.Unmarshal(temp, &resp)
if tempUnmarshal != nil {
var errResp struct {
Error string `json:"error"`
}
errRet := json.Unmarshal(temp, &errResp)
if errRet != nil {
return nil, err
}
if errResp.Error != "" {
return nil, errors.New(errResp.Error)
}
}
return resp, nil
}

View File

@@ -10,6 +10,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -63,7 +65,8 @@ func TestGetTradeHistory(t *testing.T) {
func TestGetChartData(t *testing.T) {
t.Parallel()
_, err := p.GetChartData("BTC_XMR", "1405699200", "1405699400", "300")
_, err := p.GetChartData("BTC_XMR",
time.Unix(1405699200, 0), time.Unix(1405699400, 0), "300")
if err != nil {
t.Error("Test faild - Poloniex GetChartData() error", err)
}
@@ -510,7 +513,6 @@ func TestWsPriceAggregateOrderbook(t *testing.T) {
t.Error(err)
}
}
func TestWsHandleAccountData(t *testing.T) {
t.Parallel()
err := p.getCurrencyIDMap()
@@ -530,3 +532,39 @@ func TestWsHandleAccountData(t *testing.T) {
}
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCLTC")
_, err := p.GetHistoricCandles(currencyPair, asset.Spot, time.Unix(1588741402, 0), time.Unix(1588745003, 0), kline.FiveMin)
if err != nil {
t.Fatal(err)
}
_, err = p.GetHistoricCandles(currencyPair, asset.Spot, time.Unix(1588741402, 0), time.Unix(1588745003, 0), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
currencyPair.Quote = currency.NewCode("LTCC")
_, err = p.GetHistoricCandles(currencyPair, asset.Spot, time.Unix(1588741402, 0), time.Unix(1588745003, 0), kline.FiveMin)
if err == nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTCLTC")
_, err := p.GetHistoricCandlesExtended(currencyPair, asset.Spot, time.Unix(1588741402, 0), time.Unix(1588745003, 0), kline.FiveMin)
if err != nil {
t.Fatal(err)
}
_, err = p.GetHistoricCandlesExtended(currencyPair, asset.Spot, time.Unix(1588741402, 0), time.Unix(1588745003, 0), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
currencyPair.Quote = currency.NewCode("LTCC")
_, err = p.GetHistoricCandlesExtended(currencyPair, asset.Spot, time.Unix(1588741402, 0), time.Unix(1588745003, 0), kline.FiveMin)
if err == nil {
t.Fatal(err)
}
}

View File

@@ -107,9 +107,22 @@ func (p *Poloniex) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.OneDay.Word(): true,
},
},
},
}
@@ -638,6 +651,43 @@ func (p *Poloniex) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (p *Poloniex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (p *Poloniex) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !p.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
candles, err := p.GetChartData(p.FormatExchangeCurrency(pair, a).String(),
start, end,
p.FormatExchangeKlineInterval(interval))
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: p.Name,
Interval: interval,
Pair: pair,
Asset: a,
}
for x := range candles {
ret.Candles = append(ret.Candles, kline.Candle{
Time: time.Unix(candles[x].Date, 0),
Open: candles[x].Open,
High: candles[x].Close,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (p *Poloniex) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return p.GetHistoricCandles(pair, a, start, end, interval)
}

View File

@@ -78,14 +78,23 @@ func (y *Yobit) GetDepth(symbol string) (Orderbook, error) {
}
// GetTrades returns the trades for a specific currency
func (y *Yobit) GetTrades(symbol string) ([]Trades, error) {
func (y *Yobit) GetTrades(symbol string, start int64, sortAsc bool) ([]Trades, error) {
type Response struct {
Data map[string][]Trades
}
response := Response{}
path := fmt.Sprintf("%s/%s/%s/%s", y.API.Endpoints.URL, apiPublicVersion, publicTrades, symbol)
v := url.Values{}
if sortAsc {
v.Set("order", "ASC")
} else {
v.Set("order", "DESC")
}
if start != 0 {
v.Set("since", strconv.FormatInt(start, 10))
}
var response Response
path := y.API.Endpoints.URL + "/" + apiPublicVersion + "/" + publicTrades + "/" + symbol + "?" + v.Encode()
return response.Data[symbol], y.SendHTTPRequest(path, &response.Data)
}

View File

@@ -82,7 +82,7 @@ func TestGetDepth(t *testing.T) {
func TestGetTrades(t *testing.T) {
t.Parallel()
_, err := y.GetTrades("btc_usd")
_, err := y.GetTrades("btc_usd", 0, false)
if err != nil {
t.Error("GetTrades() error", err)
}

View File

@@ -565,6 +565,11 @@ func (y *Yobit) ValidateCredentials() error {
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (y *Yobit) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
func (y *Yobit) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (y *Yobit) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}

49
exchanges/zb/ratelimit.go Normal file
View File

@@ -0,0 +1,49 @@
package zb
import (
"time"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"golang.org/x/time/rate"
)
const (
zbRateInterval = time.Second
zbAuthLimit = 60
zbUnauthLimit = 60
zbKlineDataInterval = time.Second * 2
zbKlineDataLimit = 1
// Used to match endpints to rate limits
klineFunc request.EndpointLimit = iota
)
// RateLimit implements the request.Limiter interface
type RateLimit struct {
Auth *rate.Limiter
UnAuth *rate.Limiter
KlineData *rate.Limiter
}
// Limit limits the outbound requests
func (r *RateLimit) Limit(f request.EndpointLimit) error {
switch f {
case request.Auth:
time.Sleep(r.Auth.Reserve().Delay())
case klineFunc:
time.Sleep(r.KlineData.Reserve().Delay())
default:
time.Sleep(r.UnAuth.Reserve().Delay())
}
return nil
}
// SetRateLimit returns the rate limit for the exchange
func SetRateLimit() *RateLimit {
return &RateLimit{
Auth: request.NewRateLimit(zbRateInterval, zbAuthLimit),
UnAuth: request.NewRateLimit(zbRateInterval, zbUnauthLimit),
KlineData: request.NewRateLimit(zbKlineDataInterval, zbKlineDataLimit),
}
}

View File

@@ -36,9 +36,6 @@ const (
zbGetOrdersGet = "getOrders"
zbWithdraw = "withdraw"
zbDepositAddress = "getUserAddress"
zbRateInterval = time.Second
zbReqRate = 60
)
// ZB is the overarching type across this package
@@ -61,7 +58,7 @@ func (z *ZB) SpotNewOrder(arg SpotNewOrderRequestParams) (int64, error) {
vals.Set("price", strconv.FormatFloat(arg.Price, 'f', -1, 64))
vals.Set("tradeType", string(arg.Type))
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result)
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result, request.Auth)
if err != nil {
return 0, err
}
@@ -89,7 +86,7 @@ func (z *ZB) CancelExistingOrder(orderID int64, symbol string) error {
vals.Set("currency", symbol)
var result response
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result)
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result, request.Auth)
if err != nil {
return err
}
@@ -109,7 +106,7 @@ func (z *ZB) GetAccountInformation() (AccountsResponse, error) {
vals.Set("accesskey", z.API.Credentials.Key)
vals.Set("method", "getAccountInfo")
return result, z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result)
return result, z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result, request.Auth)
}
// GetUnfinishedOrdersIgnoreTradeType returns unfinished orders
@@ -122,7 +119,7 @@ func (z *ZB) GetUnfinishedOrdersIgnoreTradeType(currency string, pageindex, page
vals.Set("pageIndex", strconv.FormatInt(pageindex, 10))
vals.Set("pageSize", strconv.FormatInt(pagesize, 10))
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result)
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result, request.Auth)
return result, err
}
@@ -135,7 +132,7 @@ func (z *ZB) GetOrders(currency string, pageindex, side int64) ([]Order, error)
vals.Set("currency", currency)
vals.Set("pageIndex", strconv.FormatInt(pageindex, 10))
vals.Set("tradeType", strconv.FormatInt(side, 10))
return response, z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &response)
return response, z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &response, request.Auth)
}
// GetMarkets returns market information including pricing, symbols and
@@ -144,7 +141,7 @@ func (z *ZB) GetMarkets() (map[string]MarketResponseItem, error) {
endpoint := fmt.Sprintf("%s/%s/%s", z.API.Endpoints.URL, zbAPIVersion, zbMarkets)
var res map[string]MarketResponseItem
err := z.SendHTTPRequest(endpoint, &res)
err := z.SendHTTPRequest(endpoint, &res, request.UnAuth)
if err != nil {
return nil, err
}
@@ -170,7 +167,7 @@ func (z *ZB) GetLatestSpotPrice(symbol string) (float64, error) {
func (z *ZB) GetTicker(symbol string) (TickerResponse, error) {
urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.API.Endpoints.URL, zbAPIVersion, zbTicker, symbol)
var res TickerResponse
err := z.SendHTTPRequest(urlPath, &res)
err := z.SendHTTPRequest(urlPath, &res, request.UnAuth)
return res, err
}
@@ -178,7 +175,7 @@ func (z *ZB) GetTicker(symbol string) (TickerResponse, error) {
func (z *ZB) GetTickers() (map[string]TickerChildResponse, error) {
urlPath := fmt.Sprintf("%s/%s/%s", z.API.Endpoints.URL, zbAPIVersion, zbTickers)
resp := make(map[string]TickerChildResponse)
err := z.SendHTTPRequest(urlPath, &resp)
err := z.SendHTTPRequest(urlPath, &resp, request.UnAuth)
return resp, err
}
@@ -187,7 +184,7 @@ func (z *ZB) GetOrderbook(symbol string) (OrderbookResponse, error) {
urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.API.Endpoints.URL, zbAPIVersion, zbDepth, symbol)
var res OrderbookResponse
err := z.SendHTTPRequest(urlPath, &res)
err := z.SendHTTPRequest(urlPath, &res, request.UnAuth)
if err != nil {
return res, err
}
@@ -213,10 +210,10 @@ func (z *ZB) GetOrderbook(symbol string) (OrderbookResponse, error) {
// GetSpotKline returns Kline data
func (z *ZB) GetSpotKline(arg KlinesRequestParams) (KLineResponse, error) {
vals := url.Values{}
vals.Set("type", string(arg.Type))
vals.Set("type", arg.Type)
vals.Set("market", arg.Symbol)
if arg.Since != "" {
vals.Set("since", arg.Since)
if arg.Since > 0 {
vals.Set("since", strconv.FormatInt(arg.Since, 10))
}
if arg.Size != 0 {
vals.Set("size", fmt.Sprintf("%d", arg.Size))
@@ -226,7 +223,7 @@ func (z *ZB) GetSpotKline(arg KlinesRequestParams) (KLineResponse, error) {
var res KLineResponse
var rawKlines map[string]interface{}
err := z.SendHTTPRequest(urlPath, &rawKlines)
err := z.SendHTTPRequest(urlPath, &rawKlines, klineFunc)
if err != nil {
return res, err
}
@@ -273,11 +270,11 @@ func (z *ZB) GetCryptoAddress(currency currency.Code) (UserAddress, error) {
vals.Set("currency", currency.Lower().String())
return resp,
z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &resp)
z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &resp, request.Auth)
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (z *ZB) SendHTTPRequest(path string, result interface{}) error {
func (z *ZB) SendHTTPRequest(path string, result interface{}, f request.EndpointLimit) error {
return z.SendPayload(context.Background(), &request.Item{
Method: http.MethodGet,
Path: path,
@@ -285,11 +282,12 @@ func (z *ZB) SendHTTPRequest(path string, result interface{}) error {
Verbose: z.Verbose,
HTTPDebugging: z.HTTPDebugging,
HTTPRecording: z.HTTPRecording,
Endpoint: f,
})
}
// SendAuthenticatedHTTPRequest sends authenticated requests to the zb API
func (z *ZB) SendAuthenticatedHTTPRequest(httpMethod string, params url.Values, result interface{}) error {
func (z *ZB) SendAuthenticatedHTTPRequest(httpMethod string, params url.Values, result interface{}, f request.EndpointLimit) error {
if !z.AllowAuthenticatedRequest() {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, z.Name)
}
@@ -328,6 +326,7 @@ func (z *ZB) SendAuthenticatedHTTPRequest(httpMethod string, params url.Values,
Verbose: z.Verbose,
HTTPDebugging: z.HTTPDebugging,
HTTPRecording: z.HTTPRecording,
Endpoint: f,
})
if err != nil {
return err
@@ -427,7 +426,7 @@ func (z *ZB) Withdraw(currency, address, safepassword string, amount, fees float
vals.Set("safePwd", safepassword)
var resp response
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &resp)
err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &resp, request.Auth)
if err != nil {
return "", err
}

View File

@@ -8,6 +8,7 @@ import (
"os"
"strconv"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -15,6 +16,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -153,11 +156,9 @@ func TestGetMarkets(t *testing.T) {
}
func TestGetSpotKline(t *testing.T) {
t.Parallel()
arg := KlinesRequestParams{
Symbol: "btc_usdt",
Type: TimeIntervalFiveMinutes,
Type: kline.OneMin.Short() + "in",
Size: 10,
}
_, err := z.GetSpotKline(arg)
@@ -838,3 +839,78 @@ func TestWsCreateSubUserResponse(t *testing.T) {
t.Error(err)
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("btc_usdt")
startTime := time.Now().Add(-time.Hour * 1)
_, err := z.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneHour)
if err != nil {
t.Fatal(err)
}
_, err = z.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.Interval(time.Hour*7))
if err == nil {
t.Fatal("unexpected result")
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("btc_usdt")
start := time.Now().AddDate(0, -2, 0)
end := time.Now()
_, err := z.GetHistoricCandlesExtended(currencyPair, asset.Spot, start, end, kline.OneHour)
if err != nil {
t.Fatal(err)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"1min",
},
{
"OneHour",
kline.OneHour,
"1hour",
},
{
"OneDay",
kline.OneDay,
"1day",
},
{
"ThreeDay",
kline.ThreeDay,
"3day",
},
{
"OneWeek",
kline.OneWeek,
"1week",
},
{
"AllOther",
kline.FifteenDay,
"",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := z.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -112,10 +112,10 @@ type SpotNewOrderResponse struct {
// KlinesRequestParams represents Klines request data.
type KlinesRequestParams struct {
Symbol string // 交易对, zb_qc,zb_usdt,zb_btc...
Type TimeInterval // K线类型, 1min, 3min, 15min, 30min, 1hour......
Since string // 从这个时间戳之后的
Size int // 返回数据的条数限制(默认为1000如果返回数据多于1000条那么只返回1000条)
Symbol string // 交易对, zb_qc,zb_usdt,zb_btc...
Type string // K线类型, 1min, 3min, 15min, 30min, 1hour......
Since int64 // 从这个时间戳之后的
Size int // 返回数据的条数限制(默认为1000如果返回数据多于1000条那么只返回1000条)
}
// KLineResponseData Kline Data
@@ -149,26 +149,6 @@ type UserAddress struct {
} `json:"message"`
}
// TimeInterval represents interval enum.
type TimeInterval string
// TimeInterval vars
var (
TimeIntervalMinute = TimeInterval("1min")
TimeIntervalThreeMinutes = TimeInterval("3min")
TimeIntervalFiveMinutes = TimeInterval("5min")
TimeIntervalFifteenMinutes = TimeInterval("15min")
TimeIntervalThirtyMinutes = TimeInterval("30min")
TimeIntervalHour = TimeInterval("1hour")
TimeIntervalTwoHours = TimeInterval("2hour")
TimeIntervalFourHours = TimeInterval("4hour")
TimeIntervalSixHours = TimeInterval("6hour")
TimeIntervalTwelveHours = TimeInterval("12hour")
TimeIntervalDay = TimeInterval("1day")
TimeIntervalThreeDays = TimeInterval("3day")
TimeIntervalWeek = TimeInterval("1week")
)
// WithdrawalFees the large list of predefined withdrawal fees
// Prone to change, using highest value
var WithdrawalFees = map[currency.Code]float64{

View File

@@ -106,16 +106,36 @@ func (z *ZB) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
kline.ThreeDay.Word(): true,
kline.OneWeek.Word(): true,
},
ResultLimit: 1000,
},
},
}
z.Requester = request.New(z.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
// TODO: Implement full rate limit for endpoints
request.WithLimiter(request.NewBasicRateLimit(zbRateInterval, zbReqRate)))
request.WithLimiter(SetRateLimit()))
z.API.Endpoints.URLDefault = zbTradeURL
z.API.Endpoints.URL = z.API.Endpoints.URLDefault
@@ -699,7 +719,68 @@ func (z *ZB) ValidateCredentials() error {
return z.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (z *ZB) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (z *ZB) FormatExchangeKlineInterval(in kline.Interval) string {
switch in {
case kline.OneMin, kline.ThreeMin,
kline.FiveMin, kline.FifteenMin, kline.ThirtyMin:
return in.Short() + "in"
case kline.OneHour, kline.TwoHour, kline.FourHour, kline.SixHour, kline.TwelveHour:
return in.Short()[:len(in.Short())-1] + "hour"
case kline.OneDay:
return "1day"
case kline.ThreeDay:
return "3day"
case kline.OneWeek:
return "1week"
}
return ""
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (z *ZB) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !z.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
klineParams := KlinesRequestParams{
Type: z.FormatExchangeKlineInterval(interval),
Symbol: z.FormatExchangeCurrency(pair, a).String(),
}
candles, err := z.GetSpotKline(klineParams)
if err != nil {
return kline.Item{}, err
}
ret := kline.Item{
Exchange: z.Name,
Pair: pair,
Asset: a,
Interval: interval,
}
for x := range candles.Data {
if candles.Data[x].KlineTime.Before(start) || candles.Data[x].KlineTime.After(end) {
continue
}
ret.Candles = append(ret.Candles, kline.Candle{
Time: candles.Data[x].KlineTime,
Open: candles.Data[x].Open,
High: candles.Data[x].Close,
Low: candles.Data[x].Low,
Close: candles.Data[x].Close,
Volume: candles.Data[x].Volume,
})
}
ret.SortCandlesByTimestamp(false)
return ret, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (z *ZB) GetHistoricCandlesExtended(p currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return z.GetHistoricCandles(p, a, start, end, interval)
}