Lean  $LEAN_TAG$
Api.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 System;
17 using System.Collections.Generic;
18 using System.IO;
19 using System.Linq;
20 using System.Net;
21 using System.Net.Http;
22 using Newtonsoft.Json;
23 using Newtonsoft.Json.Linq;
24 using RestSharp;
25 using RestSharp.Extensions;
27 using QuantConnect.Logging;
30 using QuantConnect.Orders;
32 using QuantConnect.Util;
34 using Python.Runtime;
35 using System.Threading;
36 using System.Net.Http.Headers;
37 using System.Collections.Concurrent;
38 using System.Text;
39 
41 {
42  /// <summary>
43  /// QuantConnect.com Interaction Via API.
44  /// </summary>
45  public class Api : IApi, IDownloadProvider
46  {
47  private readonly BlockingCollection<Lazy<HttpClient>> _clientPool;
48  private string _dataFolder;
49 
50  /// <summary>
51  /// Returns the underlying API connection
52  /// </summary>
53  protected ApiConnection ApiConnection { get; private set; }
54 
55  /// <summary>
56  /// Creates a new instance of <see cref="Api"/>
57  /// </summary>
58  public Api()
59  {
60  _clientPool = new BlockingCollection<Lazy<HttpClient>>(new ConcurrentQueue<Lazy<HttpClient>>(), 5);
61  for (int i = 0; i < _clientPool.BoundedCapacity; i++)
62  {
63  _clientPool.Add(new Lazy<HttpClient>());
64  }
65  }
66 
67  /// <summary>
68  /// Initialize the API with the given variables
69  /// </summary>
70  public virtual void Initialize(int userId, string token, string dataFolder)
71  {
72  ApiConnection = new ApiConnection(userId, token);
73  _dataFolder = dataFolder?.Replace("\\", "/", StringComparison.InvariantCulture);
74 
75  //Allow proper decoding of orders from the API.
76  JsonConvert.DefaultSettings = () => new JsonSerializerSettings
77  {
78  Converters = { new OrderJsonConverter() }
79  };
80  }
81 
82  /// <summary>
83  /// Check if Api is successfully connected with correct credentials
84  /// </summary>
86 
87  /// <summary>
88  /// Create a project with the specified name and language via QuantConnect.com API
89  /// </summary>
90  /// <param name="name">Project name</param>
91  /// <param name="language">Programming language to use</param>
92  /// <param name="organizationId">Optional param for specifying organization to create project under.
93  /// If none provided web defaults to preferred.</param>
94  /// <returns>Project object from the API.</returns>
95 
96  public ProjectResponse CreateProject(string name, Language language, string organizationId = null)
97  {
98  var request = new RestRequest("projects/create", Method.POST)
99  {
100  RequestFormat = DataFormat.Json
101  };
102 
103  // Only include organization Id if its not null or empty
104  string jsonParams;
105  if (string.IsNullOrEmpty(organizationId))
106  {
107  jsonParams = JsonConvert.SerializeObject(new
108  {
109  name,
110  language
111  });
112  }
113  else
114  {
115  jsonParams = JsonConvert.SerializeObject(new
116  {
117  name,
118  language,
119  organizationId
120  });
121  }
122 
123  request.AddParameter("application/json", jsonParams, ParameterType.RequestBody);
124 
125  ApiConnection.TryRequest(request, out ProjectResponse result);
126  return result;
127  }
128 
129  /// <summary>
130  /// Get details about a single project
131  /// </summary>
132  /// <param name="projectId">Id of the project</param>
133  /// <returns><see cref="ProjectResponse"/> that contains information regarding the project</returns>
134 
135  public ProjectResponse ReadProject(int projectId)
136  {
137  var request = new RestRequest("projects/read", Method.POST)
138  {
139  RequestFormat = DataFormat.Json
140  };
141 
142  request.AddParameter("application/json", JsonConvert.SerializeObject(new
143  {
144  projectId
145  }), ParameterType.RequestBody);
146 
147  ApiConnection.TryRequest(request, out ProjectResponse result);
148  return result;
149  }
150 
151  /// <summary>
152  /// List details of all projects
153  /// </summary>
154  /// <returns><see cref="ProjectResponse"/> that contains information regarding the project</returns>
155 
157  {
158  var request = new RestRequest("projects/read", Method.POST)
159  {
160  RequestFormat = DataFormat.Json
161  };
162 
163  ApiConnection.TryRequest(request, out ProjectResponse result);
164  return result;
165  }
166 
167 
168  /// <summary>
169  /// Add a file to a project
170  /// </summary>
171  /// <param name="projectId">The project to which the file should be added</param>
172  /// <param name="name">The name of the new file</param>
173  /// <param name="content">The content of the new file</param>
174  /// <returns><see cref="ProjectFilesResponse"/> that includes information about the newly created file</returns>
175 
176  public RestResponse AddProjectFile(int projectId, string name, string content)
177  {
178  var request = new RestRequest("files/create", Method.POST)
179  {
180  RequestFormat = DataFormat.Json
181  };
182 
183  request.AddParameter("application/json", JsonConvert.SerializeObject(new
184  {
185  projectId,
186  name,
187  content
188  }), ParameterType.RequestBody);
189 
190  ApiConnection.TryRequest(request, out RestResponse result);
191  return result;
192  }
193 
194 
195  /// <summary>
196  /// Update the name of a file
197  /// </summary>
198  /// <param name="projectId">Project id to which the file belongs</param>
199  /// <param name="oldFileName">The current name of the file</param>
200  /// <param name="newFileName">The new name for the file</param>
201  /// <returns><see cref="RestResponse"/> indicating success</returns>
202 
203  public RestResponse UpdateProjectFileName(int projectId, string oldFileName, string newFileName)
204  {
205  var request = new RestRequest("files/update", Method.POST)
206  {
207  RequestFormat = DataFormat.Json
208  };
209 
210  request.AddParameter("application/json", JsonConvert.SerializeObject(new
211  {
212  projectId,
213  name = oldFileName,
214  newName = newFileName
215  }), ParameterType.RequestBody);
216 
217  ApiConnection.TryRequest(request, out RestResponse result);
218  return result;
219  }
220 
221 
222  /// <summary>
223  /// Update the contents of a file
224  /// </summary>
225  /// <param name="projectId">Project id to which the file belongs</param>
226  /// <param name="fileName">The name of the file that should be updated</param>
227  /// <param name="newFileContents">The new contents of the file</param>
228  /// <returns><see cref="RestResponse"/> indicating success</returns>
229 
230  public RestResponse UpdateProjectFileContent(int projectId, string fileName, string newFileContents)
231  {
232  var request = new RestRequest("files/update", Method.POST)
233  {
234  RequestFormat = DataFormat.Json
235  };
236 
237  request.AddParameter("application/json", JsonConvert.SerializeObject(new
238  {
239  projectId,
240  name = fileName,
241  content = newFileContents
242  }), ParameterType.RequestBody);
243 
244  ApiConnection.TryRequest(request, out RestResponse result);
245  return result;
246  }
247 
248 
249  /// <summary>
250  /// Read all files in a project
251  /// </summary>
252  /// <param name="projectId">Project id to which the file belongs</param>
253  /// <returns><see cref="ProjectFilesResponse"/> that includes the information about all files in the project</returns>
254 
256  {
257  var request = new RestRequest("files/read", Method.POST)
258  {
259  RequestFormat = DataFormat.Json
260  };
261 
262  request.AddParameter("application/json", JsonConvert.SerializeObject(new
263  {
264  projectId
265  }), ParameterType.RequestBody);
266 
267  ApiConnection.TryRequest(request, out ProjectFilesResponse result);
268  return result;
269  }
270 
271  /// <summary>
272  /// Read all nodes in a project.
273  /// </summary>
274  /// <param name="projectId">Project id to which the nodes refer</param>
275  /// <returns><see cref="ProjectNodesResponse"/> that includes the information about all nodes in the project</returns>
277  {
278  var request = new RestRequest("projects/nodes/read", Method.POST)
279  {
280  RequestFormat = DataFormat.Json
281  };
282 
283  request.AddParameter("application/json", JsonConvert.SerializeObject(new
284  {
285  projectId
286  }), ParameterType.RequestBody);
287 
288  ApiConnection.TryRequest(request, out ProjectNodesResponse result);
289  return result;
290  }
291 
292  /// <summary>
293  /// Update the active state of some nodes to true.
294  /// If you don't provide any nodes, all the nodes become inactive and AutoSelectNode is true.
295  /// </summary>
296  /// <param name="projectId">Project id to which the nodes refer</param>
297  /// <param name="nodes">List of node ids to update</param>
298  /// <returns><see cref="ProjectNodesResponse"/> that includes the information about all nodes in the project</returns>
299  public ProjectNodesResponse UpdateProjectNodes(int projectId, string[] nodes)
300  {
301  var request = new RestRequest("projects/nodes/update", Method.POST)
302  {
303  RequestFormat = DataFormat.Json
304  };
305 
306  request.AddParameter("application/json", JsonConvert.SerializeObject(new
307  {
308  projectId,
309  nodes
310  }), ParameterType.RequestBody);
311 
312  ApiConnection.TryRequest(request, out ProjectNodesResponse result);
313  return result;
314  }
315 
316  /// <summary>
317  /// Read a file in a project
318  /// </summary>
319  /// <param name="projectId">Project id to which the file belongs</param>
320  /// <param name="fileName">The name of the file</param>
321  /// <returns><see cref="ProjectFilesResponse"/> that includes the file information</returns>
322 
323  public ProjectFilesResponse ReadProjectFile(int projectId, string fileName)
324  {
325  var request = new RestRequest("files/read", Method.POST)
326  {
327  RequestFormat = DataFormat.Json
328  };
329 
330  request.AddParameter("application/json", JsonConvert.SerializeObject(new
331  {
332  projectId,
333  name = fileName
334  }), ParameterType.RequestBody);
335 
336  ApiConnection.TryRequest(request, out ProjectFilesResponse result);
337  return result;
338  }
339 
340  /// <summary>
341  /// Gets a list of LEAN versions with their corresponding basic descriptions
342  /// </summary>
344  {
345  var request = new RestRequest("lean/versions/read", Method.POST)
346  {
347  RequestFormat = DataFormat.Json
348  };
349 
350  ApiConnection.TryRequest(request, out VersionsResponse result);
351  return result;
352  }
353 
354  /// <summary>
355  /// Delete a file in a project
356  /// </summary>
357  /// <param name="projectId">Project id to which the file belongs</param>
358  /// <param name="name">The name of the file that should be deleted</param>
359  /// <returns><see cref="RestResponse"/> that includes the information about all files in the project</returns>
360 
361  public RestResponse DeleteProjectFile(int projectId, string name)
362  {
363  var request = new RestRequest("files/delete", Method.POST)
364  {
365  RequestFormat = DataFormat.Json
366  };
367 
368  request.AddParameter("application/json", JsonConvert.SerializeObject(new
369  {
370  projectId,
371  name,
372  }), ParameterType.RequestBody);
373 
374  ApiConnection.TryRequest(request, out RestResponse result);
375  return result;
376  }
377 
378  /// <summary>
379  /// Delete a project
380  /// </summary>
381  /// <param name="projectId">Project id we own and wish to delete</param>
382  /// <returns>RestResponse indicating success</returns>
383 
384  public RestResponse DeleteProject(int projectId)
385  {
386  var request = new RestRequest("projects/delete", Method.POST)
387  {
388  RequestFormat = DataFormat.Json
389  };
390 
391  request.AddParameter("application/json", JsonConvert.SerializeObject(new
392  {
393  projectId
394  }), ParameterType.RequestBody);
395 
396  ApiConnection.TryRequest(request, out RestResponse result);
397  return result;
398  }
399 
400  /// <summary>
401  /// Create a new compile job request for this project id.
402  /// </summary>
403  /// <param name="projectId">Project id we wish to compile.</param>
404  /// <returns>Compile object result</returns>
405 
406  public Compile CreateCompile(int projectId)
407  {
408  var request = new RestRequest("compile/create", Method.POST)
409  {
410  RequestFormat = DataFormat.Json
411  };
412 
413  request.AddParameter("application/json", JsonConvert.SerializeObject(new
414  {
415  projectId
416  }), ParameterType.RequestBody);
417 
418  ApiConnection.TryRequest(request, out Compile result);
419  return result;
420  }
421 
422  /// <summary>
423  /// Read a compile packet job result.
424  /// </summary>
425  /// <param name="projectId">Project id we sent for compile</param>
426  /// <param name="compileId">Compile id return from the creation request</param>
427  /// <returns><see cref="Compile"/></returns>
428 
429  public Compile ReadCompile(int projectId, string compileId)
430  {
431  var request = new RestRequest("compile/read", Method.POST)
432  {
433  RequestFormat = DataFormat.Json
434  };
435 
436  request.AddParameter("application/json", JsonConvert.SerializeObject(new
437  {
438  projectId,
439  compileId
440  }), ParameterType.RequestBody);
441 
442  ApiConnection.TryRequest(request, out Compile result);
443  return result;
444  }
445 
446  /// <summary>
447  /// Sends a notification
448  /// </summary>
449  /// <param name="notification">The notification to send</param>
450  /// <param name="projectId">The project id</param>
451  /// <returns><see cref="RestResponse"/> containing success response and errors</returns>
452  public virtual RestResponse SendNotification(Notification notification, int projectId)
453  {
454  throw new NotImplementedException($"{nameof(Api)} does not support sending notifications");
455  }
456 
457  /// <summary>
458  /// Create a new backtest request and get the id.
459  /// </summary>
460  /// <param name="projectId">Id for the project to backtest</param>
461  /// <param name="compileId">Compile id for the project</param>
462  /// <param name="backtestName">Name for the new backtest</param>
463  /// <returns><see cref="Backtest"/>t</returns>
464 
465  public Backtest CreateBacktest(int projectId, string compileId, string backtestName)
466  {
467  var request = new RestRequest("backtests/create", Method.POST)
468  {
469  RequestFormat = DataFormat.Json
470  };
471 
472  request.AddParameter("application/json", JsonConvert.SerializeObject(new
473  {
474  projectId,
475  compileId,
476  backtestName
477  }), ParameterType.RequestBody);
478 
479  ApiConnection.TryRequest(request, out BacktestResponseWrapper result);
480 
481  // Use API Response values for Backtest Values
482  result.Backtest.Success = result.Success;
483  result.Backtest.Errors = result.Errors;
484 
485  // Return only the backtest object
486  return result.Backtest;
487  }
488 
489  /// <summary>
490  /// Read out a backtest in the project id specified.
491  /// </summary>
492  /// <param name="projectId">Project id to read</param>
493  /// <param name="backtestId">Specific backtest id to read</param>
494  /// <param name="getCharts">True will return backtest charts</param>
495  /// <returns><see cref="Backtest"/></returns>
496 
497  public Backtest ReadBacktest(int projectId, string backtestId, bool getCharts = true)
498  {
499  var request = new RestRequest("backtests/read", Method.POST)
500  {
501  RequestFormat = DataFormat.Json
502  };
503 
504  request.AddParameter("application/json", JsonConvert.SerializeObject(new
505  {
506  projectId,
507  backtestId
508  }), ParameterType.RequestBody);
509 
510  ApiConnection.TryRequest(request, out BacktestResponseWrapper result);
511 
512  if (!result.Success)
513  {
514  // place an empty place holder so we can return any errors back to the user and not just null
515  result.Backtest = new Backtest { BacktestId = backtestId };
516  }
517  // Go fetch the charts if the backtest is completed and success
518  else if (getCharts && result.Backtest.Completed)
519  {
520  // For storing our collected charts
521  var updatedCharts = new Dictionary<string, Chart>();
522 
523  // Create backtest requests for each chart that is empty
524  foreach (var chart in result.Backtest.Charts)
525  {
526  if (!chart.Value.Series.IsNullOrEmpty())
527  {
528  continue;
529  }
530 
531  var chartRequest = new RestRequest("backtests/read", Method.POST)
532  {
533  RequestFormat = DataFormat.Json
534  };
535 
536  chartRequest.AddParameter("application/json", JsonConvert.SerializeObject(new
537  {
538  projectId,
539  backtestId,
540  chart = chart.Key
541  }), ParameterType.RequestBody);
542 
543  // Add this chart to our updated collection
544  if (ApiConnection.TryRequest(chartRequest, out BacktestResponseWrapper chartResponse) && chartResponse.Success)
545  {
546  updatedCharts.Add(chart.Key, chartResponse.Backtest.Charts[chart.Key]);
547  }
548  }
549 
550  // Update our result
551  foreach(var updatedChart in updatedCharts)
552  {
553  result.Backtest.Charts[updatedChart.Key] = updatedChart.Value;
554  }
555  }
556 
557  // Use API Response values for Backtest Values
558  result.Backtest.Success = result.Success;
559  result.Backtest.Errors = result.Errors;
560 
561  // Return only the backtest object
562  return result.Backtest;
563  }
564 
565  /// <summary>
566  /// Returns the orders of the specified backtest and project id.
567  /// </summary>
568  /// <param name="projectId">Id of the project from which to read the orders</param>
569  /// <param name="backtestId">Id of the backtest from which to read the orders</param>
570  /// <param name="start">Starting index of the orders to be fetched. Required if end > 100</param>
571  /// <param name="end">Last index of the orders to be fetched. Note that end - start must be less than 100</param>
572  /// <remarks>Will throw an <see cref="WebException"/> if there are any API errors</remarks>
573  /// <returns>The list of <see cref="Order"/></returns>
574 
575  public List<ApiOrderResponse> ReadBacktestOrders(int projectId, string backtestId, int start = 0, int end = 100)
576  {
577  var request = new RestRequest("backtests/read/orders", Method.POST)
578  {
579  RequestFormat = DataFormat.Json
580  };
581 
582  request.AddParameter("application/json", JsonConvert.SerializeObject(new
583  {
584  start,
585  end,
586  projectId,
587  backtestId
588  }), ParameterType.RequestBody);
589 
590  return MakeRequestOrThrow<OrdersResponseWrapper>(request, nameof(ReadBacktestOrders)).Orders;
591  }
592 
593  /// <summary>
594  /// Update a backtest name
595  /// </summary>
596  /// <param name="projectId">Project for the backtest we want to update</param>
597  /// <param name="backtestId">Backtest id we want to update</param>
598  /// <param name="name">Name we'd like to assign to the backtest</param>
599  /// <param name="note">Note attached to the backtest</param>
600  /// <returns><see cref="RestResponse"/></returns>
601 
602  public RestResponse UpdateBacktest(int projectId, string backtestId, string name = "", string note = "")
603  {
604  var request = new RestRequest("backtests/update", Method.POST)
605  {
606  RequestFormat = DataFormat.Json
607  };
608 
609  request.AddParameter("application/json", JsonConvert.SerializeObject(new
610  {
611  projectId,
612  backtestId,
613  name,
614  note
615  }), ParameterType.RequestBody);
616 
617  ApiConnection.TryRequest(request, out RestResponse result);
618  return result;
619  }
620 
621  /// <summary>
622  /// List all the backtest summaries for a project
623  /// </summary>
624  /// <param name="projectId">Project id we'd like to get a list of backtest for</param>
625  /// <param name="includeStatistics">True for include statistics in the response, false otherwise</param>
626  /// <returns><see cref="BacktestList"/></returns>
627 
628  public BacktestSummaryList ListBacktests(int projectId, bool includeStatistics = true)
629  {
630  var request = new RestRequest("backtests/list", Method.POST)
631  {
632  RequestFormat = DataFormat.Json
633  };
634 
635  var obj = new Dictionary<string, object>()
636  {
637  { "projectId", projectId },
638  { "includeStatistics", includeStatistics }
639  };
640 
641  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
642 
643  ApiConnection.TryRequest(request, out BacktestSummaryList result);
644  return result;
645  }
646 
647  /// <summary>
648  /// Delete a backtest from the specified project and backtestId.
649  /// </summary>
650  /// <param name="projectId">Project for the backtest we want to delete</param>
651  /// <param name="backtestId">Backtest id we want to delete</param>
652  /// <returns><see cref="RestResponse"/></returns>
653 
654  public RestResponse DeleteBacktest(int projectId, string backtestId)
655  {
656  var request = new RestRequest("backtests/delete", Method.POST)
657  {
658  RequestFormat = DataFormat.Json
659  };
660 
661  request.AddParameter("application/json", JsonConvert.SerializeObject(new
662  {
663  projectId,
664  backtestId
665  }), ParameterType.RequestBody);
666 
667  ApiConnection.TryRequest(request, out RestResponse result);
668  return result;
669  }
670 
671  /// <summary>
672  /// Updates the tags collection for a backtest
673  /// </summary>
674  /// <param name="projectId">Project for the backtest we want to update</param>
675  /// <param name="backtestId">Backtest id we want to update</param>
676  /// <param name="tags">The new backtest tags</param>
677  /// <returns><see cref="RestResponse"/></returns>
678  public RestResponse UpdateBacktestTags(int projectId, string backtestId, IReadOnlyCollection<string> tags)
679  {
680  var request = new RestRequest("backtests/tags/update", Method.POST)
681  {
682  RequestFormat = DataFormat.Json
683  };
684 
685  request.AddParameter("application/json", JsonConvert.SerializeObject(new
686  {
687  projectId,
688  backtestId,
689  tags
690  }), ParameterType.RequestBody);
691 
692  ApiConnection.TryRequest(request, out RestResponse result);
693  return result;
694  }
695 
696  /// <summary>
697  /// Create a live algorithm.
698  /// </summary>
699  /// <param name="projectId">Id of the project on QuantConnect</param>
700  /// <param name="compileId">Id of the compilation on QuantConnect</param>
701  /// <param name="nodeId">Id of the node that will run the algorithm</param>
702  /// <param name="brokerageSettings">Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials
703  /// in order to process the given orders. Each key in this dictionary represents a required field/credential
704  /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: {
705  /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying,
706  /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage
707  /// (see <see cref="Brokerages.BrokerageName"/>)</param>
708  /// <param name="versionId">The version of the Lean used to run the algorithm.
709  /// -1 is master, however, sometimes this can create problems with live deployments.
710  /// If you experience problems using, try specifying the version of Lean you would like to use.</param>
711  /// <param name="dataProviders">Dictionary with data providers credentials. Each data provider requires certain credentials
712  /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name
713  /// and its corresponding value is another dictionary with the required key-value pairs of credential
714  /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper",
715  /// "username": "testUsername", "password": "testPassword"}}"</param>
716  /// <returns>Information regarding the new algorithm <see cref="CreateLiveAlgorithmResponse"/></returns>
718  string compileId,
719  string nodeId,
720  Dictionary<string, object> brokerageSettings,
721  string versionId = "-1",
722  Dictionary<string, object> dataProviders = null)
723  {
724  var request = new RestRequest("live/create", Method.POST)
725  {
726  RequestFormat = DataFormat.Json
727  };
728 
729  request.AddParameter("application/json", JsonConvert.SerializeObject(
731  (projectId,
732  compileId,
733  nodeId,
734  brokerageSettings,
735  versionId,
736  dataProviders
737  )
738  ), ParameterType.RequestBody);
739 
740  ApiConnection.TryRequest(request, out CreateLiveAlgorithmResponse result);
741  return result;
742  }
743 
744  /// <summary>
745  /// Create a live algorithm.
746  /// </summary>
747  /// <param name="projectId">Id of the project on QuantConnect</param>
748  /// <param name="compileId">Id of the compilation on QuantConnect</param>
749  /// <param name="nodeId">Id of the node that will run the algorithm</param>
750  /// <param name="brokerageSettings">Python Dictionary with brokerage specific settings. Each brokerage requires certain specific credentials
751  /// in order to process the given orders. Each key in this dictionary represents a required field/credential
752  /// to provide to the brokerage API and its value represents the value of that field. For example: "brokerageSettings: {
753  /// "id": "Binance", "binance-api-secret": "123ABC", "binance-api-key": "ABC123"}. It is worth saying,
754  /// that this dictionary must always contain an entry whose key is "id" and its value is the name of the brokerage
755  /// (see <see cref="Brokerages.BrokerageName"/>)</param>
756  /// <param name="versionId">The version of the Lean used to run the algorithm.
757  /// -1 is master, however, sometimes this can create problems with live deployments.
758  /// If you experience problems using, try specifying the version of Lean you would like to use.</param>
759  /// <param name="dataProviders">Python Dictionary with data providers credentials. Each data provider requires certain credentials
760  /// in order to retrieve data from their API. Each key in this dictionary describes a data provider name
761  /// and its corresponding value is another dictionary with the required key-value pairs of credential
762  /// names and values. For example: "dataProviders: { "InteractiveBrokersBrokerage" : { "id": 12345, "environment" : "paper",
763  /// "username": "testUsername", "password": "testPassword"}}"</param>
764  /// <returns>Information regarding the new algorithm <see cref="CreateLiveAlgorithmResponse"/></returns>
765 
766  public CreateLiveAlgorithmResponse CreateLiveAlgorithm(int projectId, string compileId, string nodeId, PyObject brokerageSettings, string versionId = "-1", PyObject dataProviders = null)
767  {
768  return CreateLiveAlgorithm(projectId, compileId, nodeId, ConvertToDictionary(brokerageSettings), versionId, dataProviders != null ? ConvertToDictionary(dataProviders) : null);
769  }
770 
771  /// <summary>
772  /// Converts a given Python dictionary into a C# <see cref="Dictionary{string, object}"/>
773  /// </summary>
774  /// <param name="brokerageSettings">Python dictionary to be converted</param>
775  private static Dictionary<string, object> ConvertToDictionary(PyObject brokerageSettings)
776  {
777  using (Py.GIL())
778  {
779  var stringBrokerageSettings = brokerageSettings.ToString();
780  return JsonConvert.DeserializeObject<Dictionary<string, object>>(stringBrokerageSettings);
781  }
782  }
783 
784  /// <summary>
785  /// Get a list of live running algorithms for user
786  /// </summary>
787  /// <param name="status">Filter the statuses of the algorithms returned from the api</param>
788  /// <param name="startTime">Earliest launched time of the algorithms returned by the Api</param>
789  /// <param name="endTime">Latest launched time of the algorithms returned by the Api</param>
790  /// <returns><see cref="LiveList"/></returns>
791 
793  DateTime? startTime = null,
794  DateTime? endTime = null)
795  {
796  // Only the following statuses are supported by the Api
797  if (status.HasValue &&
798  status != AlgorithmStatus.Running &&
799  status != AlgorithmStatus.RuntimeError &&
800  status != AlgorithmStatus.Stopped &&
801  status != AlgorithmStatus.Liquidated)
802  {
803  throw new ArgumentException(
804  "The Api only supports Algorithm Statuses of Running, Stopped, RuntimeError and Liquidated");
805  }
806 
807  var request = new RestRequest("live/read", Method.POST)
808  {
809  RequestFormat = DataFormat.Json
810  };
811 
812  var epochStartTime = startTime == null ? 0 : Time.DateTimeToUnixTimeStamp(startTime.Value);
813  var epochEndTime = endTime == null ? Time.DateTimeToUnixTimeStamp(DateTime.UtcNow) : Time.DateTimeToUnixTimeStamp(endTime.Value);
814 
815  JObject obj = new JObject
816  {
817  { "start", epochStartTime },
818  { "end", epochEndTime }
819  };
820 
821  if (status.HasValue)
822  {
823  obj.Add("status", status.ToString());
824  }
825 
826  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
827 
828  ApiConnection.TryRequest(request, out LiveList result);
829  return result;
830  }
831 
832  /// <summary>
833  /// Read out a live algorithm in the project id specified.
834  /// </summary>
835  /// <param name="projectId">Project id to read</param>
836  /// <param name="deployId">Specific instance id to read</param>
837  /// <returns><see cref="LiveAlgorithmResults"/></returns>
838 
839  public LiveAlgorithmResults ReadLiveAlgorithm(int projectId, string deployId)
840  {
841  var request = new RestRequest("live/read", Method.POST)
842  {
843  RequestFormat = DataFormat.Json
844  };
845 
846  request.AddParameter("application/json", JsonConvert.SerializeObject(new
847  {
848  projectId,
849  deployId
850  }), ParameterType.RequestBody);
851 
852  ApiConnection.TryRequest(request, out LiveAlgorithmResults result);
853  return result;
854  }
855 
856  /// <summary>
857  /// Returns the orders of the specified project id live algorithm.
858  /// </summary>
859  /// <param name="projectId">Id of the project from which to read the live orders</param>
860  /// <param name="start">Starting index of the orders to be fetched. Required if end > 100</param>
861  /// <param name="end">Last index of the orders to be fetched. Note that end - start must be less than 100</param>
862  /// <remarks>Will throw an <see cref="WebException"/> if there are any API errors</remarks>
863  /// <returns>The list of <see cref="Order"/></returns>
864 
865  public List<ApiOrderResponse> ReadLiveOrders(int projectId, int start = 0, int end = 100)
866  {
867  var request = new RestRequest("live/read/orders", Method.POST)
868  {
869  RequestFormat = DataFormat.Json
870  };
871 
872  request.AddParameter("application/json", JsonConvert.SerializeObject(new
873  {
874  start,
875  end,
876  projectId
877  }), ParameterType.RequestBody);
878 
879  return MakeRequestOrThrow<OrdersResponseWrapper>(request, nameof(ReadLiveOrders)).Orders;
880  }
881 
882  /// <summary>
883  /// Liquidate a live algorithm from the specified project and deployId.
884  /// </summary>
885  /// <param name="projectId">Project for the live instance we want to stop</param>
886  /// <returns><see cref="RestResponse"/></returns>
887 
888  public RestResponse LiquidateLiveAlgorithm(int projectId)
889  {
890  var request = new RestRequest("live/update/liquidate", Method.POST)
891  {
892  RequestFormat = DataFormat.Json
893  };
894 
895  request.AddParameter("application/json", JsonConvert.SerializeObject(new
896  {
897  projectId
898  }), ParameterType.RequestBody);
899 
900  ApiConnection.TryRequest(request, out RestResponse result);
901  return result;
902  }
903 
904  /// <summary>
905  /// Stop a live algorithm from the specified project and deployId.
906  /// </summary>
907  /// <param name="projectId">Project for the live instance we want to stop</param>
908  /// <returns><see cref="RestResponse"/></returns>
909 
910  public RestResponse StopLiveAlgorithm(int projectId)
911  {
912  var request = new RestRequest("live/update/stop", Method.POST)
913  {
914  RequestFormat = DataFormat.Json
915  };
916 
917  request.AddParameter("application/json", JsonConvert.SerializeObject(new
918  {
919  projectId
920  }), ParameterType.RequestBody);
921 
922  ApiConnection.TryRequest(request, out RestResponse result);
923  return result;
924  }
925 
926  /// <summary>
927  /// Gets the logs of a specific live algorithm
928  /// </summary>
929  /// <param name="projectId">Project Id of the live running algorithm</param>
930  /// <param name="algorithmId">Algorithm Id of the live running algorithm</param>
931  /// <param name="startTime">No logs will be returned before this time</param>
932  /// <param name="endTime">No logs will be returned after this time</param>
933  /// <returns><see cref="LiveLog"/> List of strings that represent the logs of the algorithm</returns>
934 
935  public LiveLog ReadLiveLogs(int projectId, string algorithmId, DateTime? startTime = null, DateTime? endTime = null)
936  {
937  var epochStartTime = startTime == null ? 0 : Time.DateTimeToUnixTimeStamp(startTime.Value);
938  var epochEndTime = endTime == null ? Time.DateTimeToUnixTimeStamp(DateTime.UtcNow) : Time.DateTimeToUnixTimeStamp(endTime.Value);
939 
940  var request = new RestRequest("live/read/log", Method.POST)
941  {
942  RequestFormat = DataFormat.Json
943  };
944 
945  request.AddParameter("application/json", JsonConvert.SerializeObject(new
946  {
947  format = "json",
948  projectId,
949  algorithmId,
950  start = epochStartTime,
951  end = epochEndTime
952  }), ParameterType.RequestBody);
953 
954  ApiConnection.TryRequest(request, out LiveLog result);
955  return result;
956  }
957 
958  /// <summary>
959  /// Gets the link to the downloadable data.
960  /// </summary>
961  /// <param name="filePath">File path representing the data requested</param>
962  /// <param name="organizationId">Organization to download from</param>
963  /// <returns><see cref="DataLink"/> to the downloadable data.</returns>
964  public DataLink ReadDataLink(string filePath, string organizationId)
965  {
966  if (filePath == null)
967  {
968  throw new ArgumentException("Api.ReadDataLink(): Filepath must not be null");
969  }
970 
971  // Prepare filePath for request
972  filePath = FormatPathForDataRequest(filePath);
973 
974  var request = new RestRequest("data/read", Method.POST)
975  {
976  RequestFormat = DataFormat.Json
977  };
978 
979  request.AddParameter("application/json", JsonConvert.SerializeObject(new
980  {
981  format = "link",
982  filePath,
983  organizationId
984  }), ParameterType.RequestBody);
985 
986  ApiConnection.TryRequest(request, out DataLink result);
987  return result;
988  }
989 
990  /// <summary>
991  /// Get valid data entries for a given filepath from data/list
992  /// </summary>
993  /// <returns></returns>
994  public DataList ReadDataDirectory(string filePath)
995  {
996  if (filePath == null)
997  {
998  throw new ArgumentException("Api.ReadDataDirectory(): Filepath must not be null");
999  }
1000 
1001  // Prepare filePath for request
1002  filePath = FormatPathForDataRequest(filePath);
1003 
1004  // Verify the filePath for this request is at least three directory deep
1005  // (requirement of endpoint)
1006  if (filePath.Count(x => x == '/') < 3)
1007  {
1008  throw new ArgumentException($"Api.ReadDataDirectory(): Data directory requested must be at least" +
1009  $" three directories deep. FilePath: {filePath}");
1010  }
1011 
1012  var request = new RestRequest("data/list", Method.POST)
1013  {
1014  RequestFormat = DataFormat.Json
1015  };
1016 
1017  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1018  {
1019  filePath
1020  }), ParameterType.RequestBody);
1021 
1022  ApiConnection.TryRequest(request, out DataList result);
1023  return result;
1024  }
1025 
1026  /// <summary>
1027  /// Gets data prices from data/prices
1028  /// </summary>
1029  public DataPricesList ReadDataPrices(string organizationId)
1030  {
1031  var request = new RestRequest("data/prices", Method.POST)
1032  {
1033  RequestFormat = DataFormat.Json
1034  };
1035 
1036  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1037  {
1038  organizationId
1039  }), ParameterType.RequestBody);
1040 
1041  ApiConnection.TryRequest(request, out DataPricesList result);
1042  return result;
1043  }
1044 
1045  /// <summary>
1046  /// Read out the report of a backtest in the project id specified.
1047  /// </summary>
1048  /// <param name="projectId">Project id to read</param>
1049  /// <param name="backtestId">Specific backtest id to read</param>
1050  /// <returns><see cref="BacktestReport"/></returns>
1051  public BacktestReport ReadBacktestReport(int projectId, string backtestId)
1052  {
1053  var request = new RestRequest("backtests/read/report", Method.POST)
1054  {
1055  RequestFormat = DataFormat.Json
1056  };
1057 
1058  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1059  {
1060  backtestId,
1061  projectId
1062  }), ParameterType.RequestBody);
1063 
1064  BacktestReport report = new BacktestReport();
1065  var finish = DateTime.UtcNow.AddMinutes(1);
1066  while (DateTime.UtcNow < finish && !report.Success)
1067  {
1068  Thread.Sleep(10000);
1069  ApiConnection.TryRequest(request, out report);
1070  }
1071  return report;
1072  }
1073 
1074  /// <summary>
1075  /// Method to purchase and download data from QuantConnect
1076  /// </summary>
1077  /// <param name="filePath">File path representing the data requested</param>
1078  /// <param name="organizationId">Organization to buy the data with</param>
1079  /// <returns>A <see cref="bool"/> indicating whether the data was successfully downloaded or not.</returns>
1080 
1081  public bool DownloadData(string filePath, string organizationId)
1082  {
1083  // Get a link to the data
1084  var dataLink = ReadDataLink(filePath, organizationId);
1085 
1086  // Make sure the link was successfully retrieved
1087  if (!dataLink.Success)
1088  {
1089  Log.Trace($"Api.DownloadData(): Failed to get link for {filePath}. " +
1090  $"Errors: {string.Join(',', dataLink.Errors)}");
1091  return false;
1092  }
1093 
1094  // Make sure the directory exist before writing
1095  var directory = Path.GetDirectoryName(filePath);
1096  if (!Directory.Exists(directory))
1097  {
1098  Directory.CreateDirectory(directory);
1099  }
1100 
1101  var client = BorrowClient();
1102  try
1103  {
1104  // Download the file
1105  var uri = new Uri(dataLink.Url);
1106  using var dataStream = client.Value.GetStreamAsync(uri);
1107 
1108  using var fileStream = new FileStream(FileExtension.ToNormalizedPath(filePath), FileMode.Create);
1109  dataStream.Result.CopyTo(fileStream);
1110  }
1111  catch
1112  {
1113  Log.Error($"Api.DownloadData(): Failed to download zip for path ({filePath})");
1114  return false;
1115  }
1116  finally
1117  {
1118  ReturnClient(client);
1119  }
1120 
1121  return true;
1122  }
1123 
1124  /// <summary>
1125  /// Get the algorithm status from the user with this algorithm id.
1126  /// </summary>
1127  /// <param name="algorithmId">String algorithm id we're searching for.</param>
1128  /// <returns>Algorithm status enum</returns>
1129 
1130  public virtual AlgorithmControl GetAlgorithmStatus(string algorithmId)
1131  {
1132  return new AlgorithmControl()
1133  {
1134  ChartSubscription = "*"
1135  };
1136  }
1137 
1138  /// <summary>
1139  /// Algorithm passes back its current status to the UX.
1140  /// </summary>
1141  /// <param name="status">Status of the current algorithm</param>
1142  /// <param name="algorithmId">String algorithm id we're setting.</param>
1143  /// <param name="message">Message for the algorithm status event</param>
1144  /// <returns>Algorithm status enum</returns>
1145 
1146  public virtual void SetAlgorithmStatus(string algorithmId, AlgorithmStatus status, string message = "")
1147  {
1148  //
1149  }
1150 
1151  /// <summary>
1152  /// Send the statistics to storage for performance tracking.
1153  /// </summary>
1154  /// <param name="algorithmId">Identifier for algorithm</param>
1155  /// <param name="unrealized">Unrealized gainloss</param>
1156  /// <param name="fees">Total fees</param>
1157  /// <param name="netProfit">Net profi</param>
1158  /// <param name="holdings">Algorithm holdings</param>
1159  /// <param name="equity">Total equity</param>
1160  /// <param name="netReturn">Net return for the deployment</param>
1161  /// <param name="volume">Volume traded</param>
1162  /// <param name="trades">Total trades since inception</param>
1163  /// <param name="sharpe">Sharpe ratio since inception</param>
1164 
1165  public virtual void SendStatistics(string algorithmId, decimal unrealized, decimal fees, decimal netProfit, decimal holdings, decimal equity, decimal netReturn, decimal volume, int trades, double sharpe)
1166  {
1167  //
1168  }
1169 
1170  /// <summary>
1171  /// Send an email to the user associated with the specified algorithm id
1172  /// </summary>
1173  /// <param name="algorithmId">The algorithm id</param>
1174  /// <param name="subject">The email subject</param>
1175  /// <param name="body">The email message body</param>
1176 
1177  public virtual void SendUserEmail(string algorithmId, string subject, string body)
1178  {
1179  //
1180  }
1181 
1182  /// <summary>
1183  /// Local implementation for downloading data to algorithms
1184  /// </summary>
1185  /// <param name="address">URL to download</param>
1186  /// <param name="headers">KVP headers</param>
1187  /// <param name="userName">Username for basic authentication</param>
1188  /// <param name="password">Password for basic authentication</param>
1189  /// <returns></returns>
1190  public virtual string Download(string address, IEnumerable<KeyValuePair<string, string>> headers, string userName, string password)
1191  {
1192  return Encoding.UTF8.GetString(DownloadBytes(address, headers, userName, password));
1193  }
1194 
1195  /// <summary>
1196  /// Local implementation for downloading data to algorithms
1197  /// </summary>
1198  /// <param name="address">URL to download</param>
1199  /// <param name="headers">KVP headers</param>
1200  /// <param name="userName">Username for basic authentication</param>
1201  /// <param name="password">Password for basic authentication</param>
1202  /// <returns>A stream from which the data can be read</returns>
1203  /// <remarks>Stream.Close() most be called to avoid running out of resources</remarks>
1204  public virtual byte[] DownloadBytes(string address, IEnumerable<KeyValuePair<string, string>> headers, string userName, string password)
1205  {
1206  var client = BorrowClient();
1207  try
1208  {
1209  client.Value.DefaultRequestHeaders.Clear();
1210 
1211  // Add a user agent header in case the requested URI contains a query.
1212  client.Value.DefaultRequestHeaders.TryAddWithoutValidation("user-agent", "QCAlgorithm.Download(): User Agent Header");
1213 
1214  if (headers != null)
1215  {
1216  foreach (var header in headers)
1217  {
1218  client.Value.DefaultRequestHeaders.Add(header.Key, header.Value);
1219  }
1220  }
1221 
1222  if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1223  {
1224  var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}"));
1225  client.Value.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
1226  }
1227 
1228  return client.Value.GetByteArrayAsync(new Uri(address)).Result;
1229  }
1230  catch (Exception exception)
1231  {
1232  var message = $"Api.DownloadBytes(): Failed to download data from {address}";
1233  if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1234  {
1235  message += $" with username: {userName} and password {password}";
1236  }
1237 
1238  throw new WebException($"{message}. Please verify the source for missing http:// or https://", exception);
1239  }
1240  finally
1241  {
1242  client.Value.DefaultRequestHeaders.Clear();
1243  ReturnClient(client);
1244  }
1245  }
1246 
1247  /// <summary>
1248  /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
1249  /// </summary>
1250  /// <filterpriority>2</filterpriority>
1251  public virtual void Dispose()
1252  {
1253  // Dispose of the HttpClient pool
1254  _clientPool.CompleteAdding();
1255  foreach (var client in _clientPool.GetConsumingEnumerable())
1256  {
1257  if (client.IsValueCreated)
1258  {
1259  client.Value.DisposeSafely();
1260  }
1261  }
1262  _clientPool.DisposeSafely();
1263  }
1264 
1265  /// <summary>
1266  /// Generate a secure hash for the authorization headers.
1267  /// </summary>
1268  /// <returns>Time based hash of user token and timestamp.</returns>
1269  public static string CreateSecureHash(int timestamp, string token)
1270  {
1271  // Create a new hash using current UTC timestamp.
1272  // Hash must be generated fresh each time.
1273  var data = $"{token}:{timestamp.ToStringInvariant()}";
1274  return data.ToSHA256();
1275  }
1276 
1277  /// <summary>
1278  /// Will read the organization account status
1279  /// </summary>
1280  /// <param name="organizationId">The target organization id, if null will return default organization</param>
1281  public Account ReadAccount(string organizationId = null)
1282  {
1283  var request = new RestRequest("account/read", Method.POST)
1284  {
1285  RequestFormat = DataFormat.Json
1286  };
1287 
1288  if (organizationId != null)
1289  {
1290  request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody);
1291  }
1292 
1293  ApiConnection.TryRequest(request, out Account account);
1294  return account;
1295  }
1296 
1297  /// <summary>
1298  /// Fetch organization data from web API
1299  /// </summary>
1300  /// <param name="organizationId"></param>
1301  /// <returns></returns>
1302  public Organization ReadOrganization(string organizationId = null)
1303  {
1304  var request = new RestRequest("organizations/read", Method.POST)
1305  {
1306  RequestFormat = DataFormat.Json
1307  };
1308 
1309  if (organizationId != null)
1310  {
1311  request.AddParameter("application/json", JsonConvert.SerializeObject(new { organizationId }), ParameterType.RequestBody);
1312  }
1313 
1314  ApiConnection.TryRequest(request, out OrganizationResponse response);
1315  return response.Organization;
1316  }
1317 
1318  /// <summary>
1319  /// Estimate optimization with the specified parameters via QuantConnect.com API
1320  /// </summary>
1321  /// <param name="projectId">Project ID of the project the optimization belongs to</param>
1322  /// <param name="name">Name of the optimization</param>
1323  /// <param name="target">Target of the optimization, see examples in <see cref="PortfolioStatistics"/></param>
1324  /// <param name="targetTo">Target extremum of the optimization, for example "max" or "min"</param>
1325  /// <param name="targetValue">Optimization target value</param>
1326  /// <param name="strategy">Optimization strategy, <see cref="GridSearchOptimizationStrategy"/></param>
1327  /// <param name="compileId">Optimization compile ID</param>
1328  /// <param name="parameters">Optimization parameters</param>
1329  /// <param name="constraints">Optimization constraints</param>
1330  /// <returns>Estimate object from the API.</returns>
1332  int projectId,
1333  string name,
1334  string target,
1335  string targetTo,
1336  decimal? targetValue,
1337  string strategy,
1338  string compileId,
1339  HashSet<OptimizationParameter> parameters,
1340  IReadOnlyList<Constraint> constraints)
1341  {
1342  var request = new RestRequest("optimizations/estimate", Method.POST)
1343  {
1344  RequestFormat = DataFormat.Json
1345  };
1346 
1347  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1348  {
1349  projectId,
1350  name,
1351  target,
1352  targetTo,
1353  targetValue,
1354  strategy,
1355  compileId,
1356  parameters,
1357  constraints
1358  }), ParameterType.RequestBody);
1359 
1360  ApiConnection.TryRequest(request, out EstimateResponseWrapper response);
1361  return response.Estimate;
1362  }
1363 
1364  /// <summary>
1365  /// Create an optimization with the specified parameters via QuantConnect.com API
1366  /// </summary>
1367  /// <param name="projectId">Project ID of the project the optimization belongs to</param>
1368  /// <param name="name">Name of the optimization</param>
1369  /// <param name="target">Target of the optimization, see examples in <see cref="PortfolioStatistics"/></param>
1370  /// <param name="targetTo">Target extremum of the optimization, for example "max" or "min"</param>
1371  /// <param name="targetValue">Optimization target value</param>
1372  /// <param name="strategy">Optimization strategy, <see cref="GridSearchOptimizationStrategy"/></param>
1373  /// <param name="compileId">Optimization compile ID</param>
1374  /// <param name="parameters">Optimization parameters</param>
1375  /// <param name="constraints">Optimization constraints</param>
1376  /// <param name="estimatedCost">Estimated cost for optimization</param>
1377  /// <param name="nodeType">Optimization node type <see cref="OptimizationNodes"/></param>
1378  /// <param name="parallelNodes">Number of parallel nodes for optimization</param>
1379  /// <returns>BaseOptimization object from the API.</returns>
1381  int projectId,
1382  string name,
1383  string target,
1384  string targetTo,
1385  decimal? targetValue,
1386  string strategy,
1387  string compileId,
1388  HashSet<OptimizationParameter> parameters,
1389  IReadOnlyList<Constraint> constraints,
1390  decimal estimatedCost,
1391  string nodeType,
1392  int parallelNodes)
1393  {
1394  var request = new RestRequest("optimizations/create", Method.POST)
1395  {
1396  RequestFormat = DataFormat.Json
1397  };
1398 
1399  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1400  {
1401  projectId,
1402  name,
1403  target,
1404  targetTo,
1405  targetValue,
1406  strategy,
1407  compileId,
1408  parameters,
1409  constraints,
1410  estimatedCost,
1411  nodeType,
1412  parallelNodes
1413  }), ParameterType.RequestBody);
1414 
1415  ApiConnection.TryRequest(request, out OptimizationList result);
1416  return result.Optimizations.FirstOrDefault();
1417  }
1418 
1419  /// <summary>
1420  /// List all the optimizations for a project
1421  /// </summary>
1422  /// <param name="projectId">Project id we'd like to get a list of optimizations for</param>
1423  /// <returns>A list of BaseOptimization objects, <see cref="BaseOptimization"/></returns>
1424  public List<BaseOptimization> ListOptimizations(int projectId)
1425  {
1426  var request = new RestRequest("optimizations/list", Method.POST)
1427  {
1428  RequestFormat = DataFormat.Json
1429  };
1430 
1431  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1432  {
1433  projectId,
1434  }), ParameterType.RequestBody);
1435 
1436  ApiConnection.TryRequest(request, out OptimizationList result);
1437  return result.Optimizations;
1438  }
1439 
1440  /// <summary>
1441  /// Read an optimization
1442  /// </summary>
1443  /// <param name="optimizationId">Optimization id for the optimization we want to read</param>
1444  /// <returns><see cref="Optimization"/></returns>
1445  public Optimization ReadOptimization(string optimizationId)
1446  {
1447  var request = new RestRequest("optimizations/read", Method.POST)
1448  {
1449  RequestFormat = DataFormat.Json
1450  };
1451 
1452  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1453  {
1454  optimizationId
1455  }), ParameterType.RequestBody);
1456 
1457  ApiConnection.TryRequest(request, out OptimizationResponseWrapper response);
1458  return response.Optimization;
1459  }
1460 
1461  /// <summary>
1462  /// Abort an optimization
1463  /// </summary>
1464  /// <param name="optimizationId">Optimization id for the optimization we want to abort</param>
1465  /// <returns><see cref="RestResponse"/></returns>
1466  public RestResponse AbortOptimization(string optimizationId)
1467  {
1468  var request = new RestRequest("optimizations/abort", Method.POST)
1469  {
1470  RequestFormat = DataFormat.Json
1471  };
1472 
1473  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1474  {
1475  optimizationId
1476  }), ParameterType.RequestBody);
1477 
1478  ApiConnection.TryRequest(request, out RestResponse result);
1479  return result;
1480  }
1481 
1482  /// <summary>
1483  /// Update an optimization
1484  /// </summary>
1485  /// <param name="optimizationId">Optimization id we want to update</param>
1486  /// <param name="name">Name we'd like to assign to the optimization</param>
1487  /// <returns><see cref="RestResponse"/></returns>
1488  public RestResponse UpdateOptimization(string optimizationId, string name = null)
1489  {
1490  var request = new RestRequest("optimizations/update", Method.POST)
1491  {
1492  RequestFormat = DataFormat.Json
1493  };
1494 
1495  var obj = new JObject
1496  {
1497  { "optimizationId", optimizationId }
1498  };
1499 
1500  if (name.HasValue())
1501  {
1502  obj.Add("name", name);
1503  }
1504 
1505  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1506 
1507  ApiConnection.TryRequest(request, out RestResponse result);
1508  return result;
1509  }
1510 
1511  /// <summary>
1512  /// Delete an optimization
1513  /// </summary>
1514  /// <param name="optimizationId">Optimization id for the optimization we want to delete</param>
1515  /// <returns><see cref="RestResponse"/></returns>
1516  public RestResponse DeleteOptimization(string optimizationId)
1517  {
1518  var request = new RestRequest("optimizations/delete", Method.POST)
1519  {
1520  RequestFormat = DataFormat.Json
1521  };
1522 
1523  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1524  {
1525  optimizationId
1526  }), ParameterType.RequestBody);
1527 
1528  ApiConnection.TryRequest(request, out RestResponse result);
1529  return result;
1530  }
1531 
1532  /// <summary>
1533  /// Download the object store files associated with the given organization ID and key
1534  /// </summary>
1535  /// <param name="organizationId">Organization ID we would like to get the Object Store files from</param>
1536  /// <param name="keys">Keys for the Object Store files</param>
1537  /// <param name="destinationFolder">Folder in which the object store files will be stored</param>
1538  /// <returns>True if the object store files were retrieved correctly, false otherwise</returns>
1539  public bool GetObjectStore(string organizationId, List<string> keys, string destinationFolder = null)
1540  {
1541  var request = new RestRequest("object/get", Method.POST)
1542  {
1543  RequestFormat = DataFormat.Json
1544  };
1545 
1546  request.AddParameter("application/json", JsonConvert.SerializeObject(new
1547  {
1548  organizationId,
1549  keys
1550  }), ParameterType.RequestBody);
1551 
1552  ApiConnection.TryRequest(request, out GetObjectStoreResponse result);
1553 
1554  if (result == null || !result.Success)
1555  {
1556  Log.Error($"Api.GetObjectStore(): Failed to get the jobId to request the download URL for the object store files."
1557  + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1558  return false;
1559  }
1560 
1561  var jobId = result.JobId;
1562  var getUrlRequest = new RestRequest("object/get", Method.POST)
1563  {
1564  RequestFormat = DataFormat.Json
1565  };
1566  getUrlRequest.AddParameter("application/json", JsonConvert.SerializeObject(new
1567  {
1568  organizationId,
1569  jobId
1570  }), ParameterType.RequestBody);
1571 
1572  var frontier = DateTime.UtcNow + TimeSpan.FromMinutes(5);
1573  while (string.IsNullOrEmpty(result?.Url) && (DateTime.UtcNow < frontier))
1574  {
1575  Thread.Sleep(3000);
1576  ApiConnection.TryRequest(getUrlRequest, out result);
1577  }
1578 
1579  if (result == null || string.IsNullOrEmpty(result.Url))
1580  {
1581  Log.Error($"Api.GetObjectStore(): Failed to get the download URL from the jobId {jobId}."
1582  + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1583  return false;
1584  }
1585 
1586  var directory = destinationFolder ?? Directory.GetCurrentDirectory();
1587  var client = BorrowClient();
1588 
1589  try
1590  {
1591  if (client.Value.Timeout != TimeSpan.FromMinutes(20))
1592  {
1593  client.Value.Timeout = TimeSpan.FromMinutes(20);
1594  }
1595 
1596  // Download the file
1597  var uri = new Uri(result.Url);
1598  using var byteArray = client.Value.GetByteArrayAsync(uri);
1599 
1600  Compression.UnzipToFolder(byteArray.Result, directory);
1601  }
1602  catch (Exception e)
1603  {
1604  Log.Error($"Api.GetObjectStore(): Failed to download zip for path ({directory}). Error: {e.Message}");
1605  return false;
1606  }
1607  finally
1608  {
1609  ReturnClient(client);
1610  }
1611 
1612  return true;
1613  }
1614 
1615  /// <summary>
1616  /// Get Object Store properties given the organization ID and the Object Store key
1617  /// </summary>
1618  /// <param name="organizationId">Organization ID we would like to get the Object Store from</param>
1619  /// <param name="key">Key for the Object Store file</param>
1620  /// <returns><see cref="PropertiesObjectStoreResponse"/></returns>
1621  /// <remarks>It does not work when the object store is a directory</remarks>
1622  public PropertiesObjectStoreResponse GetObjectStoreProperties(string organizationId, string key)
1623  {
1624  var request = new RestRequest("object/properties", Method.POST)
1625  {
1626  RequestFormat = DataFormat.Json
1627  };
1628 
1629  request.AddParameter("organizationId", organizationId);
1630  request.AddParameter("key", key);
1631 
1632  ApiConnection.TryRequest(request, out PropertiesObjectStoreResponse result);
1633 
1634  if (result == null || !result.Success)
1635  {
1636  Log.Error($"Api.ObjectStore(): Failed to get the properties for the object store key {key}." + (result != null ? $" Errors: {string.Join(",", result.Errors)}" : ""));
1637  }
1638  return result;
1639  }
1640 
1641  /// <summary>
1642  /// Upload files to the Object Store
1643  /// </summary>
1644  /// <param name="organizationId">Organization ID we would like to upload the file to</param>
1645  /// <param name="key">Key to the Object Store file</param>
1646  /// <param name="objectData">File (as an array of bytes) to be uploaded</param>
1647  /// <returns><see cref="RestResponse"/></returns>
1648  public RestResponse SetObjectStore(string organizationId, string key, byte[] objectData)
1649  {
1650  var request = new RestRequest("object/set", Method.POST)
1651  {
1652  RequestFormat = DataFormat.Json
1653  };
1654 
1655  request.AddParameter("organizationId", organizationId);
1656  request.AddParameter("key", key);
1657  request.AddFileBytes("objectData", objectData, "objectData");
1658  request.AlwaysMultipartFormData = true;
1659 
1660  ApiConnection.TryRequest(request, out RestResponse result);
1661  return result;
1662  }
1663 
1664  /// <summary>
1665  /// Request to delete Object Store metadata of a specific organization and key
1666  /// </summary>
1667  /// <param name="organizationId">Organization ID we would like to delete the Object Store file from</param>
1668  /// <param name="key">Key to the Object Store file</param>
1669  /// <returns><see cref="RestResponse"/></returns>
1670  public RestResponse DeleteObjectStore(string organizationId, string key)
1671  {
1672  var request = new RestRequest("object/delete", Method.POST)
1673  {
1674  RequestFormat = DataFormat.Json
1675  };
1676 
1677  var obj = new Dictionary<string, object>
1678  {
1679  { "organizationId", organizationId },
1680  { "key", key }
1681  };
1682 
1683  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1684 
1685  ApiConnection.TryRequest(request, out RestResponse result);
1686  return result;
1687  }
1688 
1689  /// <summary>
1690  /// Request to list Object Store files of a specific organization and path
1691  /// </summary>
1692  /// <param name="organizationId">Organization ID we would like to list the Object Store files from</param>
1693  /// <param name="path">Path to the Object Store files</param>
1694  /// <returns><see cref="ListObjectStoreResponse"/></returns>
1695  public ListObjectStoreResponse ListObjectStore(string organizationId, string path)
1696  {
1697  var request = new RestRequest("object/list", Method.POST)
1698  {
1699  RequestFormat = DataFormat.Json
1700  };
1701 
1702  var obj = new Dictionary<string, object>
1703  {
1704  { "organizationId", organizationId },
1705  { "path", path }
1706  };
1707 
1708  request.AddParameter("application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1709 
1710  ApiConnection.TryRequest(request, out ListObjectStoreResponse result);
1711  return result;
1712  }
1713 
1714  /// <summary>
1715  /// Helper method to normalize path for api data requests
1716  /// </summary>
1717  /// <param name="filePath">Filepath to format</param>
1718  /// <param name="dataFolder">The data folder to use</param>
1719  /// <returns>Normalized path</returns>
1720  public static string FormatPathForDataRequest(string filePath, string dataFolder = null)
1721  {
1722  if (filePath == null)
1723  {
1724  Log.Error("Api.FormatPathForDataRequest(): Cannot format null string");
1725  return null;
1726  }
1727 
1728  dataFolder ??= Globals.DataFolder;
1729  // Normalize windows paths to linux format
1730  dataFolder = dataFolder.Replace("\\", "/", StringComparison.InvariantCulture);
1731  filePath = filePath.Replace("\\", "/", StringComparison.InvariantCulture);
1732 
1733  // First remove data root directory from path for request if included
1734  if (filePath.StartsWith(dataFolder, StringComparison.InvariantCulture))
1735  {
1736  filePath = filePath.Substring(dataFolder.Length);
1737  }
1738 
1739  // Trim '/' from start, this can cause issues for _dataFolders without final directory separator in the config
1740  filePath = filePath.TrimStart('/');
1741  return filePath;
1742  }
1743 
1744  /// <summary>
1745  /// Helper method that will execute the given api request and throw an exception if it fails
1746  /// </summary>
1747  private T MakeRequestOrThrow<T>(RestRequest request, string callerName)
1748  where T : RestResponse
1749  {
1750  if (!ApiConnection.TryRequest(request, out T result))
1751  {
1752  var errors = string.Empty;
1753  if (result != null && result.Errors != null && result.Errors.Count > 0)
1754  {
1755  errors = $". Errors: ['{string.Join(",", result.Errors)}']";
1756  }
1757  throw new WebException($"{callerName} api request failed{errors}");
1758  }
1759 
1760  return result;
1761  }
1762 
1763  /// <summary>
1764  /// Borrows and HTTP client from the pool
1765  /// </summary>
1766  private Lazy<HttpClient> BorrowClient()
1767  {
1768  using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(10));
1769  return _clientPool.Take(cancellationTokenSource.Token);
1770  }
1771 
1772  /// <summary>
1773  /// Returns the HTTP client to the pool
1774  /// </summary>
1775  private void ReturnClient(Lazy<HttpClient> client)
1776  {
1777  _clientPool.Add(client);
1778  }
1779  }
1780 }