Lean  $LEAN_TAG$
NumeraiSignalExport.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;
17 using Newtonsoft.Json.Linq;
19 using System;
20 using System.Collections.Generic;
21 using System.IO;
22 using System.Net.Http;
23 using System.Text;
24 
26 {
27  /// <summary>
28  /// Exports signals of the desired positions to Numerai API.
29  /// Accepts signals in percentage i.e numerai_ticker:"IBM US", signal:0.234
30  /// </summary>
31  /// <remarks>It does not take into account flags as
32  /// NUMERAI_COMPUTE_ID (https://github.com/numerai/numerapi/blob/master/numerapi/signalsapi.py#L164) and
33  /// TRIGGER_ID(https://github.com/numerai/numerapi/blob/master/numerapi/signalsapi.py#L164)</remarks>
35  {
36  /// <summary>
37  /// Numerai API submission endpoint
38  /// </summary>
39  private readonly Uri _destination;
40 
41  /// <summary>
42  /// PUBLIC_ID provided by Numerai
43  /// </summary>
44  private readonly string _publicId;
45 
46  /// <summary>
47  /// SECRET_ID provided by Numerai
48  /// </summary>
49  private readonly string _secretId;
50 
51  /// <summary>
52  /// ID of the Numerai Model being used
53  /// </summary>
54  private readonly string _modelId;
55 
56  /// <summary>
57  /// Signal file's name
58  /// </summary>
59  private readonly string _fileName;
60 
61  /// <summary>
62  /// Algorithm being ran
63  /// </summary>
64  private IAlgorithm _algorithm;
65 
66  /// <summary>
67  /// Dictionary to obtain corresponding Numerai Market name for the given LEAN market name
68  /// </summary>
69  private readonly Dictionary<string, string> _numeraiMarketFormat = new() // There can be stocks from other markets
70  {
71  {Market.USA, "US" },
72  {Market.SGX, "SP" }
73  };
74 
75  /// <summary>
76  /// Hashset of Numerai allowed SecurityTypes
77  /// </summary>
78  private readonly HashSet<SecurityType> _allowedSecurityTypes = new()
79  {
80  SecurityType.Equity
81  };
82 
83  /// <summary>
84  /// The name of this signal export
85  /// </summary>
86  protected override string Name { get; } = "Numerai";
87 
88  /// <summary>
89  /// Hashset property of Numerai allowed SecurityTypes
90  /// </summary>
91  protected override HashSet<SecurityType> AllowedSecurityTypes => _allowedSecurityTypes;
92 
93  /// <summary>
94  /// NumeraiSignalExport Constructor. It obtains the required information for Numerai API requests
95  /// </summary>
96  /// <param name="publicId">PUBLIC_ID provided by Numerai</param>
97  /// <param name="secretId">SECRET_ID provided by Numerai</param>
98  /// <param name="modelId">ID of the Numerai Model being used</param>
99  /// <param name="fileName">Signal file's name</param>
100  public NumeraiSignalExport(string publicId, string secretId, string modelId, string fileName = "predictions.csv")
101  {
102  _destination = new Uri("https://api-tournament.numer.ai");
103  _publicId = publicId;
104  _secretId = secretId;
105  _modelId = modelId;
106  _fileName = fileName;
107  }
108 
109  /// <summary>
110  /// Verifies all the given holdings are accepted by Numerai, creates a message with those holdings in the expected
111  /// Numerai API format and sends them to Numerai API
112  /// </summary>
113  /// <param name="parameters">A list of portfolio holdings expected to be sent to Numerai API and the algorithm being ran</param>
114  /// <returns>True if the positions were sent to Numerai API correctly and no errors were returned, false otherwise</returns>
115  public override bool Send(SignalExportTargetParameters parameters)
116  {
117  if (!base.Send(parameters))
118  {
119  return false;
120  }
121 
122  _algorithm = parameters.Algorithm;
123 
124  if (parameters.Targets.Count < 10)
125  {
126  _algorithm.Error($"Numerai Signals API accepts minimum 10 different signals, just found {parameters.Targets.Count}");
127  return false;
128  }
129 
130  if (!ConvertTargetsToNumerai(parameters, out string positions))
131  {
132  return false;
133  }
134  var result = SendPositions(positions);
135 
136  return result;
137  }
138 
139  /// <summary>
140  /// Verifies each holding's signal is between 0 and 1 (exclusive)
141  /// </summary>
142  /// <param name="parameters">A list of portfolio holdings expected to be sent to Numerai API</param>
143  /// <param name="positions">A message with the desired positions in the expected Numerai API format</param>
144  /// <returns>True if a string message with the positions could be obtained, false otherwise</returns>
145  protected bool ConvertTargetsToNumerai(SignalExportTargetParameters parameters, out string positions)
146  {
147  positions = "numerai_ticker,signal\n";
148  foreach ( var holding in parameters.Targets)
149  {
150  if (holding.Quantity <= 0 || holding.Quantity >= 1)
151  {
152  _algorithm.Error($"All signals must be between 0 and 1 (exclusive), but {holding.Symbol.Value} signal was {holding.Quantity}");
153  return false;
154  }
155 
156  positions += $"{parameters.Algorithm.Ticker(holding.Symbol)} {_numeraiMarketFormat[holding.Symbol.ID.Market]},{holding.Quantity.ToStringInvariant()}\n";
157  }
158 
159  return true;
160  }
161 
162  /// <summary>
163  /// Sends the given positions message to Numerai API. It first sends an authentication POST request then a
164  /// PUT request to put the positions in certain endpoint and finally sends a submission POST request
165  /// </summary>
166  /// <param name="positions">A message with the desired positions in the expected Numerai API format</param>
167  /// <returns>True if the positions were sent to Numerai API correctly and no errors were returned, false otherwise</returns>
168  private bool SendPositions(string positions)
169  {
170  // AUTHENTICATION REQUEST
171  var authQuery = @"query($filename: String!
172  $modelId: String) {
173  submissionUploadSignalsAuth(filename: $filename
174  modelId: $modelId) {
175  filename
176  url
177  }
178  }";
179 
180  var arguments = new
181  {
182  filename = _fileName,
183  modelId = _modelId
184  };
185  var argumentsMessage = JsonConvert.SerializeObject(arguments);
186 
187  using var variables = new StringContent(argumentsMessage, Encoding.UTF8, "application/json");
188  using var query = new StringContent(authQuery, Encoding.UTF8, "application/json");
189 
190  var httpMessage = new MultipartFormDataContent
191  {
192  { query, "query"},
193  { variables, "variables" }
194  };
195 
196  using var authRequest = new HttpRequestMessage(HttpMethod.Post, _destination);
197  authRequest.Headers.Add("Accept", "application/json");
198  authRequest.Headers.Add("Authorization", $"Token {_publicId}${_secretId}");
199  authRequest.Content = httpMessage;
200  var response = HttpClient.SendAsync(authRequest).Result;
201  var responseContent = response.Content.ReadAsStringAsync().Result;
202  if (!response.IsSuccessStatusCode)
203  {
204  _algorithm.Error($"Numerai API returned HttpRequestException {response.StatusCode}");
205  return false;
206  }
207 
208  var parsedResponseContent = JObject.Parse(responseContent);
209  if (!parsedResponseContent["data"]["submissionUploadSignalsAuth"].HasValues)
210  {
211  _algorithm.Error($"Numerai API returned the following errors: {string.Join(",", parsedResponseContent["errors"])}");
212  return false;
213  }
214 
215  var putUrl = new Uri((string)parsedResponseContent["data"]["submissionUploadSignalsAuth"]["url"]);
216  var submissionFileName = (string)parsedResponseContent["data"]["submissionUploadSignalsAuth"]["filename"];
217 
218  // PUT REQUEST
219  // Create positions stream
220  var positionsStream = new MemoryStream();
221  using var writer = new StreamWriter(positionsStream);
222  writer.Write(positions);
223  writer.Flush();
224  positionsStream.Position = 0;
225  using var putRequest = new HttpRequestMessage(HttpMethod.Put, putUrl)
226  {
227  Content = new StreamContent(positionsStream)
228  };
229  var putResponse = HttpClient.SendAsync(putRequest).Result;
230 
231  // SUBMISSION REQUEST
232  var createQuery = @"mutation($filename: String!
233  $modelId: String
234  $triggerId: String) {
235  createSignalsSubmission(filename: $filename
236  modelId: $modelId
237  triggerId: $triggerId
238  source: ""numerapi"") {
239  id
240  firstEffectiveDate
241  }
242  }";
243 
244  var createArguments = new
245  {
246  filename = submissionFileName,
247  modelId = _modelId
248  };
249  var createArgumentsMessage = JsonConvert.SerializeObject(createArguments);
250 
251  using var submissionQuery = new StringContent(createQuery, Encoding.UTF8, "application/json");
252  using var submissionVariables = new StringContent(createArgumentsMessage, Encoding.UTF8, "application/json");
253 
254  var submissionMessage = new MultipartFormDataContent
255  {
256  {submissionQuery, "query"},
257  {submissionVariables, "variables"}
258  };
259 
260  using var submissionRequest = new HttpRequestMessage(HttpMethod.Post, _destination);
261  submissionRequest.Headers.Add("Authorization", $"Token {_publicId}${_secretId}");
262  submissionRequest.Content = submissionMessage;
263  var submissionResponse = HttpClient.SendAsync(submissionRequest).Result;
264  var submissionResponseContent = submissionResponse.Content.ReadAsStringAsync().Result;
265  if (!submissionResponse.IsSuccessStatusCode)
266  {
267  _algorithm.Error($"Numerai API returned HttpRequestException {submissionResponseContent}");
268  return false;
269  }
270 
271  var parsedSubmissionResponseContent = JObject.Parse(submissionResponseContent);
272  if (!parsedSubmissionResponseContent["data"]["createSignalsSubmission"].HasValues)
273  {
274  _algorithm.Error($"Numerai API returned the following errors: {string.Join(",", parsedSubmissionResponseContent["errors"])}");
275  return false;
276  }
277 
278  return true;
279  }
280  }
281 }