/*
* 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;
using Newtonsoft.Json.Linq;
using QuantConnect.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
{
///
/// Exports signals of the desired positions to Numerai API.
/// Accepts signals in percentage i.e numerai_ticker:"IBM US", signal:0.234
///
/// It does not take into account flags as
/// NUMERAI_COMPUTE_ID (https://github.com/numerai/numerapi/blob/master/numerapi/signalsapi.py#L164) and
/// TRIGGER_ID(https://github.com/numerai/numerapi/blob/master/numerapi/signalsapi.py#L164)
public class NumeraiSignalExport : BaseSignalExport
{
///
/// Numerai API submission endpoint
///
private readonly Uri _destination;
///
/// PUBLIC_ID provided by Numerai
///
private readonly string _publicId;
///
/// SECRET_ID provided by Numerai
///
private readonly string _secretId;
///
/// ID of the Numerai Model being used
///
private readonly string _modelId;
///
/// Signal file's name
///
private readonly string _fileName;
///
/// Algorithm being ran
///
private IAlgorithm _algorithm;
///
/// Dictionary to obtain corresponding Numerai Market name for the given LEAN market name
///
private readonly Dictionary _numeraiMarketFormat = new() // There can be stocks from other markets
{
{Market.USA, "US" },
{Market.SGX, "SP" }
};
///
/// Hashset of Numerai allowed SecurityTypes
///
private readonly HashSet _allowedSecurityTypes = new()
{
SecurityType.Equity
};
///
/// The name of this signal export
///
protected override string Name { get; } = "Numerai";
///
/// Hashset property of Numerai allowed SecurityTypes
///
protected override HashSet AllowedSecurityTypes => _allowedSecurityTypes;
///
/// NumeraiSignalExport Constructor. It obtains the required information for Numerai API requests
///
/// PUBLIC_ID provided by Numerai
/// SECRET_ID provided by Numerai
/// ID of the Numerai Model being used
/// Signal file's name
public NumeraiSignalExport(string publicId, string secretId, string modelId, string fileName = "predictions.csv")
{
_destination = new Uri("https://api-tournament.numer.ai");
_publicId = publicId;
_secretId = secretId;
_modelId = modelId;
_fileName = fileName;
}
///
/// Verifies all the given holdings are accepted by Numerai, creates a message with those holdings in the expected
/// Numerai API format and sends them to Numerai API
///
/// A list of portfolio holdings expected to be sent to Numerai API and the algorithm being ran
/// True if the positions were sent to Numerai API correctly and no errors were returned, false otherwise
public override bool Send(SignalExportTargetParameters parameters)
{
if (!base.Send(parameters))
{
return false;
}
_algorithm = parameters.Algorithm;
if (parameters.Targets.Count < 10)
{
_algorithm.Error($"Numerai Signals API accepts minimum 10 different signals, just found {parameters.Targets.Count}");
return false;
}
if (!ConvertTargetsToNumerai(parameters, out string positions))
{
return false;
}
var result = SendPositions(positions);
return result;
}
///
/// Verifies each holding's signal is between 0 and 1 (exclusive)
///
/// A list of portfolio holdings expected to be sent to Numerai API
/// A message with the desired positions in the expected Numerai API format
/// True if a string message with the positions could be obtained, false otherwise
protected bool ConvertTargetsToNumerai(SignalExportTargetParameters parameters, out string positions)
{
positions = "numerai_ticker,signal\n";
foreach ( var holding in parameters.Targets)
{
if (holding.Quantity <= 0 || holding.Quantity >= 1)
{
_algorithm.Error($"All signals must be between 0 and 1 (exclusive), but {holding.Symbol.Value} signal was {holding.Quantity}");
return false;
}
positions += $"{parameters.Algorithm.Ticker(holding.Symbol)} {_numeraiMarketFormat[holding.Symbol.ID.Market]},{holding.Quantity.ToStringInvariant()}\n";
}
return true;
}
///
/// Sends the given positions message to Numerai API. It first sends an authentication POST request then a
/// PUT request to put the positions in certain endpoint and finally sends a submission POST request
///
/// A message with the desired positions in the expected Numerai API format
/// True if the positions were sent to Numerai API correctly and no errors were returned, false otherwise
private bool SendPositions(string positions)
{
// AUTHENTICATION REQUEST
var authQuery = @"query($filename: String!
$modelId: String) {
submissionUploadSignalsAuth(filename: $filename
modelId: $modelId) {
filename
url
}
}";
var arguments = new
{
filename = _fileName,
modelId = _modelId
};
var argumentsMessage = JsonConvert.SerializeObject(arguments);
using var variables = new StringContent(argumentsMessage, Encoding.UTF8, "application/json");
using var query = new StringContent(authQuery, Encoding.UTF8, "application/json");
var httpMessage = new MultipartFormDataContent
{
{ query, "query"},
{ variables, "variables" }
};
using var authRequest = new HttpRequestMessage(HttpMethod.Post, _destination);
authRequest.Headers.Add("Accept", "application/json");
authRequest.Headers.Add("Authorization", $"Token {_publicId}${_secretId}");
authRequest.Content = httpMessage;
var response = HttpClient.SendAsync(authRequest).Result;
var responseContent = response.Content.ReadAsStringAsync().Result;
if (!response.IsSuccessStatusCode)
{
_algorithm.Error($"Numerai API returned HttpRequestException {response.StatusCode}");
return false;
}
var parsedResponseContent = JObject.Parse(responseContent);
if (!parsedResponseContent["data"]["submissionUploadSignalsAuth"].HasValues)
{
_algorithm.Error($"Numerai API returned the following errors: {string.Join(",", parsedResponseContent["errors"])}");
return false;
}
var putUrl = new Uri((string)parsedResponseContent["data"]["submissionUploadSignalsAuth"]["url"]);
var submissionFileName = (string)parsedResponseContent["data"]["submissionUploadSignalsAuth"]["filename"];
// PUT REQUEST
// Create positions stream
var positionsStream = new MemoryStream();
using var writer = new StreamWriter(positionsStream);
writer.Write(positions);
writer.Flush();
positionsStream.Position = 0;
using var putRequest = new HttpRequestMessage(HttpMethod.Put, putUrl)
{
Content = new StreamContent(positionsStream)
};
var putResponse = HttpClient.SendAsync(putRequest).Result;
// SUBMISSION REQUEST
var createQuery = @"mutation($filename: String!
$modelId: String
$triggerId: String) {
createSignalsSubmission(filename: $filename
modelId: $modelId
triggerId: $triggerId
source: ""numerapi"") {
id
firstEffectiveDate
}
}";
var createArguments = new
{
filename = submissionFileName,
modelId = _modelId
};
var createArgumentsMessage = JsonConvert.SerializeObject(createArguments);
using var submissionQuery = new StringContent(createQuery, Encoding.UTF8, "application/json");
using var submissionVariables = new StringContent(createArgumentsMessage, Encoding.UTF8, "application/json");
var submissionMessage = new MultipartFormDataContent
{
{submissionQuery, "query"},
{submissionVariables, "variables"}
};
using var submissionRequest = new HttpRequestMessage(HttpMethod.Post, _destination);
submissionRequest.Headers.Add("Authorization", $"Token {_publicId}${_secretId}");
submissionRequest.Content = submissionMessage;
var submissionResponse = HttpClient.SendAsync(submissionRequest).Result;
var submissionResponseContent = submissionResponse.Content.ReadAsStringAsync().Result;
if (!submissionResponse.IsSuccessStatusCode)
{
_algorithm.Error($"Numerai API returned HttpRequestException {submissionResponseContent}");
return false;
}
var parsedSubmissionResponseContent = JObject.Parse(submissionResponseContent);
if (!parsedSubmissionResponseContent["data"]["createSignalsSubmission"].HasValues)
{
_algorithm.Error($"Numerai API returned the following errors: {string.Join(",", parsedSubmissionResponseContent["errors"])}");
return false;
}
return true;
}
}
}