Skip to content

Commit

Permalink
CoinbasePro migrate to Advanced version (#14)
Browse files Browse the repository at this point in the history
* refactor: authentication process
remove: extra config passphrase token

* feat: GetOpenOrder & GetCashBalance

* refactor: reduce ByteToHexString implementation

* refactor: add description to Account, Order Models

* feat: handle stopLimit in OpenOrders mthd

* feat: CancelOrder()

* feat: mock IRestClient.BuildUri

* feat: timeInForce prop in GetOpenOrders

* feat: add coinbase api + client OOP, dry, kiss
refactor: new enums for deserialized object
* part of implementation independently restclient

* feat: missed limit order type ioc
* create abstract for limit order types

* feat: PlaceOrder api
feat: description to some method
refactor: change string type to enum in models
refactor: remove old code

* feat: property in test's config file

* fix: marketTrade nullable properties

* feat: custom json converter decimal<->string

* refactor: rename PlaceOrder to CreateOrder like in business logic

* feat: CreateOrder return model
refactor: remove public write opportunity in some properties
refactor: clean brokerage placeOrder mthd

* rename: enum failure cancel order reason

* feat: handler of bad create order response

* refactor: GetCashBalance()

* remove: old get tick mthd

* remove: old ExecuteRestRequest()

* fix: add missed brokerId in PlaceOrder()

* refactor/remove: old parts of app (big commit)
refactor: GDAXBrokerageFactory
refactor: initialize(), some init of prop\variable
refactor: constructot of GDAXBrokerage
feat: CanSubscribe()
remove: old public\private rateGate
remove: old Ex
remove: old GetAuthenticationToken()
remove: old GetTick()
remove: FillMonitorAction() -> strange polling process
refactor: unify DataQueueHandler with main brokerage class
refactor: SetJob()
refactor: test to new one constructor and changes...
* neat code, be happy

* feat: auth WS connection
refactor: subscription on Symbol update
test: tamplate for testing ws connection

* feat: handle level2 & trade change event

* refactor: Additional test class
refactor: msg of log debug
feat: add mark about error response failure message

* feat: add exception in Market Trade endpoint

* fix: GetHistory()

* refacotr: enum orderSide, missed apiPrefix

* refactor: create json setting global instance

* fix: price buy precision in market order

* fix: Coinbase(GDAX) Tool box

* remove: deprecated PriceProvider

* revert: old globals.datafolder in toolbox

* remove: not used enteties
remove: old auth method
remove: mock offline tests
remove: missed xml comment

* update: readme fees & supported order types

* rename: gdax.brokerage -> coinbase.brokerage (huge commit)

* remove: not use enum failureCancelOrderReason

* feat: handle wrong http response status code

* feat: description about UpdateOrder

* fix: all enums register to CamelCase

* test: coinbaseApi

* fix: several PR remarks

* feat: sync subs on ws user update
feat: add order provider
feat: add description on enteties

* fix: ternary style to simple if
fix: missed null checker in AuthenticateRequest + test
fix: access modifier in GetSign()
test: CancelOrder with wrong OrderId in CoinbaseApi
refactor: rename method in coinbaseApi
feat: description for mthds

* fix: style of error msg

* feat: add sync context for ws subscriptions

* refactor: reduce subs\unsubs ws code

* feat: add _sequenceNumbers variable for ws

* remove: extra prop EventTime

* feat: add unsubscribe proccess before subscribe

* refactor: increase waitone in test

* remove: random data from tests
refactor: coinbase api test to hardcode value
remove: extra new rows
remove: missed value in EmitQuoteTick

* remove: old not used code

* feat: CoinbaseOrderProperties

* remove: extra parsing obj

* remove: manualResetEvent which blocked WS responses
remove: restore data process cuz it use base logic from class BaseWS

* rename: MarketName in brokerage

* feat: BrokerageUpdateOrder()

* feat: handle order update from WS response
refactor: cancellatioToken
test: small fixes

* fix: CoinbaseOrderProperty in PlaceOrder()

* feat: uncomment ValidateSubscription()
remove: Exchange Name from TradeTick()

* feat: add warning WS in seq number

* rename: config-url to old ones

* feat: download tickers in ToolBox

* remove: skipping of delisted tickers in ToolBox

* refactor: resubscription process

* rename: gdax -> coinbase in test

* fix: coinbase paramLess ctor

* feat: support usdc pair

* refactor: history brokerage test
feat: add BTCUSDC symbol in history test
fix: invalid url initialization in history test

* remove: not uses tests

* rename: market Gdax -> coinbase

* refactor: gdaxModel -> coinbaseModel in factory

* fix: exchangeInfoDownloader test

* fix: tick clone and symbol quote
feature: add comment
fix: ignore to explicit

* feat: test for USD,USDC,USDT

* fix: code format style
  • Loading branch information
Romazes authored Jan 5, 2024
1 parent 2c10283 commit 7550d79
Show file tree
Hide file tree
Showing 69 changed files with 5,390 additions and 3,215 deletions.
13 changes: 6 additions & 7 deletions .github/workflows/gh-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ jobs:
build:
runs-on: ubuntu-20.04
env:
QC_GDAX_API_SECRET: ${{ secrets.QC_GDAX_API_SECRET }}
QC_GDAX_API_KEY: ${{ secrets.QC_GDAX_API_KEY }}
QC_GDAX_PASSPHRASE: ${{ secrets.QC_GDAX_PASSPHRASE }}
QC_GDAX_URL: ${{ secrets.QC_GDAX_URL }}
QC_GDAX_REST_API: ${{ secrets.QC_GDAX_REST_API }}
QC_COINBASE_API_SECRET: ${{ secrets.QC_COINBASE_API_SECRET }}
QC_COINBASE_API_KEY: ${{ secrets.QC_COINBASE_API_KEY }}
QC_COINBASE_URL: ${{ secrets.QC_COINBASE_URL }}
QC_COINBASE_REST_API: ${{ secrets.QC_COINBASE_REST_API }}
QC_JOB_USER_ID: ${{ secrets.JOB_USER_ID }}
QC_API_ACCESS_TOKEN: ${{ secrets.API_ACCESS_TOKEN }}
QC_JOB_ORGANIZATION_ID: ${{ secrets.JOB_ORGANIZATION_ID }}
Expand Down Expand Up @@ -41,7 +40,7 @@ jobs:
run: mv Lean ../Lean

- name: Build
run: dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 QuantConnect.GDAXBrokerage.sln
run: dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 QuantConnect.CoinbaseBrokerage.sln

- name: Run Tests
run: dotnet test ./QuantConnect.GDAXBrokerage.Tests/bin/Release/QuantConnect.GDAXBrokerage.Tests.dll
run: dotnet test ./QuantConnect.CoinbaseBrokerage.Tests/bin/Release/QuantConnect.CoinbaseBrokerage.Tests.dll
210 changes: 210 additions & 0 deletions QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2023 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using RestSharp;
using System.Linq;
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using QuantConnect.Brokerages;
using QuantConnect.Configuration;
using System.Collections.Generic;
using QuantConnect.CoinbaseBrokerage.Api;
using QuantConnect.CoinbaseBrokerage.Models.Enums;
using QuantConnect.CoinbaseBrokerage.Models.WebSocket;

namespace QuantConnect.CoinbaseBrokerage.Tests
{
[TestFixture]
[Explicit("Use tests for more clarification of API")]
public class CoinbaseApiTests
{
private CoinbaseApi CoinbaseApi { get; set; }

[SetUp]
public void Setup()
{
var apiKey = Config.Get("coinbase-api-key");
var apiKeySecret = Config.Get("coinbase-api-secret");

CoinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret);
}

[TestCase("", "")]
[TestCase("1", "2")]
public void InvalidAuthenticationCredentialsShouldThrowException(string apiKey, string apiKeySecret)
{
var coinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret);

// call random endpoint with incorrect credential
Assert.Throws<Exception>(() => coinbaseApi.GetAccounts());
}

[Test]
public void GetListAccounts()
{
var accounts = CoinbaseApi.GetAccounts();

Assert.Greater(accounts.Count(), 0);

foreach(var account in accounts)
{
Assert.IsTrue(account.Active);
Assert.IsNotEmpty(account.Name);
Assert.IsNotEmpty(account.Currency);
Assert.IsNotEmpty(account.AvailableBalance.Currency);
Assert.GreaterOrEqual(account.AvailableBalance.Value, 0);
Assert.That(account.CreatedAt, Is.LessThan(DateTime.UtcNow));
}
}

[TestCase(OrderStatus.UnknownOrderStatus)]
[TestCase(OrderStatus.Open)]
[TestCase(OrderStatus.Filled)]
[TestCase(OrderStatus.Cancelled)]
[TestCase(OrderStatus.Expired)]
[TestCase(OrderStatus.Failed)]
public void GetListOrdersWithDifferentOrderStatus(OrderStatus orderStatus)
{
var orders = CoinbaseApi.GetOrders(orderStatus);
Assert.IsNotNull(orders);
}

[TestCase(OrderStatus.Pending)]
public void GetListOrderWithNotSupportedOrderStatus(OrderStatus orderStatus)
{
Assert.Throws<Exception>(() => CoinbaseApi.GetOrders(orderStatus));
}

[TestCase(CandleGranularity.OneMinute, "25/12/2023 06:30:15", "25/12/2023 06:35:15")]
[TestCase(CandleGranularity.OneHour, "25/12/2023 06:30:15", "25/12/2023 11:35:15")]
[TestCase(CandleGranularity.OneDay, "24/12/2023 06:30:15", "26/12/2023 06:35:15")]
public void GetProductCandlesWithDifferentCandleGranularity(CandleGranularity candleGranularity, string startDate, string endDate, string productId = "BTC-USDC")
{
var startDateTime = DateTime.ParseExact(startDate, "dd/MM/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);
var endDateTime = DateTime.ParseExact(endDate, "dd/MM/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);

var candles = CoinbaseApi.GetProductCandles(productId, startDateTime, endDateTime, candleGranularity);

Assert.IsNotNull(candles);
Assert.Greater(candles.Count(), 0);
}

[TestCase(CandleGranularity.UnknownGranularity, "25/12/2023 06:30:15", "25/12/2023 06:35:15")]
public void GetProductCandlesWithNotSupportedCandleGranularity(CandleGranularity candleGranularity, string startDate, string endDate, string productId = "BTC-USDC")
{
var startDateTime = DateTime.ParseExact(startDate, "dd/MM/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);
var endDateTime = DateTime.ParseExact(endDate, "dd/MM/yyyy HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);

Assert.Throws<Exception>(() => CoinbaseApi.GetProductCandles(productId, startDateTime, endDateTime, candleGranularity));
}

[Test]
public void ParseWebSocketLevel2DataResponse()
{
#region Json Response
string jsonLevel2Message = @"{
""channel"": ""l2_data"",
""client_id"": """",
""timestamp"": ""2023-12-22T11:03:21.339953956Z"",
""sequence_num"": 18,
""events"": [
{
""type"": ""update"",
""product_id"": ""BTC-USD"",
""updates"": [
{
""side"": ""bid"",
""event_time"": ""2023-12-22T11:03:21.141822Z"",
""price_level"": ""43640.01"",
""new_quantity"": ""0""
},
{
""side"": ""offer"",
""event_time"": ""2023-12-22T11:03:21.141822Z"",
""price_level"": ""44162.87"",
""new_quantity"": ""0.77908682""
}
]
}
]
}";
#endregion

var obj = JObject.Parse(jsonLevel2Message);

var level2Data = obj.ToObject<CoinbaseWebSocketMessage<CoinbaseLevel2Event>>();

Assert.IsNotNull(level2Data);
Assert.AreEqual("l2_data", level2Data.Channel);
Assert.IsEmpty(level2Data.ClientId);
Assert.IsNotEmpty(level2Data.SequenceNumber);
Assert.IsInstanceOf<DateTimeOffset>(level2Data.Timestamp);

Assert.Greater(level2Data.Events.Count, 0);
Assert.AreEqual("BTC-USD", level2Data.Events[0].ProductId);
Assert.IsTrue(level2Data.Events[0].Type == WebSocketEventType.Update);
Assert.Greater(level2Data.Events[0].Updates.Count, 0);
foreach (var tick in level2Data.Events[0].Updates)
{
Assert.GreaterOrEqual(tick.NewQuantity, 0);
Assert.GreaterOrEqual(tick.PriceLevel, 0);
Assert.IsInstanceOf<CoinbaseLevel2UpdateSide>(tick.Side);
}
}

[TestCase("/api/v3/brokerage/orders", null, "Unauthorized")]
[TestCase("/api/v3/brokerage/orders", "", "Unauthorized")]
[TestCase("/api/v3/brokerage/orders", "{null}", "Bad Request")]
[TestCase("/api/v3/brokerage/orders", "[]", "Unauthorized")]
public void ValidateCoinbaseRestRequestWithWrongBodyParameter(string uriPath, object bodyData, string message)
{
var apiKey = Config.Get("coinbase-api-key");
var apiKeySecret = Config.Get("coinbase-api-secret");
var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com");

var request = new RestRequest($"{uriPath}", Method.POST);

var _apiClient = new CoinbaseApiClient(apiKey, apiKeySecret, restApiUrl, 30);

request.AddJsonBody(bodyData);

var exception = Assert.Throws<Exception>(() => _apiClient.ExecuteRequest(request));
Assert.IsTrue(exception.Message.Contains(message));
}

[TestCase("", "INVALID_CANCEL_REQUEST")]
[TestCase("44703527-de90-4aac-ae52-8e6910dee426", "UNKNOWN_CANCEL_ORDER")]
public void CancelOrderWithWrongOrderId(string orderId, string errorMessage)
{
var fakeBrokerIds = new List<string>()
{
orderId
};

var response = CoinbaseApi.CancelOrders(fakeBrokerIds);

Assert.IsFalse(response.Success);
Assert.AreEqual(errorMessage, response.FailureReason);
}

private CoinbaseApi CreateCoinbaseApi(string apiKey, string apiKeySecret)
{
var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com");

return new CoinbaseApi(new SymbolPropertiesDatabaseSymbolMapper(Market.Coinbase), null, apiKey, apiKeySecret, restApiUrl);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using NUnit.Framework;
using System.Threading;
using QuantConnect.Logging;
using QuantConnect.Algorithm;
using QuantConnect.Configuration;
using QuantConnect.Lean.Engine.DataFeeds;

namespace QuantConnect.CoinbaseBrokerage.Tests
{
[TestFixture]
public class CoinbaseBrokerageAdditionalTests
{
[Explicit("`user` channel sometimes doesn't subscribed in WebSocket.Open event")]
[TestCase(5)]
public void BrokerageConnectionAndReconnectionTest(int amountAttempt)
{
int counter = 0;
var cancellationTokenSource = new CancellationTokenSource();
var resetEvent = new AutoResetEvent(false);

using (var brokerage = GetBrokerage())
{
brokerage.Message += (_, brokerageMessageEvent) =>
{
Log.Debug("");
Log.Debug($"Brokerage:Error: {brokerageMessageEvent.Message}");
resetEvent.Set();
};

do
{
Log.Debug("");
Log.Debug($"BrokerageConnectionAndReconnectionTest: connection attempt: #{counter}");
brokerage.Connect();
Assert.IsTrue(brokerage.IsConnected);

// cool down
Assert.IsTrue(resetEvent.WaitOne(TimeSpan.FromSeconds(60), cancellationTokenSource.Token));

//Assert.IsFalse(hasError);

Log.Debug("");
Log.Debug($"BrokerageConnectionAndReconnectionTest: disconnect attempt: #{counter}");
brokerage.Disconnect();
Assert.IsFalse(brokerage.IsConnected);

// cool down
resetEvent.WaitOne(TimeSpan.FromSeconds(10), cancellationTokenSource.Token);

} while (++counter < amountAttempt);
}
}

private static CoinbaseBrokerage GetBrokerage()
{
var wssUrl = Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com");
var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com");
var apiKey = Config.Get("coinbase-api-key");
var apiSecret = Config.Get("coinbase-api-secret");
var algorithm = new QCAlgorithm();
var aggregator = new AggregationManager();

return new CoinbaseBrokerage(wssUrl, apiKey, apiSecret, restApiUrl, algorithm, aggregator, null);
}
}
}
Loading

0 comments on commit 7550d79

Please sign in to comment.