chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 QuantConnect.Interfaces;
|
||||
using QuantConnect.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class to send signals to different 3rd party API's
|
||||
/// </summary>
|
||||
public abstract class BaseSignalExport : ISignalExportTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Lazy initialization of HttpClient to be used to sent signals to different 3rd party API's
|
||||
/// </summary>
|
||||
private Lazy<HttpClient> _lazyClient = new Lazy<HttpClient>();
|
||||
|
||||
/// <summary>
|
||||
/// List of all SecurityTypes present in LEAN
|
||||
/// </summary>
|
||||
private HashSet<SecurityType> _defaultAllowedSecurityTypes = new HashSet<SecurityType>
|
||||
{
|
||||
SecurityType.Equity,
|
||||
SecurityType.Forex,
|
||||
SecurityType.Option,
|
||||
SecurityType.Future,
|
||||
SecurityType.FutureOption,
|
||||
SecurityType.Crypto,
|
||||
SecurityType.CryptoFuture,
|
||||
SecurityType.Cfd,
|
||||
SecurityType.IndexOption,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The name of this signal export
|
||||
/// </summary>
|
||||
protected abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Property to access a HttpClient
|
||||
/// </summary>
|
||||
|
||||
protected HttpClient HttpClient => _lazyClient.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Default hashset of allowed Security types
|
||||
/// </summary>
|
||||
protected virtual HashSet<SecurityType> AllowedSecurityTypes
|
||||
{
|
||||
get => _defaultAllowedSecurityTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends positions to different 3rd party API's
|
||||
/// </summary>
|
||||
/// <param name="parameters">Holdings the user have defined to be sent to certain 3rd party API and the algorithm being ran</param>
|
||||
/// <returns>True if the positions were sent correctly and the 3rd party API sent no errors. False, otherwise</returns>
|
||||
public virtual bool Send(SignalExportTargetParameters parameters)
|
||||
{
|
||||
if (parameters.Targets.Count == 0)
|
||||
{
|
||||
parameters.Algorithm.Debug("Portfolio target is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
return VerifyTargets(parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the security type of every holding in the given list is allowed
|
||||
/// </summary>
|
||||
/// <param name="parameters">Holdings the user have defined to be sent to certain 3rd party API and the algorithm being ran</param>
|
||||
/// <returns>True if all the targets were allowed, false otherwise</returns>
|
||||
private bool VerifyTargets(SignalExportTargetParameters parameters)
|
||||
{
|
||||
foreach (var signal in parameters.Targets)
|
||||
{
|
||||
if (!AllowedSecurityTypes.Contains(signal.Symbol.SecurityType))
|
||||
{
|
||||
parameters.Algorithm.Debug($"{signal.Symbol.SecurityType} security type is not supported by {Name}. Allowed security types: [{string.Join(",", AllowedSecurityTypes)}]");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If created, dispose of HttpClient we used for the requests to the different 3rd party API's
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_lazyClient.IsValueCreated)
|
||||
{
|
||||
_lazyClient.Value.DisposeSafely();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
/*
|
||||
* 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 QuantConnect.Interfaces;
|
||||
using QuantConnect.Securities;
|
||||
using QuantConnect.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports signals of desired positions to Collective2 API using JSON and HTTPS.
|
||||
/// Accepts signals in quantity(number of shares) i.e symbol:"SPY", quant:40
|
||||
/// </summary>
|
||||
public class Collective2SignalExport : BaseSignalExport
|
||||
{
|
||||
/// <summary>
|
||||
/// Hashset of symbols whose market is unknown but have already been seen by
|
||||
/// this signal export manager
|
||||
/// </summary>
|
||||
private HashSet<string> _unknownMarketSymbols;
|
||||
|
||||
/// <summary>
|
||||
/// Hashset of security types seen that are unsupported by C2 API
|
||||
/// </summary>
|
||||
private HashSet<SecurityType> _unknownSecurityTypes;
|
||||
|
||||
/// <summary>
|
||||
/// API key provided by Collective2
|
||||
/// </summary>
|
||||
private readonly string _apiKey;
|
||||
|
||||
/// <summary>
|
||||
/// Trading system's ID number
|
||||
/// </summary>
|
||||
private readonly int _systemId;
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm being ran
|
||||
/// </summary>
|
||||
private IAlgorithm _algorithm;
|
||||
|
||||
/// <summary>
|
||||
/// C2 accepts only standard minilots (10,000 currency units).
|
||||
/// The smallest quantity C2 trades is "1" which is a mini-lot.
|
||||
/// Thus, the smallest trade a strategy manager can type into C2 is, for example,
|
||||
/// MarketOrder("EURUSD", 1m)
|
||||
/// which will trade 10,000 Euros.
|
||||
/// No fractions nor numbers smaller than 1 are accepted by C2.
|
||||
/// https://support.collective2.com/hc/en-us/articles/360038042774-Forex-minilots
|
||||
/// </summary>
|
||||
private const int _forexMinilots = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to track if the minilot warning has already been printed.
|
||||
/// </summary>
|
||||
private bool _isForexMinilotsWarningPrinted;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to track if the warning has already been printed.
|
||||
/// </summary>
|
||||
private bool _isZeroPriceWarningPrinted;
|
||||
|
||||
/// <summary>
|
||||
/// Collective2 API endpoint
|
||||
/// </summary>
|
||||
public Uri Destination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this signal export
|
||||
/// </summary>
|
||||
protected override string Name { get; } = "Collective2";
|
||||
|
||||
/// <summary>
|
||||
/// Lazy initialization of Symbol Properties Database
|
||||
/// </summary>
|
||||
private static Lazy<SymbolPropertiesDatabase> _symbolPropertiesDatabase = new (() => SymbolPropertiesDatabase.FromDataFolder());
|
||||
|
||||
/// <summary>
|
||||
/// Lazy initialization of ten seconds rate limiter
|
||||
/// </summary>
|
||||
private static Lazy<RateGate> _tenSecondsRateLimiter = new Lazy<RateGate>(() => new RateGate(100, TimeSpan.FromMilliseconds(1000)));
|
||||
|
||||
/// <summary>
|
||||
/// Lazy initialization of one hour rate limiter
|
||||
/// </summary>
|
||||
private static Lazy<RateGate> _hourlyRateLimiter = new Lazy<RateGate>(() => new RateGate(1000, TimeSpan.FromHours(1)));
|
||||
|
||||
/// <summary>
|
||||
/// Lazy initialization of one day rate limiter
|
||||
/// </summary>
|
||||
private static Lazy<RateGate> _dailyRateLimiter = new Lazy<RateGate>(() => new RateGate(20000, TimeSpan.FromDays(1)));
|
||||
|
||||
/// <summary>
|
||||
/// Collective2SignalExport constructor. It obtains the entry information for Collective2 API requests.
|
||||
/// See API documentation at https://trade.collective2.com/c2-api
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API key provided by Collective2</param>
|
||||
/// <param name="systemId">Trading system's ID number</param>
|
||||
/// <param name="useWhiteLabelApi">Whether to use the white-label API instead of the general one</param>
|
||||
public Collective2SignalExport(string apiKey, int systemId, bool useWhiteLabelApi = false)
|
||||
{
|
||||
_unknownMarketSymbols = new HashSet<string>();
|
||||
_unknownSecurityTypes = new HashSet<SecurityType>();
|
||||
_apiKey = apiKey;
|
||||
_systemId = systemId;
|
||||
|
||||
// SetDesiredPositions: The list of positions that must exist in the strategy.
|
||||
// https://api-docs.collective2.com/apis/general/swagger/strategies/c2_api_strategies/setdesiredpositions_post#strategies/c2_api_strategies/setdesiredpositions_post/t=request&path=positions
|
||||
Destination = new Uri(useWhiteLabelApi
|
||||
? "https://api4-wl.collective2.com/Strategies/SetDesiredPositions"
|
||||
: "https://api4-general.collective2.com/Strategies/SetDesiredPositions");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a JSON message with the desired positions using the expected
|
||||
/// Collective2 API format and then sends it
|
||||
/// </summary>
|
||||
/// <param name="parameters">A list of holdings from the portfolio
|
||||
/// expected to be sent to Collective2 API and the algorithm being ran</param>
|
||||
/// <returns>True if the positions were sent correctly and Collective2 sent no errors, false otherwise</returns>
|
||||
public override bool Send(SignalExportTargetParameters parameters)
|
||||
{
|
||||
if (!base.Send(parameters))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ConvertHoldingsToCollective2(parameters, out List<Collective2Position> positions))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var message = CreateMessage(positions);
|
||||
_tenSecondsRateLimiter.Value.WaitToProceed();
|
||||
_hourlyRateLimiter.Value.WaitToProceed();
|
||||
_dailyRateLimiter.Value.WaitToProceed();
|
||||
var result = SendPositions(message);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of targets to a list of Collective2 positions
|
||||
/// </summary>
|
||||
/// <param name="parameters">A list of targets from the portfolio
|
||||
/// expected to be sent to Collective2 API and the algorithm being ran</param>
|
||||
/// <param name="positions">A list of Collective2 positions</param>
|
||||
/// <returns>True if the given targets could be converted to a Collective2Position list, false otherwise</returns>
|
||||
protected bool ConvertHoldingsToCollective2(SignalExportTargetParameters parameters, out List<Collective2Position> positions)
|
||||
{
|
||||
_algorithm = parameters.Algorithm;
|
||||
var targets = parameters.Targets;
|
||||
positions = [];
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
_algorithm.Error("One portfolio target was null");
|
||||
return false;
|
||||
}
|
||||
|
||||
var securityType = GetSecurityTypeAcronym(target.Symbol.SecurityType);
|
||||
if (securityType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var maturityMonthYear = GetMaturityMonthYear(target.Symbol);
|
||||
if (maturityMonthYear?.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var exchangeSymbol = new C2ExchangeSymbol
|
||||
{
|
||||
Symbol = GetSymbol(target.Symbol),
|
||||
Currency = GetCurrency(_algorithm, target.Symbol),
|
||||
SecurityExchange = GetMICExchangeCode(target.Symbol),
|
||||
SecurityType = securityType,
|
||||
MaturityMonthYear = maturityMonthYear,
|
||||
PutOrCall = GetPutOrCallValue(target.Symbol),
|
||||
StrikePrice = GetStrikePrice(target.Symbol)
|
||||
};
|
||||
|
||||
// Quantity must be non-zero.
|
||||
// To close a position, simply omit it from the Positions array.
|
||||
var quantity = ConvertPercentageToQuantity(_algorithm, target);
|
||||
if (quantity == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
positions.Add(new() { ExchangeSymbol = exchangeSymbol, Quantity = quantity });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a given percentage of a position into the number of shares of it
|
||||
/// </summary>
|
||||
/// <param name="algorithm">Algorithm being ran</param>
|
||||
/// <param name="target">Desired position to be sent to the Collective2 API</param>
|
||||
/// <returns>Number of shares hold of the given position</returns>
|
||||
protected int ConvertPercentageToQuantity(IAlgorithm algorithm, PortfolioTarget target)
|
||||
{
|
||||
var numberShares = PortfolioTarget.Percent(algorithm, target.Symbol, target.Quantity);
|
||||
if (numberShares == null)
|
||||
{
|
||||
if (algorithm.Securities.TryGetValue(target.Symbol, out var security) && security.Price == 0 && target.Quantity == 0)
|
||||
{
|
||||
if (!_isZeroPriceWarningPrinted)
|
||||
{
|
||||
_isZeroPriceWarningPrinted = true;
|
||||
algorithm.Debug($"Warning: Collective2 failed to calculate target quantity for {target}. The price for {target.Symbol} is 0, and the target quantity is 0. Will return 0 for all similar cases.");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
throw new InvalidOperationException($"Collective2 failed to calculate target quantity for {target}");
|
||||
}
|
||||
|
||||
var quantity = (int)numberShares.Quantity;
|
||||
|
||||
if (target.Symbol.ID.SecurityType == SecurityType.Forex)
|
||||
{
|
||||
quantity /= _forexMinilots;
|
||||
if (!_isForexMinilotsWarningPrinted && quantity == 0)
|
||||
{
|
||||
_isForexMinilotsWarningPrinted = true;
|
||||
algorithm.Debug($"Warning: Collective2 failed to calculate target quantity for {target}. The smallest quantity C2 trades is \"1\" which is a mini-lot (10,000 currency units), and the target quantity is {numberShares.Quantity}. Will return 0 for all similar cases.");
|
||||
}
|
||||
}
|
||||
|
||||
return quantity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the list of desired positions with the needed credentials in JSON format
|
||||
/// </summary>
|
||||
/// <param name="positions">List of Collective2 positions to be sent to Collective2 API</param>
|
||||
/// <returns>A JSON request string of the desired positions to be sent by a POST request to Collective2 API</returns>
|
||||
protected string CreateMessage(List<Collective2Position> positions)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
StrategyId = _systemId,
|
||||
Positions = positions,
|
||||
};
|
||||
|
||||
var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
|
||||
var jsonMessage = JsonConvert.SerializeObject(payload, settings);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the desired positions list in JSON format to Collective2 API using a POST request. It logs
|
||||
/// the message retrieved by the Collective2 API if there was a HttpRequestException
|
||||
/// </summary>
|
||||
/// <param name="message">A JSON request string of the desired positions list with the credentials</param>
|
||||
/// <returns>True if the positions were sent correctly and Collective2 API sent no errors, false otherwise</returns>
|
||||
private bool SendPositions(string message)
|
||||
{
|
||||
using var httpMessage = new StringContent(message, Encoding.UTF8, "application/json");
|
||||
|
||||
//Add the QuantConnect app header
|
||||
httpMessage.Headers.Add("X-AppId", "OPA1N90E71");
|
||||
|
||||
//Add the Authorization header
|
||||
HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiKey);
|
||||
|
||||
//Send the message
|
||||
using HttpResponseMessage response = HttpClient.PostAsync(Destination, httpMessage).Result;
|
||||
|
||||
//Parse it
|
||||
var responseObject = response.Content.ReadFromJsonAsync<C2Response>().Result;
|
||||
|
||||
//For debugging purposes, append the message sent to Collective2 to the algorithms log
|
||||
var debuggingMessage = Logging.Log.DebuggingEnabled ? $" | Message={message}" : string.Empty;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_algorithm.Error($"Collective2 API returned the following errors: {string.Join(",", PrintErrors(responseObject.ResponseStatus.Errors))}{debuggingMessage}");
|
||||
return false;
|
||||
}
|
||||
else if (responseObject.Results.Count > 0)
|
||||
{
|
||||
_algorithm.Debug($"Collective2: NewSignals={string.Join(',', responseObject.Results[0].NewSignals)} | CanceledSignals={string.Join(',', responseObject.Results[0].CanceledSignals)}{debuggingMessage}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string PrintErrors(List<ResponseError> errors)
|
||||
{
|
||||
if (errors?.Count == 0)
|
||||
{
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (var error in errors)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"({error.ErrorCode}) {error.FieldName}: {error.Message}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The main C2 response class for this endpoint
|
||||
/// </summary>
|
||||
private class C2Response
|
||||
{
|
||||
[JsonProperty(PropertyName = "Results")]
|
||||
public virtual List<DesiredPositionResponse> Results { get; set; }
|
||||
|
||||
|
||||
[JsonProperty(PropertyName = "ResponseStatus")]
|
||||
public ResponseStatus ResponseStatus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Results object
|
||||
/// </summary>
|
||||
private class DesiredPositionResponse
|
||||
{
|
||||
[JsonProperty(PropertyName = "NewSignals")]
|
||||
public List<long> NewSignals { get; set; } = new List<long>();
|
||||
|
||||
|
||||
[JsonProperty(PropertyName = "CanceledSignals")]
|
||||
public List<long> CanceledSignals { get; set; } = new List<long>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the given symbol in the expected C2 format
|
||||
/// </summary>
|
||||
private string GetSymbol(Symbol symbol)
|
||||
{
|
||||
if (CurrencyPairUtil.TryDecomposeCurrencyPair(symbol, out var baseCurrency, out var quoteCurrency))
|
||||
{
|
||||
return $"{baseCurrency}/{quoteCurrency}";
|
||||
}
|
||||
else if (symbol.SecurityType.IsOption())
|
||||
{
|
||||
return symbol.Underlying.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return symbol.ID.Symbol;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Symbol currency. USD for Forex.
|
||||
/// </summary>
|
||||
private string GetCurrency(IAlgorithm algorithm, Symbol symbol)
|
||||
{
|
||||
if (symbol.ID.SecurityType == SecurityType.Forex)
|
||||
{
|
||||
return "USD";
|
||||
}
|
||||
|
||||
if (algorithm.Securities.TryGetValue(symbol, out var security))
|
||||
{
|
||||
return security.QuoteCurrency.Symbol;
|
||||
}
|
||||
|
||||
var properties = _symbolPropertiesDatabase.Value.GetSymbolProperties(symbol.ID.Market, symbol, symbol.ID.SecurityType, algorithm.AccountCurrency);
|
||||
return properties.QuoteCurrency;
|
||||
}
|
||||
|
||||
private string GetMICExchangeCode(Symbol symbol)
|
||||
{
|
||||
if (symbol.SecurityType == SecurityType.Equity || symbol.SecurityType.IsOption())
|
||||
{
|
||||
return "DEFAULT";
|
||||
}
|
||||
|
||||
switch (symbol.ID.Market)
|
||||
{
|
||||
case Market.India:
|
||||
return "XNSE";
|
||||
case Market.HKFE:
|
||||
return "XHKF";
|
||||
case Market.NYSELIFFE:
|
||||
return "XNLI";
|
||||
case Market.EUREX:
|
||||
return "XEUR";
|
||||
case Market.ICE:
|
||||
return "IEPA";
|
||||
case Market.CBOE:
|
||||
return "XCBO";
|
||||
case Market.CFE:
|
||||
return "XCBF";
|
||||
case Market.CBOT:
|
||||
return "XCBT";
|
||||
case Market.COMEX:
|
||||
return "XCEC";
|
||||
case Market.NYMEX:
|
||||
return "XNYM";
|
||||
case Market.SGX:
|
||||
return "XSES";
|
||||
case Market.FXCM:
|
||||
return symbol.ID.Market.ToUpper();
|
||||
case Market.OSE:
|
||||
case Market.CME:
|
||||
return $"X{symbol.ID.Market.ToUpper()}";
|
||||
default:
|
||||
if (_unknownMarketSymbols.Add(symbol.Value))
|
||||
{
|
||||
_algorithm.Debug($"The market of the symbol {symbol.Value} was unexpected: {symbol.ID.Market}. Using 'DEFAULT' as market");
|
||||
}
|
||||
|
||||
return "DEFAULT";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the given security type in the format C2 expects
|
||||
/// </summary>
|
||||
private string GetSecurityTypeAcronym(SecurityType securityType)
|
||||
{
|
||||
switch (securityType)
|
||||
{
|
||||
case SecurityType.Equity:
|
||||
return "CS";
|
||||
case SecurityType.Future:
|
||||
return "FUT";
|
||||
case SecurityType.Option:
|
||||
case SecurityType.IndexOption:
|
||||
return "OPT";
|
||||
case SecurityType.Forex:
|
||||
return "FOR";
|
||||
default:
|
||||
if (_unknownSecurityTypes.Add(securityType))
|
||||
{
|
||||
_algorithm.Debug($"Unexpected security type found: {securityType}. Collective2 just accepts: Equity, Future, Option, Index Option and Stock");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the expiration date in the format C2 expects
|
||||
/// </summary>
|
||||
private string GetMaturityMonthYear(Symbol symbol)
|
||||
{
|
||||
var delistingDate = symbol.GetDelistingDate();
|
||||
if (delistingDate == Time.EndOfTime) // The given symbol is equity or forex
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (delistingDate < _algorithm.Securities[symbol].LocalTime.Date) // The given symbol has already expired
|
||||
{
|
||||
_algorithm.Error($"Instrument {symbol} has already expired. Its delisting date was: {delistingDate}. This signal won't be sent to Collective2.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"{delistingDate:yyyyMMdd}";
|
||||
}
|
||||
|
||||
private int? GetPutOrCallValue(Symbol symbol)
|
||||
{
|
||||
if (symbol.SecurityType.IsOption())
|
||||
{
|
||||
switch (symbol.ID.OptionRight)
|
||||
{
|
||||
case OptionRight.Put:
|
||||
return 0;
|
||||
case OptionRight.Call:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private decimal? GetStrikePrice(Symbol symbol)
|
||||
{
|
||||
if (symbol.SecurityType.IsOption())
|
||||
{
|
||||
return symbol.ID.StrikePrice;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The C2 ResponseStatus object
|
||||
/// </summary>
|
||||
private class ResponseStatus
|
||||
{
|
||||
/* Example:
|
||||
|
||||
"ResponseStatus":
|
||||
{
|
||||
"ErrorCode": ""401",
|
||||
"Message": ""Unauthorized",
|
||||
"Errors": [
|
||||
{
|
||||
"ErrorCode": "2015",
|
||||
"FieldName": "APIKey",
|
||||
"Message": ""Unknown API Key"
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
[JsonProperty(PropertyName = "ErrorCode")]
|
||||
public string ErrorCode { get; set; }
|
||||
|
||||
|
||||
[JsonProperty(PropertyName = "Message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
|
||||
[JsonProperty(PropertyName = "Errors")]
|
||||
public List<ResponseError> Errors { get; set; }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ResponseError object
|
||||
/// </summary>
|
||||
private class ResponseError
|
||||
{
|
||||
[JsonProperty(PropertyName = "ErrorCode")]
|
||||
public string ErrorCode { get; set; }
|
||||
|
||||
|
||||
[JsonProperty(PropertyName = "FieldName")]
|
||||
public string FieldName { get; set; }
|
||||
|
||||
|
||||
[JsonProperty(PropertyName = "Message")]
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores position's needed information to be serialized in JSON format
|
||||
/// and then sent to Collective2 API
|
||||
/// </summary>
|
||||
protected class Collective2Position
|
||||
{
|
||||
/// <summary>
|
||||
/// Position symbol
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "exchangeSymbol")]
|
||||
public C2ExchangeSymbol ExchangeSymbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of shares/contracts of the given symbol. Positive quantites are long positions
|
||||
/// and negative short positions.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "quantity")]
|
||||
public decimal Quantity { get; set; } // number of shares, not % of the portfolio
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Collective2 symbol
|
||||
/// </summary>
|
||||
protected class C2ExchangeSymbol
|
||||
{
|
||||
/// <summary>
|
||||
/// The exchange root symbol e.g. AAPL
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "symbol")]
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The 3-character ISO instrument currency. E.g. 'USD'
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "currency")]
|
||||
public string Currency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MIC Exchange code e.g. DEFAULT (for stocks & options),
|
||||
/// XCME, XEUR, XICE, XLIF, XNYB, XNYM, XASX, XCBF, XCBT, XCEC,
|
||||
/// XKBT, XSES. See details at http://www.iso15022.org/MIC/homepageMIC.htm
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "securityExchange")]
|
||||
public string SecurityExchange { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The SecurityType e.g. 'CS'(Common Stock), 'FUT' (Future), 'OPT' (Option), 'FOR' (Forex)
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "securityType")]
|
||||
public string SecurityType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MaturityMonthYear e.g. '202103' (March 2021), or if the contract requires a day: '20210521' (May 21, 2021)
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "maturityMonthYear")]
|
||||
public string MaturityMonthYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Option PutOrCall e.g. 0 = Put, 1 = Call
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "putOrCall")]
|
||||
public int? PutOrCall { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ISO Option Strike Price. Zero means none
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "strikePrice")]
|
||||
public decimal? StrikePrice { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports signals of the desired positions to CrunchDAO API.
|
||||
/// Accepts signals in percentage i.e ticker:"SPY", date: "2020-10-04", signal:0.54
|
||||
/// </summary>
|
||||
public class CrunchDAOSignalExport : BaseSignalExport
|
||||
{
|
||||
/// <summary>
|
||||
/// CrunchDAO API key
|
||||
/// </summary>
|
||||
private readonly string _apiKey;
|
||||
|
||||
/// <summary>
|
||||
/// CrunchDAO API endpoint
|
||||
/// </summary>
|
||||
private readonly Uri _destination;
|
||||
|
||||
/// <summary>
|
||||
/// Model ID or Name
|
||||
/// </summary>
|
||||
private readonly string _model;
|
||||
|
||||
/// <summary>
|
||||
/// Submission Name (Optional)
|
||||
/// </summary>
|
||||
private readonly string _submissionName;
|
||||
|
||||
/// <summary>
|
||||
/// Comment (Optional)
|
||||
/// </summary>
|
||||
private readonly string _comment;
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm being ran
|
||||
/// </summary>
|
||||
private IAlgorithm _algorithm;
|
||||
|
||||
/// <summary>
|
||||
/// HashSet of allowed SecurityTypes for CrunchDAO
|
||||
/// </summary>
|
||||
private readonly HashSet<SecurityType> _allowedSecurityTypes = new()
|
||||
{
|
||||
SecurityType.Equity
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The name of this signal export
|
||||
/// </summary>
|
||||
protected override string Name { get; } = "CrunchDAO";
|
||||
|
||||
/// <summary>
|
||||
/// HashSet property of allowed SecurityTypes for CrunchDAO
|
||||
/// </summary>
|
||||
protected override HashSet<SecurityType> AllowedSecurityTypes => _allowedSecurityTypes;
|
||||
|
||||
/// <summary>
|
||||
/// CrunchDAOSignalExport constructor. It obtains the required information for CrunchDAO API requests.
|
||||
/// See (https://colab.research.google.com/drive/1YW1xtHrIZ8ZHW69JvNANWowmxPcnkNu0?authuser=1#scrollTo=aPyWNxtuDc-X)
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API key provided by CrunchDAO</param>
|
||||
/// <param name="model">Model ID or Name</param>
|
||||
/// <param name="submissionName">Submission Name (Optional)</param>
|
||||
/// <param name="comment">Comment (Optional)</param>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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</summary>
|
||||
/// <param name="parameters">A list of holdings from the portfolio,
|
||||
/// expected to be sent to CrunchDAO API and the algorithm being ran</param>
|
||||
/// <returns>True if the positions were sent to CrunchDAO succesfully and errors were returned, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the list of holdings into a CSV format string
|
||||
/// </summary>
|
||||
/// <param name="parameters">A list of holdings from the portfolio,
|
||||
/// expected to be sent to CrunchDAO API and the algorithm being ran</param>
|
||||
/// <param name="positions">A CSV format string of the given holdings with the required features(ticker, date, signal)</param>
|
||||
/// <returns>True if a string message with the positions could be obtained, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="positions">A CSV format string of the given holdings with the required features</param>
|
||||
/// <returns>True if the positions were sent to CrunchDAO successfully and errors were returned. False, otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there is an open round, if so it assigns the current round ID to currentRoundId
|
||||
/// parameter
|
||||
/// </summary>
|
||||
/// <param name="currentRoundId">Current round ID</param>
|
||||
/// <returns>True if there is an open round, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there is a submission for the current round, if so it assigns its ID to
|
||||
/// lastSubmissionId
|
||||
/// </summary>
|
||||
/// <param name="currentRoundId">Current round ID</param>
|
||||
/// <param name="lastSubmissionId">Last submission ID (for the current round)</param>
|
||||
/// <returns>True if there's a submission for the current round, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the last submission for the current round
|
||||
/// </summary>
|
||||
/// <param name="lastSubmissionId">Last submission ID for the current round</param>
|
||||
/// <returns>True if the last submission could be deleted sucessfully, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports signals of the desired positions to Numerai API.
|
||||
/// Accepts signals in percentage i.e numerai_ticker:"IBM US", signal:0.234
|
||||
/// </summary>
|
||||
/// <remarks>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)</remarks>
|
||||
public class NumeraiSignalExport : BaseSignalExport
|
||||
{
|
||||
/// <summary>
|
||||
/// Numerai API submission endpoint
|
||||
/// </summary>
|
||||
private readonly Uri _destination;
|
||||
|
||||
/// <summary>
|
||||
/// PUBLIC_ID provided by Numerai
|
||||
/// </summary>
|
||||
private readonly string _publicId;
|
||||
|
||||
/// <summary>
|
||||
/// SECRET_ID provided by Numerai
|
||||
/// </summary>
|
||||
private readonly string _secretId;
|
||||
|
||||
/// <summary>
|
||||
/// ID of the Numerai Model being used
|
||||
/// </summary>
|
||||
private readonly string _modelId;
|
||||
|
||||
/// <summary>
|
||||
/// Signal file's name
|
||||
/// </summary>
|
||||
private readonly string _fileName;
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm being ran
|
||||
/// </summary>
|
||||
private IAlgorithm _algorithm;
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary to obtain corresponding Numerai Market name for the given LEAN market name
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string> _numeraiMarketFormat = new() // There can be stocks from other markets
|
||||
{
|
||||
{Market.USA, "US" },
|
||||
{Market.SGX, "SP" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Hashset of Numerai allowed SecurityTypes
|
||||
/// </summary>
|
||||
private readonly HashSet<SecurityType> _allowedSecurityTypes = new()
|
||||
{
|
||||
SecurityType.Equity
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The name of this signal export
|
||||
/// </summary>
|
||||
protected override string Name { get; } = "Numerai";
|
||||
|
||||
/// <summary>
|
||||
/// Hashset property of Numerai allowed SecurityTypes
|
||||
/// </summary>
|
||||
protected override HashSet<SecurityType> AllowedSecurityTypes => _allowedSecurityTypes;
|
||||
|
||||
/// <summary>
|
||||
/// NumeraiSignalExport Constructor. It obtains the required information for Numerai API requests
|
||||
/// </summary>
|
||||
/// <param name="publicId">PUBLIC_ID provided by Numerai</param>
|
||||
/// <param name="secretId">SECRET_ID provided by Numerai</param>
|
||||
/// <param name="modelId">ID of the Numerai Model being used</param>
|
||||
/// <param name="fileName">Signal file's name</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="parameters">A list of portfolio holdings expected to be sent to Numerai API and the algorithm being ran</param>
|
||||
/// <returns>True if the positions were sent to Numerai API correctly and no errors were returned, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies each holding's signal is between 0 and 1 (exclusive)
|
||||
/// </summary>
|
||||
/// <param name="parameters">A list of portfolio holdings expected to be sent to Numerai API</param>
|
||||
/// <param name="positions">A message with the desired positions in the expected Numerai API format</param>
|
||||
/// <returns>True if a string message with the positions could be obtained, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="positions">A message with the desired positions in the expected Numerai API format</param>
|
||||
/// <returns>True if the positions were sent to Numerai API correctly and no errors were returned, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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 Python.Runtime;
|
||||
using QuantConnect.Interfaces;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Python;
|
||||
using QuantConnect.Securities;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
|
||||
{
|
||||
/// <summary>
|
||||
/// Class manager to send portfolio targets to different 3rd party API's
|
||||
/// For example, it allows Collective2, CrunchDAO and Numerai signal export providers
|
||||
/// </summary>
|
||||
public class SignalExportManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Records the time of the first order event of a group of events
|
||||
/// </summary>
|
||||
private ReferenceWrapper<DateTime> _initialOrderEventTimeUtc = new(Time.EndOfTime);
|
||||
|
||||
/// <summary>
|
||||
/// List of signal export providers
|
||||
/// </summary>
|
||||
private List<ISignalExportTarget> _signalExports;
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm being ran
|
||||
/// </summary>
|
||||
private readonly IAlgorithm _algorithm;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to indicate if the user has tried to send signals with live mode off
|
||||
/// </summary>
|
||||
private bool _isLiveWarningModeLog;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximim time span elapsed to export signals after an order event
|
||||
/// If null, disable automatic export.
|
||||
/// </summary>
|
||||
public TimeSpan? AutomaticExportTimeSpan { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// SignalExportManager Constructor, obtains the entry information needed to send signals
|
||||
/// and initializes the fields to be used
|
||||
/// </summary>
|
||||
/// <param name="algorithm">Algorithm being run</param>
|
||||
public SignalExportManager(IAlgorithm algorithm)
|
||||
{
|
||||
_algorithm = algorithm;
|
||||
_isLiveWarningModeLog = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new signal exports provider
|
||||
/// </summary>
|
||||
/// <param name="signalExport">Signal export provider</param>
|
||||
public void AddSignalExportProvider(ISignalExportTarget signalExport)
|
||||
{
|
||||
_signalExports ??= [];
|
||||
_signalExports.Add(signalExport);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new signal exports provider
|
||||
/// </summary>
|
||||
/// <param name="signalExport">Signal export provider</param>
|
||||
public void AddSignalExportProvider(PyObject signalExport)
|
||||
{
|
||||
var managedSignalExport = PythonUtil.CreateInstanceOrWrapper<ISignalExportTarget>(
|
||||
signalExport,
|
||||
py => new SignalExportTargetPythonWrapper(py)
|
||||
);
|
||||
AddSignalExportProvider(managedSignalExport);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one or more new signal exports providers
|
||||
/// </summary>
|
||||
/// <param name="signalExports">One or more signal export provider</param>
|
||||
public void AddSignalExportProviders(params ISignalExportTarget[] signalExports)
|
||||
{
|
||||
signalExports.DoForEach(AddSignalExportProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one or more new signal exports providers
|
||||
/// </summary>
|
||||
/// <param name="signalExports">One or more signal export provider</param>
|
||||
public void AddSignalExportProviders(PyObject signalExports)
|
||||
{
|
||||
using var _ = Py.GIL();
|
||||
if (!signalExports.IsIterable())
|
||||
{
|
||||
AddSignalExportProvider(signalExports);
|
||||
return;
|
||||
}
|
||||
PyList.AsList(signalExports).DoForEach(AddSignalExportProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the portfolio targets from the algorihtm's Portfolio and sends them with the
|
||||
/// algorithm being ran to the signal exports providers already set
|
||||
/// </summary>
|
||||
/// <returns>True if the target list could be obtained from the algorithm's Portfolio and they
|
||||
/// were successfully sent to the signal export providers</returns>
|
||||
public bool SetTargetPortfolioFromPortfolio()
|
||||
{
|
||||
if (!GetPortfolioTargets(out PortfolioTarget[] targets))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var result = SetTargetPortfolio(targets);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an array of portfolio targets from algorithm's Portfolio and returns them.
|
||||
/// See <see cref="PortfolioTarget.Percent(IAlgorithm, Symbol, decimal, bool, string)"/> for more
|
||||
/// information about how each symbol quantity was calculated
|
||||
/// </summary>
|
||||
/// <param name="targets">An array of portfolio targets from the algorithm's Portfolio</param>
|
||||
/// <returns>True if TotalPortfolioValue was bigger than zero, false otherwise</returns>
|
||||
protected bool GetPortfolioTargets(out PortfolioTarget[] targets)
|
||||
{
|
||||
var totalPortfolioValue = _algorithm.Portfolio.TotalPortfolioValue;
|
||||
if (totalPortfolioValue <= 0)
|
||||
{
|
||||
_algorithm.Error("Total portfolio value was less than or equal to 0");
|
||||
targets = Array.Empty<PortfolioTarget>();
|
||||
return false;
|
||||
}
|
||||
|
||||
targets = GetPortfolioTargets(totalPortfolioValue).ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the portfolio targets with the given entries and sends them with the algorithm
|
||||
/// being ran to the signal exports providers set, as long as the algorithm is in live mode
|
||||
/// </summary>
|
||||
/// <param name="portfolioTargets">One or more portfolio targets to be sent to the defined signal export providers</param>
|
||||
/// <returns>True if the portfolio targets could be sent to the different signal export providers successfully, false otherwise</returns>
|
||||
public bool SetTargetPortfolio(params PortfolioTarget[] portfolioTargets)
|
||||
{
|
||||
if (!_algorithm.LiveMode)
|
||||
{
|
||||
if (!_isLiveWarningModeLog)
|
||||
{
|
||||
_algorithm.Debug("Portfolio targets are only sent in live mode");
|
||||
_isLiveWarningModeLog = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_signalExports.IsNullOrEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (portfolioTargets == null || portfolioTargets.Length == 0)
|
||||
{
|
||||
_algorithm.Debug("No portfolio target given");
|
||||
return false;
|
||||
}
|
||||
|
||||
var targets = new List<PortfolioTarget>(portfolioTargets);
|
||||
var signalExportTargetParameters = new SignalExportTargetParameters
|
||||
{
|
||||
Targets = targets,
|
||||
Algorithm = _algorithm
|
||||
};
|
||||
|
||||
var result = true;
|
||||
foreach (var signalExport in _signalExports)
|
||||
{
|
||||
result &= signalExport.Send(signalExportTargetParameters);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<PortfolioTarget> GetPortfolioTargets(decimal totalPortfolioValue)
|
||||
{
|
||||
foreach (var holding in _algorithm.Portfolio.Values)
|
||||
{
|
||||
var security = _algorithm.Securities[holding.Symbol];
|
||||
|
||||
// Skip non-tradeable securities except canonical futures as some signal providers
|
||||
// like Collective2 accept them.
|
||||
// See https://collective2.com/api-docs/latest#Basic_submitsignal_format
|
||||
if (!security.IsTradable && !security.Symbol.IsCanonical())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var marginParameters = new InitialMarginParameters(security, holding.Quantity);
|
||||
var adjustedPercent = Math.Abs(security.BuyingPowerModel.GetInitialMarginRequirement(marginParameters) / totalPortfolioValue);
|
||||
|
||||
// See PortfolioTarget.Percent:
|
||||
// we normalize the target buying power by the leverage so we work in the land of margin
|
||||
var holdingPercent = adjustedPercent * security.BuyingPowerModel.GetLeverage(security);
|
||||
|
||||
// FreePortfolioValue is used for orders not to be rejected due to volatility when using SetHoldings and CalculateOrderQuantity
|
||||
// Then, we need to substract its value from the TotalPortfolioValue and obtain again the holding percentage for our holding
|
||||
var adjustedHoldingPercent = (holdingPercent * totalPortfolioValue) / _algorithm.Portfolio.TotalPortfolioValueLessFreeBuffer;
|
||||
if (holding.Quantity < 0)
|
||||
{
|
||||
adjustedHoldingPercent *= -1;
|
||||
}
|
||||
|
||||
yield return new PortfolioTarget(holding.Symbol, adjustedHoldingPercent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// New order event handler: on order status changes (filled, partially filled, cancelled etc).
|
||||
/// </summary>
|
||||
/// <param name="orderEvent">Event information</param>
|
||||
public void OnOrderEvent(OrderEvent orderEvent)
|
||||
{
|
||||
if (_initialOrderEventTimeUtc.Value == Time.EndOfTime && orderEvent.Status.IsFill())
|
||||
{
|
||||
_initialOrderEventTimeUtc = new(orderEvent.UtcTime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the target portfolio after order events.
|
||||
/// </summary>
|
||||
/// <param name="currentTimeUtc">The current time of synchronous events</param>
|
||||
public void Flush(DateTime currentTimeUtc)
|
||||
{
|
||||
var initialOrderEventTimeUtc = _initialOrderEventTimeUtc.Value;
|
||||
if (_signalExports.IsNullOrEmpty() || initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTimeUtc - initialOrderEventTimeUtc < AutomaticExportTimeSpan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SetTargetPortfolioFromPortfolio();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// SetTargetPortfolioFromPortfolio logs all known error on LEAN side.
|
||||
// Exceptions occurs in the ISignalExportTarget.Send method (user-defined).
|
||||
_algorithm.Error($"Failed to send portfolio target(s). Reason: {exception.Message}.{Environment.NewLine}{exception.StackTrace}");
|
||||
}
|
||||
_initialOrderEventTimeUtc = new(Time.EndOfTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 QuantConnect.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to wrap objects needed to send signals to the different 3rd party API's
|
||||
/// </summary>
|
||||
public class SignalExportTargetParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// List of portfolio targets to be sent to some 3rd party API
|
||||
/// </summary>
|
||||
public List<PortfolioTarget> Targets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm being ran
|
||||
/// </summary>
|
||||
public IAlgorithm Algorithm { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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 QuantConnect.Interfaces;
|
||||
using QuantConnect.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports signals of desired positions to vBase stamping API using JSON and HTTPS.
|
||||
/// Accepts signals in quantity(number of shares) i.e symbol:"SPY", quant:40
|
||||
/// </summary>
|
||||
public class VBaseSignalExport : BaseSignalExport
|
||||
{
|
||||
private const string ApiBaseUrl = "https://app.vbase.com/api";
|
||||
|
||||
/// <summary>
|
||||
/// API key provided by vBase
|
||||
/// </summary>
|
||||
private readonly string _apiKey;
|
||||
|
||||
/// <summary>
|
||||
/// The collection CID (SHA3-256 hash of collection name) to which we stamp signals
|
||||
/// </summary>
|
||||
private readonly string _collectionCid;
|
||||
|
||||
/// <summary>
|
||||
/// Whether vBase should store the stamped file (defaults true)
|
||||
/// </summary>
|
||||
private readonly bool _storeStampedFile;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this request is idempotent (if true only first identical portfolio stored)
|
||||
/// </summary>
|
||||
private readonly bool _idempotent;
|
||||
|
||||
/// <summary>
|
||||
/// The name of this signal export
|
||||
/// </summary>
|
||||
protected override string Name => "vBase";
|
||||
|
||||
private static RateGate _requestsRateLimiter;
|
||||
|
||||
private readonly Uri _stampApiUrl;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VBaseSignalExport"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiKey">The API key for vBase authentication.</param>
|
||||
/// <param name="collectionName">The target collection name.</param>
|
||||
/// <param name="storeStampedFile">Whether to store the stamped file (default true).</param>
|
||||
/// <param name="idempotent">
|
||||
/// A boolean indicating whether to make the request idempotent.
|
||||
/// If the request is idempotent, only the first stamp for a given portfolio will be made.
|
||||
/// If the request is not idempotent, a new stamp will be made for each request.
|
||||
/// </param>
|
||||
public VBaseSignalExport(
|
||||
string apiKey,
|
||||
string collectionName,
|
||||
bool storeStampedFile = true,
|
||||
bool idempotent = false)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_apiKey))
|
||||
{
|
||||
throw new ArgumentException("vBaseSignalExport: API key not provided");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(collectionName))
|
||||
{
|
||||
throw new ArgumentException("vBaseSignalExport: Collection name not provided");
|
||||
}
|
||||
|
||||
_stampApiUrl = new Uri(ApiBaseUrl.TrimEnd('/') + "/v1/stamp/"); ;
|
||||
|
||||
var collectionCidBytes = SHA3_256.HashData(Encoding.UTF8.GetBytes(collectionName));
|
||||
_collectionCid = "0x" + collectionCidBytes.ToHexString().ToLowerInvariant();
|
||||
|
||||
_storeStampedFile = storeStampedFile;
|
||||
_idempotent = idempotent;
|
||||
_requestsRateLimiter = new RateGate(10, TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts targets to CSV and posts them to vBase stamping endpoint
|
||||
/// </summary>
|
||||
/// <param name="parameters">Signal export parameters (targets + algorithm)</param>
|
||||
/// <returns>True if request succeeded</returns>
|
||||
public override bool Send(SignalExportTargetParameters parameters)
|
||||
{
|
||||
if (!base.Send(parameters))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var csv = BuildCsv(parameters);
|
||||
_requestsRateLimiter?.WaitToProceed();
|
||||
return Stamp(csv, parameters.Algorithm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a CSV (sym,wt) for the given targets
|
||||
/// </summary>
|
||||
/// <param name="parameters">Signal export parameters</param>
|
||||
/// <returns>Resulting CSV string</returns>
|
||||
protected virtual string BuildCsv(SignalExportTargetParameters parameters)
|
||||
{
|
||||
var csv = "sym,wt\n";
|
||||
|
||||
foreach (var target in parameters.Targets)
|
||||
{
|
||||
csv += $"{target.Symbol.Value},{target.Quantity.ToStringInvariant()}\n";
|
||||
}
|
||||
return csv;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the CSV payload to the vBase stamping API
|
||||
/// </summary>
|
||||
private bool Stamp(string csv, IAlgorithm algorithm)
|
||||
{
|
||||
try
|
||||
{
|
||||
var contentPairs = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("collectionCid", _collectionCid),
|
||||
new KeyValuePair<string, string>("data", csv),
|
||||
new KeyValuePair<string, string>("storeStampedFile", _storeStampedFile.ToString()),
|
||||
new KeyValuePair<string, string>("idempotent", _idempotent.ToString())
|
||||
};
|
||||
|
||||
using var httpContent = new FormUrlEncodedContent(contentPairs);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, _stampApiUrl)
|
||||
{
|
||||
Content = httpContent
|
||||
};
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
|
||||
|
||||
using var response = HttpClient.SendAsync(request).Result;
|
||||
var body = response.Content.ReadAsStringAsync().Result;
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
algorithm.Error($"vBase API returned {response.StatusCode}. Body: {body}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
algorithm.Error($"vBase signal export failed: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user