/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using Newtonsoft.Json.Linq; using QuantConnect.Interfaces; using System; using System.Collections.Generic; using System.IO; using System.Net.Http; namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports { /// /// Exports signals of the desired positions to CrunchDAO API. /// Accepts signals in percentage i.e ticker:"SPY", date: "2020-10-04", signal:0.54 /// public class CrunchDAOSignalExport : BaseSignalExport { /// /// CrunchDAO API key /// private readonly string _apiKey; /// /// CrunchDAO API endpoint /// private readonly Uri _destination; /// /// Model ID or Name /// private readonly string _model; /// /// Submission Name (Optional) /// private readonly string _submissionName; /// /// Comment (Optional) /// private readonly string _comment; /// /// Algorithm being ran /// private IAlgorithm _algorithm; /// /// HashSet of allowed SecurityTypes for CrunchDAO /// private readonly HashSet _allowedSecurityTypes = new() { SecurityType.Equity }; /// /// The name of this signal export /// protected override string Name { get; } = "CrunchDAO"; /// /// HashSet property of allowed SecurityTypes for CrunchDAO /// protected override HashSet AllowedSecurityTypes => _allowedSecurityTypes; /// /// CrunchDAOSignalExport constructor. It obtains the required information for CrunchDAO API requests. /// See (https://colab.research.google.com/drive/1YW1xtHrIZ8ZHW69JvNANWowmxPcnkNu0?authuser=1#scrollTo=aPyWNxtuDc-X) /// /// API key provided by CrunchDAO /// Model ID or Name /// Submission Name (Optional) /// Comment (Optional) public CrunchDAOSignalExport(string apiKey, string model, string submissionName = "", string comment = "") { _apiKey = apiKey; _model = model; _submissionName = submissionName; _comment = comment; _destination = new Uri($"https://api.tournament.crunchdao.com/v3/alpha-submissions?apiKey={apiKey}"); } /// /// Verifies every holding is a stock, creates a message with the desired positions /// using the expected CrunchDAO API format, verifies there is an open round and then /// sends the positions with the other required body features. If another signal was /// submitted before, it deletes the last signal and sends the new one /// A list of holdings from the portfolio, /// expected to be sent to CrunchDAO API and the algorithm being ran /// True if the positions were sent to CrunchDAO succesfully and errors were returned, false otherwise public override bool Send(SignalExportTargetParameters parameters) { if (!base.Send(parameters)) { return false; } if (!ConvertToCSVFormat(parameters, out string positions)) { return false; } if (!GetCurrentRoundID(out int currentRoundId)) { return false; } if (GetLastSubmissionId(currentRoundId, out int lastSubmissionId)) { _algorithm.Debug($"You have already submitted a signal for round {currentRoundId}. Your last submission is going to be overwritten with the new one"); if (!DeleteLastSubmission(lastSubmissionId)) { return false; } } var result = SendPositions(positions); return result; } /// /// Converts the list of holdings into a CSV format string /// /// A list of holdings from the portfolio, /// expected to be sent to CrunchDAO API and the algorithm being ran /// A CSV format string of the given holdings with the required features(ticker, date, signal) /// True if a string message with the positions could be obtained, false otherwise protected bool ConvertToCSVFormat(SignalExportTargetParameters parameters, out string positions) { var holdings = parameters.Targets; _algorithm = parameters.Algorithm; positions = "ticker,date,signal\n"; foreach (var holding in holdings) { if (holding.Quantity < 0 || holding.Quantity > 1) { _algorithm.Error($"All signals must be between 0 and 1, but {holding.Symbol.Value} signal was {holding.Quantity}"); return false; } positions += $"{_algorithm.Ticker(holding.Symbol)},{_algorithm.Securities[holding.Symbol].LocalTime.ToString("yyyy-MM-dd")},{holding.Quantity.ToStringInvariant()}\n"; } return true; } /// /// Sends the desired positions, with the other required features, to CrunchDAO API using a POST request. It logs /// the message retrieved by the API if there was a HttpRequestException /// /// A CSV format string of the given holdings with the required features /// True if the positions were sent to CrunchDAO successfully and errors were returned. False, otherwise private bool SendPositions(string positions) { // Create positions stream var positionsStream = new MemoryStream(); using var writer = new StreamWriter(positionsStream); writer.Write(positions); writer.Flush(); positionsStream.Position = 0; // Create the required body features for the POST request using var file = new StreamContent(positionsStream); using var model = new StringContent(_model); using var submissionName = new StringContent(_submissionName); using var comment = new StringContent(_comment); // Crete the httpMessage to be sent and add the different POST request body features using var httpMessage = new MultipartFormDataContent { { model, "model" }, { submissionName, "label" }, { comment, "comment" }, { file, "file", "submission.csv" } }; // Send the httpMessage using HttpResponseMessage response = HttpClient.PostAsync(_destination, httpMessage).Result; if (response.StatusCode == System.Net.HttpStatusCode.Locked || response.StatusCode == System.Net.HttpStatusCode.Forbidden) { var responseContent = response.Content.ReadAsStringAsync().Result; var parsedResponseContent = JObject.Parse(responseContent); _algorithm.Error($"CrunchDAO API returned code: {parsedResponseContent["code"]} message:{parsedResponseContent["message"]}"); return false; } if (!response.IsSuccessStatusCode) { _algorithm.Error($"CrunchDAO API returned HttpRequestException {response.StatusCode}"); return false; } return true; } /// /// Checks if there is an open round, if so it assigns the current round ID to currentRoundId /// parameter /// /// Current round ID /// True if there is an open round, false otherwise private bool GetCurrentRoundID(out int currentRoundId) { // Assign a default value to currentRoundId currentRoundId = -1; using HttpResponseMessage roundIdResponse = HttpClient.GetAsync("https://api.tournament.crunchdao.com/v2/rounds/@current").Result; if (roundIdResponse.StatusCode == System.Net.HttpStatusCode.NotFound) { var responseContent = roundIdResponse.Content.ReadAsStringAsync().Result; var parsedResponseContent = JObject.Parse(responseContent); _algorithm.Error($"CrunchDAO API returned code: {parsedResponseContent["code"]} message:{parsedResponseContent["message"]}"); return false; } else if (!roundIdResponse.IsSuccessStatusCode) { _algorithm.Error($"CrunchDAO API returned HttpRequestException {roundIdResponse.StatusCode}"); return false; } var roundIdResponseContent = roundIdResponse.Content.ReadAsStringAsync().Result; currentRoundId = (int)(JObject.Parse(roundIdResponseContent)["id"]); return true; } /// /// Checks if there is a submission for the current round, if so it assigns its ID to /// lastSubmissionId /// /// Current round ID /// Last submission ID (for the current round) /// True if there's a submission for the current round, false otherwise private bool GetLastSubmissionId(int currentRoundId, out int lastSubmissionId) { using HttpResponseMessage submissionIdResponse = HttpClient.GetAsync($"https://tournament.crunchdao.com/api/v3/alpha-submissions?includeAll=false&roundId={currentRoundId}&apiKey={_apiKey}").Result; if (!submissionIdResponse.IsSuccessStatusCode) { _algorithm.Error($"CrunchDAO API returned the following Error Code: {submissionIdResponse.StatusCode}"); lastSubmissionId = -1; return false; } var submissionIdResponseContent = submissionIdResponse.Content.ReadAsStringAsync().Result; var parsedSubmissionIdResponseContent = JArray.Parse(submissionIdResponseContent); if (!parsedSubmissionIdResponseContent.HasValues) { // Default value for lastSubmissionId lastSubmissionId = -1; return false; } lastSubmissionId = (int)parsedSubmissionIdResponseContent[0]["id"]; return true; } /// /// Deletes the last submission for the current round /// /// Last submission ID for the current round /// True if the last submission could be deleted sucessfully, false otherwise private bool DeleteLastSubmission(int lastSubmissionId) { using HttpResponseMessage deleteSubmissionResponse = HttpClient.DeleteAsync($"https://tournament.crunchdao.com/api/v3/alpha-submissions/{lastSubmissionId}?&apiKey={_apiKey}").Result; if (!deleteSubmissionResponse.IsSuccessStatusCode) { var responseContent = deleteSubmissionResponse.Content.ReadAsStringAsync().Result; var parsedResponseContent = JObject.Parse(responseContent); _algorithm.Error($"CrunchDAO API returned code: {parsedResponseContent["code"]} message:{parsedResponseContent["message"]}. Last submission could not be deleted"); return false; } _algorithm.Debug($"Last submission has been deleted"); return true; } } }