Lean  $LEAN_TAG$
CrunchDAOSignalExport.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 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 Newtonsoft.Json.Linq;
18 using System;
19 using System.Collections.Generic;
20 using System.IO;
21 using System.Net.Http;
22 
24 {
25  /// <summary>
26  /// Exports signals of the desired positions to CrunchDAO API.
27  /// Accepts signals in percentage i.e ticker:"SPY", date: "2020-10-04", signal:0.54
28  /// </summary>
30  {
31  /// <summary>
32  /// CrunchDAO API key
33  /// </summary>
34  private readonly string _apiKey;
35 
36  /// <summary>
37  /// CrunchDAO API endpoint
38  /// </summary>
39  private readonly Uri _destination;
40 
41  /// <summary>
42  /// Model ID or Name
43  /// </summary>
44  private readonly string _model;
45 
46  /// <summary>
47  /// Submission Name (Optional)
48  /// </summary>
49  private readonly string _submissionName;
50 
51  /// <summary>
52  /// Comment (Optional)
53  /// </summary>
54  private readonly string _comment;
55 
56  /// <summary>
57  /// Algorithm being ran
58  /// </summary>
59  private IAlgorithm _algorithm;
60 
61  /// <summary>
62  /// HashSet of allowed SecurityTypes for CrunchDAO
63  /// </summary>
64  private readonly HashSet<SecurityType> _allowedSecurityTypes = new()
65  {
66  SecurityType.Equity
67  };
68 
69  /// <summary>
70  /// The name of this signal export
71  /// </summary>
72  protected override string Name { get; } = "CrunchDAO";
73 
74  /// <summary>
75  /// HashSet property of allowed SecurityTypes for CrunchDAO
76  /// </summary>
77  protected override HashSet<SecurityType> AllowedSecurityTypes => _allowedSecurityTypes;
78 
79  /// <summary>
80  /// CrunchDAOSignalExport constructor. It obtains the required information for CrunchDAO API requests.
81  /// See (https://colab.research.google.com/drive/1YW1xtHrIZ8ZHW69JvNANWowmxPcnkNu0?authuser=1#scrollTo=aPyWNxtuDc-X)
82  /// </summary>
83  /// <param name="apiKey">API key provided by CrunchDAO</param>
84  /// <param name="model">Model ID or Name</param>
85  /// <param name="submissionName">Submission Name (Optional)</param>
86  /// <param name="comment">Comment (Optional)</param>
87  public CrunchDAOSignalExport(string apiKey, string model, string submissionName = "", string comment = "")
88  {
89  _apiKey = apiKey;
90  _model = model;
91  _submissionName = submissionName;
92  _comment = comment;
93  _destination = new Uri($"https://api.tournament.crunchdao.com/v3/alpha-submissions?apiKey={apiKey}");
94  }
95 
96  /// <summary>
97  /// Verifies every holding is a stock, creates a message with the desired positions
98  /// using the expected CrunchDAO API format, verifies there is an open round and then
99  /// sends the positions with the other required body features. If another signal was
100  /// submitted before, it deletes the last signal and sends the new one</summary>
101  /// <param name="parameters">A list of holdings from the portfolio,
102  /// expected to be sent to CrunchDAO API and the algorithm being ran</param>
103  /// <returns>True if the positions were sent to CrunchDAO succesfully and errors were returned, false otherwise</returns>
104  public override bool Send(SignalExportTargetParameters parameters)
105  {
106  if (!base.Send(parameters))
107  {
108  return false;
109  }
110 
111  if (!ConvertToCSVFormat(parameters, out string positions))
112  {
113  return false;
114  }
115 
116  if (!GetCurrentRoundID(out int currentRoundId))
117  {
118  return false;
119  }
120 
121  if (GetLastSubmissionId(currentRoundId, out int lastSubmissionId))
122  {
123  _algorithm.Debug($"You have already submitted a signal for round {currentRoundId}. Your last submission is going to be overwritten with the new one");
124  if (!DeleteLastSubmission(lastSubmissionId))
125  {
126  return false;
127  }
128  }
129 
130  var result = SendPositions(positions);
131  return result;
132  }
133 
134  /// <summary>
135  /// Converts the list of holdings into a CSV format string
136  /// </summary>
137  /// <param name="parameters">A list of holdings from the portfolio,
138  /// expected to be sent to CrunchDAO API and the algorithm being ran</param>
139  /// <param name="positions">A CSV format string of the given holdings with the required features(ticker, date, signal)</param>
140  /// <returns>True if a string message with the positions could be obtained, false otherwise</returns>
141  protected bool ConvertToCSVFormat(SignalExportTargetParameters parameters, out string positions)
142  {
143  var holdings = parameters.Targets;
144  _algorithm = parameters.Algorithm;
145  positions = "ticker,date,signal\n";
146 
147  foreach (var holding in holdings)
148  {
149  if (holding.Quantity < 0 || holding.Quantity > 1)
150  {
151  _algorithm.Error($"All signals must be between 0 and 1, but {holding.Symbol.Value} signal was {holding.Quantity}");
152  return false;
153  }
154 
155  positions += $"{_algorithm.Ticker(holding.Symbol)},{_algorithm.Securities[holding.Symbol].LocalTime.ToString("yyyy-MM-dd")},{holding.Quantity.ToStringInvariant()}\n";
156  }
157 
158  return true;
159  }
160 
161  /// <summary>
162  /// Sends the desired positions, with the other required features, to CrunchDAO API using a POST request. It logs
163  /// the message retrieved by the API if there was a HttpRequestException
164  /// </summary>
165  /// <param name="positions">A CSV format string of the given holdings with the required features</param>
166  /// <returns>True if the positions were sent to CrunchDAO successfully and errors were returned. False, otherwise</returns>
167  private bool SendPositions(string positions)
168  {
169  // Create positions stream
170  var positionsStream = new MemoryStream();
171  using var writer = new StreamWriter(positionsStream);
172  writer.Write(positions);
173  writer.Flush();
174  positionsStream.Position = 0;
175 
176  // Create the required body features for the POST request
177  using var file = new StreamContent(positionsStream);
178  using var model = new StringContent(_model);
179  using var submissionName = new StringContent(_submissionName);
180  using var comment = new StringContent(_comment);
181 
182  // Crete the httpMessage to be sent and add the different POST request body features
183  using var httpMessage = new MultipartFormDataContent
184  {
185  { model, "model" },
186  { submissionName, "label" },
187  { comment, "comment" },
188  { file, "file", "submission.csv" }
189  };
190 
191  // Send the httpMessage
192  using HttpResponseMessage response = HttpClient.PostAsync(_destination, httpMessage).Result;
193  if (response.StatusCode == System.Net.HttpStatusCode.Locked || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
194  {
195  var responseContent = response.Content.ReadAsStringAsync().Result;
196  var parsedResponseContent = JObject.Parse(responseContent);
197  _algorithm.Error($"CrunchDAO API returned code: {parsedResponseContent["code"]} message:{parsedResponseContent["message"]}");
198  return false;
199  }
200 
201  if (!response.IsSuccessStatusCode)
202  {
203  _algorithm.Error($"CrunchDAO API returned HttpRequestException {response.StatusCode}");
204  return false;
205  }
206 
207  return true;
208  }
209 
210  /// <summary>
211  /// Checks if there is an open round, if so it assigns the current round ID to currentRoundId
212  /// parameter
213  /// </summary>
214  /// <param name="currentRoundId">Current round ID</param>
215  /// <returns>True if there is an open round, false otherwise</returns>
216  private bool GetCurrentRoundID(out int currentRoundId)
217  {
218  // Assign a default value to currentRoundId
219  currentRoundId = -1;
220  using HttpResponseMessage roundIdResponse = HttpClient.GetAsync("https://api.tournament.crunchdao.com/v2/rounds/@current").Result;
221  if (roundIdResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
222  {
223  var responseContent = roundIdResponse.Content.ReadAsStringAsync().Result;
224  var parsedResponseContent = JObject.Parse(responseContent);
225  _algorithm.Error($"CrunchDAO API returned code: {parsedResponseContent["code"]} message:{parsedResponseContent["message"]}");
226  return false;
227  }
228  else if (!roundIdResponse.IsSuccessStatusCode)
229  {
230  _algorithm.Error($"CrunchDAO API returned HttpRequestException {roundIdResponse.StatusCode}");
231  return false;
232  }
233 
234  var roundIdResponseContent = roundIdResponse.Content.ReadAsStringAsync().Result;
235  currentRoundId = (int)(JObject.Parse(roundIdResponseContent)["id"]);
236  return true;
237  }
238 
239  /// <summary>
240  /// Checks if there is a submission for the current round, if so it assigns its ID to
241  /// lastSubmissionId
242  /// </summary>
243  /// <param name="currentRoundId">Current round ID</param>
244  /// <param name="lastSubmissionId">Last submission ID (for the current round)</param>
245  /// <returns>True if there's a submission for the current round, false otherwise</returns>
246  private bool GetLastSubmissionId(int currentRoundId, out int lastSubmissionId)
247  {
248  using HttpResponseMessage submissionIdResponse = HttpClient.GetAsync($"https://tournament.crunchdao.com/api/v3/alpha-submissions?includeAll=false&roundId={currentRoundId}&apiKey={_apiKey}").Result;
249 
250  if (!submissionIdResponse.IsSuccessStatusCode)
251  {
252  _algorithm.Error($"CrunchDAO API returned the following Error Code: {submissionIdResponse.StatusCode}");
253  lastSubmissionId = -1;
254  return false;
255  }
256 
257  var submissionIdResponseContent = submissionIdResponse.Content.ReadAsStringAsync().Result;
258  var parsedSubmissionIdResponseContent = JArray.Parse(submissionIdResponseContent);
259  if (!parsedSubmissionIdResponseContent.HasValues)
260  {
261  // Default value for lastSubmissionId
262  lastSubmissionId = -1;
263  return false;
264  }
265 
266  lastSubmissionId = (int)parsedSubmissionIdResponseContent[0]["id"];
267  return true;
268  }
269 
270  /// <summary>
271  /// Deletes the last submission for the current round
272  /// </summary>
273  /// <param name="lastSubmissionId">Last submission ID for the current round</param>
274  /// <returns>True if the last submission could be deleted sucessfully, false otherwise</returns>
275  private bool DeleteLastSubmission(int lastSubmissionId)
276  {
277  using HttpResponseMessage deleteSubmissionResponse = HttpClient.DeleteAsync($"https://tournament.crunchdao.com/api/v3/alpha-submissions/{lastSubmissionId}?&apiKey={_apiKey}").Result;
278  if (!deleteSubmissionResponse.IsSuccessStatusCode)
279  {
280  var responseContent = deleteSubmissionResponse.Content.ReadAsStringAsync().Result;
281  var parsedResponseContent = JObject.Parse(responseContent);
282  _algorithm.Error($"CrunchDAO API returned code: {parsedResponseContent["code"]} message:{parsedResponseContent["message"]}. Last submission could not be deleted");
283  return false;
284  }
285 
286  _algorithm.Debug($"Last submission has been deleted");
287  return true;
288  }
289  }
290 }