diff --git a/.github/workflows/gh-actions.yml b/.github/workflows/gh-actions.yml index 579653c..1d5b7d9 100644 --- a/.github/workflows/gh-actions.yml +++ b/.github/workflows/gh-actions.yml @@ -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 }} @@ -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 diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs new file mode 100644 index 0000000..02112f4 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs @@ -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(() => 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(() => 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(() => 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>(); + + Assert.IsNotNull(level2Data); + Assert.AreEqual("l2_data", level2Data.Channel); + Assert.IsEmpty(level2Data.ClientId); + Assert.IsNotEmpty(level2Data.SequenceNumber); + Assert.IsInstanceOf(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(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(() => _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() + { + 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); + } + } +} diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs new file mode 100644 index 0000000..bee9b4f --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageAdditionalTests.cs @@ -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); + } + } +} diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageDataQueueHandlerTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageDataQueueHandlerTests.cs new file mode 100644 index 0000000..b45bab0 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageDataQueueHandlerTests.cs @@ -0,0 +1,318 @@ +/* +* 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 System.Linq; +using NUnit.Framework; +using System.Threading; +using QuantConnect.Data; +using QuantConnect.Logging; +using Microsoft.CodeAnalysis; +using QuantConnect.Data.Market; +using System.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage.Tests +{ + /// + /// The class contains DataQueueHandler's tests + /// + [TestFixture] + public partial class CoinbaseBrokerageTests + { + private CoinbaseBrokerage _brokerage { get => (CoinbaseBrokerage)Brokerage; } + + private static readonly Symbol BTCUSDC = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase); + + private static IEnumerable TestParameters + { + get + { + yield return new TestCaseData(BTCUSDC, Resolution.Tick); + yield return new TestCaseData(BTCUSDC, Resolution.Second); + yield return new TestCaseData(BTCUSDC, Resolution.Minute); + yield return new TestCaseData(Symbol.Create("ETHUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Minute); + yield return new TestCaseData(Symbol.Create("GRTUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Second); + } + } + + [Test, TestCaseSource(nameof(TestParameters))] + public void StreamsData(Symbol symbol, Resolution resolution) + { + var startTime = DateTime.UtcNow; + var cancelationToken = new CancellationTokenSource(); + var subscriptionEvent = new ManualResetEvent(false); + + SubscriptionDataConfig[] configs; + if (resolution == Resolution.Tick) + { + var tradeConfig = new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, resolution), + tickType: TickType.Trade); + var quoteConfig = new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, resolution), + tickType: TickType.Quote); + configs = new[] { tradeConfig, quoteConfig }; + } + else + { + configs = new[] + { + GetSubscriptionDataConfig(symbol, resolution), + GetSubscriptionDataConfig(symbol, resolution) + }; + } + + _brokerage.Message += (_, brokerageMessageEvent) => + { + Log.Debug(""); + Log.Debug($"Brokerage:Error: {brokerageMessageEvent.Message}"); + subscriptionEvent.Set(); + }; + + var trade = new ManualResetEvent(false); + var quote = new ManualResetEvent(false); + foreach (var config in configs) + { + ProcessFeed(_brokerage.Subscribe(config, (s, e) => { }), + cancelationToken, + (tick) => + { + if (tick != null) + { + Assert.GreaterOrEqual(tick.EndTime.Ticks, startTime.Ticks); + + Log.Debug(""); + + Assert.That(tick.Symbol, Is.EqualTo(config.Symbol)); + Assert.NotZero(tick.Price); + Assert.IsTrue(tick.Price > 0, "Price was not greater then zero"); + Assert.IsTrue(tick.Value > 0, "Value was not greater then zero"); + + if (tick is Tick) + { + Log.Debug($"Tick: {tick}"); + + } + + if ((tick as Tick)?.TickType == TickType.Trade || tick is TradeBar) + { + Log.Debug($"TradeBar: {tick}"); + + if (resolution != Resolution.Tick) + { + Assert.IsTrue(tick.DataType == MarketDataType.TradeBar); + } + + trade.Set(); + } + + if ((tick as Tick)?.TickType == TickType.Quote || tick is QuoteBar) + { + Log.Debug($"QuoteBar: {tick}"); + if (resolution != Resolution.Tick) + { + Assert.IsTrue(tick.DataType == MarketDataType.QuoteBar); + } + quote.Set(); + } + } + }); + } + + Assert.IsFalse(subscriptionEvent.WaitOne(TimeSpan.FromSeconds(35))); + Assert.IsTrue(trade.WaitOne(resolution.ToTimeSpan() + TimeSpan.FromSeconds(30))); + Assert.IsTrue(quote.WaitOne(resolution.ToTimeSpan() + TimeSpan.FromSeconds(30))); + + foreach (var config in configs) + { + _brokerage.Unsubscribe(config); + } + + Thread.Sleep(2000); + + cancelationToken.Cancel(); + } + + private static IEnumerable LiquidSymbolsSubscriptionConfigs + { + get + { + var liquidSymbols = new (string, Resolution)[10] + { + ("SOLUSD",Resolution.Tick), + ("BTCUSD", Resolution.Second), + ("ETHUSD", Resolution.Tick), + ("XRPUSD", Resolution.Second), + ("ADAUSD", Resolution.Tick), + ("AVAXUSD", Resolution.Second), + ("DOGEUSD", Resolution.Tick), + ("DOTUSD", Resolution.Second), + ("LINKUSD", Resolution.Tick), + ("MATICUSD", Resolution.Second) + }; + + var symbols = new List(); + foreach (var (ticker, resolution) in liquidSymbols) + { + var symbol = Symbol.Create(ticker, SecurityType.Crypto, Market.Coinbase); + + if (resolution == Resolution.Tick) + { + var tradeConfig = new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, resolution), + tickType: TickType.Trade); + var quoteConfig = new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, resolution), + tickType: TickType.Quote); + + symbols.AddRange(new[] { tradeConfig, quoteConfig }); + } + else + { + symbols.AddRange(new[] { GetSubscriptionDataConfig(symbol, resolution), GetSubscriptionDataConfig(symbol, resolution) }); + } + } + + yield return new TestCaseData(symbols); + } + } + + [Test, TestCaseSource(nameof(LiquidSymbolsSubscriptionConfigs))] + public void SubscribeOnMultipleSymbols(List liquidSymbolsSubscriptionConfigs) + { + var cancelationToken = new CancellationTokenSource(); + var startTime = DateTime.UtcNow; + var tickResetEvent = new ManualResetEvent(false); + + _brokerage.Message += (_, brokerageMessageEvent) => + { + Log.Debug(""); + Log.Debug($"Brokerage:Error: {brokerageMessageEvent.Message}"); + cancelationToken.Cancel(); + }; + + var symbolTicks = new Dictionary(); + foreach (var config in liquidSymbolsSubscriptionConfigs) + { + ProcessFeed(_brokerage.Subscribe(config, (s, e) => { }), + cancelationToken, + (tick) => + { + if (tick != null) + { + Assert.GreaterOrEqual(tick.EndTime.Ticks, startTime.Ticks); + + Assert.That(tick.Symbol, Is.EqualTo(config.Symbol)); + Assert.NotZero(tick.Price); + Assert.IsTrue(tick.Price > 0, "Price was not greater then zero"); + Assert.IsTrue(tick.Value > 0, "Value was not greater then zero"); + + if (!symbolTicks.TryGetValue(tick.Symbol, out var symbol)) + { + symbolTicks[tick.Symbol] = true; + } + + if (symbolTicks.Count == liquidSymbolsSubscriptionConfigs.Count / 2) + { + tickResetEvent.Set(); + } + } + }); + } + + if (!tickResetEvent.WaitOne(TimeSpan.FromSeconds(180), cancelationToken.Token)) + { + Assert.Fail("Reset event has not signaled or cancellationToken was canceled"); + } + + foreach (var config in liquidSymbolsSubscriptionConfigs) + { + _brokerage.Unsubscribe(config); + } + + Thread.Sleep(2000); + + cancelationToken.Cancel(); + } + + private static IEnumerable> BitcoinTradingPairs + { + get + { + yield return new List + { + Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase), + Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase), + Symbol.Create("BTCUSDT", SecurityType.Crypto, Market.Coinbase) + }; + } + } + + [TestCaseSource(nameof(BitcoinTradingPairs))] + public void SubscribeOnDifferentUSDTickers(List symbols) + { + var resetEvent = new ManualResetEvent(false); + var cancelationTokenSource = new CancellationTokenSource(); + + var configs = new List(); + + var dataReceivedForType = new Dictionary<(Type, Symbol), int>(); + + foreach (var symbol in symbols) + { + configs.Add(GetSubscriptionDataConfig(symbol, Resolution.Second)); + configs.Add(GetSubscriptionDataConfig(symbol, Resolution.Second)); + + dataReceivedForType.Add((typeof(QuoteBar), symbol), 0); + dataReceivedForType.Add((typeof(TradeBar), symbol), 0); + } + + foreach (var config in configs) + { + ProcessFeed(_brokerage.Subscribe(config, (s, e) => { }), + cancelationTokenSource, + (tick) => + { + if (tick != null) + { + Log.Debug($"Tick: {tick}"); + + if (tick is TradeBar tb) + { + dataReceivedForType[(tb.GetType(), tb.Symbol)] += 1; + } + + if (tick is QuoteBar qb) + { + dataReceivedForType[(qb.GetType(), qb.Symbol)] += 1; + } + + if (dataReceivedForType.Values.All(x => x > 0)) + { + resetEvent.Set(); + } + } + }); + } + + Assert.IsTrue(resetEvent.WaitOne(TimeSpan.FromSeconds(30), cancelationTokenSource.Token)); + + foreach (var config in configs) + { + _brokerage.Unsubscribe(config); + } + + Thread.Sleep(2000); + + cancelationTokenSource.Cancel(); + } + } +} diff --git a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageFactoryTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageFactoryTests.cs similarity index 83% rename from QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageFactoryTests.cs rename to QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageFactoryTests.cs index 7e9849b..27ba4a3 100644 --- a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageFactoryTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageFactoryTests.cs @@ -14,19 +14,18 @@ */ using NUnit.Framework; -using QuantConnect.Brokerages.GDAX; -using QuantConnect.Interfaces; using QuantConnect.Util; +using QuantConnect.Interfaces; -namespace QuantConnect.Tests.Brokerages.GDAX +namespace QuantConnect.CoinbaseBrokerage.Tests { [TestFixture] - public class GDAXBrokerageFactoryTests + public class CoinbaseBrokerageFactoryTests { [Test] public void InitializesFactoryFromComposer() { - using var factory = Composer.Instance.Single(instance => instance.BrokerageType == typeof(GDAXBrokerage)); + using var factory = Composer.Instance.Single(instance => instance.BrokerageType == typeof(CoinbaseBrokerage)); Assert.IsNotNull(factory); } } diff --git a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageHistoryProviderTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs similarity index 56% rename from QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageHistoryProviderTests.cs rename to QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs index 4669f1a..1fd3f0f 100644 --- a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageHistoryProviderTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageHistoryProviderTests.cs @@ -14,39 +14,41 @@ */ using System; -using System.Linq; using NodaTime; +using System.Linq; using NUnit.Framework; -using QuantConnect.Brokerages; -using QuantConnect.Brokerages.GDAX; -using QuantConnect.Configuration; using QuantConnect.Data; +using QuantConnect.Tests; +using QuantConnect.Logging; +using QuantConnect.Securities; using QuantConnect.Data.Market; +using System.Collections.Generic; +using QuantConnect.Configuration; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Lean.Engine.HistoricalData; -using QuantConnect.Logging; -using QuantConnect.Securities; -using RestSharp; -namespace QuantConnect.Tests.Brokerages.GDAX +namespace QuantConnect.CoinbaseBrokerage.Tests { [TestFixture] - public class GDAXBrokerageHistoryProviderTests + public class CoinbaseBrokerageHistoryProviderTests { [Test, TestCaseSource(nameof(TestParameters))] public void GetsHistory(Symbol symbol, Resolution resolution, TickType tickType, TimeSpan period, bool shouldBeEmpty) { - var restClient = new RestClient(Config.Get("gdax-rest-api", "https://api.pro.coinbase.com")); - var webSocketClient = new WebSocketClientWrapper(); var aggregator = new AggregationManager(); - var brokerage = new GDAXBrokerage( - Config.Get("gdax-url", "wss://ws-feed.pro.coinbase.com"), webSocketClient, restClient, - Config.Get("gdax-api-key"), Config.Get("gdax-api-secret"), Config.Get("gdax-passphrase"), null, null, aggregator, null); + var brokerage = new CoinbaseBrokerage( + Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com"), + Config.Get("coinbase-api-key"), + Config.Get("coinbase-api-secret"), + Config.Get("coinbase-rest-api", "https://api.coinbase.com"), + null, + aggregator, + null); var historyProvider = new BrokerageHistoryProvider(); historyProvider.SetBrokerage(brokerage); - historyProvider.Initialize(new HistoryProviderInitializeParameters(null, null, null, null, null, null, null, false, new DataPermissionManager())); + historyProvider.Initialize(new HistoryProviderInitializeParameters(null, null, null, null, null, null, null, false, new DataPermissionManager(), null)); var now = DateTime.UtcNow; @@ -87,34 +89,39 @@ public void GetsHistory(Symbol symbol, Resolution resolution, TickType tickType, Log.Trace("Data points retrieved: " + historyProvider.DataPointCount); } - private static TestCaseData[] TestParameters() + private static IEnumerable TestParameters { - TestGlobals.Initialize(); - var btcusd = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.GDAX); - - return new[] + get { + TestGlobals.Initialize(); + var BTCUSD = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase); + var BTCUSDC = Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase); + // valid parameters - new TestCaseData(btcusd, Resolution.Minute, TickType.Trade, Time.OneHour, false), - new TestCaseData(btcusd, Resolution.Hour, TickType.Trade, Time.OneDay, false), - new TestCaseData(btcusd, Resolution.Daily, TickType.Trade, TimeSpan.FromDays(15), false), + yield return new TestCaseData(BTCUSD, Resolution.Minute, TickType.Trade, TimeSpan.FromDays(5), false); + yield return new TestCaseData(BTCUSD, Resolution.Minute, TickType.Trade, Time.OneHour, false); + yield return new TestCaseData(BTCUSD, Resolution.Hour, TickType.Trade, Time.OneDay, false); + yield return new TestCaseData(BTCUSD, Resolution.Daily, TickType.Trade, TimeSpan.FromDays(15), false); + + yield return new TestCaseData(BTCUSDC, Resolution.Minute, TickType.Trade, Time.OneHour, false); + yield return new TestCaseData(BTCUSDC, Resolution.Hour, TickType.Trade, Time.OneDay, false); // quote tick type, no error, empty result - new TestCaseData(btcusd, Resolution.Daily, TickType.Quote, TimeSpan.FromDays(15), true), + yield return new TestCaseData(BTCUSD, Resolution.Daily, TickType.Quote, TimeSpan.FromDays(15), true); // invalid resolution, no error, empty result - new TestCaseData(btcusd, Resolution.Tick, TickType.Trade, TimeSpan.FromSeconds(15), true), - new TestCaseData(btcusd, Resolution.Second, TickType.Trade, Time.OneMinute, true), + yield return new TestCaseData(BTCUSD, Resolution.Tick, TickType.Trade, TimeSpan.FromSeconds(15), true); + yield return new TestCaseData(BTCUSD, Resolution.Second, TickType.Trade, Time.OneMinute, true); // invalid period, no error, empty result - new TestCaseData(btcusd, Resolution.Daily, TickType.Trade, TimeSpan.FromDays(-15), true), + yield return new TestCaseData(BTCUSD, Resolution.Daily, TickType.Trade, TimeSpan.FromDays(-15), true); // invalid symbol, no error, empty result - new TestCaseData(Symbol.Create("ABCXYZ", SecurityType.Crypto, Market.GDAX), Resolution.Daily, TickType.Trade, TimeSpan.FromDays(15), true), + yield return new TestCaseData(Symbol.Create("ABCXYZ", SecurityType.Crypto, Market.Coinbase), Resolution.Daily, TickType.Trade, TimeSpan.FromDays(15), true); // invalid security type, no error, empty result - new TestCaseData(Symbols.EURGBP, Resolution.Daily, TickType.Trade, TimeSpan.FromDays(15), true) - }; + yield return new TestCaseData(Symbols.EURGBP, Resolution.Daily, TickType.Trade, TimeSpan.FromDays(15), true); + } } } } diff --git a/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs new file mode 100644 index 0000000..cd10be5 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseBrokerageTests.cs @@ -0,0 +1,257 @@ +/* + * 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 Moq; +using System; +using NUnit.Framework; +using System.Threading; +using QuantConnect.Orders; +using QuantConnect.Interfaces; +using QuantConnect.Securities; +using QuantConnect.Brokerages; +using System.Collections.Generic; +using QuantConnect.Configuration; +using QuantConnect.Tests.Brokerages; +using QuantConnect.CoinbaseBrokerage.Api; +using QuantConnect.Lean.Engine.DataFeeds; +using QuantConnect.Tests.Common.Securities; + +namespace QuantConnect.CoinbaseBrokerage.Tests +{ + [TestFixture] + public partial class CoinbaseBrokerageTests : BrokerageTests + { + #region Properties + protected override Symbol Symbol => Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase); + + protected virtual ISymbolMapper SymbolMapper => new SymbolPropertiesDatabaseSymbolMapper(Market.Coinbase); + + protected CoinbaseApi _api; + + /// + /// Gets the security type associated with the + /// + protected override SecurityType SecurityType => SecurityType.Crypto; + + protected override decimal GetDefaultQuantity() + { + return 0.000023m; + } + #endregion + + protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISecurityProvider securityProvider) + { + var securities = new SecurityManager(new TimeKeeper(DateTime.UtcNow, TimeZones.NewYork)) + { + {Symbol, CreateSecurity(Symbol)} + }; + + var transactions = new SecurityTransactionManager(null, securities); + transactions.SetOrderProcessor(new FakeOrderProcessor()); + + var algorithmSettings = new AlgorithmSettings(); + var algorithm = new Mock(); + algorithm.Setup(a => a.Transactions).Returns(transactions); + algorithm.Setup(a => a.BrokerageModel).Returns(new CoinbaseBrokerageModel()); + algorithm.Setup(a => a.Portfolio).Returns(new SecurityPortfolioManager(securities, transactions, algorithmSettings)); + algorithm.Setup(a => a.Securities).Returns(securities); + + var apiKey = Config.Get("coinbase-api-key"); + var apiSecret = Config.Get("coinbase-api-secret"); + var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); + var webSocketUrl = Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com"); + + _api = new CoinbaseApi(SymbolMapper, null, apiKey, apiSecret, restApiUrl); + + return new CoinbaseBrokerage(webSocketUrl, apiKey, apiSecret, restApiUrl, algorithm.Object, orderProvider, new AggregationManager(), null); + } + + /// + /// Returns wether or not the brokers order methods implementation are async + /// + protected override bool IsAsync() + { + return false; + } + + protected override decimal GetAskPrice(Symbol symbol) + { + var brokerageSymbol = SymbolMapper.GetBrokerageSymbol(symbol); + var tick = _api.GetMarketTrades(brokerageSymbol); + return tick.BestAsk; + } + + protected override void ModifyOrderUntilFilled(Order order, OrderTestParameters parameters, double secondsTimeout = 90) + { + Assert.Pass("Order update not supported"); + } + + [Test(Description = "Coinbase doesn't support margin trading")] + public override void GetAccountHoldings() + { + Assert.That(Brokerage.GetAccountHoldings().Count == 0); + } + + // stop market orders no longer supported (since 3/23/2019) + // no stop limit support + private static TestCaseData[] OrderParameters => new[] + { + new TestCaseData(new MarketOrderTestParameters(Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase))), + new TestCaseData(new LimitOrderTestParameters(Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase), 305m, 300m)), + }; + + [Test, TestCaseSource(nameof(OrderParameters))] + public override void CancelOrders(OrderTestParameters parameters) + { + base.CancelOrders(parameters); + } + + [Test, TestCaseSource(nameof(OrderParameters))] + public override void LongFromZero(OrderTestParameters parameters) + { + base.LongFromZero(parameters); + } + + [Test, TestCaseSource(nameof(OrderParameters))] + public override void CloseFromLong(OrderTestParameters parameters) + { + base.CloseFromLong(parameters); + } + + [Test, TestCaseSource(nameof(OrderParameters))] + public override void ShortFromZero(OrderTestParameters parameters) + { + base.ShortFromZero(parameters); + } + + [Test, TestCaseSource(nameof(OrderParameters))] + public override void CloseFromShort(OrderTestParameters parameters) + { + base.CloseFromShort(parameters); + } + + [Test, TestCaseSource(nameof(OrderParameters))] + public override void ShortFromLong(OrderTestParameters parameters) + { + base.ShortFromLong(parameters); + } + + [Test, TestCaseSource(nameof(OrderParameters))] + public override void LongFromShort(OrderTestParameters parameters) + { + base.LongFromShort(parameters); + } + + [TestCase("BTCUSDC")] + public void GetTick(string ticker) + { + var symbol = Symbol.Create(ticker, SecurityType.Crypto, Market.Coinbase); + var brokerageSymbol = SymbolMapper.GetBrokerageSymbol(symbol); + + var tick = _api.GetMarketTrades(brokerageSymbol); + + Assert.IsNotNull(tick); + Assert.Greater(tick.BestAsk, 0); + Assert.Greater(tick.BestBid, 0); + } + + private static IEnumerable<(OrderTestParameters, decimal, decimal, bool)> UpdateOrderPrams() + { + var symbol = Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase); + var limitTestParam = new LimitOrderTestParameters(symbol, 10_000m, 9_000m, new CoinbaseOrderProperties()); + yield return (limitTestParam, 0.0000328m, 12_000m, true); + yield return (limitTestParam, 12_000m, 0.0000328m, false); + } + + [TestCaseSource(nameof(UpdateOrderPrams))] + public void UpdateOrderTest((OrderTestParameters orderTestParam, decimal newAmount, decimal newLimitPrice, bool isSuccessfullyUpdated) testData) + { + var order = testData.orderTestParam.CreateLongOrder(GetDefaultQuantity()); + var errorMessage = ""; + var statusResetEvent = new AutoResetEvent(false); + + OrderProvider.Add(order); + + Brokerage.Message += (_, BrokerageMessageEvent) => + { + errorMessage = BrokerageMessageEvent.Message; + }; + + EventHandler> orderStatusCallback = (sender, orderEvents) => + { + var orderEvent = orderEvents[0]; + + order.Status = orderEvent.Status; + + if (orderEvent.Status == OrderStatus.Invalid) + { + Assert.Fail("Unexpected order status: " + orderEvent.Status); + } + else + { + statusResetEvent.Set(); + } + }; + + Brokerage.OrdersStatusChanged += orderStatusCallback; + + var placeLimitOrder = Brokerage.PlaceOrder(order); + + Assert.IsTrue(placeLimitOrder); + Assert.IsTrue(statusResetEvent.WaitOne(TimeSpan.FromSeconds(2)) && order.Status == OrderStatus.Submitted); + + var updateOrder = new UpdateOrderRequest(DateTime.UtcNow, order.Id, new UpdateOrderFields() { Quantity = testData.newAmount, LimitPrice = testData.newLimitPrice }); + + order.ApplyUpdateOrderRequest(updateOrder); + + var updateResult = Brokerage.UpdateOrder(order); + + if (testData.isSuccessfullyUpdated) + { + Assert.IsTrue(updateResult); + Assert.IsTrue(statusResetEvent.WaitOne(TimeSpan.FromSeconds(2)) && order.Status == OrderStatus.UpdateSubmitted); + Assert.IsEmpty(errorMessage); + } + else + { + Assert.IsFalse(updateResult); + Assert.IsNotEmpty(errorMessage); + Assert.IsTrue(order.Status == OrderStatus.Submitted); + } + + Brokerage.OrdersStatusChanged -= orderStatusCallback; + } + + private static IEnumerable UpdateOrderWrongPrams() + { + var symbol = Symbol.Create("BTCUSDC", SecurityType.Crypto, Market.Coinbase); + yield return new MarketOrderTestParameters(symbol); + yield return new StopLimitOrderTestParameters(symbol, 10_000m, 5_000m); + } + + [Test, TestCaseSource(nameof(UpdateOrderWrongPrams))] + public void UpdateOrderWithWrongParameters(OrderTestParameters orderTestParam) + { + var order = orderTestParam switch + { + MarketOrderTestParameters m => m.CreateLongMarketOrder(1), + StopLimitOrderTestParameters sl => sl.CreateLongOrder(1), + _ => throw new NotImplementedException("The Order type is not implemented.") + }; + + Assert.Throws(() => Brokerage.UpdateOrder(order)); + } + } +} diff --git a/QuantConnect.GDAXBrokerage.Tests/GDAXExchangeInfoDownloaderTests.cs b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseExchangeInfoDownloaderTests.cs similarity index 85% rename from QuantConnect.GDAXBrokerage.Tests/GDAXExchangeInfoDownloaderTests.cs rename to QuantConnect.CoinbaseBrokerage.Tests/CoinbaseExchangeInfoDownloaderTests.cs index 2a54c41..c3cec53 100644 --- a/QuantConnect.GDAXBrokerage.Tests/GDAXExchangeInfoDownloaderTests.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/CoinbaseExchangeInfoDownloaderTests.cs @@ -13,21 +13,21 @@ * limitations under the License. */ -using NUnit.Framework; -using QuantConnect.ToolBox; -using QuantConnect.Util; using System; using System.Linq; +using NUnit.Framework; +using QuantConnect.Util; +using QuantConnect.ToolBox; -namespace QuantConnect.Tests.Brokerages.GDAX +namespace QuantConnect.CoinbaseBrokerage.Tests { [TestFixture] - public class GDAXExchangeInfoDownloaderTests + public class CoinbaseExchangeInfoDownloaderTests { [Test] public void GetsExchangeInfo() { - var eid = Composer.Instance.GetExportedValueByTypeName("GDAXExchangeInfoDownloader"); + var eid = Composer.Instance.GetExportedValueByTypeName("CoinbaseExchangeInfoDownloader"); var tickers = eid.Get().ToList(); Assert.IsTrue(tickers.Any()); @@ -38,7 +38,7 @@ public void GetsExchangeInfo() var data = tickerLine.Split(","); Assert.AreEqual(10, data.Length); var ticker = data[1]; - Assert.Greater(string.Compare(ticker, previousTicker, StringComparison.Ordinal), 0); + Assert.AreNotEqual(previousTicker, ticker); previousTicker = ticker; } } diff --git a/QuantConnect.GDAXBrokerage.Tests/QuantConnect.GDAXBrokerage.Tests.csproj b/QuantConnect.CoinbaseBrokerage.Tests/QuantConnect.CoinbaseBrokerage.Tests.csproj similarity index 72% rename from QuantConnect.GDAXBrokerage.Tests/QuantConnect.GDAXBrokerage.Tests.csproj rename to QuantConnect.CoinbaseBrokerage.Tests/QuantConnect.CoinbaseBrokerage.Tests.csproj index 71c1090..739c4ee 100644 --- a/QuantConnect.GDAXBrokerage.Tests/QuantConnect.GDAXBrokerage.Tests.csproj +++ b/QuantConnect.CoinbaseBrokerage.Tests/QuantConnect.CoinbaseBrokerage.Tests.csproj @@ -9,10 +9,10 @@ Copyright © 2021 UnitTest bin\$(Configuration)\ - QuantConnect.GDAXBrokerage.Tests - QuantConnect.GDAXBrokerage.Tests - QuantConnect.GDAXBrokerage.Tests - QuantConnect.GDAXBrokerage.Tests + QuantConnect.CoinbaseBrokerage.Tests + QuantConnect.CoinbaseBrokerage.Tests + QuantConnect.CoinbaseBrokerage.Tests + QuantConnect.CoinbaseBrokerage.Tests false @@ -28,8 +28,8 @@ - - + + diff --git a/QuantConnect.GDAXBrokerage.Tests/TestSetup.cs b/QuantConnect.CoinbaseBrokerage.Tests/TestSetup.cs similarity index 95% rename from QuantConnect.GDAXBrokerage.Tests/TestSetup.cs rename to QuantConnect.CoinbaseBrokerage.Tests/TestSetup.cs index 00f60ad..309d641 100644 --- a/QuantConnect.GDAXBrokerage.Tests/TestSetup.cs +++ b/QuantConnect.CoinbaseBrokerage.Tests/TestSetup.cs @@ -20,7 +20,7 @@ using QuantConnect.Logging; using QuantConnect.Configuration; -namespace QuantConnect.Tests.Brokerages.GDAX +namespace QuantConnect.CoinbaseBrokerage.Tests { [TestFixture] public class TestSetup @@ -63,6 +63,7 @@ private static void SetUp() Log.LogHandler = new CompositeLogHandler(); Log.Trace("TestSetup(): starting..."); ReloadConfiguration(); + Log.DebuggingEnabled = Config.GetBool("debug-mode"); } private static TestCaseData[] TestParameters diff --git a/QuantConnect.CoinbaseBrokerage.Tests/config.json b/QuantConnect.CoinbaseBrokerage.Tests/config.json new file mode 100644 index 0000000..6906dd3 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage.Tests/config.json @@ -0,0 +1,7 @@ +{ + "data-folder": "../../../../Lean/Data/", + "coinbase-rest-api": "https://api.coinbase.com", + "coinbase-url": "wss://advanced-trade-ws.coinbase.com", + "coinbase-api-key": "", + "coinbase-api-secret": "" +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs new file mode 100644 index 0000000..5f5b95a --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloader.cs @@ -0,0 +1,109 @@ +/* + * 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 NodaTime; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Securities; +using QuantConnect.Brokerages; +using QuantConnect.Data.Market; +using QuantConnect.Configuration; +using System.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage.ToolBox +{ + /// + /// Coinbase Data Downloader class + /// + public class CoinbaseDownloader : IDataDownloader + { + /// + /// Get historical data enumerable for a single symbol, type and resolution given this start and end time (in UTC). + /// + /// model class for passing in parameters for historical data + /// Enumerable of base data for this symbol + public IEnumerable Get(DataDownloaderGetParameters dataDownloaderGetParameters) + { + var symbol = dataDownloaderGetParameters.Symbol; + var resolution = dataDownloaderGetParameters.Resolution; + var startUtc = dataDownloaderGetParameters.StartUtc; + var endUtc = dataDownloaderGetParameters.EndUtc; + var tickType = dataDownloaderGetParameters.TickType; + + if (tickType != TickType.Trade) + { + return Enumerable.Empty(); + } + + var type = default(Type); + if(resolution == Resolution.Tick) + { + type = typeof(Tick); + } + else if(tickType == TickType.Trade) + { + type = typeof(TradeBar); + } + else + { + type = typeof(OpenInterest); + } + + var historyRequest = new HistoryRequest( + startUtc, + endUtc, + type, + symbol, + resolution, + SecurityExchangeHours.AlwaysOpen(DateTimeZone.Utc), + DateTimeZone.Utc, + resolution, + false, + false, + DataNormalizationMode.Raw, + tickType); + + var brokerage = CreateBrokerage(); + var data = brokerage.GetHistory(historyRequest); + return data; + } + + /// + /// Creates and initializes a new instance of the class for Coinbase integration. + /// + /// + /// This method retrieves necessary configuration values such as API key, API secret, and API URL from the application configuration. + /// + /// + /// A new instance of the class configured for Coinbase integration. + /// + /// + /// + /// + /// + /// var coinbaseBrokerage = CreateBrokerage(); + /// // Use the coinbaseBrokerage instance for trading and other operations. + /// + /// + private Brokerage CreateBrokerage() + { + var apiKey = Config.Get("coinbase-api-key", ""); + var apiSecret = Config.Get("coinbase-api-secret", ""); + var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); + return new CoinbaseBrokerage(string.Empty, apiKey, apiSecret, restApiUrl, null, null, null); + } + } +} diff --git a/QuantConnect.GDAXBrokerage.ToolBox/GDAXDownloaderProgram.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloaderProgram.cs similarity index 81% rename from QuantConnect.GDAXBrokerage.ToolBox/GDAXDownloaderProgram.cs rename to QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloaderProgram.cs index f6967e7..7e760cf 100644 --- a/QuantConnect.GDAXBrokerage.ToolBox/GDAXDownloaderProgram.cs +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseDownloaderProgram.cs @@ -19,22 +19,23 @@ using QuantConnect.Util; using QuantConnect.Logging; using System.Globalization; +using QuantConnect.ToolBox; using System.Collections.Generic; -namespace QuantConnect.ToolBox.GDAXDownloader +namespace QuantConnect.CoinbaseBrokerage.ToolBox { - public static class GDAXDownloaderProgram + public static class CoinbaseDownloaderProgram { /// - /// GDAX Downloader Toolbox Project For LEAN Algorithmic Trading Engine. + /// Coinbase Downloader Toolbox Project For LEAN Algorithmic Trading Engine. /// - public static void GDAXDownloader(IList tickers, string resolution, DateTime fromDate, DateTime toDate) + public static void CoinbaseDownloader(IList tickers, string resolution, DateTime fromDate, DateTime toDate) { Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); if (resolution.IsNullOrEmpty() || tickers.IsNullOrEmpty()) { - Console.WriteLine("GDAXDownloader ERROR: '--tickers=' or '--resolution=' parameter is missing"); + Console.WriteLine($"{nameof(CoinbaseDownloader)}:ERROR: '--tickers=' or '--resolution=' parameter is missing"); Console.WriteLine("--tickers=ETHUSD,ETHBTC,BTCUSD,etc."); Console.WriteLine("--resolution=Second/Minute/Hour/Daily"); Environment.Exit(1); @@ -44,10 +45,10 @@ public static void GDAXDownloader(IList tickers, string resolution, Date { // Load settings from config.json var dataDirectory = Globals.DataFolder; - //todo: will download any exchange but always save as gdax + //todo: will download any exchange but always save as coinbase // Create an instance of the downloader - const string market = Market.GDAX; - var downloader = new GDAXDownloader(); + const string market = Market.Coinbase; + var downloader = new CoinbaseDownloader(); foreach (var ticker in tickers) { // Download the data @@ -78,7 +79,7 @@ public static void GDAXDownloader(IList tickers, string resolution, Date /// public static void ExchangeInfoDownloader() { - new ExchangeInfoUpdater(new GDAXExchangeInfoDownloader()) + new ExchangeInfoUpdater(new CoinbaseExchangeInfoDownloader()) .Run(); } } diff --git a/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs new file mode 100644 index 0000000..df9457a --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/CoinbaseExchangeInfoDownloader.cs @@ -0,0 +1,75 @@ +/* + * 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 QuantConnect.ToolBox; +using System.Collections.Generic; +using QuantConnect.Configuration; +using QuantConnect.CoinbaseBrokerage.Api; + +namespace QuantConnect.CoinbaseBrokerage.ToolBox +{ + /// + /// Coinbase implementation of + /// + public class CoinbaseExchangeInfoDownloader : IExchangeInfoDownloader + { + /// + /// Market name + /// + public string Market => QuantConnect.Market.Coinbase; + + /// + /// Security Type + /// + public string SecurityType => QuantConnect.SecurityType.Crypto.SecurityTypeToLower(); + + /// + /// Pulling data from a remote source + /// + /// Enumerable of exchange info + public IEnumerable Get() + { + var coinbaseApi = CreateCoinbaseApi(); + var products = coinbaseApi.GetProducts(); + + foreach (var product in products) + { + var symbol = product.ProductId.Replace("-", string.Empty); + var description = $"{product.BaseName}-{product.QuoteName}"; + var quoteCurrency = product.QuoteCurrencyId; + var contractMultiplier = 1; + var minimum_price_variation = product.QuoteIncrement; + var lot_size = product.BaseIncrement; + var marketTicker = product.ProductId; + var minimum_order_size = product.BaseMinSize; + + // market,symbol,type,description,quote_currency,contract_multiplier,minimum_price_variation,lot_size,market_ticker,minimum_order_size + yield return $"{Market},{symbol},{SecurityType},{description},{quoteCurrency},{contractMultiplier},{minimum_price_variation},{lot_size},{marketTicker},{minimum_order_size}"; + } + } + + /// + /// Creates and initializes a new instance of the class integration. + /// + /// + private CoinbaseApi CreateCoinbaseApi() + { + var apiKey = Config.Get("coinbase-api-key"); + var apiSecret = Config.Get("coinbase-api-secret"); + var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com"); + return new CoinbaseApi(null, null, apiKey, apiSecret, restApiUrl); + } + } +} diff --git a/QuantConnect.GDAXBrokerage.ToolBox/Program.cs b/QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs similarity index 81% rename from QuantConnect.GDAXBrokerage.ToolBox/Program.cs rename to QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs index 18efd53..285ca03 100644 --- a/QuantConnect.GDAXBrokerage.ToolBox/Program.cs +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/Program.cs @@ -13,12 +13,11 @@ * limitations under the License. */ -using QuantConnect.Configuration; -using QuantConnect.ToolBox.GDAXDownloader; using System; +using QuantConnect.Configuration; using static QuantConnect.Configuration.ApplicationParser; -namespace QuantConnect.TemplateBrokerage.ToolBox +namespace QuantConnect.CoinbaseBrokerage.ToolBox { internal static class Program { @@ -35,6 +34,11 @@ private static void Main(string[] args) PrintMessageAndExit(1, "ERROR: --app value is required"); } + if(string.IsNullOrEmpty(Config.GetValue("coinbase-api-key")) || string.IsNullOrEmpty(Config.GetValue("coinbase-api-secret"))) + { + PrintMessageAndExit(1, "ERROR: check configs: 'coinbase-api-key' or 'coinbase-api-secret'"); + } + var targetAppName = targetApp.ToString(); if (targetAppName.Contains("download") || targetAppName.Contains("dl")) { @@ -44,11 +48,11 @@ private static void Main(string[] args) var toDate = optionsObject.ContainsKey("to-date") ? Parse.DateTimeExact(optionsObject["to-date"].ToString(), "yyyyMMdd-HH:mm:ss") : DateTime.UtcNow; - GDAXDownloaderProgram.GDAXDownloader(tickers, resolution, fromDate, toDate); + CoinbaseDownloaderProgram.CoinbaseDownloader(tickers, resolution, fromDate, toDate); } else if (targetAppName.Contains("updater") || targetAppName.EndsWith("spu")) { - GDAXDownloaderProgram.ExchangeInfoDownloader(); + CoinbaseDownloaderProgram.ExchangeInfoDownloader(); } else { diff --git a/QuantConnect.GDAXBrokerage.ToolBox/QuantConnect.GDAXBrokerage.ToolBox.csproj b/QuantConnect.CoinbaseBrokerage.ToolBox/QuantConnect.CoinbaseBrokerage.ToolBox.csproj similarity index 71% rename from QuantConnect.GDAXBrokerage.ToolBox/QuantConnect.GDAXBrokerage.ToolBox.csproj rename to QuantConnect.CoinbaseBrokerage.ToolBox/QuantConnect.CoinbaseBrokerage.ToolBox.csproj index 162799b..573315d 100644 --- a/QuantConnect.GDAXBrokerage.ToolBox/QuantConnect.GDAXBrokerage.ToolBox.csproj +++ b/QuantConnect.CoinbaseBrokerage.ToolBox/QuantConnect.CoinbaseBrokerage.ToolBox.csproj @@ -7,14 +7,14 @@ net6.0 Copyright © 2021 bin\$(Configuration)\ - QuantConnect.GDAXBrokerage.ToolBox - QuantConnect.GDAXBrokerage.ToolBox - QuantConnect.GDAXBrokerage.ToolBox - QuantConnect.GDAXBrokerage.ToolBox + QuantConnect.CoinbaseBrokerage.ToolBox + QuantConnect.CoinbaseBrokerage.ToolBox + QuantConnect.CoinbaseBrokerage.ToolBox + QuantConnect.CoinbaseBrokerage.ToolBox false true false - QuantConnect LEAN GDAX Brokerage: Brokerage Template toolbox plugin for Lean + QuantConnect LEAN Coinbase Brokerage: Coinbase Brokerage toolbox plugin for Lean full @@ -29,6 +29,6 @@ - + diff --git a/QuantConnect.GDAXBrokerage.sln b/QuantConnect.CoinbaseBrokerage.sln similarity index 90% rename from QuantConnect.GDAXBrokerage.sln rename to QuantConnect.CoinbaseBrokerage.sln index fa38e53..3430b0f 100644 --- a/QuantConnect.GDAXBrokerage.sln +++ b/QuantConnect.CoinbaseBrokerage.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31205.134 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33723.286 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect", "..\Lean\Common\QuantConnect.csproj", "{477827EE-5908-48AB-B6A4-DAB6E85D96DF}" EndProject @@ -15,11 +15,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.Tests", "..\Le EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.ToolBox", "..\Lean\ToolBox\QuantConnect.ToolBox.csproj", "{88885A3A-5028-4EFB-9244-47FDB04725CE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.GDAXBrokerage.Tests", "QuantConnect.GDAXBrokerage.Tests\QuantConnect.GDAXBrokerage.Tests.csproj", "{1F07E32C-3242-423C-BAF6-D8DC27311F78}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.CoinbaseBrokerage.Tests", "QuantConnect.CoinbaseBrokerage.Tests\QuantConnect.CoinbaseBrokerage.Tests.csproj", "{1F07E32C-3242-423C-BAF6-D8DC27311F78}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.GDAXBrokerage.ToolBox", "QuantConnect.GDAXBrokerage.ToolBox\QuantConnect.GDAXBrokerage.ToolBox.csproj", "{49F0C9B2-DBA6-44CB-91CE-854B370FDFAE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.CoinbaseBrokerage.ToolBox", "QuantConnect.CoinbaseBrokerage.ToolBox\QuantConnect.CoinbaseBrokerage.ToolBox.csproj", "{49F0C9B2-DBA6-44CB-91CE-854B370FDFAE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.GDAXBrokerage", "QuantConnect.GDAXBrokerage\QuantConnect.GDAXBrokerage.csproj", "{503FE329-D93D-461A-8FE0-EF582FEF19A3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.CoinbaseBrokerage", "QuantConnect.CoinbaseBrokerage\QuantConnect.CoinbaseBrokerage.csproj", "{503FE329-D93D-461A-8FE0-EF582FEF19A3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs new file mode 100644 index 0000000..c6f2f8e --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApi.cs @@ -0,0 +1,427 @@ +/* + * 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 Newtonsoft.Json; +using QuantConnect.Util; +using QuantConnect.Orders; +using System.Globalization; +using QuantConnect.Brokerages; +using QuantConnect.Securities; +using System.Collections.Generic; +using QuantConnect.CoinbaseBrokerage.Models; +using QuantConnect.CoinbaseBrokerage.Converters; +using QuantConnect.CoinbaseBrokerage.Models.Enums; +using QuantConnect.CoinbaseBrokerage.Models.Requests; +using BrokerageEnums = QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Api; + +public class CoinbaseApi : IDisposable +{ + /// + /// Represents the maximum number of occurrences allowed per unit of time for a gate limit. + /// + /// + /// Refer to the documentation for more details: + /// . + /// + private const int maxGateLimitOccurrences = 30; + + /// + /// Represents an instance of the Coinbase API client used for communication with the Coinbase API. + /// + private readonly CoinbaseApiClient _apiClient; + + /// + /// Represents the prefix used for API endpoints in the application. + /// + private readonly string _apiPrefix = "/api/v3"; + + private JsonSerializerSettings _jsonSerializerSettings = new() + { + Converters = new List() { new CoinbaseDecimalStringConverter() }, + NullValueHandling = NullValueHandling.Ignore + }; + + /// + /// Symbol mapper + /// + private ISymbolMapper SymbolMapper { get; } + + /// + /// Security provider + /// + private ISecurityProvider SecurityProvider { get; } + + public CoinbaseApi(ISymbolMapper symbolMapper, ISecurityProvider securityProvider, + string apiKey, string apiKeySecret, string restApiUrl) + { + SymbolMapper = symbolMapper; + SecurityProvider = securityProvider; + _apiClient = new CoinbaseApiClient(apiKey, apiKeySecret, restApiUrl, maxGateLimitOccurrences); + } + + /// + /// Generates WebSocket signatures for authentication. + /// + /// The WebSocket channel for which the signature is generated. + /// A collection of product identifiers for which the signature is generated. + /// + /// A tuple containing the API key, timestamp, and signature required for WebSocket authentication. + /// + /// + /// The parameter specifies the WebSocket channel, + /// and contains a collection of product identifiers for which the authentication signature is generated. + /// + /// + /// This example demonstrates how to use the GetWebSocketSignatures method: + /// + /// var (apiKey, timestamp, signature) = GetWebSocketSignatures("trades", new List { "BTC-USD", "ETH-USD" }); + /// + /// + public (string apiKey, string timestamp, string signature) GetWebSocketSignatures(string channel, ICollection productIds) + { + return _apiClient.GenerateWebSocketSignature(channel, productIds); + } + + /// + /// Retrieves a list of Coinbase accounts associated with the authenticated user's brokerage. + /// + /// An IEnumerable of CoinbaseAccount objects representing the user's brokerage accounts. + public IEnumerable GetAccounts() + { + var request = new RestRequest($"{_apiPrefix}/brokerage/accounts", Method.GET); + + var response = _apiClient.ExecuteRequest(request); + + return JsonConvert.DeserializeObject(response.Content).Accounts; + } + + /// + /// Retrieves a collection of historical Coinbase orders based on the specified order status. + /// + /// The status of the orders to retrieve. + /// + /// An IEnumerable of CoinbaseOrder representing historical orders matching the specified order status. + /// + /// + /// The method constructs a request to the Coinbase API for retrieving historical orders. + /// The optional parameter allows filtering orders based on their status. + /// If the is set to , + /// all historical orders, regardless of their status, will be retrieved. + /// + public IEnumerable GetOrders(BrokerageEnums.OrderStatus orderStatus) + { + var request = new RestRequest($"{_apiPrefix}/brokerage/orders/historical/batch", Method.GET); + + if (orderStatus != BrokerageEnums.OrderStatus.UnknownOrderStatus) + { + request.AddQueryParameter("order_status", orderStatus.ToStringInvariant().ToUpperInvariant()); + } + + var response = _apiClient.ExecuteRequest(request); + + return JsonConvert.DeserializeObject(response.Content).Orders; + } + + /// + /// Cancels multiple Coinbase orders identified by their broker IDs. + /// + /// A list of broker IDs representing the orders to be canceled. + /// + /// A CoinbaseCancelOrderResult representing the result of the cancellation operation. + /// + /// + /// The method constructs a request to the Coinbase API for canceling multiple orders in batch. + /// The parameter contains a list of broker IDs that uniquely identify + /// the orders to be canceled. The method returns a result representing the outcome of the cancellation operation. + /// + public CoinbaseCancelOrderResult CancelOrders(List brokerIds) + { + var request = new RestRequest($"{_apiPrefix}/brokerage/orders/batch_cancel", Method.POST); + + request.AddJsonBody(JsonConvert.SerializeObject(new { order_ids = brokerIds })); + + var response = _apiClient.ExecuteRequest(request); + + // It always returns result, even if we have sent invalid orderId + // The Coinbase doesn't support combo orders as a result we return First cancel order response + return JsonConvert.DeserializeObject(response.Content).Result.First(); + } + + /// + /// Get snapshot information, by product ID, about the last trades (ticks), best bid/ask, and 24h volume. + /// + /// The trading pair, i.e., 'BTC-USD'. + /// Number of trades to return. Correct Range between [1:1000] + /// An instance of the struct. + /// Thrown when the provided productId is null or empty. + public CoinbaseMarketTrades GetMarketTrades(string productId, int limit = 1) + { + if (string.IsNullOrEmpty(productId)) + { + throw new ArgumentException($"{nameof(CoinbaseApi)}.{nameof(GetMarketTrades)}: productId is null or empty"); + } + + if (limit > 1000) + { + throw new ArgumentException($"{nameof(CoinbaseApi)}.{nameof(GetMarketTrades)}: Please provide a limit equal to or below 1000."); + } + + var request = new RestRequest($"{_apiPrefix}/brokerage/products/{productId}/ticker", Method.GET); + + request.AddQueryParameter("limit", limit.ToStringInvariant()); + + var response = _apiClient.ExecuteRequest(request); + + return JsonConvert.DeserializeObject(response.Content); + } + + /// + /// Get rates for a single product by product ID, grouped in buckets. + /// + /// The trading pair, i.e., 'BTC-USD'. + /// Timestamp for starting range of aggregations, in UNIX time. + /// Timestamp for ending range of aggregations, in UNIX time. + /// The time slice value for each candle. + /// An enumerable collection. + public IEnumerable GetProductCandles(string productId, DateTime start, DateTime end, CandleGranularity granularity) + { + var request = new RestRequest($"{_apiPrefix}/brokerage/products/{productId}/candles", Method.GET); + + request.AddQueryParameter("start", Time.DateTimeToUnixTimeStamp(start).ToString("F0", CultureInfo.InvariantCulture)); + request.AddQueryParameter("end", Time.DateTimeToUnixTimeStamp(end).ToString("F0", CultureInfo.InvariantCulture)); + request.AddQueryParameter("granularity", JsonConvert.SerializeObject(granularity).Replace("\"", string.Empty)); + + var response = _apiClient.ExecuteRequest(request); + + // returns data backwards should use Reverse + return JsonConvert.DeserializeObject(response.Content).Candles.Reverse(); + } + + /// + /// Get a list of the available currency pairs for trading. + /// + /// >An enumerable collection. + public IEnumerable GetProducts() + { + var request = new RestRequest($"{_apiPrefix}/brokerage/products", Method.GET); + + var response = _apiClient.ExecuteRequest(request); + + return JsonConvert.DeserializeObject(response.Content).Products; + } + + /// + /// Edits an existing limit order on Coinbase brokerage. + /// + /// The limit order to be edited. + /// A response containing information about the edited order. + public CoinbaseEditOrderResponse EditOrder(LimitOrder leanOrder) + { + var request = new RestRequest($"{_apiPrefix}/brokerage/orders/edit", Method.POST); + + request.AddJsonBody(JsonConvert.SerializeObject( + new CoinbaseEditOrderRequest(leanOrder.BrokerId.Single(), leanOrder.LimitPrice, leanOrder.AbsoluteQuantity), _jsonSerializerSettings)); + + var response = _apiClient.ExecuteRequest(request); + + return JsonConvert.DeserializeObject(response.Content); + } + + /// + /// Creates a new Coinbase order based on the specified Lean order. + /// + /// The Lean order object containing the order details. + /// + /// A CoinbaseCreateOrderResponse representing the response from Coinbase after placing the order. + /// + /// + /// The method takes a Lean order object and converts it into the required format for placing an order + /// using the Coinbase API. It then constructs a request to the Coinbase API for creating a new order, + /// sends the request, and returns the response containing information about the created order. + /// + public CoinbaseCreateOrderResponse CreateOrder(Order leanOrder) + { + var placeOrderRequest = CreateOrderRequest(leanOrder); + + var request = new RestRequest($"{_apiPrefix}/brokerage/orders", Method.POST); + + request.AddJsonBody(JsonConvert.SerializeObject(placeOrderRequest, _jsonSerializerSettings)); + + var response = _apiClient.ExecuteRequest(request); + + return JsonConvert.DeserializeObject(response.Content); + } + + /// + /// Creates a Coinbase order request based on the specified Lean order. + /// + /// The Lean order object containing the order details. + /// + /// A CoinbaseCreateOrderRequest representing the request to be sent to Coinbase for order placement. + /// + private CoinbaseCreateOrderRequest CreateOrderRequest(Order leanOrder) + { + if (leanOrder.Direction == OrderDirection.Hold) + { + throw new NotSupportedException(); + } + + var model = new CoinbaseCreateOrderRequest( + Guid.NewGuid(), + SymbolMapper.GetBrokerageSymbol(leanOrder.Symbol), + leanOrder.Direction == OrderDirection.Buy ? OrderSide.Buy : OrderSide.Sell); + + var orderProperties = leanOrder.Properties as CoinbaseOrderProperties; + + switch (leanOrder) + { + case MarketOrder: + model.OrderConfiguration = new OrderConfiguration { MarketIoc = new() }; + if (leanOrder.Direction == OrderDirection.Buy) + { + var price = GetTickerPrice(leanOrder.Symbol, leanOrder.Direction); + var minimumPriceVariation = SecurityProvider.GetSecurity(leanOrder.Symbol).SymbolProperties.MinimumPriceVariation; + model.OrderConfiguration.MarketIoc.QuoteSize = Math.Round(price * Math.Abs(leanOrder.Quantity) / minimumPriceVariation) * minimumPriceVariation; + } + else + { + model.OrderConfiguration.MarketIoc.BaseSize = Math.Abs(leanOrder.Quantity); + } + break; + case LimitOrder limitOrder when leanOrder.TimeInForce is Orders.TimeInForces.GoodTilCanceledTimeInForce: + { + model.OrderConfiguration = new OrderConfiguration + { + LimitGtc = new() + { + BaseSize = Math.Abs(leanOrder.Quantity), + LimitPrice = limitOrder.LimitPrice, + } + }; + + model.OrderConfiguration.LimitGtc.PostOnly = orderProperties?.PostOnly; + break; + } + case LimitOrder limitOrder when leanOrder.TimeInForce is Orders.TimeInForces.GoodTilDateTimeInForce tilDate: + { + model.OrderConfiguration = new OrderConfiguration + { + LimitGtd = new() + { + BaseSize = Math.Abs(leanOrder.Quantity), + LimitPrice = limitOrder.LimitPrice, + EndTime = tilDate.Expiry, + } + }; + + model.OrderConfiguration.LimitGtd.PostOnly = orderProperties?.PostOnly; + break; + } + case StopLimitOrder stopLimitOrder when leanOrder.TimeInForce is Orders.TimeInForces.GoodTilCanceledTimeInForce: + var stopLimitGtc = new StopLimitGtc() + { + BaseSize = Math.Abs(leanOrder.Quantity), + LimitPrice = stopLimitOrder.LimitPrice, + StopPrice = stopLimitOrder.StopPrice + }; + + var ticker = GetTickerPrice(leanOrder.Symbol, leanOrder.Direction); + stopLimitGtc.StopDirection = stopLimitGtc.StopPrice > ticker ? + StopDirection.StopDirectionStopUp : + StopDirection.StopDirectionStopDown; + + model.OrderConfiguration = new() { StopLimitGtc = stopLimitGtc }; + break; + case StopLimitOrder stopLimitOrder when leanOrder.TimeInForce is Orders.TimeInForces.GoodTilDateTimeInForce tilDate: + var stopLimitGtd = new StopLimitGtd() + { + EndTime = tilDate.Expiry, + StopPrice = stopLimitOrder.StopPrice, + LimitPrice = stopLimitOrder.LimitPrice, + BaseSize = Math.Abs(leanOrder.Quantity), + }; + + ticker = GetTickerPrice(leanOrder.Symbol, leanOrder.Direction); + stopLimitGtd.StopDirection = stopLimitGtd.StopPrice > ticker ? + StopDirection.StopDirectionStopUp : + StopDirection.StopDirectionStopDown; + + model.OrderConfiguration = new() { StopLimitGtd = stopLimitGtd }; + break; + default: throw new NotSupportedException($"Order type {leanOrder.Type.ToStringInvariant()} is not supported"); + }; + + if (orderProperties?.SelfTradePreventionId == true) + { + model.SelfTradePreventionId = Guid.NewGuid(); + } + + return model; + } + + /// + /// Retrieves the ticker price for the specified symbol and order direction. + /// + /// The symbol for which to retrieve the ticker price. + /// The order direction (Buy or Sell) for which to retrieve the ticker price. + /// + /// The ticker price associated with the specified symbol and order direction. + /// + /// + /// The method first attempts to retrieve the ticker price from the provided security object. + /// If the ticker price is not available or is zero, it queries the market trades for the specified symbol + /// and retrieves the BestBid or BestAsk depending on the order direction. + /// If the market trades data is also unavailable, the method throws a KeyNotFoundException. + /// + /// + /// Thrown when the ticker price cannot be resolved due to missing market trades data. + /// + private decimal GetTickerPrice(Symbol symbol, OrderDirection leanOrderDirection) + { + var security = SecurityProvider.GetSecurity(symbol); + var tickerPrice = leanOrderDirection == OrderDirection.Buy ? security.AskPrice : security.BidPrice; + if (tickerPrice == 0) + { + var brokerageSymbol = SymbolMapper.GetBrokerageSymbol(symbol); + var ticker = GetMarketTrades(brokerageSymbol); + + if (ticker.BestBid == 0 || ticker.BestAsk == 0) + { + throw new KeyNotFoundException( + $"CoinbaseBrokerage: Unable to resolve currency conversion pair: {symbol}"); + } + + tickerPrice = leanOrderDirection == OrderDirection.Buy ? ticker.BestAsk : ticker.BestBid; + } + + return tickerPrice; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting resources. + /// + /// + /// This method disposes of the underlying API client safely to release any resources held by it. + /// + public void Dispose() + { + _apiClient.DisposeSafely(); + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs new file mode 100644 index 0000000..28567e5 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Api/CoinbaseApiClient.cs @@ -0,0 +1,155 @@ +/* + * 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.Net; +using System.Text; +using System.Linq; +using QuantConnect.Util; +using System.Diagnostics; +using System.Globalization; +using System.Collections.Generic; +using System.Security.Cryptography; + +namespace QuantConnect.CoinbaseBrokerage.Api; + +/// +/// Coinbase api client implementation +/// +public class CoinbaseApiClient : IDisposable +{ + private readonly string _apiKey; + private readonly HMACSHA256 _hmacSha256; + private readonly RestClient _restClient; + private readonly RateGate _rateGate; + + public CoinbaseApiClient(string apiKey, string apiKeySecret, string restApiUrl, int maxRequestsPerSecond) + { + _apiKey = apiKey; + _restClient = new RestClient(restApiUrl); + _hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(apiKeySecret)); + _rateGate = new RateGate(maxRequestsPerSecond, Time.OneSecond); + } + + /// + /// Authenticates a given REST request by adding necessary headers such as CB-ACCESS-KEY, CB-ACCESS-SIGN, and CB-ACCESS-TIMESTAMP. + /// + /// The REST request to be authenticated. + /// + /// This method computes and adds the required authentication headers to the provided REST request, including the CB-ACCESS-KEY, + /// CB-ACCESS-SIGN (signature), and CB-ACCESS-TIMESTAMP (timestamp) headers. The signature is generated using the HMAC-SHA256 algorithm. + /// + private void AuthenticateRequest(IRestRequest request) + { + var body = request.Parameters.SingleOrDefault(b => b.Type == ParameterType.RequestBody); + + var urlPath = _restClient.BuildUri(request).AbsolutePath; + + var timestamp = GetNonce(); + + var signature = GetSign(timestamp, request.Method.ToString(), urlPath, body?.Value?.ToString() ?? string.Empty); + + request.AddHeader("CB-ACCESS-KEY", _apiKey); + request.AddHeader("CB-ACCESS-SIGN", signature); + request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp); + } + + /// + /// Executes a REST request, incorporating rate limiting using a rate gate. + /// + /// The REST request to be executed. + /// + /// An instance of representing the response of the executed request. + /// + /// + /// This method waits for the rate gate to allow the request to proceed before executing the provided REST request using + /// the underlying REST client. The rate gate is used for rate limiting to control the frequency of outgoing requests. + /// + [StackTraceHidden] + public IRestResponse ExecuteRequest(IRestRequest request) + { + _rateGate.WaitToProceed(); + + AuthenticateRequest(request); + + var response = _restClient.Execute(request); + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"{nameof(CoinbaseApiClient)}.{nameof(ExecuteRequest)} failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}"); + } + + return response; + } + + /// + /// Generates a signature for a given set of parameters using HMAC-SHA256. + /// + /// The timestamp of the request. + /// The HTTP method used for the request (e.g., GET, POST). + /// The URL path of the request. + /// The request body. + /// + /// A string representation of the generated signature in lowercase hexadecimal format. + /// + /// + /// The signature is computed using the HMAC-SHA256 algorithm and is typically used for authentication and message integrity. + /// + private string GetSign(string timeStamp, string httpMethod, string urlPath, string body) + { + var preHash = timeStamp + httpMethod + urlPath + body; + + var sig = _hmacSha256.ComputeHash(Encoding.UTF8.GetBytes(preHash)); + + return Convert.ToHexString(sig).ToLowerInvariant(); + } + + public (string apiKey, string timestamp, string signature) GenerateWebSocketSignature(string channel, ICollection productIds) + { + var timestamp = GetNonce(); + + var products = string.Join(",", productIds ?? Array.Empty()); + + var signature = GetSign(timestamp, string.Empty, channel, products); + + return (_apiKey, timestamp, signature); + } + + /// + /// Generates a unique nonce based on the current UTC time in Unix timestamp format. + /// + /// + /// A string representation of the generated nonce. + /// + /// + /// The nonce is used to ensure the uniqueness of each request, typically in the context of security and authentication. + /// + private static string GetNonce() + { + return Time.DateTimeToUnixTimeStamp(DateTime.UtcNow).ToString("F0", CultureInfo.InvariantCulture); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// + /// This method disposes of the underlying HMAC-SHA256 instance safely. + /// + public void Dispose() + { + _hmacSha256.DisposeSafely(); + } +} diff --git a/QuantConnect.GDAXBrokerage/GDAXDataQueueHandler.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs similarity index 50% rename from QuantConnect.GDAXBrokerage/GDAXDataQueueHandler.cs rename to QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs index 07d3350..2793895 100644 --- a/QuantConnect.GDAXBrokerage/GDAXDataQueueHandler.cs +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.DataQueueHandler.cs @@ -13,55 +13,28 @@ * limitations under the License. */ -using QuantConnect.Configuration; +using System; +using QuantConnect.Util; using QuantConnect.Data; -using QuantConnect.Interfaces; using QuantConnect.Packets; -using QuantConnect.Util; -using RestSharp; -using System; +using QuantConnect.Interfaces; +using QuantConnect.Configuration; using System.Collections.Generic; -namespace QuantConnect.Brokerages.GDAX +namespace QuantConnect.CoinbaseBrokerage { /// - /// An implementation of for GDAX + /// An implementation of for Coinbase /// - [BrokerageFactory(typeof(GDAXBrokerageFactory))] - public class GDAXDataQueueHandler : GDAXBrokerage, IDataQueueHandler + public partial class CoinbaseBrokerage : IDataQueueHandler { /// - /// Constructor for brokerage + /// Data Aggregator /// - public GDAXDataQueueHandler() : base("GDAX") - { - } - - /// - /// Initializes a new instance of the class - /// - public GDAXDataQueueHandler(string wssUrl, IWebSocket websocket, IRestClient restClient, string apiKey, string apiSecret, string passPhrase, IAlgorithm algorithm, - IPriceProvider priceProvider, IDataAggregator aggregator, LiveNodePacket job) - : base(wssUrl, websocket, restClient, apiKey, apiSecret, passPhrase, algorithm, priceProvider, aggregator, job) - { - Initialize( - wssUrl: wssUrl, - websocket: websocket, - restClient: restClient, - apiKey: apiKey, - apiSecret: apiSecret, - passPhrase: passPhrase, - algorithm: algorithm, - priceProvider: priceProvider, - aggregator: aggregator, - job: job - ); - } - - /// - /// The list of websocket channels to subscribe - /// - protected override string[] ChannelNames { get; } = { "heartbeat", "level2", "matches" }; + /// + /// Aggregates ticks and bars + /// + protected IDataAggregator _aggregator; /// /// Subscribe to the specified configuration @@ -82,32 +55,32 @@ public IEnumerator Subscribe(SubscriptionDataConfig dataConfig, EventH return enumerator; } + /// + /// Removes the specified configuration + /// + /// Subscription config to be removed + public void Unsubscribe(SubscriptionDataConfig dataConfig) + { + SubscriptionManager.Unsubscribe(dataConfig); + _aggregator.Remove(dataConfig); + } + /// /// Sets the job we're subscribing for /// /// Job we're subscribing for public void SetJob(LiveNodePacket job) { - var wssUrl = job.BrokerageData["gdax-url"]; - var restApi = job.BrokerageData["gdax-rest-api"]; - var restClient = new RestClient(restApi); - var webSocketClient = new WebSocketClientWrapper(); - var passPhrase = job.BrokerageData["gdax-passphrase"]; - var apiKey = job.BrokerageData["gdax-api-key"]; - var apiSecret = job.BrokerageData["gdax-api-secret"]; - var priceProvider = new ApiPriceProvider(job.UserId, job.UserToken); var aggregator = Composer.Instance.GetExportedValueByTypeName( Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"), forceTypeNameOnExisting: false); Initialize( - wssUrl: wssUrl, - websocket: webSocketClient, - restClient: restClient, - apiKey: apiKey, - apiSecret: apiSecret, - passPhrase: passPhrase, + webSocketUrl: job.BrokerageData["coinbase-url"], + apiKey: job.BrokerageData["coinbase-api-key"], + apiSecret: job.BrokerageData["coinbase-api-secret"], + restApiUrl: job.BrokerageData["coinbase-rest-api"], algorithm: null, - priceProvider: priceProvider, + orderProvider: null, aggregator: aggregator, job: job ); @@ -117,31 +90,5 @@ public void SetJob(LiveNodePacket job) Connect(); } } - - /// - /// Removes the specified configuration - /// - /// Subscription config to be removed - public void Unsubscribe(SubscriptionDataConfig dataConfig) - { - SubscriptionManager.Unsubscribe(dataConfig); - _aggregator.Remove(dataConfig); - } - - /// - /// Checks if this brokerage supports the specified symbol - /// - /// The symbol - /// returns true if brokerage supports the specified symbol; otherwise false - private static bool CanSubscribe(Symbol symbol) - { - if (symbol.Value.Contains("UNIVERSE") || - symbol.SecurityType != SecurityType.Forex && symbol.SecurityType != SecurityType.Crypto) - { - return false; - } - - return symbol.ID.Market == Market.GDAX; - } } } diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.HistoryProvider.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.HistoryProvider.cs new file mode 100644 index 0000000..759f98b --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.HistoryProvider.cs @@ -0,0 +1,154 @@ +/* + * 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 QuantConnect.Data; +using QuantConnect.Logging; +using QuantConnect.Brokerages; +using QuantConnect.Data.Market; +using System.Collections.Generic; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage +{ + /// + /// Coinbase Brokerage - IHistoryProvider implementation + /// + public partial class CoinbaseBrokerage + { + /// + /// Prevent spam to external source + /// + private bool _loggedCoinbaseSupportsOnlyTradeBars = false; + + /// + /// Gets the history for the requested security + /// + /// The historical data request + /// An enumerable of bars covering the span specified in the request + public override IEnumerable GetHistory(HistoryRequest request) + { + // Coinbase API only allows us to support history requests for TickType.Trade + if (request.TickType != TickType.Trade) + { + if (!_loggedCoinbaseSupportsOnlyTradeBars) + { + _loggedCoinbaseSupportsOnlyTradeBars = true; + _algorithm?.Debug($"Warning.{nameof(CoinbaseBrokerage)}: history provider only supports trade information, does not support quotes."); + Log.Error($"{nameof(CoinbaseBrokerage)}.{nameof(GetHistory)}(): only supports TradeBars"); + } + yield break; + } + + if (!_symbolMapper.IsKnownLeanSymbol(request.Symbol)) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "InvalidSymbol", + $"Unknown symbol: {request.Symbol.Value}, no history returned")); + yield break; + } + + if (request.Symbol.SecurityType != SecurityType.Crypto) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "InvalidSecurityType", + $"{request.Symbol.SecurityType} security type not supported, no history returned")); + yield break; + } + + if (request.StartTimeUtc >= request.EndTimeUtc) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "InvalidDateRange", + "The history request start date must precede the end date, no history returned")); + yield break; + } + + if (request.Resolution == Resolution.Tick || request.Resolution == Resolution.Second) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "InvalidResolution", + $"{request.Resolution} resolution not supported, no history returned")); + yield break; + } + + Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(GetHistory)}: Submitting request: {request.Symbol.Value}: {request.Resolution} {request.StartTimeUtc} UTC -> {request.EndTimeUtc} UTC"); + + foreach (var tradeBar in GetHistoryFromCandles(request)) + { + yield return tradeBar; + } + } + + /// + /// Returns TradeBars from Coinbase candles (only for Minute/Hour/Daily resolutions) + /// + /// The history request instance + private IEnumerable GetHistoryFromCandles(HistoryRequest request) + { + var productId = _symbolMapper.GetBrokerageSymbol(request.Symbol); + var resolutionTimeSpan = request.Resolution.ToTimeSpan(); + var granularityInSec = Convert.ToInt32(resolutionTimeSpan.TotalSeconds); + + var startTime = request.StartTimeUtc; + var endTime = request.EndTimeUtc; + var maximumRange = TimeSpan.FromSeconds(300 * granularityInSec); + + var granularity = request.Resolution switch + { + Resolution.Minute => CandleGranularity.OneMinute, + Resolution.Hour => CandleGranularity.OneHour, + Resolution.Daily => CandleGranularity.OneDay, + _ => throw new NotSupportedException($"The resolution {request.Resolution} is not supported.") + }; + + do + { + var maximumEndTime = startTime.Add(maximumRange); + if (endTime > maximumEndTime) + { + endTime = maximumEndTime; + } + + var candles = _coinbaseApi.GetProductCandles(productId, startTime, endTime, granularity); + + TradeBar lastTradeBar = null; + foreach (var candle in candles) + { + if (candle.Start.UtcDateTime < startTime) + { + // Note from Coinbase docs: + // If data points are readily available, your response may contain as many as 300 candles + // and some of those candles may precede your declared start value. + yield break; + } + + var tradeBar = new TradeBar( + candle.Start.UtcDateTime, + request.Symbol, + candle.Open, + candle.High, + candle.Low, + candle.Close, + candle.Volume, + resolutionTimeSpan + ); + + lastTradeBar = tradeBar; + yield return tradeBar; + } + + startTime = lastTradeBar?.EndTime ?? request.EndTimeUtc; + endTime = request.EndTimeUtc; + } while (startTime < request.EndTimeUtc); + } + } +} diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs new file mode 100644 index 0000000..01c345d --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Messaging.cs @@ -0,0 +1,519 @@ +/* + * 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 System.Linq; +using Newtonsoft.Json; +using System.Threading; +using QuantConnect.Util; +using QuantConnect.Orders; +using QuantConnect.Logging; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; +using QuantConnect.Securities; +using QuantConnect.Brokerages; +using QuantConnect.Orders.Fees; +using QuantConnect.Data.Market; +using System.Collections.Generic; +using System.Collections.Concurrent; +using QuantConnect.CoinbaseBrokerage.Models; +using QuantConnect.CoinbaseBrokerage.Models.Enums; +using QuantConnect.CoinbaseBrokerage.Models.Constants; +using QuantConnect.CoinbaseBrokerage.Models.WebSocket; + +namespace QuantConnect.CoinbaseBrokerage +{ + public partial class CoinbaseBrokerage + { + /// + /// Represents a collection of order books associated with symbols in a thread-safe manner. + /// + /// We use List cuz brokerage doesn't return update for USDC ticker + /// + /// The structure of the collection is as follows: + /// + /// + /// + /// This example demonstrates how the order books are associated with the BTCUSD symbol. + /// + private readonly ConcurrentDictionary> _orderBooks = new(); + + /// + /// Represents a rate limiter for controlling the frequency of WebSocket operations. + /// + /// + private RateGate _webSocketRateLimit = new(7, TimeSpan.FromSeconds(1)); + + /// + /// Represents an integer variable used to keep track of sequence numbers associated with WS feed messages. + /// + private int _sequenceNumbers = 0; + + /// + /// Use to sync subscription process on WebSocket User Updates + /// + private readonly ManualResetEvent _webSocketSubscriptionOnUserUpdateResetEvent = new(false); + + /// + /// Cancellation token source associated with this instance. + /// + private CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// Private CancellationTokenSource for managing the cancellation of resubscription operations in a WebSocket context. + /// + /// + /// This CancellationTokenSource is used specifically during the handling of the WebSocket open event to manage the resubscription process. + /// + private CancellationTokenSource _cancellationTokenSourceReSubscription; + + /// + /// Use like synchronization context for threads + /// + private readonly object _synchronizationContext = new object(); + + /// + /// Wss message handler + /// + /// + /// + protected override void OnMessage(object _, WebSocketMessage webSocketMessage) + { + var data = webSocketMessage.Data as WebSocketClientWrapper.TextMessage; + + Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(OnMessage)}: {data.Message}"); + + try + { + var obj = JObject.Parse(data.Message); + + var channel = obj[CoinbaseWebSocketChannels.Channel]?.Value(); + + //this means an error has occurred + if (channel == null) + { + Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(OnMessage)}.ERROR: {data.Message}"); + return; + } + + var newSequenceNumbers = obj["sequence_num"].Value(); + + // https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#sequence-numbers + if (newSequenceNumbers != 0 && newSequenceNumbers != _sequenceNumbers + 1) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "SequenceNumbers", + $"{nameof(CoinbaseBrokerage)}.{nameof(OnMessage)}: sequence number mismatch. If Sequence numbers are greater that a message has been dropped else ones are less can be ignored or represent a message that has arrived out of order.")); + } + + _sequenceNumbers = newSequenceNumbers; + + switch (channel) + { + case CoinbaseWebSocketChannels.MarketTrades: + var message = obj.ToObject>(); + if (message.Events[0].Type == WebSocketEventType.Update) + { + EmitTradeTick(message.Events[0]); + } + break; + case CoinbaseWebSocketChannels.User: + var orderUpdate = obj.ToObject>(); + if (orderUpdate.Events[0].Type == WebSocketEventType.Snapshot) + { + // When we have subscribed to whatever channel we should send signal to event + _webSocketSubscriptionOnUserUpdateResetEvent.Set(); + break; + } + HandleOrderUpdate(orderUpdate.Events[0].Orders, orderUpdate.Timestamp.UtcDateTime); + break; + case CoinbaseWebSocketChannels.Level2Response: + var level2Data = obj.ToObject>(); + switch (level2Data.Events[0].Type) + { + case WebSocketEventType.Snapshot: + Level2Snapshot(level2Data.Events[0]); + break; + case WebSocketEventType.Update: + Level2Update(level2Data.Events[0]); + break; + default: + throw new ArgumentException(); + }; + break; + } + } + catch (Exception ex) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, -1, $"Parsing wss message failed. Data: {ex.Message} Exception: {ex}")); + } + } + + /// + /// Handle order update based on WS message + /// + /// brokerage order + /// timestamp(UTC) is occurred event + private void HandleOrderUpdate(List orders, DateTime eventTimestampUtc) + { + foreach (var order in orders) + { + var leanOrder = OrderProvider.GetOrdersByBrokerageId(order.OrderId).FirstOrDefault(); + + if (leanOrder == null) + { + continue; + } + + // Skip: pending on brokerage + // Skip: cancel status cuz we return order message from CancelOrder() + if (order.Status == Models.Enums.OrderStatus.Pending || order.Status == Models.Enums.OrderStatus.Cancelled) + { + continue; + } + + // Skip: Order was submitted on brokerage - successfully + if (order.Status == Models.Enums.OrderStatus.Open && order.CumulativeQuantity == 0) + { + continue; + } + + // Skip: Order was filled but brokerage has not return status.Filled yet + if (order.LeavesQuantity == 0 && order.Status == Models.Enums.OrderStatus.Open) + { + continue; + } + + // order.CumulativeQuantity > 0 && order.LeavesQuantity != 0 && order.Status == Models.Enums.OrderStatus.Open + var leanOrderStatus = Orders.OrderStatus.PartiallyFilled; + + if (order.LeavesQuantity == 0 && order.Status == Models.Enums.OrderStatus.Filled) + { + leanOrderStatus = Orders.OrderStatus.Filled; + } + + CurrencyPairUtil.DecomposeCurrencyPair(leanOrder.Symbol, out _, out var quoteCurrency); + + var orderEvent = new OrderEvent( + leanOrder.Id, + leanOrder.Symbol, + eventTimestampUtc, + leanOrderStatus, + leanOrder.Direction, + order.AveragePrice.Value, + order.CumulativeQuantity.Value * Math.Sign(leanOrder.Quantity), + new OrderFee(new CashAmount(order.TotalFees.Value, quoteCurrency)) + ); + + OnOrderEvent(orderEvent); + } + } + + private void Level2Snapshot(CoinbaseLevel2Event snapshotData) + { + var symbol = _symbolMapper.GetLeanSymbol(snapshotData.ProductId, SecurityType.Crypto, MarketName); + + List orderBooks; + if (!_orderBooks.TryGetValue(symbol, out orderBooks)) + { + orderBooks = new List() + { + new DefaultOrderBook(symbol) + }; + + // Create orderBook for USDC symbol too + // The Brokerage returns always data of [BTC-USD] even if user has subscribed on [BTC-USDC] explicitly + // We need handle USDC pair too. USDC has the same data like USD. + if (snapshotData.ProductId.EndsWithInvariant("-USD")) + { + var symbolUSDC = GetSimilarSymbolUSDC(snapshotData.ProductId); + orderBooks.Add(new DefaultOrderBook(symbolUSDC)); + } + + _orderBooks[symbol] = orderBooks; + } + else + { + foreach (var orderBook in orderBooks) + { + orderBook.BestBidAskUpdated -= OnBestBidAskUpdated; + orderBook.Clear(); + } + } + + foreach (var orderBook in orderBooks) + { + foreach (var update in snapshotData.Updates) + { + if (update.Side == CoinbaseLevel2UpdateSide.Bid) + { + orderBook.UpdateBidRow(update.PriceLevel.Value, update.NewQuantity.Value); + continue; + } + + if (update.Side == CoinbaseLevel2UpdateSide.Offer) + { + orderBook.UpdateAskRow(update.PriceLevel.Value, update.NewQuantity.Value); + } + } + + orderBook.BestBidAskUpdated += OnBestBidAskUpdated; + + EmitQuoteTick(orderBook.Symbol, orderBook.BestBidPrice, orderBook.BestBidSize, orderBook.BestAskPrice, orderBook.BestAskSize); + } + } + + private void OnBestBidAskUpdated(object sender, BestBidAskUpdatedEventArgs e) + { + EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize); + } + + private void Level2Update(CoinbaseLevel2Event updateData) + { + var leanSymbol = _symbolMapper.GetLeanSymbol(updateData.ProductId, SecurityType.Crypto, MarketName); + + if (!_orderBooks.TryGetValue(leanSymbol, out var orderBooks)) + { + Log.Error($"Attempting to update a non existent order book for {leanSymbol}"); + return; + } + + foreach (var orderBook in orderBooks) + { + foreach (var update in updateData.Updates) + { + switch (update.Side) + { + case CoinbaseLevel2UpdateSide.Bid: + if (update.NewQuantity.Value == 0) + { + orderBook.RemoveBidRow(update.PriceLevel.Value); + } + else + { + orderBook.UpdateBidRow(update.PriceLevel.Value, update.NewQuantity.Value); + } + continue; + case CoinbaseLevel2UpdateSide.Offer: + if (update.NewQuantity.Value == 0) + { + orderBook.RemoveAskRow(update.PriceLevel.Value); + } + else + { + orderBook.UpdateAskRow(update.PriceLevel.Value, update.NewQuantity.Value); + } + continue; + } + } + } + } + + private void EmitTradeTick(CoinbaseMarketTradesEvent tradeUpdates) + { + foreach (var trade in tradeUpdates.Trades) + { + var symbol = _symbolMapper.GetLeanSymbol(trade.ProductId, SecurityType.Crypto, MarketName); + + var tick = new Tick + { + Value = trade.Price.Value, + Time = trade.Time.UtcDateTime, + Symbol = symbol, + TickType = TickType.Trade, + Quantity = trade.Size.Value, + }; + + lock (_synchronizationContext) + { + _aggregator.Update(tick); + } + + // Create Trade Tick for USDC symbol too + // The Brokerage returns always data of [BTC-USD] even if user has subscribed on [BTC-USDC] explicitly + // We need handle USDC pair too. USDC has the same data like USD. + if (trade.ProductId.EndsWithInvariant("-USD")) + { + var symbolUSDC = GetSimilarSymbolUSDC(trade.ProductId); + var clone = tick.Clone(fillForward: false); + clone.Symbol = symbolUSDC; + + lock (_synchronizationContext) + { + _aggregator.Update(clone); + } + } + } + } + + /// + /// Emits a new quote tick + /// + /// The symbol + /// The bid price + /// The bid size + /// The ask price + /// The ask price + private void EmitQuoteTick(Symbol symbol, decimal bidPrice, decimal bidSize, decimal askPrice, decimal askSize) + { + var tick = new Tick + { + AskPrice = askPrice, + BidPrice = bidPrice, + Time = DateTime.UtcNow, + Symbol = symbol, + TickType = TickType.Quote, + AskSize = askSize, + BidSize = bidSize + }; + tick.SetValue(); + + lock (_synchronizationContext) + { + _aggregator.Update(tick); + } + } + + /// + /// Creates WebSocket message subscriptions for the supplied symbols + /// + protected override bool Subscribe(IEnumerable symbols) + { + try + { + // cancel any previous subscription task, this can happen if the WS closes and reopens right away for some reason + _cancellationTokenSourceReSubscription?.Cancel(); + _cancellationTokenSourceReSubscription.DisposeSafely(); + } + catch (Exception ex) + { + Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(Subscribe)}.{nameof(_cancellationTokenSourceReSubscription)}:ERROR: {ex.Message}"); + } + _cancellationTokenSourceReSubscription = new(); + + // launch a task so we don't block WebSocket and can send and receive + Task.Factory.StartNew(() => + { + Log.Debug($"{nameof(CoinbaseBrokerage)}:Open on Heartbeats channel"); + ManageChannelSubscription(WebSocketSubscriptionType.Subscribe, CoinbaseWebSocketChannels.Heartbeats); + + // TODO: not working properly: https://forums.coinbasecloud.dev/t/type-error-message-failure-to-subscribe/5689 + _webSocketSubscriptionOnUserUpdateResetEvent.Reset(); + Log.Debug($"{nameof(CoinbaseBrokerage)}:Connect: on User channel"); + ManageChannelSubscription(WebSocketSubscriptionType.Subscribe, CoinbaseWebSocketChannels.User); + + if (!_webSocketSubscriptionOnUserUpdateResetEvent.WaitOne(TimeSpan.FromSeconds(30), _cancellationTokenSource.Token)) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, "SubscriptionOnWSFeed", "Failed to subscribe on `user update` channels")); + } + + SubscribeSymbolsOnDataChannels(GetSubscribed().ToList()); + }, _cancellationTokenSourceReSubscription.Token); + + return true; + } + + /// + /// Ends current subscriptions + /// + public bool Unsubscribe(IEnumerable leanSymbols) + { + SubscribeSymbolsOnDataChannels(leanSymbols.ToList(), WebSocketSubscriptionType.Unsubscribe); + + return true; + } + + /// + /// Subscribes to real-time data channels for the provided list of symbols. + /// + /// The list of symbols to subscribe to. + /// + /// This method subscribes to WebSocket channels for each provided symbol, converting them to brokerage symbols using + /// the symbol mapper. It then iterates through the available WebSocket channels and manages the subscription by + /// invoking the method with the appropriate parameters. + /// + /// + private bool SubscribeSymbolsOnDataChannels(List symbols, WebSocketSubscriptionType subscriptionType = WebSocketSubscriptionType.Subscribe) + { + var products = symbols.Select(symbol => _symbolMapper.GetBrokerageSymbol(symbol)).ToList(); + + if (products.Count == 0) + { + return false; + } + + foreach (var channel in CoinbaseWebSocketChannels.WebSocketChannelList) + { + foreach (var chunkProduct in products.Chunk(20)) + { + ManageChannelSubscription(subscriptionType, channel, chunkProduct.ToList()); + } + } + + return true; + } + + /// + /// Manages WebSocket subscriptions by subscribing or unsubscribing to a specified channel. + /// + /// The type of WebSocket subscription (subscribe or unsubscribe). + /// The channel to subscribe or unsubscribe from. + /// Optional list of product IDs associated with the subscription. + /// + /// + private void ManageChannelSubscription(WebSocketSubscriptionType subscriptionType, string channel, List productIds = null) + { + if (string.IsNullOrWhiteSpace(channel)) + { + throw new ArgumentException($"{nameof(CoinbaseBrokerage)}.{nameof(ManageChannelSubscription)}: ChannelRequired:", nameof(channel)); + } + + if (!IsConnected) + { + throw new InvalidOperationException($"{nameof(CoinbaseBrokerage)}.{nameof(ManageChannelSubscription)}: WebSocketMustBeConnected"); + } + + var (apiKey, timestamp, signature) = _coinbaseApi.GetWebSocketSignatures(channel, productIds); + + var json = JsonConvert.SerializeObject( + new CoinbaseSubscriptionMessage(apiKey, channel, productIds, signature, timestamp, subscriptionType)); + + Log.Debug($"{nameof(CoinbaseBrokerage)}.{nameof(ManageChannelSubscription)}:send json message: " + json); + + _webSocketRateLimit.WaitToProceed(); + + WebSocket.Send(json); + } + + /// + /// Retrieves a similar symbol associated with USDC (USD Coin) based on the provided product ID. + /// + /// The product ID for the USD trading pair. + /// + /// A Symbol object representing a similar symbol paired with USDC, derived from the provided product ID. + /// + /// + /// This method utilizes the SymbolMapper to convert the given product ID into a Lean Symbol with the specified parameters. + /// It assumes the product ID follows the format "{BaseCurrency}-{QuoteCurrency}" and appends "-USDC" to create the similar symbol. + /// The SecurityType is set to Crypto, and the MarketName is used during symbol mapping. + /// + private Symbol GetSimilarSymbolUSDC(string productIdUSD) + { + return _symbolMapper.GetLeanSymbol(productIdUSD.Split('-')[0] + "-USDC", SecurityType.Crypto, MarketName); + } + } +} diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Utility.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Utility.cs new file mode 100644 index 0000000..f5cfbfb --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.Utility.cs @@ -0,0 +1,44 @@ +/* + * 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 QuantConnect.CoinbaseBrokerage.Models; +using BrokerageEnums = QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage +{ + /// + /// Utility methods for Coinbase brokerage + /// + public partial class CoinbaseBrokerage + { + private static Orders.OrderStatus ConvertOrderStatus(CoinbaseOrder order) + { + if (order.CompletionPercentage > 0 && order.CompletionPercentage != 100) + { + return Orders.OrderStatus.PartiallyFilled; + } + else if (order.Status == BrokerageEnums.OrderStatus.Open) + { + return Orders.OrderStatus.Submitted; + } + else if (order.Status == BrokerageEnums.OrderStatus.Filled) + { + return Orders.OrderStatus.Filled; + } + + return Orders.OrderStatus.None; + } + } +} diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs new file mode 100644 index 0000000..a73a9a9 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerage.cs @@ -0,0 +1,542 @@ +/* + * 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.IO; +using System.Net; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using QuantConnect.Api; +using QuantConnect.Data; +using QuantConnect.Util; +using QuantConnect.Orders; +using Newtonsoft.Json.Linq; +using QuantConnect.Logging; +using QuantConnect.Packets; +using QuantConnect.Brokerages; +using QuantConnect.Securities; +using QuantConnect.Interfaces; +using QuantConnect.Orders.Fees; +using System.Collections.Generic; +using QuantConnect.Configuration; +using System.Security.Cryptography; +using System.Net.NetworkInformation; +using QuantConnect.CoinbaseBrokerage.Api; +using BrokerageEnums = QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage +{ + /// + /// Represents a partial class for interacting with the Coinbase brokerage using WebSocket communication. + /// + [BrokerageFactory(typeof(CoinbaseBrokerageFactory))] + public partial class CoinbaseBrokerage : BaseWebsocketsBrokerage + { + /// + /// Live job task packet: container for any live specific job variables + /// + private LiveNodePacket _job; + + /// + /// Provide data from external algorithm + /// + private IAlgorithm _algorithm; + + /// + /// Represents an instance of the Coinbase API. + /// + private CoinbaseApi _coinbaseApi; + + /// + /// Provides the mapping between Lean symbols and brokerage symbols + /// + private SymbolPropertiesDatabaseSymbolMapper _symbolMapper; + + /// + /// Represents the name of the market associated with the application. + /// + private static readonly string MarketName = Market.Coinbase; + + /// + /// Checks if the WebSocket connection is connected or in the process of connecting + /// + public override bool IsConnected => WebSocket.IsOpen; + + /// + /// Order provider + /// + protected IOrderProvider OrderProvider { get; private set; } + + /// + /// Initializes a new instance of the class with the specified name. + /// + public CoinbaseBrokerage() : base(MarketName) + { } + + /// + /// Initializes a new instance of the class with set of parameters. + /// + /// WebSockets url + /// api key + /// api secret + /// api url + /// the algorithm instance is required to retrieve account type + /// consolidate ticks + /// The live job packet + public CoinbaseBrokerage(string webSocketUrl, string apiKey, string apiSecret, string restApiUrl, + IAlgorithm algorithm, IDataAggregator aggregator, LiveNodePacket job) + : this(webSocketUrl, apiKey, apiSecret, restApiUrl, algorithm, algorithm?.Portfolio?.Transactions, aggregator, job) + { + + } + + /// + /// Initializes a new instance of the class with set of parameters. + /// + /// WebSockets url + /// Api key + /// Api secret + /// Api url + /// The algorithm instance is required to retrieve account type + /// The order provider + /// Consolidate ticks + /// The live job packet + public CoinbaseBrokerage(string webSocketUrl, string apiKey, string apiSecret, string restApiUrl, + IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) + : base(MarketName) + { + Initialize( + webSocketUrl: webSocketUrl, + apiKey: apiKey, + apiSecret: apiSecret, + restApiUrl: restApiUrl, + algorithm: algorithm, + orderProvider: orderProvider, + aggregator: aggregator, + job: job + ); + } + + /// + /// Initialize the instance of this class + /// + /// The web socket base url + /// api key + /// api secret + /// the algorithm instance is required to retrieve account type + /// The order provider + /// the aggregator for consolidating ticks + /// The live job packet + protected void Initialize(string webSocketUrl, string apiKey, string apiSecret, string restApiUrl, + IAlgorithm algorithm, IOrderProvider orderProvider, IDataAggregator aggregator, LiveNodePacket job) + { + if (IsInitialized) + { + return; + } + + Initialize(webSocketUrl, new WebSocketClientWrapper(), null, apiKey, apiSecret); + + _job = job; + _algorithm = algorithm; + _aggregator = aggregator; + _symbolMapper = new SymbolPropertiesDatabaseSymbolMapper(MarketName); + _coinbaseApi = new CoinbaseApi(_symbolMapper, algorithm?.Portfolio, apiKey, apiSecret, restApiUrl); + OrderProvider = orderProvider; + + SubscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager() + { + SubscribeImpl = (symbols, _) => SubscribeSymbolsOnDataChannels(symbols.ToList()), + UnsubscribeImpl = (symbols, _) => Unsubscribe(symbols) + }; + + ValidateSubscription(); + } + + #region IBrokerage + /// + /// Creates a new order + /// + /// Lean Order + /// true - order placed successfully otherwise false + public override bool PlaceOrder(Order order) + { + var response = _coinbaseApi.CreateOrder(order); + + if (!response.Success) + { + var errorMessage = + response.ErrorResponse.Value.Error == BrokerageEnums.FailureCreateOrderReason.UnknownFailureReason + ? response.ErrorResponse.Value.PreviewFailureReason : response.ErrorResponse.Value.Error.ToString(); + OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero, "CoinbaseBrokerage Order Event") + { Status = OrderStatus.Invalid, Message = errorMessage }); + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "PlaceOrderInvalid", errorMessage)); + return false; + } + + order.BrokerId.Add(response.OrderId); + + OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero, + "CoinbaseBrokerage Order Event") + { Status = OrderStatus.Submitted }); + + return true; + } + + /// + /// This operation is not supported + /// + /// + /// + public override bool UpdateOrder(Order order) + { + if (order.Type != OrderType.Limit) + { + throw new NotSupportedException($"{nameof(CoinbaseBrokerage)}.{nameof(UpdateOrder)}: Order update supports only ${nameof(OrderType.Limit)} Order Type. Please check your order type."); + } + + var response = _coinbaseApi.EditOrder(order as LimitOrder); + + if (!response.Success) + { + var errorMessage = response.Errors[0].PreviewFailureReason; + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "UpdateOrderInvalid", errorMessage)); + return false; + } + + OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero, "CoinbaseBrokerage Order Event") + { + Status = OrderStatus.UpdateSubmitted + }); + + return true; + } + + /// + /// Cancels an order + /// + /// + /// + public override bool CancelOrder(Order order) + { + var cancelOrder = _coinbaseApi.CancelOrders(order.BrokerId); + + if (!cancelOrder.Success) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, "CancelOrder", + $"Coinbase has not canceled order, error: {cancelOrder.FailureReason}")); + return false; + } + + OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero, "Coinbase Order Event") + { Status = OrderStatus.Canceled }); + + return true; + } + + /// + /// Connects the client to the broker's remote servers + /// + public override void Connect() + { + if (IsConnected) + return; + + base.Connect(); + } + + /// + /// Closes the websockets connection + /// + public override void Disconnect() + { + WebSocket.Close(); + _cancellationTokenSource.Cancel(); + } + + /// + /// Gets all orders not yet closed + /// + /// + public override List GetOpenOrders() + { + var list = new List(); + + var openOrders = _coinbaseApi.GetOrders(BrokerageEnums.OrderStatus.Open); + + foreach (var order in openOrders) + { + Order leanOrder = default; + + var symbol = _symbolMapper.GetLeanSymbol(order.ProductId, SecurityType.Crypto, MarketName); + + if (order.OrderConfiguration.MarketIoc != null) + { + var quantity = order.Side == BrokerageEnums.OrderSide.Buy ? + order.OrderConfiguration.MarketIoc.QuoteSize : Decimal.Negate(order.OrderConfiguration.MarketIoc.BaseSize); + leanOrder = new MarketOrder(symbol, quantity, order.CreatedTime, order.AverageFilledPrice); + } + else if (order.OrderConfiguration.LimitGtc != null) + { + var quantity = order.Side == BrokerageEnums.OrderSide.Buy ? order.OrderConfiguration.LimitGtc.BaseSize : Decimal.Negate(order.OrderConfiguration.LimitGtc.BaseSize); + leanOrder = new LimitOrder(symbol, quantity, order.OrderConfiguration.LimitGtc.LimitPrice, order.CreatedTime); + } + else if (order.OrderConfiguration.LimitGtd != null) + { + var quantity = order.Side == BrokerageEnums.OrderSide.Buy ? order.OrderConfiguration.LimitGtd.BaseSize : Decimal.Negate(order.OrderConfiguration.LimitGtd.BaseSize); + leanOrder = new LimitOrder(symbol, quantity, order.OrderConfiguration.LimitGtd.LimitPrice, order.CreatedTime); + leanOrder.Properties.TimeInForce = ConvertTimeInForce(order.TimeInForce, order.OrderConfiguration.LimitGtd.EndTime); + } + else if (order.OrderConfiguration.LimitIoc != null) + { + var quantity = order.Side == BrokerageEnums.OrderSide.Buy ? order.OrderConfiguration.LimitIoc.BaseSize : Decimal.Negate(order.OrderConfiguration.LimitIoc.BaseSize); + leanOrder = new LimitOrder(symbol, quantity, order.OrderConfiguration.LimitIoc.LimitPrice, order.CreatedTime); + } + else if (order.OrderConfiguration.StopLimitGtc != null) + { + var quantity = order.Side == BrokerageEnums.OrderSide.Buy ? order.OrderConfiguration.StopLimitGtc.BaseSize : Decimal.Negate(order.OrderConfiguration.StopLimitGtc.BaseSize); + leanOrder = new StopLimitOrder(symbol, quantity, order.OrderConfiguration.StopLimitGtc.StopPrice, order.OrderConfiguration.StopLimitGtc.LimitPrice, order.CreatedTime); + } + else if (order.OrderConfiguration.StopLimitGtd != null) + { + var quantity = order.Side == BrokerageEnums.OrderSide.Buy ? order.OrderConfiguration.StopLimitGtd.BaseSize : Decimal.Negate(order.OrderConfiguration.StopLimitGtd.BaseSize); + leanOrder = new StopLimitOrder(symbol, quantity, order.OrderConfiguration.StopLimitGtd.StopPrice, order.OrderConfiguration.StopLimitGtd.LimitPrice, order.CreatedTime); + leanOrder.Properties.TimeInForce = ConvertTimeInForce(order.TimeInForce, order.OrderConfiguration.StopLimitGtd.EndTime); + } + + leanOrder.Status = ConvertOrderStatus(order); + leanOrder.BrokerId.Add(order.OrderId); + + list.Add(leanOrder); + } + + return list; + } + + /// + /// Gets all open positions + /// + /// + public override List GetAccountHoldings() + { + /* + * On launching the algorithm the cash balances are pulled and stored in the cashbook. + * Try loading pre-existing currency swaps from the job packet if provided + */ + return base.GetAccountHoldings(_job?.BrokerageData, _algorithm?.Securities.Values); + } + + /// + /// Gets the total account cash balance + /// + /// + public override List GetCashBalance() + { + var list = new List(); + + var accounts = _coinbaseApi.GetAccounts(); + + foreach (var item in accounts) + { + if (item.AvailableBalance.Value > 0m) + { + list.Add(new CashAmount(item.AvailableBalance.Value, item.AvailableBalance.Currency)); + } + } + + return list; + } + + /// + /// Checks if this brokerage supports the specified symbol + /// + /// The symbol + /// returns true if brokerage supports the specified symbol; otherwise false + protected virtual bool CanSubscribe(Symbol symbol) + { + return !symbol.Value.Contains("UNIVERSE") && + symbol.SecurityType == SecurityType.Crypto && + symbol.ID.Market == MarketName; + } + + #endregion + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public override void Dispose() + { + _webSocketRateLimit.DisposeSafely(); + SubscriptionManager.DisposeSafely(); + _cancellationTokenSource.DisposeSafely(); + } + + #region Utils + + private TimeInForce ConvertTimeInForce(BrokerageEnums.TimeInForce timeInForce, DateTime expiryDate = default) + { + switch (timeInForce) + { + case BrokerageEnums.TimeInForce.GoodUntilDateTime: + return TimeInForce.GoodTilDate(expiryDate); + case BrokerageEnums.TimeInForce.GoodUntilCancelled: + default: + return TimeInForce.GoodTilCanceled; + } + } + + #endregion + + private class ModulesReadLicenseRead : QuantConnect.Api.RestResponse + { + [JsonProperty(PropertyName = "license")] + public string License; + [JsonProperty(PropertyName = "organizationId")] + public string OrganizationId; + } + + /// + /// Validate the user of this project has permission to be using it via our web API. + /// + private static void ValidateSubscription() + { + try + { + var productId = 183; + var userId = Config.GetInt("job-user-id"); + var token = Config.Get("api-access-token"); + var organizationId = Config.Get("job-organization-id", null); + // Verify we can authenticate with this user and token + var api = new ApiConnection(userId, token); + if (!api.Connected) + { + throw new ArgumentException("Invalid api user id or token, cannot authenticate subscription."); + } + // Compile the information we want to send when validating + var information = new Dictionary() + { + {"productId", productId}, + {"machineName", Environment.MachineName}, + {"userName", Environment.UserName}, + {"domainName", Environment.UserDomainName}, + {"os", Environment.OSVersion} + }; + // IP and Mac Address Information + try + { + var interfaceDictionary = new List>(); + foreach (var nic in NetworkInterface.GetAllNetworkInterfaces().Where(nic => nic.OperationalStatus == OperationalStatus.Up)) + { + var interfaceInformation = new Dictionary(); + // Get UnicastAddresses + var addresses = nic.GetIPProperties().UnicastAddresses + .Select(uniAddress => uniAddress.Address) + .Where(address => !IPAddress.IsLoopback(address)).Select(x => x.ToString()); + // If this interface has non-loopback addresses, we will include it + if (!addresses.IsNullOrEmpty()) + { + interfaceInformation.Add("unicastAddresses", addresses); + // Get MAC address + interfaceInformation.Add("MAC", nic.GetPhysicalAddress().ToString()); + // Add Interface name + interfaceInformation.Add("name", nic.Name); + // Add these to our dictionary + interfaceDictionary.Add(interfaceInformation); + } + } + information.Add("networkInterfaces", interfaceDictionary); + } + catch (Exception) + { + // NOP, not necessary to crash if fails to extract and add this information + } + // Include our OrganizationId is specified + if (!string.IsNullOrEmpty(organizationId)) + { + information.Add("organizationId", organizationId); + } + var request = new RestRequest("modules/license/read", Method.POST) { RequestFormat = DataFormat.Json }; + request.AddParameter("application/json", JsonConvert.SerializeObject(information), ParameterType.RequestBody); + api.TryRequest(request, out ModulesReadLicenseRead result); + if (!result.Success) + { + throw new InvalidOperationException($"Request for subscriptions from web failed, Response Errors : {string.Join(',', result.Errors)}"); + } + + var encryptedData = result.License; + // Decrypt the data we received + DateTime? expirationDate = null; + long? stamp = null; + bool? isValid = null; + if (encryptedData != null) + { + // Fetch the org id from the response if we are null, we need it to generate our validation key + if (string.IsNullOrEmpty(organizationId)) + { + organizationId = result.OrganizationId; + } + // Create our combination key + var password = $"{token}-{organizationId}"; + var key = SHA256.HashData(Encoding.UTF8.GetBytes(password)); + // Split the data + var info = encryptedData.Split("::"); + var buffer = Convert.FromBase64String(info[0]); + var iv = Convert.FromBase64String(info[1]); + // Decrypt our information + using var aes = new AesManaged(); + var decryptor = aes.CreateDecryptor(key, iv); + using var memoryStream = new MemoryStream(buffer); + using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read); + using var streamReader = new StreamReader(cryptoStream); + var decryptedData = streamReader.ReadToEnd(); + if (!decryptedData.IsNullOrEmpty()) + { + var jsonInfo = JsonConvert.DeserializeObject(decryptedData); + expirationDate = jsonInfo["expiration"]?.Value(); + isValid = jsonInfo["isValid"]?.Value(); + stamp = jsonInfo["stamped"]?.Value(); + } + } + // Validate our conditions + if (!expirationDate.HasValue || !isValid.HasValue || !stamp.HasValue) + { + throw new InvalidOperationException("Failed to validate subscription."); + } + + var nowUtc = DateTime.UtcNow; + var timeSpan = nowUtc - Time.UnixTimeStampToDateTime(stamp.Value); + if (timeSpan > TimeSpan.FromHours(12)) + { + throw new InvalidOperationException("Invalid API response."); + } + if (!isValid.Value) + { + throw new ArgumentException($"Your subscription is not valid, please check your product subscriptions on our website."); + } + if (expirationDate < nowUtc) + { + throw new ArgumentException($"Your subscription expired {expirationDate}, please renew in order to use this product."); + } + } + catch (Exception e) + { + Log.Error($"ValidateSubscription(): Failed during validation, shutting down. Error : {e.Message}"); + Environment.Exit(1); + } + } + } +} diff --git a/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs new file mode 100644 index 0000000..9531b00 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/CoinbaseBrokerageFactory.cs @@ -0,0 +1,102 @@ +/* + * 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 QuantConnect.Data; +using QuantConnect.Util; +using QuantConnect.Interfaces; +using QuantConnect.Securities; +using QuantConnect.Brokerages; +using QuantConnect.Configuration; +using System.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage +{ + /// + /// Factory method to create Coinbase WebSockets brokerage + /// + public class CoinbaseBrokerageFactory : BrokerageFactory + { + /// + /// Gets the brokerage data required to run the brokerage from configuration/disk + /// + /// + /// The implementation of this property will create the brokerage data dictionary required for + /// running live jobs. See + /// + public override Dictionary BrokerageData => new Dictionary + { + { "coinbase-api-key", Config.Get("coinbase-api-key")}, + { "coinbase-api-secret", Config.Get("coinbase-api-secret")}, + // Represents the configuration setting for the Coinbase API URL. + { "coinbase-rest-api", Config.Get("coinbase-rest-api", "https://api.coinbase.com")}, + // Represents the configuration setting for the Coinbase WebSocket URL. + { "coinbase-url" , Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com")}, + // load holdings if available + { "live-holdings", Config.Get("live-holdings")}, + }; + + + /// + /// Initializes a new instance of the class + /// + public CoinbaseBrokerageFactory() : base(typeof(CoinbaseBrokerage)) + { } + + /// + /// Gets a brokerage model that can be used to model this brokerage's unique behaviors + /// + /// The order provider + public override IBrokerageModel GetBrokerageModel(IOrderProvider orderProvider) => new CoinbaseBrokerageModel(); + + /// + /// Create the Brokerage instance + /// + /// + /// + /// + public override IBrokerage CreateBrokerage(Packets.LiveNodePacket job, IAlgorithm algorithm) + { + var errors = new List(); + var apiKey = Read(job.BrokerageData, "coinbase-api-key", errors); + var apiSecret = Read(job.BrokerageData, "coinbase-api-secret", errors); + var apiUrl = Read(job.BrokerageData, "coinbase-rest-api", errors); + var wsUrl = Read(job.BrokerageData, "coinbase-url", errors); + + if (errors.Count != 0) + { + // if we had errors then we can't create the instance + throw new ArgumentException(string.Join(Environment.NewLine, errors)); + } + + var aggregator = Composer.Instance.GetExportedValueByTypeName( + Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"), + forceTypeNameOnExisting: false); + + var brokerage = new CoinbaseBrokerage(wsUrl, apiKey, apiSecret, apiUrl, algorithm, aggregator, job); + + // Add the brokerage to the composer to ensure its accessible to the live data feed. + Composer.Instance.AddPart(brokerage); + + return brokerage; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public override void Dispose() + { } + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Converters/CoinbaseDecimalStringConverter.cs b/QuantConnect.CoinbaseBrokerage/Converters/CoinbaseDecimalStringConverter.cs new file mode 100644 index 0000000..42d70b0 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Converters/CoinbaseDecimalStringConverter.cs @@ -0,0 +1,82 @@ +/* + * 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 Newtonsoft.Json; +using System.Globalization; + +namespace QuantConnect.CoinbaseBrokerage.Converters; + +public class CoinbaseDecimalStringConverter : JsonConverter +{ + /// + /// Gets a value indicating whether this can read JSON. + /// + /// true if this can read JSON; otherwise, false. + public override bool CanRead => true; + + /// + /// Gets a value indicating whether this can write JSON. + /// + /// true if this can write JSON; otherwise, false. + public override bool CanWrite => true; + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The existing value has a value. + /// The calling serializer. + + /// The object value. + public override decimal ReadJson(JsonReader reader, Type objectType, decimal existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var val = reader.Value; + if (val is decimal dec) + { + return dec; + } + if (val is double d) + { + return d.SafeDecimalCast(); + } + + if (val is string str && decimal.TryParse(str, NumberStyles.Currency, CultureInfo.InvariantCulture, out var res)) + { + return res; + } + + throw new Exception(); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, decimal value, JsonSerializer serializer) + { + // Not write zero value in json schema + if (value == decimal.Zero) + { + writer.WriteNull(); + return; + } + writer.WriteValue(value.ToStringInvariant()); + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseAccount.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseAccount.cs new file mode 100644 index 0000000..c557adb --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseAccount.cs @@ -0,0 +1,159 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +/// +/// Business data of Coinbase account response +/// +public class CoinbaseAccountResponse : CoinbaseResponse +{ + /// + /// Data about all accounts + /// + [JsonProperty("accounts")] + public IEnumerable Accounts { get; set; } + + /// + /// Number of accounts returned + /// + [JsonProperty("size")] + public int Size { get; set; } +} + +/// +/// Business data of Coinbase account model +/// +public readonly struct CoinbaseAccount +{ + /// + /// Unique identifier for account. + /// + [JsonProperty("uuid")] + public string Uuid { get; } + + /// + /// Name for the account. + /// + [JsonProperty("name")] + public string Name { get; } + + /// + /// Currency symbol for the account. + /// + [JsonProperty("currency")] + public string Currency { get; } + + /// + /// Available Balance account + /// + [JsonProperty("available_balance")] + public AvailableBalance AvailableBalance { get; } + + /// + /// Whether or not this account is the user's primary account + /// + [JsonProperty("default")] + public bool Default { get; } + + /// + /// Whether or not this account is active and okay to use. + /// + [JsonProperty("active")] + public bool Active { get; } + + /// + /// Time at which this account was created. + /// + [JsonProperty("created_at")] + public DateTime CreatedAt { get; } + + /// + /// Time at which this account was updated. + /// + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; } + + /// + /// Time at which this account was deleted. + /// + [JsonProperty("deleted_at")] + public DateTime? DeletedAt { get; } + + /// + /// Possible values: [ACCOUNT_TYPE_CRYPTO, ACCOUNT_TYPE_FIAT, ACCOUNT_TYPE_VAULT] + /// + [JsonProperty("type")] + public string Type { get; } + + /// + /// Whether or not this account is ready to trade. + /// + [JsonProperty("ready")] + public bool Ready { get; } + + /// + /// Available account hold balance + /// + [JsonProperty("hold")] + public AvailableBalance Hold { get; } + + [JsonConstructor] + public CoinbaseAccount(string uuid, string name, string currency, AvailableBalance availableBalance, bool @default, + bool active, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, string type, bool ready, AvailableBalance hold) + { + Uuid = uuid; + Name = name; + Currency = currency; + AvailableBalance = availableBalance; + Default = @default; + Active = active; + CreatedAt = createdAt; + UpdatedAt = updatedAt; + DeletedAt = deletedAt; + Type = type; + Ready = ready; + Hold = hold; + } +} + +/// +/// Available balance account +/// +public readonly struct AvailableBalance +{ + /// + /// Amount of currency that this object represents. + /// + [JsonProperty("value")] + public decimal Value { get; } + + /// + /// Denomination of the currency. + /// + [JsonProperty("currency")] + public string Currency { get; } + + [JsonConstructor] + public AvailableBalance(decimal value, string currency) + { + Value = value; + Currency = currency; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseCancelOrdersResponse.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseCancelOrdersResponse.cs new file mode 100644 index 0000000..12a8f3a --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseCancelOrdersResponse.cs @@ -0,0 +1,58 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +public readonly struct CoinbaseCancelOrdersResponse +{ + [JsonProperty("results")] + public IEnumerable Result { get; } + + [JsonConstructor] + public CoinbaseCancelOrdersResponse(IEnumerable result) => Result = result; +} + + +public readonly struct CoinbaseCancelOrderResult +{ + /// + /// Whether the cancel request was submitted successfully. + /// + [JsonProperty("success")] + public bool Success { get; } + + /// + /// Failure Reason + /// + [JsonProperty("failure_reason")] + public string FailureReason { get; } + + /// + /// The IDs of order cancel request was initiated for + /// + [JsonProperty("order_id")] + public string OrderId { get; } + + [JsonConstructor] + public CoinbaseCancelOrderResult(bool success, string failureReason, string orderId) + { + Success = success; + FailureReason = failureReason; + OrderId = orderId; + } +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseCreateOrderResponse.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseCreateOrderResponse.cs new file mode 100644 index 0000000..6f282da --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseCreateOrderResponse.cs @@ -0,0 +1,150 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +public readonly struct CoinbaseCreateOrderResponse +{ + /// + /// Whether the order was created. + /// + [JsonProperty("success")] + public bool Success { get; } + + /// + /// Failure Reason + /// + [JsonProperty("failure_reason")] + [JsonConverter(typeof(StringEnumConverter))] + public FailureCreateOrderReason FailureReason { get; } + + /// + /// The ID of the order created + /// + [JsonProperty("order_id")] + public string OrderId { get; } + + /// + /// If Success - true, get success response data otherwise + /// + public SuccessResponse? SuccessResponse { get; } + + /// + /// If Success - false, get error response otherwise + /// + [JsonProperty("error_response")] + public ErrorResponse? ErrorResponse { get; } + + /// + /// Order Configuration + /// + [JsonProperty("order_configuration")] + public OrderConfiguration OrderConfiguration { get; } + + [JsonConstructor] + public CoinbaseCreateOrderResponse(bool success, FailureCreateOrderReason failureReason, string orderId, + SuccessResponse? successResponse, ErrorResponse? errorResponse, OrderConfiguration orderConfiguration) + { + Success = success; + FailureReason = failureReason; + OrderId = orderId; + SuccessResponse = successResponse; + ErrorResponse = errorResponse; + OrderConfiguration = orderConfiguration; + } +} + +/// +/// Error Response +/// +public readonly struct ErrorResponse +{ + /// + /// Error + /// + [JsonProperty("error")] + [JsonConverter(typeof(StringEnumConverter))] + public FailureCreateOrderReason Error { get; } + + /// + /// Generic error message explaining why the order was not created + /// + [JsonProperty("message")] + public string Message { get; } + + /// + /// Descriptive error message explaining why the order was not created + /// + [JsonProperty("error_details")] + public string ErrorDetails { get; } + + /// + /// Preview Failure Reason + /// + [JsonProperty("preview_failure_reason")] + public string PreviewFailureReason { get; } + + [JsonConstructor] + public ErrorResponse(FailureCreateOrderReason error, string message, string errorDetails, string previewFailureReason) + { + Error = error; + Message = message; + ErrorDetails = errorDetails; + PreviewFailureReason = previewFailureReason; + } +} + +/// +/// Success Response +/// +public readonly struct SuccessResponse +{ + /// + /// The ID of the order created + /// + [JsonProperty("order_id")] + public string OrderId { get; } + + /// + /// The product this order was created for e.g. 'BTC-USD' + /// + [JsonProperty("product_id")] + public string ProductId { get; } + + /// + /// Order Side + /// + [JsonProperty("side")] + public OrderSide Side { get; } + + /// + /// Client set unique uuid for this order + /// + [JsonProperty("client_order_id")] + public string ClientOrderId { get; } + + [JsonConstructor] + public SuccessResponse(string orderId, string productId, OrderSide side, string clientOrderId) + { + OrderId = orderId; + ProductId = productId; + Side = side; + ClientOrderId = clientOrderId; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseEditOrderResponse.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseEditOrderResponse.cs new file mode 100644 index 0000000..0d40d4d --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseEditOrderResponse.cs @@ -0,0 +1,53 @@ +/* + * 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 Newtonsoft.Json; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +public readonly struct CoinbaseEditOrderResponse +{ + /// + /// Whether the order edit request was placed. + /// + [JsonProperty("success")] + public bool Success { get; } + + [JsonProperty("errors")] + public Error[] Errors { get; } + + [JsonConstructor] + public CoinbaseEditOrderResponse(bool success, Error[] errors) + { + Success = success; + Errors = errors; + } +} + +public readonly struct Error +{ + [JsonProperty("edit_failure_reason")] + public string EditFailureReason { get; } + + [JsonProperty("preview_failure_reason")] + public string PreviewFailureReason { get; } + + [JsonConstructor] + public Error(string editFailureReason, string previewFailureReason) + { + EditFailureReason = editFailureReason; + PreviewFailureReason = previewFailureReason; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseMarketTrades.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseMarketTrades.cs new file mode 100644 index 0000000..ce57709 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseMarketTrades.cs @@ -0,0 +1,83 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; +using Newtonsoft.Json.Converters; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models; +public readonly struct CoinbaseMarketTrades +{ + [JsonProperty("trades")] + public IEnumerable Trades { get; } + + [JsonProperty("best_bid")] + public decimal BestBid { get; } + + [JsonProperty("best_ask")] + public decimal BestAsk { get; } + + [JsonConstructor] + public CoinbaseMarketTrades(IEnumerable trades, decimal bestBid, decimal bestAsk) + { + Trades = trades; + BestBid = bestBid; + BestAsk = bestAsk; + } +} + +public readonly struct Trades +{ + [JsonProperty("trade_id")] + public string TradeId { get; } + + [JsonProperty("product_id")] + public string ProductId { get; } + + [JsonProperty("price")] + public decimal Price { get; } + + [JsonProperty("size")] + public decimal Size { get; } + + [JsonProperty("time")] + public DateTime Time { get; } + + [JsonProperty("side")] + [JsonConverter(typeof(StringEnumConverter))] + public OrderSide Side { get; } + + [JsonProperty("bid")] + public decimal? Bid { get; } + + [JsonProperty("ask")] + public decimal? Ask { get; } + + [JsonConstructor] + public Trades(string tradeId, string productId, decimal price, decimal size, DateTime time, OrderSide side, + decimal? bid, decimal? ask) + { + TradeId = tradeId; + ProductId = productId; + Price = price; + Size = size; + Time = time; + Side = side; + Bid = bid; + Ask = ask; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseOrder.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseOrder.cs new file mode 100644 index 0000000..ed2c5bd --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseOrder.cs @@ -0,0 +1,476 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; +using Newtonsoft.Json.Converters; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +/// +/// Business data of Coinbase order response +/// +public class CoinbaseOrderResponse : CoinbaseResponse +{ + /// + /// A list of orders matching the query. + /// + [JsonProperty("orders")] + public IEnumerable Orders { get; set; } + + /// + /// The sequence of the db at which this state was read. + /// + [JsonProperty("sequence")] + public string Sequence { get; set; } +} + +/// +/// Order info +/// +public readonly struct CoinbaseOrder +{ + /// + /// The unique id for this order + /// + [JsonProperty("order_id")] + public string OrderId { get; } + + /// + /// The product this order was created for e.g. 'BTC-USD' + /// + [JsonProperty("product_id")] + public string ProductId { get; } + + /// + /// The id of the User owning this Order + /// + [JsonProperty("user_id")] + public string UserId { get; } + + /// + /// Order Configuration Type + /// + [JsonProperty("order_configuration")] + public OrderConfiguration OrderConfiguration { get; } + + /// + /// Possible values: [BUY, SELL] + /// + [JsonProperty("side")] + [JsonConverter(typeof(StringEnumConverter))] + public OrderSide Side { get; } + + /// + /// Client specified ID of order. + /// + [JsonProperty("client_order_id")] + public string ClientOrderId { get; } + + /// + /// Order Status + /// + [JsonProperty("status")] + [JsonConverter(typeof(StringEnumConverter))] + public OrderStatus Status { get; } + + /// + /// Time in Force policies + /// + [JsonProperty("time_in_force")] + [JsonConverter(typeof(StringEnumConverter))] + public TimeInForce TimeInForce { get; } + + /// + /// Timestamp for when the order was created + /// + [JsonProperty("created_time")] + public DateTime CreatedTime { get; } + + /// + /// The percent of total order amount that has been filled + /// + [JsonProperty("completion_percentage")] + public decimal CompletionPercentage { get; } + + /// + /// The portion (in base currency) of total order amount that has been filled + /// + [JsonProperty("filled_size")] + public decimal FilledSize { get; } + + /// + /// The average of all prices of fills for this order + /// + [JsonProperty("average_filled_price")] + public decimal AverageFilledPrice { get; } + + /// + /// Commission amount + /// + [JsonProperty("fee")] + public string Fee { get; } + + /// + /// Number of fills that have been posted for this order + /// + [JsonProperty("number_of_fills")] + public decimal NumberOfFills { get; } + + /// + /// The portion (in quote current) of total order amount that has been filled + /// + [JsonProperty("filled_value")] + public decimal FilledValue { get; } + + /// + /// Whether a cancel request has been initiated for the order, and not yet completed + /// + [JsonProperty("pending_cancel")] + public bool PendingCancel { get; } + + /// + /// Whether the order was placed with quote currency + /// + [JsonProperty("size_in_quote")] + public bool SizeInQuote { get; } + + /// + /// The total fees for the order + /// + [JsonProperty("total_fees")] + public decimal TotalFees { get; } + + /// + /// Whether the order size includes fees + /// + [JsonProperty("size_inclusive_of_fees")] + public bool SizeInclusiveOfFees { get; } + + /// + /// derived field: filled_value + total_fees for buy orders and filled_value - total_fees for sell orders. + /// + [JsonProperty("total_value_after_fees")] + public decimal TotalValueAfterFees { get; } + + /// + /// Possible values: [UNKNOWN_TRIGGER_STATUS, INVALID_ORDER_TYPE, STOP_PENDING, STOP_TRIGGERED] + /// + [JsonProperty("trigger_status")] + public string TriggerStatus { get; } + + /// + /// Possible values: [UNKNOWN_ORDER_TYPE, MARKET, LIMIT, STOP, STOP_LIMIT] + /// + [JsonProperty("order_type")] + public string OrderType { get; } + + /// + /// Possible values: [REJECT_REASON_UNSPECIFIED] + /// + [JsonProperty("reject_reason")] + public string RejectReason { get; } + + /// + /// True if the order is fully filled, false otherwise. + /// + [JsonProperty("settled")] + public bool Settled { get; } + + /// + /// Possible values: [SPOT, FUTURE] + /// + [JsonProperty("product_type")] + public string ProductType { get; } + + /// + /// Message stating why the order was rejected. + /// + [JsonProperty("reject_message")] + public string RejectMessage { get; } + + /// + /// Message stating why the order was canceled. + /// + [JsonProperty("cancel_message")] + public string CancelMessage { get; } + + /// + /// Possible values: [RETAIL_SIMPLE, RETAIL_ADVANCED] + /// + [JsonProperty("order_placement_source")] + public string OrderPlacementSource { get; } + + /// + /// The remaining hold amount (holdAmount - holdAmountReleased). [value is 0 if holdReleased is true] + /// + [JsonProperty("outstanding_hold_amount")] + public decimal OutstandingHoldAmount { get; } + + /// + /// True if order is of liquidation type. + /// + [JsonProperty("is_liquidation")] + public bool IsLiquidation { get; } + + /// + /// Time of the most recent fill for this order + /// + [JsonProperty("last_fill_time")] + public string LastFillTime { get; } + + /// + /// An array of the latest 5 edits per order + /// + [JsonProperty("edit_history")] + public EditHistory[] EditHistory { get; } + + /// + /// Not provided + /// + [JsonProperty("leverage")] + public string Leverage { get; } + + /// + /// Possible values: [UNKNOWN_MARGIN_TYPE] + /// + [JsonProperty("margin_type")] + public string MarginType { get; } + + + [JsonConstructor] + public CoinbaseOrder(string orderId, string productId, string userId, OrderConfiguration orderConfiguration, + OrderSide side, string clientOrderId, OrderStatus status, TimeInForce timeInForce, DateTime createdTime, decimal completionPercentage, + decimal filledSize, decimal averageFilledPrice, string fee, decimal numberOfFills, decimal filledValue, bool pendingCancel, + bool sizeInQuote, decimal totalFees, bool sizeInclusiveOfFees, decimal totalValueAfterFees, string triggerStatus, + string orderType, string rejectReason, bool settled, string productType, string rejectMessage, string cancelMessage, + string orderPlacementSource, decimal outstandingHoldAmount, bool isLiquidation, string lastFillTime, + EditHistory[] editHistory, string leverage, string marginType) + { + OrderId = orderId; + ProductId = productId; + UserId = userId; + OrderConfiguration = orderConfiguration; + Side = side; + ClientOrderId = clientOrderId; + Status = status; + TimeInForce = timeInForce; + CreatedTime = createdTime; + CompletionPercentage = completionPercentage; + FilledSize = filledSize; + AverageFilledPrice = averageFilledPrice; + Fee = fee; + NumberOfFills = numberOfFills; + FilledValue = filledValue; + PendingCancel = pendingCancel; + SizeInQuote = sizeInQuote; + TotalFees = totalFees; + SizeInclusiveOfFees = sizeInclusiveOfFees; + TotalValueAfterFees = totalValueAfterFees; + TriggerStatus = triggerStatus; + OrderType = orderType; + RejectReason = rejectReason; + Settled = settled; + ProductType = productType; + RejectMessage = rejectMessage; + CancelMessage = cancelMessage; + OrderPlacementSource = orderPlacementSource; + OutstandingHoldAmount = outstandingHoldAmount; + IsLiquidation = isLiquidation; + LastFillTime = lastFillTime; + EditHistory = editHistory; + Leverage = leverage; + MarginType = marginType; + } + +} + +/// +/// Order Configuration Type: Market, LimitGtc, LimitGtd, StopLimitGtc, StopLimitGtd +/// +public class OrderConfiguration +{ + /// + /// Market + /// + [JsonProperty("market_market_ioc")] + public MarketIoc MarketIoc { get; set; } + + /// + /// Limit Good till cancel + /// + [JsonProperty("limit_limit_gtc")] + public LimitGtc LimitGtc { get; set; } + + /// + /// Limit Good till day + /// + [JsonProperty("limit_limit_gtd")] + public LimitGtd LimitGtd { get; set; } + + /// + /// Limit Immediate or cancel + /// + [JsonProperty("sor_limit_ioc")] + public LimitIoc LimitIoc { get; set; } + + /// + /// Stop Limit Good till cancel + /// + [JsonProperty("stop_limit_stop_limit_gtc")] + public StopLimitGtc StopLimitGtc { get; set; } + + /// + /// Stop Limit Good till day + /// + [JsonProperty("stop_limit_stop_limit_gtd")] + public StopLimitGtd StopLimitGtd { get; set; } +} + +/// +/// Market Order Configuration Type +/// +public class MarketIoc +{ + /// + /// Amount of base currency to spend on order. Required for SELL orders. + /// + [JsonProperty("base_size")] + public decimal BaseSize { get; set; } + + /// + /// Amount of quote currency to spend on order. Required for BUY orders. + /// + [JsonProperty("quote_size")] + public decimal QuoteSize { get; set; } +} + +public abstract class Limit +{ + /// + /// Amount of base currency to spend on order + /// + [JsonProperty("base_size")] + public decimal BaseSize { get; set; } + + /// + /// Ceiling price for which the order should get filled. + /// + [JsonProperty("limit_price")] + public decimal LimitPrice { get; set; } +} + +/// +/// LimitGtc Order Configuration Type +/// [Gtc] - Good Till Cancel +/// +public class LimitGtc : Limit +{ + /// + /// The post-only flag indicates that the order should only make liquidity. + /// If any part of the order results in taking liquidity, the order will be rejected and no part of it will execute. + /// + [JsonProperty("post_only")] + public bool? PostOnly { get; set; } +} + +/// +/// LimitGtd Order Configuration Type +/// [Gtd] - Good till day +/// +public class LimitGtd : LimitGtc +{ + /// + /// Time at which the order should be cancelled if it's not filled. + /// + [JsonProperty("end_time")] + public DateTime EndTime { get; set; } +} + +/// +/// LimitIoc Order Configuration Type +/// [Ioc] - Immediate or cancel +/// +public class LimitIoc : Limit +{ } + +/// +/// StopLimitGtc Order Configuration Type +/// [Gtc] - Good Till Cancel +/// +public class StopLimitGtc : Limit +{ + /// + /// Price at which the order should trigger - if stop direction is Up, + /// then the order will trigger when the last trade price goes above this, + /// otherwise order will trigger when last trade price goes below this price. + /// + [JsonProperty("stop_price")] + public decimal StopPrice { get; set; } + + /// + /// Possible values: [STOP_DIRECTION_STOP_UP, STOP_DIRECTION_STOP_DOWN] + /// + [JsonProperty("stop_direction")] + [JsonConverter(typeof(StringEnumConverter))] + public StopDirection StopDirection { get; set; } +} + +/// +/// StopLimitGtc Order Configuration Type +/// [Gtd] - Good till day +/// +public class StopLimitGtd : StopLimitGtc +{ + /// + /// Time at which the order should be cancelled if it's not filled. + /// + [JsonProperty("end_time")] + public DateTime EndTime { get; set; } +} + +/// +/// Order's Edit History +/// +public readonly struct EditHistory +{ + /// + /// New price order + /// + [JsonProperty("price")] + public string Price { get; } + + /// + /// New size order + /// + [JsonProperty("size")] + public string Size { get; } + + /// + /// Time when changes was accepted + /// + [JsonProperty("replace_accept_timestamps")] + public DateTime ReplaceAcceptTimestamp { get; } + + [JsonConstructor] + public EditHistory(string price, string size, DateTime replaceAcceptTimestamp) + { + Price = price; + Size = size; + ReplaceAcceptTimestamp = replaceAcceptTimestamp; + } +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseProduct.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseProduct.cs new file mode 100644 index 0000000..ed719b0 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseProduct.cs @@ -0,0 +1,116 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +/// +/// Business data of Coinbase products response +/// +public class CoinbaseProductResponse +{ + /// + /// Array of objects, each representing one product. + /// + [JsonProperty("products")] + public IEnumerable Products { get; set; } + + /// + /// Number of products that were returned. + /// + [JsonProperty("num_products")] + public int NumProducts { get; set; } +} + +public readonly struct CoinbaseProduct +{ + /// + /// The trading pair. + /// + [JsonProperty("product_id")] + public string ProductId { get; } + + /// + /// Name of the base currency. + /// + [JsonProperty("base_name")] + public string BaseName { get; } + + /// + /// Name of the quote currency. + /// + [JsonProperty("quote_name")] + public string QuoteName { get; } + + /// + /// Symbol of the base currency. + /// + [JsonProperty("base_currency_id")] + public string BaseCurrencyId { get; } + + /// + /// Symbol of the quote currency. + /// + [JsonProperty("quote_currency_id")] + public string QuoteCurrencyId { get; } + + /// + /// Minimum amount price can be increased or decreased at once. + /// + [JsonProperty("price_increment")] + public decimal PriceIncrement { get; } + + /// + /// Minimum amount base value can be increased or decreased at once. + /// + [JsonProperty("base_increment")] + public decimal BaseIncrement { get; } + + /// + /// Minimum amount quote value can be increased or decreased at once. + /// + [JsonProperty("quote_increment")] + public decimal QuoteIncrement { get; } + + /// + /// Minimum size that can be represented of base currency. + /// + [JsonProperty("base_min_size")] + public decimal BaseMinSize { get; } + + /// + /// Status of the product. + /// + [JsonProperty("status")] + public string Status { get; } + + [JsonConstructor] + public CoinbaseProduct(string productId, string baseName, string quoteName, string baseCurrencyId, string quoteCurrencyId, decimal priceIncrement, + decimal baseIncrement, decimal quoteIncrement, decimal baseMinSize, string status) + { + ProductId = productId; + BaseName = baseName; + QuoteName = quoteName; + BaseCurrencyId = baseCurrencyId; + QuoteCurrencyId = quoteCurrencyId; + PriceIncrement = priceIncrement; + BaseIncrement = baseIncrement; + QuoteIncrement = quoteIncrement; + BaseMinSize = baseMinSize; + Status = status; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseProductCandles.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseProductCandles.cs new file mode 100644 index 0000000..9cdae83 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseProductCandles.cs @@ -0,0 +1,66 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +public readonly struct CoinbaseProductCandles +{ + [JsonProperty("candles")] + public IEnumerable Candles { get; } + + [JsonConstructor] + public CoinbaseProductCandles(IEnumerable candles) + { + Candles = candles; + } +} + +public readonly struct Candle +{ + [JsonConverter(typeof(UnixDateTimeConverter))] + [JsonProperty("start")] + public DateTimeOffset Start { get; } + + [JsonProperty("low")] + public decimal Low { get; } + + [JsonProperty("high")] + public decimal High { get; } + + [JsonProperty("open")] + public decimal Open { get; } + + [JsonProperty("close")] + public decimal Close { get; } + + [JsonProperty("volume")] + public decimal Volume { get; } + + [JsonConstructor] + public Candle(DateTimeOffset start, decimal low, decimal high, decimal open, decimal close, decimal volume) + { + Start = start; + Low = low; + High = high; + Open = open; + Close = close; + Volume = volume; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseResponse.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseResponse.cs new file mode 100644 index 0000000..0b9adfb --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseResponse.cs @@ -0,0 +1,37 @@ +/* + * 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 Newtonsoft.Json; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +/// +/// Coinbase default http response message +/// +public class CoinbaseResponse +{ + /// + /// Whether there are additional pages for this query. + /// + [JsonProperty("has_next")] + public bool HasNext { get; set; } + + /// + /// Cursor for paginating. Users can use this string to pass in the next call to this endpoint, + /// and repeat this process to fetch all accounts through pagination. + /// + [JsonProperty("cursor")] + public string Cursor { get; set; } +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage/Models/CoinbaseSubscriptionMessage.cs b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseSubscriptionMessage.cs new file mode 100644 index 0000000..d4111d3 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/CoinbaseSubscriptionMessage.cs @@ -0,0 +1,83 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models; + +/// +/// Represents a message used for subscribing to WebSocket channels on Coinbase. +/// +public readonly struct CoinbaseSubscriptionMessage +{ + /// + /// Gets the API key for authentication (if required). + /// + [JsonProperty("api_key")] + public string ApiKey { get; } + + /// + /// Gets the channel to subscribe to. + /// + [JsonProperty("channel")] + public string Channel { get; } + + /// + /// Gets the list of product IDs associated with the subscription. + /// + [JsonProperty("product_ids")] + public List ProductIds { get; } + + /// + /// Gets the signature for authentication (if required). + /// + [JsonProperty("signature")] + public string Signature { get; } + + /// + /// Gets the timestamp of the subscription message. + /// + [JsonProperty("timestamp")] + public string Timestamp { get; } + + /// + /// Gets the type of WebSocket subscription. + /// + [JsonProperty("type")] + public WebSocketSubscriptionType Type { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The API key for authentication (if required). + /// The channel to subscribe to. + /// The list of product IDs associated with the subscription. + /// The signature for authentication (if required). + /// The timestamp of the subscription message. + /// The type of WebSocket subscription. + [JsonConstructor] + public CoinbaseSubscriptionMessage(string apiKey, string channel, List productIds, + string signature, string timestamp, WebSocketSubscriptionType type) + { + ApiKey = apiKey; + Channel = channel; + ProductIds = productIds; + Signature = signature; + Timestamp = timestamp; + Type = type; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Constants/CoinbaseWebSocketChannels.cs b/QuantConnect.CoinbaseBrokerage/Models/Constants/CoinbaseWebSocketChannels.cs new file mode 100644 index 0000000..aec8c14 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Constants/CoinbaseWebSocketChannels.cs @@ -0,0 +1,86 @@ +/* + * 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.Collections.Generic; + +namespace QuantConnect.CoinbaseBrokerage.Models.Constants; + +/// +/// The Brokerage Coinbase WebSocket feed provides the following channels +/// +public sealed class CoinbaseWebSocketChannels +{ + /// + /// Real-time server pings to keep all connections open + /// + /// + /// Subscribe to the heartbeats channel to receive heartbeats messages for specific products every second. + /// Heartbeats include a heartbeat_counter which verifies that no messages were missed. + /// + public const string Heartbeats = "heartbeats"; + + /// + /// Only sends messages that include the authenticated user + /// + /// + /// The user channel sends updates on all of a user's open orders, including all subsequent updates of those orders. + /// If none are provided, the WebSocket subscription is open to all product IDs. + /// + public const string User = "user"; + + /// + /// All updates and easiest way to keep order book snapshot + /// Use: when subscribe on channel update + /// + /// + /// The level2 channel guarantees delivery of all updates and is the easiest way to keep a snapshot of the order book. + /// + public const string Level2Request = "level2"; + + /// + /// All updates and easiest way to keep order book snapshot + /// Use: when parse response + /// + /// + /// The level2 channel guarantees delivery of all updates and is the easiest way to keep a snapshot of the order book. + /// + public const string Level2Response = "l2_data"; + + /// + /// Real-time updates every time a market trade happens + /// + /// + /// The market_trades channel sends market trades for a specified product on a preset interval. + /// + public const string MarketTrades = "market_trades"; + + /// + /// Represents a notification about various subscription events in the system. + /// + /// + /// { ... "events":[{"subscriptions":{"heartbeats":["heartbeats"],"level2":["BTC-USD"],"market_trades":["BTC-USD"]}}]} + /// + public const string Subscriptions = "subscriptions"; + + /// + /// Represents the channel information in a subscription event. + /// + public const string Channel = "channel"; + + /// + /// Represents a collection of WebSocket channels used for subscribing to real-time data for specific symbols. + /// + public readonly static ICollection WebSocketChannelList = new string[] { Level2Request, MarketTrades }; +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/CandleGranularity.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/CandleGranularity.cs new file mode 100644 index 0000000..8850111 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/CandleGranularity.cs @@ -0,0 +1,51 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +/// +/// Represents the granularity of candlestick data for financial charting. +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum CandleGranularity +{ + /// + /// Unknown granularity. + /// + [EnumMember(Value = "UNKNOWN_GRANULARITY")] + UnknownGranularity = 0, + + /// + /// Granularity representing one-minute intervals. + /// + [EnumMember(Value = "ONE_MINUTE")] + OneMinute = 1, + + /// + /// Granularity representing one-hour intervals. + /// + [EnumMember(Value = "ONE_HOUR")] + OneHour = 2, + + /// + /// Granularity representing one-day intervals. + /// + [EnumMember(Value = "ONE_DAY")] + OneDay = 3, +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/CoinbaseLevel2UpdateSide.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/CoinbaseLevel2UpdateSide.cs new file mode 100644 index 0000000..cf44634 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/CoinbaseLevel2UpdateSide.cs @@ -0,0 +1,36 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +[JsonConverter(typeof(StringEnumConverter))] +public enum CoinbaseLevel2UpdateSide +{ + /// + /// Bid + /// + [EnumMember(Value = "bid")] + Bid = 0, + + /// + /// Ask + /// + [EnumMember(Value = "offer")] + Offer = 1, +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/FailureCreateOrderReason.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/FailureCreateOrderReason.cs new file mode 100644 index 0000000..a52bb6e --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/FailureCreateOrderReason.cs @@ -0,0 +1,75 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +/// +/// Failure Create Order Reason +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum FailureCreateOrderReason +{ + [EnumMember(Value = "UNKNOWN_FAILURE_REASON")] + UnknownFailureReason = 0, + + [EnumMember(Value = "UNSUPPORTED_ORDER_CONFIGURATION")] + UnsupportedOrderConfiguration = 1, + + [EnumMember(Value = "INVALID_SIDE")] + InvalidSide = 2, + + [EnumMember(Value = "INVALID_PRODUCT_ID")] + InvalidProductId = 3, + + [EnumMember(Value = "INVALID_SIZE_PRECISION")] + InvalidSizePrecision = 4, + + [EnumMember(Value = "INVALID_PRICE_PRECISION")] + InvalidPricePrecision = 5, + + [EnumMember(Value = "INSUFFICIENT_FUND")] + InsufficientFund = 6, + + [EnumMember(Value = "INVALID_LEDGER_BALANCE")] + InvalidLedgerBalance = 7, + + [EnumMember(Value = "ORDER_ENTRY_DISABLED")] + OrderEntryDisabled = 8, + + [EnumMember(Value = "INELIGIBLE_PAIR")] + IneligiblePair = 9, + + [EnumMember(Value = "INVALID_LIMIT_PRICE_POST_ONLY")] + InvalidLimitPricePostOnly = 10, + + [EnumMember(Value = "INVALID_LIMIT_PRICE")] + InvalidLimitPrice = 11, + + [EnumMember(Value = "INVALID_NO_LIQUIDITY")] + InvalidNoLiquidity = 12, + + [EnumMember(Value = "INVALID_REQUEST")] + InvalidRequest = 13, + + [EnumMember(Value = "COMMANDER_REJECTED_NEW_ORDER")] + CommanderRejectedNewOrder = 14, + + [EnumMember(Value = "INSUFFICIENT_FUNDS")] + InsufficientFunds = 15 +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderSide.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderSide.cs new file mode 100644 index 0000000..2229a2f --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderSide.cs @@ -0,0 +1,46 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +/// +/// Represents the side of an order, indicating whether it is a buy or sell order. +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum OrderSide +{ + /// + /// Indicates a unknown order side. + /// + [EnumMember(Value = "UNKNOWN_ORDER_SIDE")] + UnknownOrderSide = 0, + + /// + /// Indicates a buy order. + /// + [EnumMember(Value = "BUY")] + Buy = 1, + + /// + /// Indicates a sell order. + /// + [EnumMember(Value = "SELL")] + Sell = 2, +} + diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs new file mode 100644 index 0000000..5229aee --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/OrderStatus.cs @@ -0,0 +1,69 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +/// +/// Coinbase available order status +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum OrderStatus +{ + /// + /// Unknown order status + /// + [EnumMember(Value = "UNKNOWN_ORDER_STATUS")] + UnknownOrderStatus, + + /// + /// Order is not yet open + /// + [EnumMember(Value = "PENDING")] + Pending, + + /// + /// Order is waiting to be fully filled + /// + [EnumMember(Value = "OPEN")] + Open, + + /// + /// Order is 100% filled + /// + [EnumMember(Value = "FILLED")] + Filled, + + /// + /// Order was cancelled by user or system + /// + [EnumMember(Value = "CANCELLED")] + Cancelled, + + /// + /// TWAP(Time-weighted average price) order was not filled by the expiry time + /// + [EnumMember(Value = "EXPIRED")] + Expired, + + /// + /// Order cannot be placed at all + /// + [EnumMember(Value = "FAILED")] + Failed, +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/StopDirection.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/StopDirection.cs new file mode 100644 index 0000000..dd12ebf --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/StopDirection.cs @@ -0,0 +1,31 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + + +[JsonConverter(typeof(StringEnumConverter))] +public enum StopDirection +{ + [EnumMember(Value = "STOP_DIRECTION_STOP_UP")] + StopDirectionStopUp = 0, + + [EnumMember(Value = "STOP_DIRECTION_STOP_DOWN")] + StopDirectionStopDown = 1, +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/TimeInForce.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/TimeInForce.cs new file mode 100644 index 0000000..bd98a7e --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/TimeInForce.cs @@ -0,0 +1,60 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +/// +/// Time in force policies provide guarantees about the lifetime of an order. +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum TimeInForce +{ + /// + /// Unknown time in force orders + /// + [EnumMember(Value = "UNKNOWN_TIME_IN_FORCE")] + UnknownTimeInForce = 0, + + /// + /// Good until date orders are valid till a specified date or time (within a 90-day hard limit) unless + /// it has been already fulfilled or cancelled. + /// + [EnumMember(Value = "GOOD_UNTIL_DATE_TIME")] + GoodUntilDateTime = 1, + + /// + /// Good until canceled orders remain open on the book until canceled. + /// This is the default behavior if no policy is specified. + /// + [EnumMember(Value = "GOOD_UNTIL_CANCELLED")] + GoodUntilCancelled = 2, + + /// + /// Immediate or cancel orders instantly cancel the remaining size of + /// the limit order instead of opening it on the book. + /// + [EnumMember(Value = "IMMEDIATE_OR_CANCEL")] + ImmediateOrCancel = 3, + + /// + /// Fill or kill orders are rejected if the entire size cannot be matched. + /// + [EnumMember(Value = "FILL_OR_KILL")] + FillOrKill = 4, +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/WebSocketEventType.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/WebSocketEventType.cs new file mode 100644 index 0000000..b0ec4cd --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/WebSocketEventType.cs @@ -0,0 +1,33 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +[JsonConverter(typeof(StringEnumConverter))] +public enum WebSocketEventType +{ + [EnumMember(Value = "none")] + None = 0, + + [EnumMember(Value = "snapshot")] + Snapshot = 1, + + [EnumMember(Value = "update")] + Update = 2, +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Enums/WebSocketSubscriptionType.cs b/QuantConnect.CoinbaseBrokerage/Models/Enums/WebSocketSubscriptionType.cs new file mode 100644 index 0000000..fe65705 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Enums/WebSocketSubscriptionType.cs @@ -0,0 +1,30 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.CoinbaseBrokerage.Models.Enums; + +[JsonConverter(typeof(StringEnumConverter))] +public enum WebSocketSubscriptionType +{ + [EnumMember(Value = "subscribe")] + Subscribe, + + [EnumMember(Value = "unsubscribe")] + Unsubscribe +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/Requests/CoinbaseCreateOrderRequest.cs b/QuantConnect.CoinbaseBrokerage/Models/Requests/CoinbaseCreateOrderRequest.cs new file mode 100644 index 0000000..9d12676 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Requests/CoinbaseCreateOrderRequest.cs @@ -0,0 +1,53 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models.Requests; + +/// +/// Coinbase place order api request +/// +public struct CoinbaseCreateOrderRequest +{ + [JsonProperty("client_order_id")] + public Guid ClientOrderId { get; } + + [JsonProperty("product_id")] + public string ProductId { get; } + + [JsonProperty("side")] + [JsonConverter(typeof(StringEnumConverter))] + public OrderSide Side { get; } + + [JsonProperty("order_configuration")] + public OrderConfiguration OrderConfiguration { get; set; } + + /// + /// Self trade prevention ID, to prevent an order crossing against the same user + /// + [JsonProperty("self_trade_prevention_id")] + public Guid? SelfTradePreventionId { get; set; } = null; + + public CoinbaseCreateOrderRequest(Guid clientOrderId, string productId, OrderSide side): this() + { + ClientOrderId = clientOrderId; + ProductId = productId; + Side = side; + } +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage/Models/Requests/CoinbaseEditOrderRequest.cs b/QuantConnect.CoinbaseBrokerage/Models/Requests/CoinbaseEditOrderRequest.cs new file mode 100644 index 0000000..7cede4e --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/Requests/CoinbaseEditOrderRequest.cs @@ -0,0 +1,56 @@ +/* + * 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 Newtonsoft.Json; + +namespace QuantConnect.CoinbaseBrokerage.Models.Requests; + +/// +/// Coinbase edit order api request +/// +public readonly struct CoinbaseEditOrderRequest +{ + /// + /// ID of order to edit. + /// + [JsonProperty("order_id")] + public string OrderId { get; } + + /// + /// New price for order. + /// + [JsonProperty("price")] + public decimal Price { get; } + + /// + /// New size for order + /// + [JsonProperty("size")] + public decimal Size { get; } + + /// + /// Initialize new instance of + /// + /// ID of order to edit. + /// New price for order. + /// New size for order + [JsonConstructor] + public CoinbaseEditOrderRequest(string orderId, decimal price, decimal size) + { + OrderId = orderId; + Price = price; + Size = size; + } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseLevel2Event.cs b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseLevel2Event.cs new file mode 100644 index 0000000..c270444 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseLevel2Event.cs @@ -0,0 +1,44 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; +using Newtonsoft.Json.Converters; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models.WebSocket; + +public class CoinbaseLevel2Event : WebSocketEvent +{ + [JsonProperty("product_id")] + public string ProductId { get; set; } + + [JsonProperty("updates")] + public List Updates { get; set; } +} + +public class Update +{ + [JsonProperty("side")] + [JsonConverter(typeof(StringEnumConverter))] + public CoinbaseLevel2UpdateSide Side { get; set; } + + [JsonProperty("price_level")] + public decimal? PriceLevel { get; set; } + + [JsonProperty("new_quantity")] + public decimal? NewQuantity { get; set; } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseMarketTradesEvent.cs b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseMarketTradesEvent.cs new file mode 100644 index 0000000..0953ec0 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseMarketTradesEvent.cs @@ -0,0 +1,48 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models.WebSocket; + +public class CoinbaseMarketTradesEvent : WebSocketEvent +{ + [JsonProperty("trades")] + public List Trades { get; set; } = new List(); +} + +public class Trade +{ + [JsonProperty("trade_id")] + public string TradeId { get; set; } + + [JsonProperty("product_id")] + public string ProductId { get; set; } + + [JsonProperty("price")] + public decimal? Price { get; set; } + + [JsonProperty("size")] + public decimal? Size { get; set; } + + [JsonProperty("side")] + public OrderSide Side { get; set; } + + [JsonProperty("time")] + public DateTimeOffset Time { get; set; } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseUserEvent.cs b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseUserEvent.cs new file mode 100644 index 0000000..bad64e1 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseUserEvent.cs @@ -0,0 +1,114 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models.WebSocket; + +public class CoinbaseUserEvent : WebSocketEvent +{ + [JsonProperty("orders")] + public List Orders { get; set; } +} + +/// +/// Represents a response from the Coinbase WebSocket for order-related information. +/// +public class CoinbaseWebSocketOrderResponse +{ + /// + /// Unique identifier of order + /// + [JsonProperty("order_id")] + public string OrderId { get; set; } + + /// + /// Unique identifier of order specified by client + /// + [JsonProperty("client_order_id")] + public string ClientOrderId { get; set; } + + /// + /// Amount the order is filled, in base currency + /// + [JsonProperty("cumulative_quantity")] + public decimal? CumulativeQuantity { get; set; } + + /// + /// Amount remaining, in same currency as order was placed in (quote or base) + /// + [JsonProperty("leaves_quantity")] + public decimal? LeavesQuantity { get; set; } + + /// + /// Average filled price of the order so far + /// + [JsonProperty("avg_price")] + public decimal? AveragePrice { get; set; } + + /// + /// Commission paid for the order + /// + [JsonProperty("total_fees")] + public decimal? TotalFees { get; set; } + + /// + /// Order Status + /// + [JsonProperty("status")] + public OrderStatus Status { get; set; } + + /// + /// The product ID for which this order was placed + /// + [JsonProperty("product_id")] + public string ProductId { get; set; } + + /// + /// When the order was placed + /// + [JsonProperty("creation_time")] + public DateTimeOffset CreationTime { get; set; } + + /// + /// Can be one of: BUY, SELL + /// + [JsonProperty("order_side")] + public OrderSide OrderSide { get; set; } + + /// + /// Can be one of: Limit, Market, Stop Limit + /// + [JsonProperty("order_type")] + public string OrderType { get; set; } + + /// + /// Cancel Reason + /// + /// + /// "User requested cancel" + /// + [JsonProperty("cancel_reason")] + public string CancelReason { get; set; } + + /// + /// Reject Reason + /// + [JsonProperty("reject_Reason")] + public string RejectReason { get; set; } +} diff --git a/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseWebSocketMessage.cs b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseWebSocketMessage.cs new file mode 100644 index 0000000..2455b6e --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/CoinbaseWebSocketMessage.cs @@ -0,0 +1,38 @@ +/* + * 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 Newtonsoft.Json; +using System.Collections.Generic; +using System; + +namespace QuantConnect.CoinbaseBrokerage.Models.WebSocket; + +public class CoinbaseWebSocketMessage where T : WebSocketEvent +{ + [JsonProperty("channel")] + public string Channel { get; set; } + + [JsonProperty("client_id")] + public string ClientId { get; set; } + + [JsonProperty("timestamp")] + public DateTimeOffset Timestamp { get; set; } + + [JsonProperty("sequence_num")] + public string SequenceNumber { get; set; } + + [JsonProperty("events")] + public List Events { get; set; } +} \ No newline at end of file diff --git a/QuantConnect.CoinbaseBrokerage/Models/WebSocket/WebSocketEvent.cs b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/WebSocketEvent.cs new file mode 100644 index 0000000..81e39e4 --- /dev/null +++ b/QuantConnect.CoinbaseBrokerage/Models/WebSocket/WebSocketEvent.cs @@ -0,0 +1,27 @@ +/* + * 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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using QuantConnect.CoinbaseBrokerage.Models.Enums; + +namespace QuantConnect.CoinbaseBrokerage.Models.WebSocket; +public class WebSocketEvent +{ + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + public WebSocketEventType Type { get; set; } +} + diff --git a/QuantConnect.GDAXBrokerage/QuantConnect.GDAXBrokerage.csproj b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj similarity index 78% rename from QuantConnect.GDAXBrokerage/QuantConnect.GDAXBrokerage.csproj rename to QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj index 2458c14..3ba379a 100644 --- a/QuantConnect.GDAXBrokerage/QuantConnect.GDAXBrokerage.csproj +++ b/QuantConnect.CoinbaseBrokerage/QuantConnect.CoinbaseBrokerage.csproj @@ -3,16 +3,16 @@ Release AnyCPU net6.0 - QuantConnect.TemplateBrokerage - QuantConnect.GDAXBrokerage - QuantConnect.GDAXBrokerage - QuantConnect.GDAXBrokerage + QuantConnect.CoinbaseBrokerage + QuantConnect.CoinbaseBrokerage + QuantConnect.CoinbaseBrokerage + QuantConnect.CoinbaseBrokerage Library bin\$(Configuration)\ false true false - QuantConnect LEAN GDAX Brokerage: Brokerage Template plugin for Lean + Coinbase Brokerage Integration to LEAN full diff --git a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageAdditionalTests.cs b/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageAdditionalTests.cs deleted file mode 100644 index 10176f4..0000000 --- a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageAdditionalTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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.Collections.Generic; -using System.Linq; -using System.Threading; -using NUnit.Framework; -using QuantConnect.Algorithm; -using QuantConnect.Brokerages; -using QuantConnect.Brokerages.GDAX; -using QuantConnect.Configuration; -using QuantConnect.Data; -using QuantConnect.Interfaces; -using QuantConnect.Lean.Engine.DataFeeds; -using QuantConnect.Logging; -using QuantConnect.Packets; -using RestSharp; - -namespace QuantConnect.Tests.Brokerages.GDAX -{ - [TestFixture] - public class GDAXBrokerageAdditionalTests - { - [Test] - public void PublicEndpointCallsAreRateLimited() - { - using (var brokerage = GetBrokerage()) - { - brokerage.Connect(); - Assert.IsTrue(brokerage.IsConnected); - - for (var i = 0; i < 50; i++) - { - Assert.DoesNotThrow(() => brokerage.GetTick(Symbols.BTCEUR)); - } - } - } - - [Test] - public void PrivateEndpointCallsAreRateLimited() - { - using (var brokerage = GetBrokerage()) - { - brokerage.Connect(); - Assert.IsTrue(brokerage.IsConnected); - - for (var i = 0; i < 50; i++) - { - Assert.DoesNotThrow(() => brokerage.GetOpenOrders()); - } - } - } - - [Test] - public void ClientConnects() - { - using (var brokerage = GetBrokerage()) - { - var hasError = false; - - brokerage.Message += (s, e) => { hasError = true; }; - - Log.Trace("Connect #1"); - brokerage.Connect(); - Assert.IsTrue(brokerage.IsConnected); - - Assert.IsFalse(hasError); - - Log.Trace("Disconnect #1"); - brokerage.Disconnect(); - Assert.IsFalse(brokerage.IsConnected); - - Log.Trace("Connect #2"); - brokerage.Connect(); - Assert.IsTrue(brokerage.IsConnected); - - Log.Trace("Disconnect #2"); - brokerage.Disconnect(); - Assert.IsFalse(brokerage.IsConnected); - } - } - - [Test] - public void DataQueueHandlerConnectsAndSubscribes() - { - var symbols = new[] - { - "LTCUSD", "LTCEUR", "LTCBTC", - "BTCUSD", "BTCEUR", "BTCGBP", - "ETHBTC", "ETHUSD", "ETHEUR", - "BCHBTC", "BCHUSD", "BCHEUR", - "XRPUSD", "XRPEUR", "XRPBTC", - "EOSUSD", "EOSEUR", "EOSBTC", - "XLMUSD", "XLMEUR", "XLMBTC", - "ETCUSD", "ETCEUR", "ETCBTC", - "ZRXUSD", "ZRXEUR", "ZRXBTC" - } - .Select(ticker => Symbol.Create(ticker, SecurityType.Crypto, Market.GDAX)) - .ToList(); - - using (var dqh = GetDataQueueHandler()) - { - dqh.Connect(); - Assert.IsTrue(dqh.IsConnected); - - dqh.Subscribe(symbols); - - Thread.Sleep(5000); - - dqh.Unsubscribe(symbols); - - dqh.Disconnect(); - Assert.IsFalse(dqh.IsConnected); - } - } - - private static TestGDAXDataQueueHandler GetDataQueueHandler() - { - var wssUrl = Config.Get("gdax-url", "wss://ws-feed.pro.coinbase.com"); - var webSocketClient = new WebSocketClientWrapper(); - var restClient = new RestClient(Config.Get("gdax-rest-api", "https://api.pro.coinbase.com")); - var apiKey = Config.Get("gdax-api-key"); - var apiSecret = Config.Get("gdax-api-secret"); - var passPhrase = Config.Get("gdax-passphrase"); - var algorithm = new QCAlgorithm(); - var userId = Config.GetInt("job-user-id"); - var userToken = Config.Get("api-access-token"); - var priceProvider = new ApiPriceProvider(userId, userToken); - var aggregator = new AggregationManager(); - - return new TestGDAXDataQueueHandler(wssUrl, webSocketClient, restClient, apiKey, apiSecret, passPhrase, algorithm, priceProvider, aggregator, null); - } - - private static TestGDAXDataQueueHandler GetBrokerage() - { - var wssUrl = Config.Get("gdax-url", "wss://ws-feed.pro.coinbase.com"); - var webSocketClient = new WebSocketClientWrapper(); - var restClient = new RestClient(Config.Get("gdax-rest-api", "https://api.pro.coinbase.com")); - var apiKey = Config.Get("gdax-api-key"); - var apiSecret = Config.Get("gdax-api-secret"); - var passPhrase = Config.Get("gdax-passphrase"); - var algorithm = new QCAlgorithm(); - var userId = Config.GetInt("job-user-id"); - var userToken = Config.Get("api-access-token"); - var priceProvider = new ApiPriceProvider(userId, userToken); - var aggregator = new AggregationManager(); - - return new TestGDAXDataQueueHandler(wssUrl, webSocketClient, restClient, apiKey, apiSecret, passPhrase, algorithm, priceProvider, aggregator, null); - } - - private class TestGDAXDataQueueHandler : GDAXDataQueueHandler - { - public TestGDAXDataQueueHandler(string wssUrl, IWebSocket websocket, IRestClient restClient, string apiKey, - string apiSecret, - string passPhrase, - IAlgorithm algorithm, - IPriceProvider priceProvider, - IDataAggregator aggregator, - LiveNodePacket job - ) - : base(wssUrl, websocket, restClient, apiKey, apiSecret, passPhrase, algorithm, priceProvider, aggregator, job) - { - } - - public void Subscribe(IEnumerable symbols) - { - base.Subscribe(symbols); - } - } - } -} diff --git a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageIntegrationTests.cs b/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageIntegrationTests.cs deleted file mode 100644 index 201fdbf..0000000 --- a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageIntegrationTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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 QuantConnect.Brokerages.GDAX; -using NUnit.Framework; -using QuantConnect.Interfaces; -using QuantConnect.Securities; -using QuantConnect.Configuration; -using QuantConnect.Orders; -using Moq; -using QuantConnect.Brokerages; -using QuantConnect.Tests.Common.Securities; -using RestSharp; -using QuantConnect.Lean.Engine.DataFeeds; - -namespace QuantConnect.Tests.Brokerages.GDAX -{ - [TestFixture] - public class GDAXBrokerageIntegrationTests : BrokerageTests - { - #region Properties - protected override Symbol Symbol => Symbol.Create("ETHBTC", SecurityType.Crypto, Market.GDAX); - - /// - /// Gets the security type associated with the - /// - protected override SecurityType SecurityType => SecurityType.Crypto; - - protected override decimal GetDefaultQuantity() - { - return 0.01m; - } - #endregion - - protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISecurityProvider securityProvider) - { - var restClient = new RestClient(Config.Get("gdax-rest-api", "https://api.pro.coinbase.com")); - var webSocketClient = new WebSocketClientWrapper(); - - var securities = new SecurityManager(new TimeKeeper(DateTime.UtcNow, TimeZones.NewYork)) - { - {Symbol, CreateSecurity(Symbol)} - }; - - var transactions = new SecurityTransactionManager(null, securities); - transactions.SetOrderProcessor(new FakeOrderProcessor()); - - var algorithm = new Mock(); - algorithm.Setup(a => a.Transactions).Returns(transactions); - algorithm.Setup(a => a.BrokerageModel).Returns(new GDAXBrokerageModel()); - algorithm.Setup(a => a.Portfolio).Returns(new SecurityPortfolioManager(securities, transactions)); - algorithm.Setup(a => a.Securities).Returns(securities); - - var priceProvider = new Mock(); - priceProvider.Setup(a => a.GetLastPrice(It.IsAny())).Returns(1.234m); - - var aggregator = new AggregationManager(); - return new GDAXBrokerage(Config.Get("gdax-url", "wss://ws-feed.pro.coinbase.com"), webSocketClient, restClient, - Config.Get("gdax-api-key"), Config.Get("gdax-api-secret"), Config.Get("gdax-passphrase"), algorithm.Object, - priceProvider.Object, aggregator, null); - } - - /// - /// Returns wether or not the brokers order methods implementation are async - /// - protected override bool IsAsync() - { - return false; - } - - protected override decimal GetAskPrice(Symbol symbol) - { - var tick = ((GDAXBrokerage)Brokerage).GetTick(symbol); - return tick.AskPrice; - } - - protected override void ModifyOrderUntilFilled(Order order, OrderTestParameters parameters, double secondsTimeout = 90) - { - Assert.Pass("Order update not supported"); - } - - [Test] - public override void GetAccountHoldings() - { - // GDAX GetAccountHoldings() always returns an empty list - Assert.That(Brokerage.GetAccountHoldings().Count == 0); - } - - // stop market orders no longer supported (since 3/23/2019) - // no stop limit support - private static TestCaseData[] OrderParameters => new[] - { - new TestCaseData(new MarketOrderTestParameters(Symbol.Create("BTCUSD", SecurityType.Crypto, Market.GDAX))).SetName("MarketOrder"), - new TestCaseData(new LimitOrderTestParameters(Symbol.Create("BTCUSD", SecurityType.Crypto, Market.GDAX), 305m, 300m)).SetName("LimitOrder"), - }; - - [Test, TestCaseSource(nameof(OrderParameters))] - public override void CancelOrders(OrderTestParameters parameters) - { - base.CancelOrders(parameters); - } - - [Test, TestCaseSource(nameof(OrderParameters))] - public override void LongFromZero(OrderTestParameters parameters) - { - base.LongFromZero(parameters); - } - - [Test, TestCaseSource(nameof(OrderParameters))] - public override void CloseFromLong(OrderTestParameters parameters) - { - base.CloseFromLong(parameters); - } - - [Test, TestCaseSource(nameof(OrderParameters))] - public override void ShortFromZero(OrderTestParameters parameters) - { - base.ShortFromZero(parameters); - } - - [Test, TestCaseSource(nameof(OrderParameters))] - public override void CloseFromShort(OrderTestParameters parameters) - { - base.CloseFromShort(parameters); - } - - [Explicit("Order modification not allowed")] - public override void ShortFromLong(OrderTestParameters parameters) - { - base.ShortFromLong(parameters); - } - - [Explicit("Order modification not allowed")] - public override void LongFromShort(OrderTestParameters parameters) - { - base.LongFromShort(parameters); - } - } -} diff --git a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageTests.cs b/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageTests.cs deleted file mode 100644 index d63eb3b..0000000 --- a/QuantConnect.GDAXBrokerage.Tests/GDAXBrokerageTests.cs +++ /dev/null @@ -1,426 +0,0 @@ -/* - * 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 Moq; -using Newtonsoft.Json; -using NUnit.Framework; -using QuantConnect.Brokerages; -using QuantConnect.Brokerages.GDAX; -using QuantConnect.Interfaces; -using QuantConnect.Orders; -using RestSharp; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using QuantConnect.Util; -using Order = QuantConnect.Orders.Order; -using QuantConnect.Data; -using QuantConnect.Data.Market; -using QuantConnect.Lean.Engine.DataFeeds; - -namespace QuantConnect.Tests.Brokerages.GDAX -{ - [TestFixture, Parallelizable(ParallelScope.Fixtures)] - public class GDAXBrokerageTests - { - #region Declarations - - private GDAXFakeDataQueueHandler _unit; - private readonly Mock _wss = new Mock(); - private readonly Mock _rest = new Mock(); - private readonly Mock _algo = new Mock(); - private string _openOrderData; - private string _fillData; - private string _accountsData; - private string _holdingData; - private string _tickerData; - private Symbol _symbol; - private const string BrokerId = "d0c5340b-6d6c-49d9-b567-48c4bfca13d2"; - private const string MatchBrokerId = "132fb6ae-456b-4654-b4e0-d681ac05cea1"; - private const AccountType AccountType = QuantConnect.AccountType.Margin; - - #endregion - - [SetUp] - public void Setup() - { - var priceProvider = new Mock(); - priceProvider.Setup(x => x.GetLastPrice(It.IsAny())).Returns(1.234m); - - _rest.Setup(m => m.Execute(It.Is(r => r.Resource.StartsWith("/products/")))) - .Returns(new RestResponse - { - Content = File.ReadAllText("TestData//gdax_tick.txt"), - StatusCode = HttpStatusCode.OK - }); - - _rest.Setup(m => m.Execute(It.Is(r => r.Resource == "/orders" && r.Method == Method.GET))) - .Returns(new RestResponse - { - Content = "[]", - StatusCode = HttpStatusCode.OK - }); - - _rest.Setup(m => m.Execute(It.Is(r => r.Resource == "/orders" && r.Method == Method.POST))) - .Returns(new RestResponse - { - Content = File.ReadAllText("TestData//gdax_order.txt"), - StatusCode = HttpStatusCode.OK - }); - - _rest.Setup(m => m.Execute(It.Is(r => r.Resource.StartsWith("/orders/" + BrokerId) || r.Resource.StartsWith("/orders/" + MatchBrokerId)))) - .Returns(new RestResponse - { - Content = File.ReadAllText("TestData//gdax_orderById.txt"), - StatusCode = HttpStatusCode.OK - }); - - _rest.Setup(m => m.Execute(It.Is(r => r.Resource == "/fills"))) - .Returns(new RestResponse - { - Content = "[]", - StatusCode = HttpStatusCode.OK - }); - - _unit = new GDAXFakeDataQueueHandler("wss://localhost", _wss.Object, _rest.Object, "abc", "MTIz", "pass", _algo.Object, priceProvider.Object, new AggregationManager()); - - _fillData = File.ReadAllText("TestData//gdax_fill.txt"); - _openOrderData = File.ReadAllText("TestData//gdax_openOrders.txt"); - _accountsData = File.ReadAllText("TestData//gdax_accounts.txt"); - _holdingData = File.ReadAllText("TestData//gdax_holding.txt"); - _tickerData = File.ReadAllText("TestData//gdax_ticker.txt"); - - _symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.GDAX); - - _algo.Setup(a => a.BrokerageModel.AccountType).Returns(AccountType); - _algo.Setup(a => a.AccountCurrency).Returns(Currencies.USD); - } - - [TearDown] - public void TearDown() - { - _unit.Disconnect(); - _unit.DisposeSafely(); - } - - private void SetupResponse(string body, HttpStatusCode httpStatus = HttpStatusCode.OK) - { - _rest.Setup(m => m.Execute(It.Is(r => !r.Resource.StartsWith("/products/") && !r.Resource.StartsWith("/orders/" + BrokerId)))) - .Returns(new RestResponse - { - Content = body, - StatusCode = httpStatus - }); - } - - [Test] - public void IsConnectedTest() - { - _wss.Setup(w => w.IsOpen).Returns(true); - Assert.IsTrue(_unit.IsConnected); - _wss.Setup(w => w.IsOpen).Returns(false); - Assert.IsFalse(_unit.IsConnected); - } - - [Test] - public void ConnectTest() - { - SetupResponse(_accountsData); - - _wss.Setup(m => m.Connect()).Raises(m => m.Open += null, EventArgs.Empty).Verifiable(); - _wss.Setup(m => m.IsOpen).Returns(false); - _unit.Connect(); - _wss.Verify(); - } - - [Test] - public void DisconnectTest() - { - _wss.Setup(m => m.Close()).Verifiable(); - _wss.Setup(m => m.IsOpen).Returns(true); - _unit.Disconnect(); - _wss.Verify(); - } - - [Test] - public void OnOrderFillTest() - { - const decimal orderQuantity = 6.1m; - - _unit.PlaceOrder(new MarketOrder(Symbols.BTCUSD, orderQuantity, DateTime.UtcNow)); - - _rest.Setup(m => m.Execute(It.Is(r => r.Resource == "/fills"))) - .Returns(new RestResponse - { - Content = _fillData, - StatusCode = HttpStatusCode.OK - }); - - var raised = new ManualResetEvent(false); - - var isFilled = false; - var actualFee = 0m; - var actualQuantity = 0m; - - _unit.OrdersStatusChanged += (s, e) => - { - var orderEvent = e.Single(); - Assert.AreEqual("BTCUSD", orderEvent.Symbol.Value); - actualFee += orderEvent.OrderFee.Value.Amount; - Assert.AreEqual(Currencies.USD, orderEvent.OrderFee.Value.Currency); - actualQuantity += orderEvent.AbsoluteFillQuantity; - - Assert.IsTrue(actualQuantity != orderQuantity); - Assert.AreEqual(OrderStatus.PartiallyFilled, orderEvent.Status); - Assert.AreEqual(5.23512, orderEvent.FillQuantity); - Assert.AreEqual(12, actualFee); - - isFilled = true; - - raised.Set(); - }; - - raised.WaitOne(3000); - raised.DisposeSafely(); - - Assert.IsTrue(isFilled); - } - - [Test] - public void GetAuthenticationTokenTest() - { - var actual = _unit.GetAuthenticationToken("", "POST", "http://localhost"); - - Assert.IsFalse(string.IsNullOrEmpty(actual.Signature)); - Assert.IsFalse(string.IsNullOrEmpty(actual.Timestamp)); - Assert.AreEqual("pass", actual.Passphrase); - Assert.AreEqual("abc", actual.Key); - } - - [TestCase("1", HttpStatusCode.OK, OrderStatus.Submitted, 1.23, 0, OrderType.Market)] - [TestCase("1", HttpStatusCode.OK, OrderStatus.Submitted, -1.23, 0, OrderType.Market)] - [TestCase("1", HttpStatusCode.OK, OrderStatus.Submitted, 1.23, 1234.56, OrderType.Limit)] - [TestCase("1", HttpStatusCode.OK, OrderStatus.Submitted, -1.23, 1234.56, OrderType.Limit)] - [TestCase("1", HttpStatusCode.OK, OrderStatus.Submitted, 1.23, 1234.56, OrderType.StopMarket)] - [TestCase("1", HttpStatusCode.OK, OrderStatus.Submitted, -1.23, 1234.56, OrderType.StopMarket)] - [TestCase(null, HttpStatusCode.BadRequest, OrderStatus.Invalid, 1.23, 1234.56, OrderType.Market)] - [TestCase(null, HttpStatusCode.BadRequest, OrderStatus.Invalid, 1.23, 1234.56, OrderType.Limit)] - [TestCase(null, HttpStatusCode.BadRequest, OrderStatus.Invalid, 1.23, 1234.56, OrderType.StopMarket)] - public void PlaceOrderTest(string orderId, HttpStatusCode httpStatus, OrderStatus status, decimal quantity, decimal price, OrderType orderType) - { - var response = new - { - id = BrokerId, - fill_fees = "0.11" - }; - SetupResponse(JsonConvert.SerializeObject(response), httpStatus); - - _unit.OrdersStatusChanged += (s, e) => - { - var orderEvent = e.Single(); - Assert.AreEqual(status, orderEvent.Status); - if (orderId != null) - { - Assert.AreEqual("BTCUSD", orderEvent.Symbol.Value); - Assert.That((quantity > 0 && orderEvent.Direction == OrderDirection.Buy) || (quantity < 0 && orderEvent.Direction == OrderDirection.Sell)); - Assert.IsTrue(orderId == null || _unit.CachedOrderIDs.SelectMany(c => c.Value.BrokerId.Where(b => b == BrokerId)).Any()); - } - }; - - Order order; - if (orderType == OrderType.Limit) - { - order = new LimitOrder(_symbol, quantity, price, DateTime.UtcNow); - } - else if (orderType == OrderType.Market) - { - order = new MarketOrder(_symbol, quantity, DateTime.UtcNow); - } - else - { - order = new StopMarketOrder(_symbol, quantity, price, DateTime.UtcNow); - } - - var actual = _unit.PlaceOrder(order); - - Assert.IsTrue(actual || (orderId == null && !actual)); - } - - [Test] - public void GetOpenOrdersTest() - { - SetupResponse(_openOrderData); - - var marketOrder = new MarketOrder(); - marketOrder.BrokerId.Add("1"); - _unit.CachedOrderIDs.TryAdd(1, marketOrder); - - var actual = _unit.GetOpenOrders(); - - Assert.AreEqual(2, actual.Count); - Assert.AreEqual(0.01, actual.First().Quantity); - Assert.AreEqual(OrderDirection.Buy, actual.First().Direction); - Assert.AreEqual(0.1, (actual.First() as LimitOrder).LimitPrice); - - Assert.AreEqual(-1, actual.Last().Quantity); - Assert.AreEqual(OrderDirection.Sell, actual.Last().Direction); - Assert.AreEqual(1, (actual.Last() as LimitOrder).LimitPrice); - } - - [Test] - public void GetTickTest() - { - var actual = _unit.GetTick(_symbol); - Assert.AreEqual(333.98m, actual.BidPrice); - Assert.AreEqual(333.99m, actual.AskPrice); - Assert.AreEqual(5957.11914015, actual.Quantity); - } - - [Test] - public void GetCashBalanceTest() - { - SetupResponse(_accountsData); - - var actual = _unit.GetCashBalance(); - - Assert.AreEqual(2, actual.Count); - - var usd = actual.Single(a => a.Currency == Currencies.USD); - var btc = actual.Single(a => a.Currency == "BTC"); - - Assert.AreEqual(80.2301373066930000m, usd.Amount); - Assert.AreEqual(1.1, btc.Amount); - } - - [Test, Ignore("Holdings are now set to 0 swaps at the start of each launch. Not meaningful.")] - public void GetAccountHoldingsTest() - { - SetupResponse(_holdingData); - - var marketOrder = new MarketOrder(_symbol, 0.01m, DateTime.UtcNow); - marketOrder.BrokerId.Add("1"); - _unit.CachedOrderIDs.TryAdd(1, marketOrder); - - var actual = _unit.GetAccountHoldings(); - - Assert.AreEqual(0, actual.Count); - } - - [TestCase(HttpStatusCode.OK, HttpStatusCode.NotFound, false)] - [TestCase(HttpStatusCode.OK, HttpStatusCode.OK, true)] - public void CancelOrderTest(HttpStatusCode code, HttpStatusCode code2, bool expected) - { - _rest.Setup(m => m.Execute(It.Is(r => !r.Resource.EndsWith("1")))) - .Returns(new RestResponse - { - StatusCode = code - }); - - _rest.Setup(m => m.Execute(It.Is(r => !r.Resource.EndsWith("2")))) - .Returns(new RestResponse - { - StatusCode = code2 - }); - - var limitOrder = new LimitOrder(); - limitOrder.BrokerId.AddRange(new List { "1", "2" }); - var actual = _unit.CancelOrder(limitOrder); - Assert.AreEqual(expected, actual); - } - - [Test] - public void UpdateOrderTest() - { - Assert.Throws(() => _unit.UpdateOrder(new LimitOrder())); - } - - [Test] - public void SubscribeTest() - { - string actual = null; - - _wss.Setup(w => w.Send(It.IsAny())).Callback(c => actual = c); - - var gotBTCUSD = false; - var gotETHBTC = false; - - _unit.Subscribe(GetSubscriptionDataConfig(Symbol.Create("BTCUSD", SecurityType.Crypto, Market.GDAX), Resolution.Tick), (s, e) => { gotBTCUSD = true; }); - StringAssert.Contains("[\"BTC-USD\"]", actual); - _unit.Subscribe(GetSubscriptionDataConfig(Symbol.Create("ETHBTC", SecurityType.Crypto, Market.GDAX), Resolution.Tick), (s, e) => { gotETHBTC = true; }); - StringAssert.Contains("[\"BTC-USD\",\"ETH-BTC\"]", actual); - Thread.Sleep(1000); - - Assert.IsFalse(gotBTCUSD); - Assert.IsFalse(gotETHBTC); - } - - [Test] - public void UnsubscribeTest() - { - string actual = null; - _wss.Setup(w => w.IsOpen).Returns(true); - _wss.Setup(w => w.Send(It.IsAny())).Callback(c => actual = c); - _unit.Unsubscribe(new List { Symbol.Create("BTCUSD", SecurityType.Crypto, Market.GDAX) }); - StringAssert.Contains("user", actual); - StringAssert.Contains("heartbeat", actual); - StringAssert.DoesNotContain("matches", actual); - } - - [Test] - public void InvalidSymbolSubscribeTest() - { - string actual = null; - _wss.Setup(w => w.Send(It.IsAny())).Callback(c => actual = c); - - // subscribe to invalid symbol - _unit.Subscribe(new[] { Symbol.Create("BTCLTC", SecurityType.Crypto, Market.GDAX) }); - - // subscribe is not called for invalid symbols - Assert.IsNull(actual); - } - - private SubscriptionDataConfig GetSubscriptionDataConfig(Symbol symbol, Resolution resolution) - { - return new SubscriptionDataConfig( - typeof(T), - symbol, - resolution, - TimeZones.Utc, - TimeZones.Utc, - true, - true, - false); - } - - private class GDAXFakeDataQueueHandler : GDAXDataQueueHandler - { - protected override string[] ChannelNames => new[] { "heartbeat", "user" }; - - public GDAXFakeDataQueueHandler(string wssUrl, IWebSocket websocket, IRestClient restClient, string apiKey, string apiSecret, string passPhrase, IAlgorithm algorithm, - IPriceProvider priceProvider, IDataAggregator aggregator) - : base(wssUrl, websocket, restClient, apiKey, apiSecret, passPhrase, algorithm, priceProvider, aggregator, null) - { - } - - public void Subscribe(IEnumerable symbols) - { - base.Subscribe(symbols); - } - } - } -} diff --git a/QuantConnect.GDAXBrokerage.Tests/config.json b/QuantConnect.GDAXBrokerage.Tests/config.json deleted file mode 100644 index 2b72fc0..0000000 --- a/QuantConnect.GDAXBrokerage.Tests/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "data-folder": "../../../../Lean/Data/" -} \ No newline at end of file diff --git a/QuantConnect.GDAXBrokerage.ToolBox/GDAXDownloader.cs b/QuantConnect.GDAXBrokerage.ToolBox/GDAXDownloader.cs deleted file mode 100644 index d46798e..0000000 --- a/QuantConnect.GDAXBrokerage.ToolBox/GDAXDownloader.cs +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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 System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using Newtonsoft.Json; -using QuantConnect.Brokerages; -using QuantConnect.Data; -using QuantConnect.Data.Market; -using QuantConnect.Logging; - -namespace QuantConnect.ToolBox.GDAXDownloader -{ - /// - /// GDAX Data Downloader class - /// - public class GDAXDownloader : IDataDownloader - { - const int MaxDatapointsPerRequest = 200; - const int MaxRequestsPerSecond = 2; - - /// - /// Get historical data enumerable for a single symbol, type and resolution given this start and end time (in UTC). - /// - /// model class for passing in parameters for historical data - /// Enumerable of base data for this symbol - public IEnumerable Get(DataDownloaderGetParameters dataDownloaderGetParameters) - { - var symbol = dataDownloaderGetParameters.Symbol; - var resolution = dataDownloaderGetParameters.Resolution; - var startUtc = dataDownloaderGetParameters.StartUtc; - var endUtc = dataDownloaderGetParameters.EndUtc; - var tickType = dataDownloaderGetParameters.TickType; - - if (tickType != TickType.Trade) - { - return Enumerable.Empty(); - } - - // get symbol mapper for GDAX - var mapper = new SymbolPropertiesDatabaseSymbolMapper(Market.GDAX); - var brokerageTicker = mapper.GetBrokerageSymbol(symbol); - - var returnData = new List(); - var granularity = resolution.ToTimeSpan().TotalSeconds; - - DateTime windowStartTime = startUtc; - DateTime windowEndTime = startUtc; - - do - { - windowStartTime = windowEndTime; - windowEndTime = windowStartTime.AddSeconds(MaxDatapointsPerRequest * granularity); - windowEndTime = windowEndTime > endUtc ? endUtc : windowEndTime; - - Log.Trace($"Getting data for timeperiod from {windowStartTime.ToStringInvariant()} to {windowEndTime.ToStringInvariant()}.."); - - var requestURL = $"http://api.pro.coinbase.com/products/{brokerageTicker}/candles" + - $"?start={windowStartTime.ToStringInvariant()}" + - $"&end={windowEndTime.ToStringInvariant()}" + - $"&granularity={granularity.ToStringInvariant()}"; - - var request = (HttpWebRequest)WebRequest.Create(requestURL); - request.UserAgent = ".NET Framework Test Client"; - - string data = GetWithRetry(request); - returnData.AddRange(ParseCandleData(symbol, granularity, data)); - } - while (windowStartTime != windowEndTime); - - return returnData; - } - - /// - /// Get request with retry on failure - /// - /// Web request to get. - /// web response as string - string GetWithRetry(HttpWebRequest request) - { - string data = string.Empty; - int retryCount = 0; - while (data == string.Empty) - { - try - { - Thread.Sleep(1000 / MaxRequestsPerSecond + 1); - var response = (HttpWebResponse)request.GetResponse(); - var encoding = Encoding.ASCII; - - using (var reader = new StreamReader(response.GetResponseStream(), encoding)) - { - data = reader.ReadToEnd(); - } - } - catch (WebException ex) - { - ++retryCount; - if (retryCount > 3) - { - Log.Error("REQUEST FAILED: " + request.Address); - throw; - } - Log.Trace("WARNING: Web request failed with message " + ex.Message + "Retrying... " + retryCount + " times"); - } - } - return data; - } - - /// - /// Parse string response from web response - /// - /// Crypto security symbol. - /// Resolution in seconds. - /// Web response as string. - /// web response as string - List ParseCandleData(Symbol symbol, double granularity, string data) - { - List returnData = new List(); - if (data.Length > 0) - { - var parsedData = JsonConvert.DeserializeObject(data); - - foreach (var datapoint in parsedData) - { - var epochs = Parse.Double(datapoint[0]); - var tradeBar = new TradeBar() - { - Time = Time.UnixTimeStampToDateTime(epochs), - Symbol = symbol, - Low = Parse.Decimal(datapoint[1]), - High = Parse.Decimal(datapoint[2]), - Open = Parse.Decimal(datapoint[3]), - Close = Parse.Decimal(datapoint[4]), - Volume = Parse.Decimal(datapoint[5], System.Globalization.NumberStyles.Float), - Value = Parse.Decimal(datapoint[4]), - DataType = MarketDataType.TradeBar, - Period = new TimeSpan(0, 0, (int)granularity), - EndTime = Time.UnixTimeStampToDateTime(epochs).AddSeconds(granularity) - }; - returnData.Add(tradeBar); - } - } - return returnData.OrderBy(datapoint => datapoint.Time).ToList(); - } - } -} diff --git a/QuantConnect.GDAXBrokerage.ToolBox/GDAXExchangeInfoDownloader.cs b/QuantConnect.GDAXBrokerage.ToolBox/GDAXExchangeInfoDownloader.cs deleted file mode 100644 index 09f106e..0000000 --- a/QuantConnect.GDAXBrokerage.ToolBox/GDAXExchangeInfoDownloader.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using QuantConnect.Logging; -using QuantConnect.ToolBox.GDAXDownloader.Models; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace QuantConnect.ToolBox.GDAXDownloader -{ - /// - /// GDAX implementation of - /// - public class GDAXExchangeInfoDownloader : IExchangeInfoDownloader - { - private readonly Dictionary _idNameMapping = new(); - - /// - /// Market name - /// - public string Market => QuantConnect.Market.GDAX; - - /// - /// Creats an instance of the class - /// - public GDAXExchangeInfoDownloader() - { - _idNameMapping = GetCurrencyDetails(); - } - - /// - /// Pulling data from a remote source - /// - /// Enumerable of exchange info - public IEnumerable Get() - { - const string url = "https://api.exchange.coinbase.com/products"; - Dictionary headers = new() { { "User-Agent", ".NET Client" } }; - var json = url.DownloadData(headers); - var exchangeInfo = JsonConvert.DeserializeObject>(json); - foreach (var product in exchangeInfo.OrderBy(x => x.ID.Replace("-", string.Empty))) - { - // market,symbol,type,description,quote_currency,contract_multiplier,minimum_price_variation,lot_size,market_ticker,minimum_order_size - var symbol = product.ID.Replace("-", string.Empty); - var description = $"{_idNameMapping[product.BaseCurrency]}-{_idNameMapping[product.QuoteCurrency]}"; - var quoteCurrency = product.QuoteCurrency; - var contractMultiplier = 1; - var minimum_price_variation = product.QuoteIncrement; - var lot_size = product.BaseIncrement; - var marketTicker = product.ID; - var minimum_order_size = product.BaseMinSize; - yield return $"{Market},{symbol},crypto,{description},{quoteCurrency},{contractMultiplier},{minimum_price_variation},{lot_size},{marketTicker},{minimum_order_size}"; - } - } - - /// - /// Fetch currency details - /// - /// Enumerable of exchange info - private static Dictionary GetCurrencyDetails() - { - Dictionary idNameMapping = new(); - var url = $"https://api.exchange.coinbase.com/currencies"; - Dictionary headers = new() { { "User-Agent", ".NET Framework Test Client" } }; - var json = url.DownloadData(headers); - var jObject = JToken.Parse(json); - foreach (var currency in jObject) - { - try - { - var id = currency.SelectToken("id").ToString(); - idNameMapping[id] = currency.SelectToken("name").ToString(); - } - catch (Exception e) - { - Log.Trace($"GDAXExchangeInfoDownloader.GetCurrencyNameById(): {e}"); - } - } - return idNameMapping; - } - } -} diff --git a/QuantConnect.GDAXBrokerage.ToolBox/Models/Product.cs b/QuantConnect.GDAXBrokerage.ToolBox/Models/Product.cs deleted file mode 100644 index d5211a2..0000000 --- a/QuantConnect.GDAXBrokerage.ToolBox/Models/Product.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 Newtonsoft.Json; - -namespace QuantConnect.ToolBox.GDAXDownloader.Models -{ - /// - /// Represents GDAX exchange info for a product - /// - public class Product - { - [JsonProperty(PropertyName = "id")] - public string ID { get; set; } - - [JsonProperty(PropertyName = "base_currency")] - public string BaseCurrency { get; set; } - - [JsonProperty(PropertyName = "quote_currency")] - public string QuoteCurrency { get; set; } - - [JsonProperty(PropertyName = "base_min_size")] - public string BaseMinSize { get; set; } - - [JsonProperty(PropertyName = "base_max_size")] - public string BaseMaxSize { get; set; } - - [JsonProperty(PropertyName = "quote_increment")] - public string QuoteIncrement { get; set; } - - [JsonProperty(PropertyName = "base_increment")] - public string BaseIncrement { get; set; } - - [JsonProperty(PropertyName = "display_name")] - public string DisplayName { get; set; } - - [JsonProperty(PropertyName = "min_market_funds")] - public string MinMarketFunds { get; set; } - - [JsonProperty(PropertyName = "max_market_funds")] - public string MaxMarketFunds { get; set; } - - [JsonProperty(PropertyName = "margin_enabled")] - public bool MarginEnabled { get; set; } - - [JsonProperty(PropertyName = "post_only")] - public bool PostOnly { get; set; } - - [JsonProperty(PropertyName = "limit_only")] - public bool LimitOnly { get; set; } - - [JsonProperty(PropertyName = "cancel_only")] - public bool CancelOnly { get; set; } - - [JsonProperty(PropertyName = "status")] - public string Status { get; set; } - - [JsonProperty(PropertyName = "status_message")] - public string StatusMessage { get; set; } - - [JsonProperty(PropertyName = "auction_mode")] - public bool AuctionMode { get; set; } - } -} diff --git a/QuantConnect.GDAXBrokerage/AuthenticationToken.cs b/QuantConnect.GDAXBrokerage/AuthenticationToken.cs deleted file mode 100644 index a2efe80..0000000 --- a/QuantConnect.GDAXBrokerage/AuthenticationToken.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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. - */ -namespace QuantConnect.Brokerages.GDAX -{ - /// - /// Contains data used for authentication - /// - public class AuthenticationToken - { - /// - /// The key - /// - public string Key { get; set; } - /// - /// The hashed signature - /// - public string Signature { get; set; } - /// - /// The timestamp - /// - public string Timestamp { get; set; } - /// - /// The pass phrase - /// - public string Passphrase { get; set; } - } -} \ No newline at end of file diff --git a/QuantConnect.GDAXBrokerage/GDAXBrokerage.Messaging.cs b/QuantConnect.GDAXBrokerage/GDAXBrokerage.Messaging.cs deleted file mode 100644 index b0f3a2b..0000000 --- a/QuantConnect.GDAXBrokerage/GDAXBrokerage.Messaging.cs +++ /dev/null @@ -1,663 +0,0 @@ -/* - * 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 Newtonsoft.Json; -using QuantConnect.Configuration; -using QuantConnect.Data; -using QuantConnect.Data.Market; -using QuantConnect.Interfaces; -using QuantConnect.Logging; -using QuantConnect.Orders; -using QuantConnect.Orders.Fees; -using QuantConnect.Packets; -using QuantConnect.Securities; -using QuantConnect.Util; -using RestSharp; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace QuantConnect.Brokerages.GDAX -{ - public partial class GDAXBrokerage - { - #region Declarations - - /// - /// Collection of partial split messages - /// - public ConcurrentDictionary FillSplit { get; set; } - - private string _passPhrase; - private IAlgorithm _algorithm; - private readonly CancellationTokenSource _canceller = new CancellationTokenSource(); - private readonly ConcurrentDictionary _orderBooks = new ConcurrentDictionary(); - private readonly SymbolPropertiesDatabaseSymbolMapper _symbolMapper = new SymbolPropertiesDatabaseSymbolMapper(Market.GDAX); - private bool _isDataQueueHandler; - private LiveNodePacket _job; - private RateGate _websocketRateLimit = new(7, TimeSpan.FromSeconds(1)); - - /// - /// Data Aggregator - /// - protected IDataAggregator _aggregator; - - // GDAX has different rate limits for public and private endpoints - // https://docs.gdax.com/#rate-limits - internal enum GdaxEndpointType { Public, Private } - - private readonly RateGate _publicEndpointRateLimiter = new RateGate(6, TimeSpan.FromSeconds(1)); - private readonly RateGate _privateEndpointRateLimiter = new RateGate(10, TimeSpan.FromSeconds(1)); - - private IPriceProvider _priceProvider; - - private readonly CancellationTokenSource _ctsFillMonitor = new CancellationTokenSource(); - private Task _fillMonitorTask; - private readonly AutoResetEvent _fillMonitorResetEvent = new AutoResetEvent(false); - private readonly int _fillMonitorTimeout = Config.GetInt("gdax-fill-monitor-timeout", 500); - private readonly ConcurrentDictionary _pendingOrders = new ConcurrentDictionary(); - - #endregion Declarations - - /// - /// The list of websocket channels to subscribe - /// - protected virtual string[] ChannelNames { get; } = { "heartbeat" }; - - /// - /// Constructor for brokerage - /// - /// Name of brokerage - public GDAXBrokerage(string name) : base(name) - { - } - - /// - /// Constructor for brokerage - /// - /// websockets url - /// instance of websockets client - /// instance of rest client - /// api key - /// api secret - /// pass phrase - /// the algorithm instance is required to retreive account type - /// The price provider for missing FX conversion rates - /// consolidate ticks - /// The live job packet - public GDAXBrokerage(string wssUrl, IWebSocket websocket, IRestClient restClient, string apiKey, string apiSecret, string passPhrase, IAlgorithm algorithm, - IPriceProvider priceProvider, IDataAggregator aggregator, LiveNodePacket job) - : base("GDAX") - { - Initialize( - wssUrl: wssUrl, - websocket: websocket, - restClient: restClient, - apiKey: apiKey, - apiSecret: apiSecret, - passPhrase: passPhrase, - algorithm: algorithm, - priceProvider: priceProvider, - aggregator: aggregator, - job: job - ); - } - - /// - /// Wss message handler - /// - /// - /// - protected override void OnMessage(object sender, WebSocketMessage webSocketMessage) - { - var e = (WebSocketClientWrapper.TextMessage)webSocketMessage.Data; - - try - { - var raw = JsonConvert.DeserializeObject(e.Message, JsonSettings); - - if (raw.Type == "heartbeat") - { - return; - } - else if (raw.Type == "snapshot") - { - OnSnapshot(e.Message); - return; - } - else if (raw.Type == "l2update") - { - OnL2Update(e.Message); - return; - } - else if (raw.Type == "error") - { - Log.Error($"GDAXBrokerage.OnMessage.error(): Data: {Environment.NewLine}{e.Message}"); - - var error = JsonConvert.DeserializeObject(e.Message, JsonSettings); - var messageType = error.Message.Equals("Failed to subscribe", StringComparison.InvariantCultureIgnoreCase) || - error.Message.Equals("Authentication Failed", StringComparison.InvariantCultureIgnoreCase) - ? BrokerageMessageType.Error - : BrokerageMessageType.Warning; - var message = $"Message:{error.Message} - Reason:{error.Reason}"; - - OnMessage(new BrokerageMessageEvent(messageType, -1, $"GDAXBrokerage.OnMessage: {message}")); - } - else if (raw.Type == "match") - { - OnMatch(e.Message); - return; - } - else if (raw.Type == "open" || raw.Type == "change" || raw.Type == "done" || raw.Type == "received" || raw.Type == "subscriptions" || raw.Type == "last_match") - { - //known messages we don't need to handle or log - return; - } - - Log.Trace($"GDAXWebsocketsBrokerage.OnMessage: Unexpected message format: {e.Message}"); - } - catch (Exception exception) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, -1, $"Parsing wss message failed. Data: {e.Message} Exception: {exception}")); - throw; - } - } - - /// - /// Initialize the instance of this class - /// - /// The web socket base url - /// instance of websockets client - /// instance of rest client - /// api key - /// api secret - /// pass phrase - /// the algorithm instance is required to retrieve account type - /// The price provider for missing FX conversion rates - /// the aggregator for consolidating ticks - /// The live job packet - protected void Initialize(string wssUrl, IWebSocket websocket, IRestClient restClient, string apiKey, string apiSecret, - string passPhrase, IAlgorithm algorithm, IPriceProvider priceProvider, IDataAggregator aggregator, LiveNodePacket job) - { - if (IsInitialized) - { - return; - } - base.Initialize(wssUrl, websocket, restClient, apiKey, apiSecret); - _job = job; - FillSplit = new ConcurrentDictionary(); - _passPhrase = passPhrase; - _algorithm = algorithm; - _priceProvider = priceProvider; - _aggregator = aggregator; - - _isDataQueueHandler = this is GDAXDataQueueHandler; - - _fillMonitorTask = Task.Factory.StartNew(FillMonitorAction, _ctsFillMonitor.Token); - - var subscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager(); - subscriptionManager.SubscribeImpl += (s, t) => - { - Subscribe(s); - return true; - }; - subscriptionManager.UnsubscribeImpl += (s, t) => Unsubscribe(s); - - SubscriptionManager = subscriptionManager; - ValidateSubscription(); - } - - private void OnSnapshot(string data) - { - try - { - var message = JsonConvert.DeserializeObject(data); - - var symbol = _symbolMapper.GetLeanSymbol(message.ProductId, SecurityType.Crypto, Market.GDAX); - - DefaultOrderBook orderBook; - if (!_orderBooks.TryGetValue(symbol, out orderBook)) - { - orderBook = new DefaultOrderBook(symbol); - _orderBooks[symbol] = orderBook; - } - else - { - orderBook.BestBidAskUpdated -= OnBestBidAskUpdated; - orderBook.Clear(); - } - - foreach (var row in message.Bids) - { - var price = decimal.Parse(row[0], NumberStyles.Float, CultureInfo.InvariantCulture); - var size = decimal.Parse(row[1], NumberStyles.Float, CultureInfo.InvariantCulture); - orderBook.UpdateBidRow(price, size); - } - foreach (var row in message.Asks) - { - var price = decimal.Parse(row[0], NumberStyles.Float, CultureInfo.InvariantCulture); - var size = decimal.Parse(row[1], NumberStyles.Float, CultureInfo.InvariantCulture); - orderBook.UpdateAskRow(price, size); - } - - orderBook.BestBidAskUpdated += OnBestBidAskUpdated; - - if (_isDataQueueHandler) - { - EmitQuoteTick(symbol, orderBook.BestBidPrice, orderBook.BestBidSize, orderBook.BestAskPrice, orderBook.BestAskSize); - } - } - catch (Exception e) - { - Log.Error(e); - throw; - } - } - - private void OnBestBidAskUpdated(object sender, BestBidAskUpdatedEventArgs e) - { - if (_isDataQueueHandler) - { - EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize); - } - } - - private void OnL2Update(string data) - { - try - { - var message = JsonConvert.DeserializeObject(data); - - var symbol = _symbolMapper.GetLeanSymbol(message.ProductId, SecurityType.Crypto, Market.GDAX); - - var orderBook = _orderBooks[symbol]; - - foreach (var row in message.Changes) - { - var side = row[0]; - var price = Convert.ToDecimal(row[1], CultureInfo.InvariantCulture); - var size = decimal.Parse(row[2], NumberStyles.Float, CultureInfo.InvariantCulture); - if (side == "buy") - { - if (size == 0) - { - orderBook.RemoveBidRow(price); - } - else - { - orderBook.UpdateBidRow(price, size); - } - } - else if (side == "sell") - { - if (size == 0) - { - orderBook.RemoveAskRow(price); - } - else - { - orderBook.UpdateAskRow(price, size); - } - } - } - } - catch (Exception e) - { - Log.Error(e, "Data: " + data); - throw; - } - } - - private void OnMatch(string data) - { - // deserialize the current match (trade) message - var message = JsonConvert.DeserializeObject(data, JsonSettings); - - // message received from the "matches" channel - if (_isDataQueueHandler) - { - EmitTradeTick(message); - } - } - - private void EmitFillOrderEvent(Messages.Fill fill, Order order) - { - var symbol = _symbolMapper.GetLeanSymbol(fill.ProductId, SecurityType.Crypto, Market.GDAX); - - if (!FillSplit.ContainsKey(order.Id)) - { - FillSplit[order.Id] = new GDAXFill(order); - } - - var split = FillSplit[order.Id]; - split.Add(fill); - - // is this the total order at once? Is this the last split fill? - var isFinalFill = Math.Abs(fill.Size) == Math.Abs(order.Quantity) || Math.Abs(split.OrderQuantity) == Math.Abs(split.TotalQuantity); - - var status = isFinalFill ? OrderStatus.Filled : OrderStatus.PartiallyFilled; - - var direction = fill.Side == "sell" ? OrderDirection.Sell : OrderDirection.Buy; - - var fillPrice = fill.Price; - var fillQuantity = direction == OrderDirection.Sell ? -fill.Size : fill.Size; - - string currency; - if (order.PriceCurrency.IsNullOrEmpty()) - { - CurrencyPairUtil.DecomposeCurrencyPair(symbol, out string baseCurrency, out string quoteCurrency); - currency = quoteCurrency; - } - else - { - currency = order.PriceCurrency; - } - - var orderFee = new OrderFee(new CashAmount(fill.Fee, currency)); - - var orderEvent = new OrderEvent - ( - order.Id, symbol, fill.CreatedAt, status, - direction, fillPrice, fillQuantity, - orderFee, $"GDAX Fill Event {direction}" - ); - - // when the order is completely filled, we no longer need it in the active order list - if (orderEvent.Status == OrderStatus.Filled) - { - Order outOrder; - CachedOrderIDs.TryRemove(order.Id, out outOrder); - - PendingOrder removed; - _pendingOrders.TryRemove(fill.OrderId, out removed); - } - - OnOrderEvent(orderEvent); - } - - /// - /// Retrieves a price tick for a given symbol - /// - /// - /// - public Tick GetTick(Symbol symbol) - { - var brokerageSymbol = _symbolMapper.GetBrokerageSymbol(symbol); - - var req = new RestRequest($"/products/{brokerageSymbol}/ticker", Method.GET); - var response = ExecuteRestRequest(req, GdaxEndpointType.Public); - if (response.StatusCode != System.Net.HttpStatusCode.OK) - { - throw new Exception($"GDAXBrokerage.GetTick: request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}"); - } - - var tick = JsonConvert.DeserializeObject(response.Content); - return new Tick(tick.Time, symbol, tick.Bid, tick.Ask) { Quantity = tick.Volume }; - } - - /// - /// Emits a new quote tick - /// - /// The symbol - /// The bid price - /// The bid size - /// The ask price - /// The ask price - private void EmitQuoteTick(Symbol symbol, decimal bidPrice, decimal bidSize, decimal askPrice, decimal askSize) - { - _aggregator.Update(new Tick - { - AskPrice = askPrice, - BidPrice = bidPrice, - Value = (askPrice + bidPrice) / 2m, - Time = DateTime.UtcNow, - Symbol = symbol, - TickType = TickType.Quote, - AskSize = askSize, - BidSize = bidSize - }); - } - - /// - /// Emits a new trade tick from a match message - /// - private void EmitTradeTick(Messages.Matched message) - { - var symbol = _symbolMapper.GetLeanSymbol(message.ProductId, SecurityType.Crypto, Market.GDAX); - - _aggregator.Update(new Tick - { - Value = message.Price, - Time = DateTime.UtcNow, - Symbol = symbol, - TickType = TickType.Trade, - Quantity = message.Size - }); - } - - /// - /// Creates websocket message subscriptions for the supplied symbols - /// - protected override bool Subscribe(IEnumerable symbols) - { - var fullList = GetSubscribed().Union(symbols); - var pendingSymbols = new List(); - foreach (var item in fullList) - { - if (_symbolMapper.IsKnownLeanSymbol(item)) - { - pendingSymbols.Add(item); - } - else if (item.SecurityType == SecurityType.Crypto) - { - Log.Error($"Unknown GDAX symbol: {item.Value}"); - } - else - { - //todo: refactor this outside brokerage - //alternative service: http://openexchangerates.org/latest.json - PollTick(item); - } - } - - var products = pendingSymbols - .Select(s => _symbolMapper.GetBrokerageSymbol(s)) - .ToArray(); - - var payload = new - { - type = "subscribe", - product_ids = products, - channels = ChannelNames - }; - - if (payload.product_ids.Length == 0) - { - return true; - } - - var token = GetAuthenticationToken(string.Empty, "GET", "/users/self/verify"); - - var json = JsonConvert.SerializeObject(new - { - type = payload.type, - channels = payload.channels, - product_ids = payload.product_ids, - timestamp = token.Timestamp, - key = ApiKey, - passphrase = _passPhrase, - signature = token.Signature, - }); - - _websocketRateLimit.WaitToProceed(); - - WebSocket.Send(json); - - Log.Trace("GDAXBrokerage.Subscribe: Sent subscribe."); - return true; - } - - /// - /// Poll for new tick to refresh conversion rate of non-USD denomination - /// - /// - public void PollTick(Symbol symbol) - { - int delay = 36000; - var token = _canceller.Token; - var listener = Task.Factory.StartNew(() => - { - Log.Trace($"GDAXBrokerage.PollLatestTick: started polling for ticks: {symbol.Value}"); - - while (true) - { - var rate = GetConversionRate(symbol); - - var latest = new Tick - { - Value = rate, - Time = DateTime.UtcNow, - Symbol = symbol, - TickType = TickType.Quote - }; - _aggregator.Update(latest); - - int count = 0; - while (++count < delay) - { - if (token.IsCancellationRequested) break; - Thread.Sleep(1000); - } - - if (token.IsCancellationRequested) break; - } - - Log.Trace($"PollLatestTick: stopped polling for ticks: {symbol.Value}"); - }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } - - private decimal GetConversionRate(Symbol symbol) - { - try - { - return _priceProvider.GetLastPrice(symbol); - } - catch (Exception e) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, 0, $"GetConversionRate: {e.Message}")); - return 0; - } - } - - /// - /// Ends current subscriptions - /// - public bool Unsubscribe(IEnumerable symbols) - { - if (WebSocket.IsOpen) - { - var products = symbols - .Select(s => _symbolMapper.GetBrokerageSymbol(s)) - .ToArray(); - - var payload = new - { - type = "unsubscribe", - channels = ChannelNames, - product_ids = products - }; - - _websocketRateLimit.WaitToProceed(); - - WebSocket.Send(JsonConvert.SerializeObject(payload)); - } - return true; - } - - private void FillMonitorAction() - { - Log.Trace("GDAXBrokerage.FillMonitorAction(): task started"); - - try - { - foreach (var order in GetOpenOrders()) - { - _pendingOrders.TryAdd(order.BrokerId.First(), new PendingOrder(order)); - } - - while (!_ctsFillMonitor.IsCancellationRequested) - { - _fillMonitorResetEvent.WaitOne(TimeSpan.FromMilliseconds(_fillMonitorTimeout), _ctsFillMonitor.Token); - - foreach (var kvp in _pendingOrders) - { - var orderId = kvp.Key; - var pendingOrder = kvp.Value; - - var request = new RestRequest($"/fills?order_id={orderId}", Method.GET); - GetAuthenticationToken(request); - - var response = ExecuteRestRequest(request, GdaxEndpointType.Private, false); - - if (response.StatusCode != HttpStatusCode.OK) - { - OnMessage(new BrokerageMessageEvent( - BrokerageMessageType.Warning, - -1, - $"GDAXBrokerage.FillMonitorAction(): request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}")); - - continue; - } - - var fills = JsonConvert.DeserializeObject>(response.Content); - foreach (var fill in fills.OrderBy(x => x.TradeId)) - { - if (fill.TradeId <= pendingOrder.LastEmittedFillTradeId) - { - continue; - } - - EmitFillOrderEvent(fill, pendingOrder.Order); - - pendingOrder.LastEmittedFillTradeId = fill.TradeId; - } - } - } - } - catch (Exception exception) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, -1, exception.Message)); - } - - Log.Trace("GDAXBrokerage.FillMonitorAction(): task ended"); - } - - private class PendingOrder - { - public Order Order { get; } - public long LastEmittedFillTradeId { get; set; } - - public PendingOrder(Order order) - { - Order = order; - } - } - } -} diff --git a/QuantConnect.GDAXBrokerage/GDAXBrokerage.Utility.cs b/QuantConnect.GDAXBrokerage/GDAXBrokerage.Utility.cs deleted file mode 100644 index a713b6c..0000000 --- a/QuantConnect.GDAXBrokerage/GDAXBrokerage.Utility.cs +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 RestSharp; -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Text; - -namespace QuantConnect.Brokerages.GDAX -{ - /// - /// Utility methods for GDAX brokerage - /// - public partial class GDAXBrokerage - { - /// - /// Sign Header - /// - public const string SignHeader = "CB-ACCESS-SIGN"; - /// - /// Key Header - /// - public const string KeyHeader = "CB-ACCESS-KEY"; - /// - /// Timestamp Header - /// - public const string TimeHeader = "CB-ACCESS-TIMESTAMP"; - /// - /// Passphrase header - /// - public const string PassHeader = "CB-ACCESS-PASSPHRASE"; - private const string Open = "open"; - private const string Pending = "pending"; - private const string Active = "active"; - private const string Done = "done"; - private const string Settled = "settled"; - - /// - /// Creates an auth token and adds to the request - /// - /// the rest request - /// a token representing the request params - public AuthenticationToken GetAuthenticationToken(IRestRequest request) - { - var body = request.Parameters.SingleOrDefault(b => b.Type == ParameterType.RequestBody); - - string url; - if (request.Method == Method.GET && request.Parameters.Count > 0) - { - var parameters = request.Parameters.Count > 0 - ? string.Join("&", request.Parameters.Select(x => $"{x.Name}={x.Value}")) - : string.Empty; - url = $"{request.Resource}?{parameters}"; - } - else - { - url = request.Resource; - } - - - var token = GetAuthenticationToken(body?.Value.ToString() ?? string.Empty, request.Method.ToString().ToUpperInvariant(), url); - - request.AddHeader(SignHeader, token.Signature); - request.AddHeader(KeyHeader, ApiKey); - request.AddHeader(TimeHeader, token.Timestamp); - request.AddHeader(PassHeader, _passPhrase); - - return token; - } - - /// - /// Creates an auth token to sign a request - /// - /// the request body as json - /// the http method - /// the request url - /// - public AuthenticationToken GetAuthenticationToken(string body, string method, string url) - { - var token = new AuthenticationToken - { - Key = ApiKey, - Passphrase = _passPhrase, - //todo: query time server to correct for time skew - Timestamp = Time.DateTimeToUnixTimeStamp(DateTime.UtcNow).ToString(System.Globalization.CultureInfo.InvariantCulture) - }; - - byte[] data = Convert.FromBase64String(ApiSecret); - var prehash = token.Timestamp + method + url + body; - - byte[] bytes = Encoding.UTF8.GetBytes(prehash); - using (var hmac = new HMACSHA256(data)) - { - byte[] hash = hmac.ComputeHash(bytes); - token.Signature = Convert.ToBase64String(hash); - } - - return token; - } - - private static string ConvertOrderType(Orders.OrderType orderType) - { - if (orderType == Orders.OrderType.Limit || orderType == Orders.OrderType.Market) - { - return orderType.ToLower(); - } - else if (orderType == Orders.OrderType.StopMarket) - { - return "stop"; - } - else if (orderType == Orders.OrderType.StopLimit) - { - return "limit"; - } - - throw new NotSupportedException($"GDAXBrokerage.ConvertOrderType: Unsupported order type:{orderType.ToStringInvariant()}"); - } - - private static Orders.OrderStatus ConvertOrderStatus(Messages.Order order) - { - if (order.FilledSize != 0 && order.FilledSize != order.Size) - { - return Orders.OrderStatus.PartiallyFilled; - } - else if (order.Status == Open || order.Status == Pending || order.Status == Active) - { - return Orders.OrderStatus.Submitted; - } - else if (order.Status == Done || order.Status == Settled) - { - return Orders.OrderStatus.Filled; - } - - return Orders.OrderStatus.None; - } - - private IRestResponse ExecuteRestRequest(IRestRequest request, GdaxEndpointType endpointType, bool sendRateLimitMessage = true) - { - const int maxAttempts = 10; - var attempts = 0; - IRestResponse response; - - do - { - var rateLimiter = endpointType == GdaxEndpointType.Private ? _privateEndpointRateLimiter : _publicEndpointRateLimiter; - - if (!rateLimiter.WaitToProceed(TimeSpan.Zero)) - { - if (sendRateLimitMessage) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "RateLimit", - "The API request has been rate limited. To avoid this message, please reduce the frequency of API calls.")); - } - - rateLimiter.WaitToProceed(); - } - - response = RestClient.Execute(request); - // 429 status code: Too Many Requests - } while (++attempts < maxAttempts && (int) response.StatusCode == 429); - - return response; - } - } -} diff --git a/QuantConnect.GDAXBrokerage/GDAXBrokerage.cs b/QuantConnect.GDAXBrokerage/GDAXBrokerage.cs deleted file mode 100644 index dd19b7d..0000000 --- a/QuantConnect.GDAXBrokerage/GDAXBrokerage.cs +++ /dev/null @@ -1,646 +0,0 @@ -/* - * 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 Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using QuantConnect.Api; -using QuantConnect.Configuration; -using QuantConnect.Data; -using QuantConnect.Data.Market; -using QuantConnect.Logging; -using QuantConnect.Orders; -using QuantConnect.Orders.Fees; -using QuantConnect.Securities; -using QuantConnect.Util; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Security.Cryptography; -using System.Text; - -namespace QuantConnect.Brokerages.GDAX -{ - public partial class GDAXBrokerage : BaseWebsocketsBrokerage - { - private const int MaxDataPointsPerHistoricalRequest = 300; - - // These are the only currencies accepted for fiat deposits - private static readonly HashSet FiatCurrencies = new List - { - Currencies.EUR, - Currencies.GBP, - Currencies.USD - }.ToHashSet(); - - #region IBrokerage - /// - /// Checks if the websocket connection is connected or in the process of connecting - /// - public override bool IsConnected => WebSocket.IsOpen; - - /// - /// Creates a new order - /// - /// - /// - public override bool PlaceOrder(Order order) - { - var req = new RestRequest("/orders", Method.POST); - - dynamic payload = new ExpandoObject(); - - payload.size = Math.Abs(order.Quantity); - payload.side = order.Direction.ToLower(); - payload.type = ConvertOrderType(order.Type); - - if (order.Type != OrderType.Market) - { - payload.price = - (order as LimitOrder)?.LimitPrice ?? - (order as StopLimitOrder)?.LimitPrice ?? - (order as StopMarketOrder)?.StopPrice ?? 0; - } - - payload.product_id = _symbolMapper.GetBrokerageSymbol(order.Symbol); - - if (_algorithm.BrokerageModel.AccountType == AccountType.Margin) - { - payload.overdraft_enabled = true; - } - - var orderProperties = order.Properties as GDAXOrderProperties; - if (orderProperties != null) - { - if (order.Type == OrderType.Limit) - { - payload.post_only = orderProperties.PostOnly; - } - } - - if (order.Type == OrderType.StopLimit) - { - payload.stop = order.Direction == OrderDirection.Buy ? "entry" : "loss"; - payload.stop_price = (order as StopLimitOrder).StopPrice; - } - - var json = JsonConvert.SerializeObject(payload); - Log.Trace($"GDAXBrokerage.PlaceOrder(): {json}"); - req.AddJsonBody(json); - - GetAuthenticationToken(req); - var response = ExecuteRestRequest(req, GdaxEndpointType.Private); - var orderFee = OrderFee.Zero; - if (response.StatusCode == HttpStatusCode.OK && response.Content != null) - { - var raw = JsonConvert.DeserializeObject(response.Content); - - if (raw?.Id == null) - { - var errorMessage = $"Error parsing response from place order: {response.Content}"; - OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, orderFee, "GDAX Order Event") { Status = OrderStatus.Invalid, Message = errorMessage }); - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, (int)response.StatusCode, errorMessage)); - - return true; - } - - if (raw.Status == "rejected") - { - var errorMessage = "Reject reason: " + raw.RejectReason; - OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, orderFee, "GDAX Order Event") { Status = OrderStatus.Invalid, Message = errorMessage }); - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, (int)response.StatusCode, errorMessage)); - - return true; - } - - var brokerId = raw.Id; - if (CachedOrderIDs.ContainsKey(order.Id)) - { - CachedOrderIDs[order.Id].BrokerId.Add(brokerId); - } - else - { - order.BrokerId.Add(brokerId); - CachedOrderIDs.TryAdd(order.Id, order); - } - - // Add fill splits in all cases; we'll need to handle market fills too. - FillSplit.TryAdd(order.Id, new GDAXFill(order)); - - // Generate submitted event - OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, orderFee, "GDAX Order Event") { Status = OrderStatus.Submitted }); - Log.Trace($"Order submitted successfully - OrderId: {order.Id}"); - - _pendingOrders.TryAdd(brokerId, new PendingOrder(order)); - _fillMonitorResetEvent.Set(); - - return true; - } - - var message = $"Order failed, Order Id: {order.Id} timestamp: {order.Time} quantity: {order.Quantity} content: {response.Content}"; - OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, orderFee, "GDAX Order Event") { Status = OrderStatus.Invalid }); - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, -1, message)); - - return true; - } - - /// - /// This operation is not supported - /// - /// - /// - public override bool UpdateOrder(Order order) - { - throw new NotSupportedException("GDAXBrokerage.UpdateOrder: Order update not supported. Please cancel and re-create."); - } - - /// - /// Cancels an order - /// - /// - /// - public override bool CancelOrder(Order order) - { - var success = new List(); - - foreach (var id in order.BrokerId) - { - var req = new RestRequest("/orders/" + id, Method.DELETE); - GetAuthenticationToken(req); - var response = ExecuteRestRequest(req, GdaxEndpointType.Private); - success.Add(response.StatusCode == HttpStatusCode.OK); - if (response.StatusCode == HttpStatusCode.OK) - { - OnOrderEvent(new OrderEvent(order, - DateTime.UtcNow, - OrderFee.Zero, - "GDAX Order Event") { Status = OrderStatus.Canceled }); - - PendingOrder orderRemoved; - _pendingOrders.TryRemove(id, out orderRemoved); - } - } - - return success.All(a => a); - } - - /// - /// Connects the client to the broker's remote servers - /// - public override void Connect() - { - base.Connect(); - - AccountBaseCurrency = GetAccountBaseCurrency(); - } - - /// - /// Closes the websockets connection - /// - public override void Disconnect() - { - if (!_canceller.IsCancellationRequested) - { - _canceller.Cancel(); - } - WebSocket.Close(); - } - - /// - /// Gets all orders not yet closed - /// - /// - public override List GetOpenOrders() - { - var list = new List(); - - var req = new RestRequest("/orders?status=open&status=pending&status=active", Method.GET); - GetAuthenticationToken(req); - var response = ExecuteRestRequest(req, GdaxEndpointType.Private); - - if (response.StatusCode != HttpStatusCode.OK) - { - throw new Exception($"GDAXBrokerage.GetOpenOrders: request failed: [{(int) response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}"); - } - - var orders = JsonConvert.DeserializeObject(response.Content); - foreach (var item in orders) - { - Order order; - - var quantity = item.Side == "sell" ? -item.Size : item.Size; - var symbol = _symbolMapper.GetLeanSymbol(item.ProductId, SecurityType.Crypto, Market.GDAX); - var time = DateTime.UtcNow; - - if (item.Type == "market") - { - order = new MarketOrder(symbol, quantity, time, item.Price); - } - else if (!string.IsNullOrEmpty(item.Stop)) - { - order = new StopLimitOrder(symbol, quantity, item.StopPrice, item.Price, time); - } - else if (item.Type == "limit") - { - order = new LimitOrder(symbol, quantity, item.Price, time); - } - else if (item.Type == "stop") - { - order = new StopMarketOrder(symbol, quantity, item.Price, time); - } - else - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, (int)response.StatusCode, - "GDAXBrokerage.GetOpenOrders: Unsupported order type returned from brokerage: " + item.Type)); - continue; - } - - order.Status = ConvertOrderStatus(item); - order.BrokerId.Add(item.Id); - list.Add(order); - } - - foreach (var item in list) - { - if (item.Status.IsOpen()) - { - var cached = CachedOrderIDs.Where(c => c.Value.BrokerId.Contains(item.BrokerId.First())); - if (cached.Any()) - { - CachedOrderIDs[cached.First().Key] = item; - } - } - } - - return list; - } - - /// - /// Gets all open positions - /// - /// - public override List GetAccountHoldings() - { - /* - * On launching the algorithm the cash balances are pulled and stored in the cashbook. - * Try loading pre-existing currency swaps from the job packet if provided - */ - return base.GetAccountHoldings(_job?.BrokerageData, _algorithm?.Securities.Values); - } - - /// - /// Gets the total account cash balance - /// - /// - public override List GetCashBalance() - { - var list = new List(); - - var request = new RestRequest("/accounts", Method.GET); - GetAuthenticationToken(request); - var response = ExecuteRestRequest(request, GdaxEndpointType.Private); - - if (response.StatusCode != HttpStatusCode.OK) - { - throw new Exception($"GDAXBrokerage.GetCashBalance: request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}"); - } - - foreach (var item in JsonConvert.DeserializeObject(response.Content)) - { - if (item.Balance > 0) - { - list.Add(new CashAmount(item.Balance, item.Currency.ToUpperInvariant())); - } - } - - return list; - } - - /// - /// Gets the history for the requested security - /// - /// The historical data request - /// An enumerable of bars covering the span specified in the request - public override IEnumerable GetHistory(HistoryRequest request) - { - // GDAX API only allows us to support history requests for TickType.Trade - if (request.TickType != TickType.Trade) - { - yield break; - } - - if (!_symbolMapper.IsKnownLeanSymbol(request.Symbol)) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "InvalidSymbol", - $"Unknown symbol: {request.Symbol.Value}, no history returned")); - yield break; - } - - if (request.EndTimeUtc < request.StartTimeUtc) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "InvalidDateRange", - "The history request start date must precede the end date, no history returned")); - yield break; - } - - if (request.Resolution == Resolution.Tick || request.Resolution == Resolution.Second) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "InvalidResolution", - $"{request.Resolution} resolution not supported, no history returned")); - yield break; - } - - Log.Trace($"GDAXBrokerage.GetHistory(): Submitting request: {request.Symbol.Value}: {request.Resolution} {request.StartTimeUtc} UTC -> {request.EndTimeUtc} UTC"); - - foreach (var tradeBar in GetHistoryFromCandles(request)) - { - yield return tradeBar; - } - } - - /// - /// Returns TradeBars from GDAX candles (only for Minute/Hour/Daily resolutions) - /// - /// The history request instance - private IEnumerable GetHistoryFromCandles(HistoryRequest request) - { - var productId = _symbolMapper.GetBrokerageSymbol(request.Symbol); - var granularity = Convert.ToInt32(request.Resolution.ToTimeSpan().TotalSeconds); - - var startTime = request.StartTimeUtc; - var endTime = request.EndTimeUtc; - var maximumRange = TimeSpan.FromSeconds(MaxDataPointsPerHistoricalRequest * granularity); - - do - { - var maximumEndTime = startTime.Add(maximumRange); - if (endTime > maximumEndTime) - { - endTime = maximumEndTime; - } - - var restRequest = new RestRequest($"/products/{productId}/candles?start={startTime:o}&end={endTime:o}&granularity={granularity}", Method.GET); - var response = ExecuteRestRequest(restRequest, GdaxEndpointType.Public); - - if (response.StatusCode != HttpStatusCode.OK) - { - OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "HistoryError", - $"History request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}")); - yield break; - } - - var bars = ParseCandleData(request.Symbol, granularity, response.Content, startTime); - - TradeBar lastPointReceived = null; - foreach (var datapoint in bars.OrderBy(x => x.Time)) - { - lastPointReceived = datapoint; - yield return datapoint; - } - - startTime = lastPointReceived?.EndTime ?? request.EndTimeUtc; - endTime = request.EndTimeUtc; - } while (startTime < request.EndTimeUtc); - } - - /// - /// Parse TradeBars from JSON response - /// https://docs.pro.coinbase.com/#get-historic-rates - /// - private static IEnumerable ParseCandleData(Symbol symbol, int granularity, string data, DateTime startTimeUtc) - { - if (data.Length == 0) - { - yield break; - } - - var parsedData = JsonConvert.DeserializeObject(data); - var period = TimeSpan.FromSeconds(granularity); - - foreach (var datapoint in parsedData) - { - var time = Time.UnixTimeStampToDateTime(double.Parse(datapoint[0], CultureInfo.InvariantCulture)); - - if (time < startTimeUtc) - { - // Note from GDAX docs: - // If data points are readily available, your response may contain as many as 300 candles - // and some of those candles may precede your declared start value. - yield break; - } - - var close = datapoint[4].ToDecimal(); - - yield return new TradeBar - { - Symbol = symbol, - Time = time, - Period = period, - Open = datapoint[3].ToDecimal(), - High = datapoint[2].ToDecimal(), - Low = datapoint[1].ToDecimal(), - Close = close, - Value = close, - Volume = decimal.Parse(datapoint[5], NumberStyles.Float, CultureInfo.InvariantCulture) - }; - } - } - - #endregion - - /// - /// Gets the account base currency - /// - private string GetAccountBaseCurrency() - { - var req = new RestRequest("/accounts", Method.GET); - GetAuthenticationToken(req); - var response = ExecuteRestRequest(req, GdaxEndpointType.Private); - - if (response.StatusCode != HttpStatusCode.OK) - { - throw new Exception($"GDAXBrokerage.GetAccountBaseCurrency(): request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}"); - } - - var result = JsonConvert.DeserializeObject(response.Content) - .Where(account => FiatCurrencies.Contains(account.Currency)) - // we choose the first fiat currency which has the largest available quantity - .OrderByDescending(account => account.Available).ThenBy(account => account.Currency) - .FirstOrDefault()?.Currency; - - return result ?? Currencies.USD; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public override void Dispose() - { - _ctsFillMonitor.Cancel(); - _fillMonitorTask.Wait(TimeSpan.FromSeconds(5)); - - _canceller.DisposeSafely(); - _aggregator.DisposeSafely(); - - _publicEndpointRateLimiter.Dispose(); - _privateEndpointRateLimiter.Dispose(); - - _websocketRateLimit.DisposeSafely(); - } - - private class ModulesReadLicenseRead : Api.RestResponse - { - [JsonProperty(PropertyName = "license")] - public string License; - [JsonProperty(PropertyName = "organizationId")] - public string OrganizationId; - } - - /// - /// Validate the user of this project has permission to be using it via our web API. - /// - private static void ValidateSubscription() - { - try - { - var productId = 183; - var userId = Config.GetInt("job-user-id"); - var token = Config.Get("api-access-token"); - var organizationId = Config.Get("job-organization-id", null); - // Verify we can authenticate with this user and token - var api = new ApiConnection(userId, token); - if (!api.Connected) - { - throw new ArgumentException("Invalid api user id or token, cannot authenticate subscription."); - } - // Compile the information we want to send when validating - var information = new Dictionary() - { - {"productId", productId}, - {"machineName", System.Environment.MachineName}, - {"userName", System.Environment.UserName}, - {"domainName", System.Environment.UserDomainName}, - {"os", System.Environment.OSVersion} - }; - // IP and Mac Address Information - try - { - var interfaceDictionary = new List>(); - foreach (var nic in NetworkInterface.GetAllNetworkInterfaces().Where(nic => nic.OperationalStatus == OperationalStatus.Up)) - { - var interfaceInformation = new Dictionary(); - // Get UnicastAddresses - var addresses = nic.GetIPProperties().UnicastAddresses - .Select(uniAddress => uniAddress.Address) - .Where(address => !IPAddress.IsLoopback(address)).Select(x => x.ToString()); - // If this interface has non-loopback addresses, we will include it - if (!addresses.IsNullOrEmpty()) - { - interfaceInformation.Add("unicastAddresses", addresses); - // Get MAC address - interfaceInformation.Add("MAC", nic.GetPhysicalAddress().ToString()); - // Add Interface name - interfaceInformation.Add("name", nic.Name); - // Add these to our dictionary - interfaceDictionary.Add(interfaceInformation); - } - } - information.Add("networkInterfaces", interfaceDictionary); - } - catch (Exception) - { - // NOP, not necessary to crash if fails to extract and add this information - } - // Include our OrganizationId is specified - if (!string.IsNullOrEmpty(organizationId)) - { - information.Add("organizationId", organizationId); - } - var request = new RestRequest("modules/license/read", Method.POST) { RequestFormat = DataFormat.Json }; - request.AddParameter("application/json", JsonConvert.SerializeObject(information), ParameterType.RequestBody); - api.TryRequest(request, out ModulesReadLicenseRead result); - if (!result.Success) - { - throw new InvalidOperationException($"Request for subscriptions from web failed, Response Errors : {string.Join(',', result.Errors)}"); - } - - var encryptedData = result.License; - // Decrypt the data we received - DateTime? expirationDate = null; - long? stamp = null; - bool? isValid = null; - if (encryptedData != null) - { - // Fetch the org id from the response if we are null, we need it to generate our validation key - if (string.IsNullOrEmpty(organizationId)) - { - organizationId = result.OrganizationId; - } - // Create our combination key - var password = $"{token}-{organizationId}"; - var key = SHA256.HashData(Encoding.UTF8.GetBytes(password)); - // Split the data - var info = encryptedData.Split("::"); - var buffer = Convert.FromBase64String(info[0]); - var iv = Convert.FromBase64String(info[1]); - // Decrypt our information - using var aes = new AesManaged(); - var decryptor = aes.CreateDecryptor(key, iv); - using var memoryStream = new MemoryStream(buffer); - using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read); - using var streamReader = new StreamReader(cryptoStream); - var decryptedData = streamReader.ReadToEnd(); - if (!decryptedData.IsNullOrEmpty()) - { - var jsonInfo = JsonConvert.DeserializeObject(decryptedData); - expirationDate = jsonInfo["expiration"]?.Value(); - isValid = jsonInfo["isValid"]?.Value(); - stamp = jsonInfo["stamped"]?.Value(); - } - } - // Validate our conditions - if (!expirationDate.HasValue || !isValid.HasValue || !stamp.HasValue) - { - throw new InvalidOperationException("Failed to validate subscription."); - } - - var nowUtc = DateTime.UtcNow; - var timeSpan = nowUtc - Time.UnixTimeStampToDateTime(stamp.Value); - if (timeSpan > TimeSpan.FromHours(12)) - { - throw new InvalidOperationException("Invalid API response."); - } - if (!isValid.Value) - { - throw new ArgumentException($"Your subscription is not valid, please check your product subscriptions on our website."); - } - if (expirationDate < nowUtc) - { - throw new ArgumentException($"Your subscription expired {expirationDate}, please renew in order to use this product."); - } - } - catch (Exception e) - { - Log.Error($"ValidateSubscription(): Failed during validation, shutting down. Error : {e.Message}"); - System.Environment.Exit(1); - } - } - } -} diff --git a/QuantConnect.GDAXBrokerage/GDAXBrokerageFactory.cs b/QuantConnect.GDAXBrokerage/GDAXBrokerageFactory.cs deleted file mode 100644 index 0a32917..0000000 --- a/QuantConnect.GDAXBrokerage/GDAXBrokerageFactory.cs +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 System.Collections.Generic; -using QuantConnect.Configuration; -using QuantConnect.Data; -using QuantConnect.Interfaces; -using QuantConnect.Securities; -using QuantConnect.Util; -using RestSharp; - -namespace QuantConnect.Brokerages.GDAX -{ - /// - /// Factory method to create GDAX Websockets brokerage - /// - public class GDAXBrokerageFactory : BrokerageFactory - { - /// - /// Factory constructor - /// - public GDAXBrokerageFactory() : base(typeof(GDAXBrokerage)) - { - } - - /// - /// Not required - /// - public override void Dispose() - { - } - - /// - /// provides brokerage connection data - /// - public override Dictionary BrokerageData => new Dictionary - { - // Sandbox environment for paper trading available using 'wss://ws-feed-public.sandbox.pro.coinbase.com' - { "gdax-url" , Config.Get("gdax-url", "wss://ws-feed.pro.coinbase.com")}, - // Sandbox environment for paper trading available using 'https://api-public.sandbox.pro.coinbase.com' - { "gdax-rest-api", Config.Get("gdax-rest-api", "https://api.pro.coinbase.com")}, - { "gdax-api-secret", Config.Get("gdax-api-secret")}, - { "gdax-api-key", Config.Get("gdax-api-key")}, - { "gdax-passphrase", Config.Get("gdax-passphrase")}, - - // load holdings if available - { "live-holdings", Config.Get("live-holdings")}, - }; - - /// - /// The brokerage model - /// - /// The order provider - public override IBrokerageModel GetBrokerageModel(IOrderProvider orderProvider) => new GDAXBrokerageModel(); - - /// - /// Create the Brokerage instance - /// - /// - /// - /// - public override IBrokerage CreateBrokerage(Packets.LiveNodePacket job, IAlgorithm algorithm) - { - var required = new[] { "gdax-url", "gdax-api-secret", "gdax-api-key", "gdax-passphrase" }; - - foreach (var item in required) - { - if (string.IsNullOrEmpty(job.BrokerageData[item])) - throw new Exception($"GDAXBrokerageFactory.CreateBrokerage: Missing {item} in config.json"); - } - - var restApi = BrokerageData["gdax-rest-api"]; - if (job.BrokerageData.ContainsKey("gdax-rest-api")) - { - restApi = job.BrokerageData["gdax-rest-api"]; - } - - var restClient = new RestClient(restApi); - var webSocketClient = new WebSocketClientWrapper(); - var priceProvider = new ApiPriceProvider(job.UserId, job.UserToken); - var aggregator = Composer.Instance.GetExportedValueByTypeName(Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"), forceTypeNameOnExisting: false); - - IBrokerage brokerage; - if (job.DataQueueHandler.Contains("GDAXDataQueueHandler")) - { - var dataQueueHandler = new GDAXDataQueueHandler(job.BrokerageData["gdax-url"], webSocketClient, - restClient, job.BrokerageData["gdax-api-key"], job.BrokerageData["gdax-api-secret"], - job.BrokerageData["gdax-passphrase"], algorithm, priceProvider, aggregator, job); - - Composer.Instance.AddPart(dataQueueHandler); - - brokerage = dataQueueHandler; - } - else - { - brokerage = new GDAXBrokerage(job.BrokerageData["gdax-url"], webSocketClient, - restClient, job.BrokerageData["gdax-api-key"], job.BrokerageData["gdax-api-secret"], - job.BrokerageData["gdax-passphrase"], algorithm, priceProvider, aggregator, job); - } - - return brokerage; - } - } -} diff --git a/QuantConnect.GDAXBrokerage/GDAXFill.cs b/QuantConnect.GDAXBrokerage/GDAXFill.cs deleted file mode 100644 index ef2346f..0000000 --- a/QuantConnect.GDAXBrokerage/GDAXFill.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 QuantConnect.Brokerages.GDAX.Messages; -using System.Collections.Generic; -using System.Linq; - -namespace QuantConnect.Brokerages.GDAX -{ - /// - /// Tracks fill messages - /// - public class GDAXFill - { - private readonly List _messages = new List(); - - /// - /// The Lean order - /// - public Orders.Order Order { get; } - - /// - /// Lean orderId - /// - public int OrderId => Order.Id; - - /// - /// Total amount executed across all fills - /// - /// - public decimal TotalQuantity => _messages.Sum(m => m.Size); - - /// - /// Original order quantity - /// - public decimal OrderQuantity => Order.Quantity; - - /// - /// Creates instance of GDAXFill - /// - /// - public GDAXFill(Orders.Order order) - { - Order = order; - } - - /// - /// Adds a trade message - /// - /// - public void Add(Fill msg) - { - _messages.Add(msg); - } - } -} diff --git a/QuantConnect.GDAXBrokerage/Messages.cs b/QuantConnect.GDAXBrokerage/Messages.cs deleted file mode 100644 index 5e3d70a..0000000 --- a/QuantConnect.GDAXBrokerage/Messages.cs +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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 Newtonsoft.Json; -using System; -using System.Collections.Generic; - -namespace QuantConnect.Brokerages.GDAX.Messages -{ - - //several simple objects to facilitate json conversion -#pragma warning disable 1591 - - public class BaseMessage - { - public string Type { get; set; } - public long Sequence { get; set; } - public DateTime Time { get; set; } - [JsonProperty("product_id")] - public string ProductId { get; set; } - } - - public class Done : BaseMessage - { - public decimal Price { get; set; } - [JsonProperty("order_id")] - public string OrderId { get; set; } - public string Reason { get; set; } - public string Side { get; set; } - public decimal RemainingSize { get; set; } - } - - public class Matched : BaseMessage - { - [JsonProperty("trade_id")] - public int TradeId { get; set; } - [JsonProperty("maker_order_id")] - public string MakerOrderId { get; set; } - [JsonProperty("taker_order_id")] - public string TakerOrderId { get; set; } - public decimal Size { get; set; } - public decimal Price { get; set; } - public string Side { get; set; } - [JsonProperty("taker_user_id")] - public string TakerUserId { get; set; } - [JsonProperty("user_id")] - public string UserId { get; set; } - [JsonProperty("taker_profile_id")] - public string TakerProfileId { get; set; } - [JsonProperty("profile_id")] - public string ProfileId { get; set; } - } - - public class Heartbeat : BaseMessage - { - [JsonProperty("last_trade_id")] - public int LastTradeId { get; set; } - } - - public class Error : BaseMessage - { - public string Message { get; set; } - public string Reason { get; set; } - } - - public class Subscribe - { - public string Type { get; set; } - [JsonProperty("product_ids")] - public IList ProductIds { get; set; } - public string Signature { get; set; } - public string Key { get; set; } - public string Passphrase { get; set; } - public string Timestamp { get; set; } - } - - public class Open : BaseMessage - { - [JsonProperty("order_id")] - public string OrderId { get; set; } - public decimal Price { get; set; } - [JsonProperty("remaining_size")] - public decimal RemainingSize { get; set; } - public string Side { get; set; } - } - - public class Change : Open - { - [JsonProperty("new_funds")] - public decimal NewFunds { get; set; } - [JsonProperty("old_funds")] - public decimal OldFunds { get; set; } - } - - public class Order - { - public string Id { get; set; } - public decimal Price { get; set; } - public decimal Size { get; set; } - [JsonProperty("product_id")] - public string ProductId { get; set; } - public string Side { get; set; } - public string Stp { get; set; } - public string Type { get; set; } - [JsonProperty("time_in_force")] - public string TimeInForce { get; set; } - [JsonProperty("post_only")] - public bool PostOnly { get; set; } - [JsonProperty("reject_reason")] - public string RejectReason { get; set; } - [JsonProperty("fill_fees")] - public decimal FillFees { get; set; } - [JsonProperty("filled_size")] - public decimal FilledSize { get; set; } - [JsonProperty("executed_value")] - public decimal ExecutedValue { get; set; } - public string Status { get; set; } - public bool Settled { get; set; } - public string Stop { get; set; } - [JsonProperty("stop_price")] - public decimal StopPrice { get; set; } - } - - public class Fill - { - [JsonProperty("created_at")] - public DateTime CreatedAt { get; set; } - - [JsonProperty("trade_id")] - public long TradeId { get; set; } - - [JsonProperty("product_id")] - public string ProductId { get; set; } - - [JsonProperty("order_id")] - public string OrderId { get; set; } - - [JsonProperty("user_id")] - public string UserId { get; set; } - - [JsonProperty("profile_id")] - public string ProfileId { get; set; } - - [JsonProperty("liquidity")] - public string Liquidity { get; set; } - - [JsonProperty("price")] - public decimal Price { get; set; } - - [JsonProperty("size")] - public decimal Size { get; set; } - - [JsonProperty("fee")] - public decimal Fee { get; set; } - - [JsonProperty("side")] - public string Side { get; set; } - - [JsonProperty("settled")] - public bool Settled { get; set; } - - [JsonProperty("usd_volume")] - public decimal UsdVolume { get; set; } - } - - public class Account - { - public string Id { get; set; } - public string Currency { get; set; } - public decimal Balance { get; set; } - public decimal Hold { get; set; } - public decimal Available { get; set; } - [JsonProperty("profile_id")] - public string ProfileId { get; set; } - } - - public class Tick - { - [JsonProperty("product_id")] - public string ProductId { get; set; } - [JsonProperty("trade_id")] - public string TradeId { get; set; } - public decimal Price { get; set; } - public decimal Size { get; set; } - public decimal Bid { get; set; } - public decimal Ask { get; set; } - public decimal Volume { get; set; } - public DateTime Time { get; set; } - } - - public class Ticker : BaseMessage - { - [JsonProperty("trade_id")] - public string TradeId { get; set; } - [JsonProperty("last_size")] - public decimal LastSize { get; set; } - [JsonProperty("best_bid")] - public decimal BestBid { get; set; } - [JsonProperty("best_ask")] - public decimal BestAsk { get; set; } - public decimal Price { get; set; } - public string Side { get; set; } - } - - public class Snapshot : BaseMessage - { - public List Bids { get; set; } - public List Asks { get; set; } - } - - public class L2Update : BaseMessage - { - public List Changes { get; set; } - } - -#pragma warning restore 1591 - -} diff --git a/README.md b/README.md index 038ec6e..9c64839 100644 --- a/README.md +++ b/README.md @@ -82,14 +82,13 @@ Follow these steps to start local live trading with the Coinbase Pro brokerage: Use sandbox? (live, paper): live ``` -5. Enter your API key, API secret, and passphrase. +5. Enter your API key, API secret. ``` $ lean live "My Project" API key: 6d3ef5ca2d2fa52e4ee55624b0471261 API secret: **************************************************************************************** - Passphrase: **************** ``` To create new API credentials, see the [API settings page](https://pro.coinbase.com/profile/api) on the Coinbase Pro website. @@ -136,7 +135,6 @@ Coinbase Pro supports trading crypto and the following order types: - Market Order - Limit Order -- Stop Market Order - Stop-Limit Order @@ -153,13 +151,13 @@ Lean models the brokerage behavior for backtesting purposes. The margin model is You can set the Brokerage Model with the following statements - SetBrokerageModel(BrokerageName.GDAX, AccountType.Cash); + SetBrokerageModel(BrokerageName.Coinbase, AccountType.Cash); [Read Documentation](https://www.quantconnect.com/docs/v2/our-platform/live-trading/brokerages/coinbase-pro) ### Fees -We model the order fees of Coinbase Pro at the $50K-100K pricing tier for all Crypto pairs, which is a 0.5% maker and taker fee for most pairs. The following table shows the Coinbase Pro Stable Pairs, which charge a 0% maker fee and a 0.1% taker fee: +We model the order fees of Coinbase Pro at the $50K-100K pricing tier for all Crypto pairs, which is a 0.60% maker and taker 0.80% fee for most pairs. The following table shows the Coinbase Pro Stable Pairs, which charge a 0% maker fee and a 0.001% taker fee: ||||| |:----:|:----:|:----:|:----:| |DAIUSDC|DAIUSD|GYENUSD|PAXUSD| @@ -174,7 +172,8 @@ We model the adjustments Coinbase Pro has made to their fees over time. The foll |----:|----:|----:| |Time < 3/23/2019 1:30AM|0|0.3| |3/23/2019 1:30AM <= Time < 10/8/2019 12:30AM|0.15|0.25| -|10/8/2019 12:30AM <= Time|0.5|0.5| +|10/8/2019 12:30AM <= Time < 12/21/2023 1:00 AM|0.5|0.5| +|12/21/2023 1:00 AM <= Time|0.6|0.8| To check the latest fees at all the fee levels, see the [What are the fees on Coinbase Pro?](https://help.coinbase.com/en/pro/trading-and-funding/trading-rules-and-fees/fees) page on the Coinbase Pro website.