/*
* 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;
}
}
}