Lean  $LEAN_TAG$
CoinbaseBrokerageModel.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014-2023 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Linq;
18 using QuantConnect.Orders;
22 using System.Collections.Generic;
23 using QuantConnect.Util;
24 
26 {
27  /// <summary>
28  /// Represents a brokerage model for interacting with the Coinbase exchange.
29  /// This class extends the default brokerage model.
30  /// </summary>
32  {
33  /// <summary>
34  /// Marks the end of stop market order support on Coinbase Pro.
35  /// For backtesting purposes, this field '_stopMarketOrderSupportEndDate' specifies the date when the
36  /// market structure update was applied, affecting the handling of historical data or simulations
37  /// involving stop market orders. Details: https://blog.coinbase.com/coinbase-pro-market-structure-update-fbd9d49f43d7
38  /// </summary>
39  private readonly DateTime _stopMarketOrderSupportEndDate = new DateTime(2019, 3, 23, 1, 0, 0);
40 
41  /// <summary>
42  /// Notifies users that order updates are not supported by the current brokerage model.
43  /// </summary>
44  private readonly BrokerageMessageEvent _message = new(BrokerageMessageType.Warning, 0, Messages.DefaultBrokerageModel.OrderUpdateNotSupported);
45 
46  /// <summary>
47  /// Represents a set of order types supported by the current brokerage model.
48  /// </summary>
49  private readonly HashSet<OrderType> _supportedOrderTypes = new()
50  {
51  OrderType.Limit,
52  OrderType.Market,
53  OrderType.StopLimit,
54  OrderType.StopMarket
55  };
56 
57  /// <summary>
58  /// Gets a map of the default markets to be used for each security type
59  /// </summary>
60  public override IReadOnlyDictionary<SecurityType, string> DefaultMarkets => GetDefaultMarkets(Market.Coinbase);
61 
62  /// <summary>
63  /// Initializes a new instance of the <see cref="CoinbaseBrokerageModel"/> class
64  /// </summary>
65  /// <param name="accountType">The type of account to be modelled, defaults to <see cref="AccountType.Cash"/></param>
66  public CoinbaseBrokerageModel(AccountType accountType = AccountType.Cash)
67  : base(accountType)
68  {
69  if (accountType == AccountType.Margin)
70  {
71  throw new ArgumentException(Messages.CoinbaseBrokerageModel.UnsupportedAccountType, nameof(accountType));
72  }
73  }
74 
75  /// <summary>
76  /// Coinbase global leverage rule
77  /// </summary>
78  /// <param name="security"></param>
79  /// <returns></returns>
80  public override decimal GetLeverage(Security security)
81  {
82  // margin trading is not currently supported by Coinbase
83  return 1m;
84  }
85 
86  /// <summary>
87  /// Get the benchmark for this model
88  /// </summary>
89  /// <param name="securities">SecurityService to create the security with if needed</param>
90  /// <returns>The benchmark for this brokerage</returns>
91  public override IBenchmark GetBenchmark(SecurityManager securities)
92  {
93  var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase);
94  return SecurityBenchmark.CreateInstance(securities, symbol);
95  }
96 
97  /// <summary>
98  /// Provides Coinbase fee model
99  /// </summary>
100  /// <param name="security"></param>
101  /// <returns></returns>
102  public override IFeeModel GetFeeModel(Security security)
103  {
104  return new CoinbaseFeeModel();
105  }
106 
107  /// <summary>
108  /// Determines whether the brokerage supports updating an existing order for the specified security.
109  /// </summary>
110  /// <param name="security">The security of the order</param>
111  /// <param name="order">The order to be updated</param>
112  /// <param name="request">The requested update to be made to the order</param>
113  /// <param name="message">If this function returns false, a brokerage message detailing why the order may not be updated</param>
114  /// <returns><c>true</c> if the brokerage supports updating orders; otherwise, <c>false</c>.</returns>
115  /// <remarks>Coinbase: Only limit order types, with time in force type of good-till-cancelled can be edited.</remarks>
116  public override bool CanUpdateOrder(Security security, Order order, UpdateOrderRequest request, out BrokerageMessageEvent message)
117  {
118  if (order == null || security == null || request == null)
119  {
120  var parameter = order == null ? nameof(order) : nameof(security);
121  throw new ArgumentNullException(parameter, $"{parameter} parameter cannot be null. Please provide a valid {parameter} for submission.");
122  }
123 
124  if (order.Type != OrderType.Limit)
125  {
126  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
127  $"Order with type {order.Type} can't be modified, only LIMIT.");
128  return false;
129  }
130 
132  {
133  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
134  $"Order's parameter 'TimeInForce' is not instance of Good Til Cancelled class.");
135  return false;
136  }
137 
138  if (order.Status is not (OrderStatus.New or OrderStatus.PartiallyFilled or OrderStatus.Submitted or OrderStatus.UpdateSubmitted))
139  {
140  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
141  $"Order with status {order.Status} can't be modified");
142  return false;
143  }
144 
145  if (request.Quantity.HasValue && !IsOrderSizeLargeEnough(security, Math.Abs(request.Quantity.Value)))
146  {
147  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
148  Messages.DefaultBrokerageModel.InvalidOrderQuantity(security, request.Quantity.Value));
149  return false;
150  }
151 
152  message = null;
153  return true;
154  }
155 
156  /// <summary>
157  /// Evaluates whether exchange will accept order. Will reject order update
158  /// </summary>
159  /// <param name="security">The security of the order</param>
160  /// <param name="order">The order to be processed</param>
161  /// <param name="message">If this function returns false, a brokerage message detailing why the order may not be submitted</param>
162  /// <returns>True if the brokerage could process the order, false otherwise</returns>
163  public override bool CanSubmitOrder(Security security, Order order, out BrokerageMessageEvent message)
164  {
165  if(order == null || security == null)
166  {
167  var parameter = order == null ? nameof(order) : nameof(security);
168  throw new ArgumentNullException(parameter, $"{parameter} parameter cannot be null. Please provide a valid {parameter} for submission.");
169  }
170 
171  if (order.BrokerId != null && order.BrokerId.Any())
172  {
173  message = _message;
174  return false;
175  }
176 
177  if (!IsValidOrderSize(security, order.Quantity, out message))
178  {
179  return false;
180  }
181 
182  if (security.Type != SecurityType.Crypto)
183  {
184  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
185  Messages.DefaultBrokerageModel.UnsupportedSecurityType(this, security));
186  return false;
187  }
188 
189  if (!_supportedOrderTypes.Contains(order.Type))
190  {
191  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
192  Messages.DefaultBrokerageModel.UnsupportedOrderType(this, order, _supportedOrderTypes));
193  return false;
194  }
195 
196  if (order.Type == OrderType.StopMarket && order.Time >= _stopMarketOrderSupportEndDate)
197  {
198  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
199  Messages.CoinbaseBrokerageModel.StopMarketOrdersNoLongerSupported(_stopMarketOrderSupportEndDate));
200 
201  return false;
202  }
203 
204  if (!IsOrderSizeLargeEnough(security, Math.Abs(order.Quantity)))
205  {
206  message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
207  Messages.DefaultBrokerageModel.InvalidOrderQuantity(security, order.Quantity));
208  return false;
209  }
210 
211  return base.CanSubmitOrder(security, order, out message);
212  }
213 
214  /// <summary>
215  /// Gets a new buying power model for the security, returning the default model with the security's configured leverage.
216  /// For cash accounts, leverage = 1 is used.
217  /// </summary>
218  /// <param name="security">The security to get a buying power model for</param>
219  /// <returns>The buying power model for this brokerage/security</returns>
221  {
222  // margin trading is not currently supported by Coinbase
223  return new CashBuyingPowerModel();
224  }
225 
226  /// <summary>
227  /// Returns true if the order size is large enough for the given security.
228  /// </summary>
229  /// <param name="security">The security of the order</param>
230  /// <param name="orderQuantity">The order quantity</param>
231  /// <returns>True if the order size is large enough, false otherwise</returns>
232  protected virtual bool IsOrderSizeLargeEnough(Security security, decimal orderQuantity)
233  {
234 #pragma warning disable CA1062
235  return !security!.SymbolProperties.MinimumOrderSize.HasValue ||
236  orderQuantity >= security.SymbolProperties.MinimumOrderSize;
237 #pragma warning restore CA1062
238  }
239 
240  /// <summary>
241  /// Gets the default markets for different security types, with an option to override the market name for Crypto securities.
242  /// </summary>
243  /// <param name="marketName">The default market name for Crypto securities.</param>
244  /// <returns>
245  /// A read-only dictionary where the keys are <see cref="SecurityType"/> and the values are market names.
246  /// </returns>
247  protected static IReadOnlyDictionary<SecurityType, string> GetDefaultMarkets(string marketName)
248  {
249  var map = DefaultMarketMap.ToDictionary();
250  map[SecurityType.Crypto] = marketName;
251  return map.ToReadOnlyDictionary();
252  }
253  }
254 }