chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,127 @@
/*
* 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 System;
using Python.Runtime;
using QuantConnect.Interfaces;
using System.Collections.Generic;
using QuantConnect.Util;
namespace QuantConnect.Algorithm.Framework.Alphas.Analysis
{
/// <summary>
/// Encapsulates the storage of insights.
/// </summary>
public class InsightManager : InsightCollection
{
private readonly IAlgorithm _algorithm;
private IInsightScoreFunction _insightScoreFunction;
/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="algorithm">The associated algorithm instance</param>
public InsightManager(IAlgorithm algorithm)
{
_algorithm = algorithm;
}
/// <summary>
/// Process a new time step handling insights scoring
/// </summary>
/// <param name="utcNow">The current utc time</param>
public void Step(DateTime utcNow)
{
_insightScoreFunction?.Score(this, utcNow);
}
/// <summary>
/// Sets the insight score function to use
/// </summary>
/// <param name="insightScoreFunction">Model that scores insights</param>
public void SetInsightScoreFunction(IInsightScoreFunction insightScoreFunction)
{
_insightScoreFunction = insightScoreFunction;
}
/// <summary>
/// Sets the insight score function to use
/// </summary>
/// <param name="insightScoreFunction">Model that scores insights</param>
public void SetInsightScoreFunction(PyObject insightScoreFunction)
{
_insightScoreFunction = PythonUtil.CreateInstanceOrWrapper<IInsightScoreFunction>(
insightScoreFunction,
py => new InsightScoreFunctionPythonWrapper(py)
);
}
/// <summary>
/// Expire the insights of the given symbols
/// </summary>
/// <param name="symbols">Symbol we want to expire insights for</param>
public void Expire(IEnumerable<Symbol> symbols)
{
if (symbols == null)
{
return;
}
foreach (var symbol in symbols)
{
if (TryGetValue(symbol, out var insights))
{
Expire(insights);
}
}
}
/// <summary>
/// Cancel the insights of the given symbols
/// </summary>
/// <param name="symbols">Symbol we want to cancel insights for</param>
public void Cancel(IEnumerable<Symbol> symbols)
{
Expire(symbols);
}
/// <summary>
/// Expire the given insights
/// </summary>
/// <param name="insights">Insights to expire</param>
public void Expire(IEnumerable<Insight> insights)
{
if (insights == null)
{
return;
}
var currentUtcTime = _algorithm.UtcTime;
foreach (var insight in insights)
{
insight.Expire(currentUtcTime);
}
}
/// <summary>
/// Cancel the given insights
/// </summary>
/// <param name="insights">Insights to cancel</param>
public void Cancel(IEnumerable<Insight> insights)
{
Expire(insights);
}
}
}
@@ -0,0 +1,47 @@
/*
* 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 System;
using System.Collections.Generic;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Defines a collection of insights that were generated at the same time step
/// </summary>
public class GeneratedInsightsCollection
{
/// <summary>
/// The utc date time the insights were generated
/// </summary>
public DateTime DateTimeUtc { get; }
/// <summary>
/// The generated insights
/// </summary>
public Insight[] Insights { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GeneratedInsightsCollection"/> class
/// </summary>
/// <param name="dateTimeUtc">The utc date time the sinals were generated</param>
/// <param name="insights">The generated insights</param>
public GeneratedInsightsCollection(DateTime dateTimeUtc, Insight[] insights)
{
DateTimeUtc = dateTimeUtc;
Insights = insights;
}
}
}
@@ -0,0 +1,31 @@
/*
* 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 System;
using QuantConnect.Algorithm.Framework.Alphas.Analysis;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Abstraction in charge of scoring insights
/// </summary>
public interface IInsightScoreFunction
{
/// <summary>
/// Method to evaluate and score insights for each time step
/// </summary>
void Score(InsightManager insightManager, DateTime utcTime);
}
}
@@ -0,0 +1,747 @@
/*
* 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 System;
using System.Collections.Generic;
using Newtonsoft.Json;
using QuantConnect.Algorithm.Framework.Alphas.Serialization;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Defines a alpha prediction for a single symbol generated by the algorithm
/// </summary>
/// <remarks>
/// Serialization of this type is delegated to the <see cref="InsightJsonConverter"/> which uses the <see cref="SerializedInsight"/> as a model.
/// </remarks>
[JsonConverter(typeof(InsightJsonConverter))]
public class Insight
{
private readonly IPeriodSpecification _periodSpecification;
/// <summary>
/// Gets the unique identifier for this insight
/// </summary>
public Guid Id { get; protected set; }
/// <summary>
/// Gets the group id this insight belongs to, null if not in a group
/// </summary>
public Guid? GroupId { get; protected set; }
/// <summary>
/// Gets an identifier for the source model that generated this insight.
/// </summary>
public string SourceModel { get; set; }
/// <summary>
/// Gets the utc time this insight was generated
/// </summary>
/// <remarks>
/// The algorithm framework handles setting this value appropriately.
/// If providing custom <see cref="Insight"/> implementation, be sure
/// to set this value to algorithm.UtcTime when the insight is generated.
/// </remarks>
public DateTime GeneratedTimeUtc { get; set; }
/// <summary>
/// Gets the insight's prediction end time. This is the time when this
/// insight prediction is expected to be fulfilled. This time takes into
/// account market hours, weekends, as well as the symbol's data resolution
/// </summary>
public DateTime CloseTimeUtc { get; set; }
/// <summary>
/// Gets the symbol this insight is for
/// </summary>
public Symbol Symbol { get; private set; }
/// <summary>
/// Gets the type of insight, for example, price insight or volatility insight
/// </summary>
public InsightType Type { get; private set; }
/// <summary>
/// Gets the initial reference value this insight is predicting against. The value is dependent on the specified <see cref="InsightType"/>
/// </summary>
public decimal ReferenceValue { get; set; }
/// <summary>
/// Gets the final reference value, used for scoring, this insight is predicting against. The value is dependent on the specified <see cref="InsightType"/>
/// </summary>
public decimal ReferenceValueFinal { get; set; }
/// <summary>
/// Gets the predicted direction, down, flat or up
/// </summary>
public InsightDirection Direction { get; private set; }
/// <summary>
/// Gets the period over which this insight is expected to come to fruition
/// </summary>
public TimeSpan Period { get; internal set; }
/// <summary>
/// Gets the predicted percent change in the insight type (price/volatility)
/// </summary>
public double? Magnitude { get; private set; }
/// <summary>
/// Gets the confidence in this insight
/// </summary>
public double? Confidence { get; private set; }
/// <summary>
/// Gets the portfolio weight of this insight
/// </summary>
public double? Weight { get; private set; }
/// <summary>
/// Gets the most recent scores for this insight
/// </summary>
public InsightScore Score { get; protected set; }
/// <summary>
/// Gets the estimated value of this insight in the account currency
/// </summary>
public decimal EstimatedValue { get; set; }
/// <summary>
/// The insight's tag containing additional information
/// </summary>
public string Tag { get; protected set; }
/// <summary>
/// Determines whether or not this insight is considered expired at the specified <paramref name="utcTime"/>
/// </summary>
/// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
/// <returns>True if this insight is expired, false otherwise</returns>
public bool IsExpired(DateTime utcTime)
{
return CloseTimeUtc < utcTime;
}
/// <summary>
/// Determines whether or not this insight is considered active at the specified <paramref name="utcTime"/>
/// </summary>
/// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
/// <returns>True if this insight is active, false otherwise</returns>
public bool IsActive(DateTime utcTime)
{
return !IsExpired(utcTime);
}
/// <summary>
/// Expire this insight
/// </summary>
/// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
public void Expire(DateTime utcTime)
{
if (IsActive(utcTime))
{
CloseTimeUtc = utcTime.Add(-Time.OneSecond);
Period = CloseTimeUtc - GeneratedTimeUtc;
}
}
/// <summary>
/// Cancel this insight
/// </summary>
/// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
public void Cancel(DateTime utcTime)
{
Expire(utcTime);
}
/// <summary>
/// Initializes a new instance of the <see cref="Insight"/> class
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="period">The period over which the prediction will come true</param>
/// <param name="type">The type of insight, price/volatility</param>
/// <param name="direction">The predicted direction</param>
/// <param name="tag">The insight's tag containing additional information</param>
public Insight(Symbol symbol, TimeSpan period, InsightType type, InsightDirection direction, string tag = "")
: this(symbol, period, type, direction, null, null, null, null, tag)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Insight"/> class
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="period">The period over which the prediction will come true</param>
/// <param name="type">The type of insight, price/volatility</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percentage change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">An identifier defining the model that generated this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
public Insight(Symbol symbol, TimeSpan period, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
{
Id = Guid.NewGuid();
Score = new InsightScore();
SourceModel = sourceModel;
Symbol = symbol;
Type = type;
Direction = direction;
Period = period;
// Optional
Magnitude = magnitude;
Confidence = confidence;
Weight = weight;
Tag = tag;
_periodSpecification = new TimeSpanPeriodSpecification(period);
}
/// <summary>
/// Initializes a new instance of the <see cref="Insight"/> class
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="expiryFunc">Func that defines the expiry time</param>
/// <param name="type">The type of insight, price/volatility</param>
/// <param name="direction">The predicted direction</param>
/// <param name="tag">The insight's tag containing additional information</param>
public Insight(Symbol symbol, Func<DateTime, DateTime> expiryFunc, InsightType type, InsightDirection direction, string tag = "")
: this(symbol, expiryFunc, type, direction, null, null, null, null, tag)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Insight"/> class
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="expiryFunc">Func that defines the expiry time</param>
/// <param name="type">The type of insight, price/volatility</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percentage change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">An identifier defining the model that generated this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
public Insight(Symbol symbol, Func<DateTime, DateTime> expiryFunc, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
: this(symbol, new FuncPeriodSpecification(expiryFunc), type, direction, magnitude, confidence, sourceModel, weight, tag)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Insight"/> class.
/// This constructor is provided mostly for testing purposes. When running inside an algorithm,
/// the generated and close times are set based on the algorithm's time.
/// </summary>
/// <param name="generatedTimeUtc">The time this insight was generated in utc</param>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="period">The period over which the prediction will come true</param>
/// <param name="type">The type of insight, price/volatility</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percentage change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">An identifier defining the model that generated this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
public Insight(DateTime generatedTimeUtc, Symbol symbol, TimeSpan period, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
: this(symbol, period, type, direction, magnitude, confidence, sourceModel, weight, tag)
{
GeneratedTimeUtc = generatedTimeUtc;
CloseTimeUtc = generatedTimeUtc + period;
}
/// <summary>
/// Private constructor used to keep track of how a user defined the insight period.
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="periodSpec">A specification defining how the insight's period was defined, via time span, via resolution/barcount, via close time</param>
/// <param name="type">The type of insight, price/volatility</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percentage change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">An identifier defining the model that generated this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
private Insight(Symbol symbol, IPeriodSpecification periodSpec, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
{
Id = Guid.NewGuid();
Score = new InsightScore();
SourceModel = sourceModel;
Symbol = symbol;
Type = type;
Direction = direction;
// Optional
Magnitude = magnitude;
Confidence = confidence;
Weight = weight;
Tag = tag;
_periodSpecification = periodSpec;
// keep existing behavior of Insight.Price such that we set the period immediately
var period = (periodSpec as TimeSpanPeriodSpecification)?.Period;
if (period != null)
{
Period = period.Value;
}
}
/// <summary>
/// Sets the insight period and close times if they have not already been set.
/// </summary>
/// <param name="exchangeHours">The insight's security exchange hours</param>
public void SetPeriodAndCloseTime(SecurityExchangeHours exchangeHours)
{
if (GeneratedTimeUtc == default(DateTime))
{
throw new InvalidOperationException(Messages.Insight.GeneratedTimeUtcNotSet(this));
}
_periodSpecification.SetPeriodAndCloseTime(this, exchangeHours);
}
/// <summary>
/// Creates a deep clone of this insight instance
/// </summary>
/// <returns>A new insight with identical values, but new instances</returns>
public virtual Insight Clone()
{
return new Insight(Symbol, Period, Type, Direction, Magnitude, Confidence, weight: Weight, tag: Tag)
{
GeneratedTimeUtc = GeneratedTimeUtc,
CloseTimeUtc = CloseTimeUtc,
Score = Score,
Id = Id,
EstimatedValue = EstimatedValue,
ReferenceValue = ReferenceValue,
ReferenceValueFinal = ReferenceValueFinal,
SourceModel = SourceModel,
GroupId = GroupId
};
}
/// <summary>
/// Creates a new insight for predicting the percent change in price over the specified period
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="resolution">The resolution used to define the insight's period and also used to determine the insight's close time</param>
/// <param name="barCount">The number of resolution time steps to make in market hours to compute the insight's closing time</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percent change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">The model generating this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
/// <returns>A new insight object for the specified parameters</returns>
public static Insight Price(Symbol symbol, Resolution resolution, int barCount, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
{
if (barCount < 1)
{
throw new ArgumentOutOfRangeException(nameof(barCount), Messages.Insight.InvalidBarCount);
}
var spec = new ResolutionBarCountPeriodSpecification(resolution, barCount);
return new Insight(symbol, spec, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
}
/// <summary>
/// Creates a new insight for predicting the percent change in price over the specified period
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="closeTimeLocal">The insight's closing time in the security's exchange time zone</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percent change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">The model generating this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
/// <returns>A new insight object for the specified parameters</returns>
public static Insight Price(Symbol symbol, DateTime closeTimeLocal, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
{
var spec = closeTimeLocal == Time.EndOfTime ? (IPeriodSpecification)
new EndOfTimeCloseTimePeriodSpecification() : new CloseTimePeriodSpecification(closeTimeLocal);
return new Insight(symbol, spec, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
}
/// <summary>
/// Creates a new insight for predicting the percent change in price over the specified period
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="period">The period over which the prediction will come true</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percent change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">The model generating this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
/// <returns>A new insight object for the specified parameters</returns>
public static Insight Price(Symbol symbol, TimeSpan period, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
{
if (period < Time.OneSecond)
{
throw new ArgumentOutOfRangeException(nameof(period), Messages.Insight.InvalidPeriod);
}
var spec = period == Time.EndOfTimeTimeSpan ? (IPeriodSpecification)
new EndOfTimeCloseTimePeriodSpecification() : new TimeSpanPeriodSpecification(period);
return new Insight(symbol, spec, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
}
/// <summary>
/// Creates a new insight for predicting the percent change in price over the specified period
/// </summary>
/// <param name="symbol">The symbol this insight is for</param>
/// <param name="expiryFunc">Func that defines the expiry time</param>
/// <param name="direction">The predicted direction</param>
/// <param name="magnitude">The predicted magnitude as a percent change</param>
/// <param name="confidence">The confidence in this insight</param>
/// <param name="sourceModel">The model generating this insight</param>
/// <param name="weight">The portfolio weight of this insight</param>
/// <param name="tag">The insight's tag containing additional information</param>
/// <returns>A new insight object for the specified parameters</returns>
public static Insight Price(Symbol symbol, Func<DateTime, DateTime> expiryFunc, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
{
return new Insight(symbol, expiryFunc, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
}
/// <summary>
/// Creates a new, unique group id and sets it on each insight
/// </summary>
/// <param name="insights">The insights to be grouped</param>
public static IEnumerable<Insight> Group(params Insight[] insights)
{
if (insights == null)
{
throw new ArgumentNullException(nameof(insights));
}
var groupId = Guid.NewGuid();
foreach (var insight in insights)
{
if (insight.GroupId.HasValue)
{
throw new InvalidOperationException(Messages.Insight.InsightAlreadyAssignedToAGroup(insight));
}
insight.GroupId = groupId;
}
return insights;
}
/// <summary>
/// Creates a new, unique group id and sets it on each insight
/// </summary>
/// <param name="insight">The insight to be grouped</param>
public static IEnumerable<Insight> Group(Insight insight) => Group(new[] {insight});
/// <summary>
/// Creates a new <see cref="Insight"/> object from the specified serialized form
/// </summary>
/// <param name="serializedInsight">The insight DTO</param>
/// <returns>A new insight containing the information specified</returns>
public static Insight FromSerializedInsight(SerializedInsight serializedInsight)
{
var sid = SecurityIdentifier.Parse(serializedInsight.Symbol);
var insight = new Insight(
Time.UnixTimeStampToDateTime(serializedInsight.CreatedTime),
new Symbol(sid, serializedInsight.Ticker ?? sid.Symbol),
TimeSpan.FromSeconds(serializedInsight.Period),
serializedInsight.Type,
serializedInsight.Direction,
serializedInsight.Magnitude,
serializedInsight.Confidence,
serializedInsight.SourceModel,
serializedInsight.Weight,
serializedInsight.Tag
)
{
Id = Guid.Parse(serializedInsight.Id),
CloseTimeUtc = Time.UnixTimeStampToDateTime(serializedInsight.CloseTime),
EstimatedValue = serializedInsight.EstimatedValue,
ReferenceValue = serializedInsight.ReferenceValue,
ReferenceValueFinal = serializedInsight.ReferenceValueFinal,
GroupId = string.IsNullOrEmpty(serializedInsight.GroupId) ? (Guid?) null : Guid.Parse(serializedInsight.GroupId)
};
// only set score values if non-zero or if they're the final scores
if (serializedInsight.ScoreIsFinal)
{
insight.Score.SetScore(InsightScoreType.Magnitude, serializedInsight.ScoreMagnitude, insight.CloseTimeUtc);
insight.Score.SetScore(InsightScoreType.Direction, serializedInsight.ScoreDirection, insight.CloseTimeUtc);
insight.Score.Finalize(insight.CloseTimeUtc);
}
else
{
if (serializedInsight.ScoreMagnitude != 0)
{
insight.Score.SetScore(InsightScoreType.Magnitude, serializedInsight.ScoreMagnitude, insight.CloseTimeUtc);
}
if (serializedInsight.ScoreDirection != 0)
{
insight.Score.SetScore(InsightScoreType.Direction, serializedInsight.ScoreDirection, insight.CloseTimeUtc);
}
}
return insight;
}
/// <summary>
/// Computes the insight closing time from the given generated time, resolution and bar count.
/// This will step through market hours using the given resolution, respecting holidays, early closes, weekends, etc..
/// </summary>
/// <param name="exchangeHours">The exchange hours of the insight's security</param>
/// <param name="generatedTimeUtc">The insight's generated time in utc</param>
/// <param name="resolution">The resolution used to 'step-through' market hours to compute a reasonable close time</param>
/// <param name="barCount">The number of resolution steps to take</param>
/// <returns>The insight's closing time in utc</returns>
public static DateTime ComputeCloseTime(SecurityExchangeHours exchangeHours, DateTime generatedTimeUtc, Resolution resolution, int barCount)
{
if (barCount < 1)
{
throw new ArgumentOutOfRangeException(nameof(barCount), Messages.Insight.InvalidBarCount);
}
// remap ticks to seconds
resolution = resolution == Resolution.Tick ? Resolution.Second : resolution;
if (resolution == Resolution.Hour)
{
// remap hours to minutes to avoid complications w/ stepping through
// for example 9->10 is an hour step but market opens at 9:30
barCount *= 60;
resolution = Resolution.Minute;
}
var barSize = resolution.ToTimeSpan();
var startTimeLocal = generatedTimeUtc.ConvertFromUtc(exchangeHours.TimeZone);
var closeTimeLocal = Time.GetEndTimeForTradeBars(exchangeHours, startTimeLocal, barSize, barCount, false);
return closeTimeLocal.ConvertToUtc(exchangeHours.TimeZone);
}
/// <summary>
/// computs the insight closing time from the given generated time and period
/// </summary>
/// <param name="exchangeHours">The exchange hours of the insight's security</param>
/// <param name="generatedTimeUtc">The insight's generated time in utc</param>
/// <param name="period">The insight's period</param>
/// <returns>The insight's closing time in utc</returns>
public static DateTime ComputeCloseTime(SecurityExchangeHours exchangeHours, DateTime generatedTimeUtc, TimeSpan period)
{
if (period < Time.OneSecond)
{
throw new ArgumentOutOfRangeException(nameof(period), Messages.Insight.InvalidPeriod);
}
var barSize = period.ToHigherResolutionEquivalent(false);
// remap ticks to seconds
barSize = barSize == Resolution.Tick ? Resolution.Second : barSize;
// remap hours to minutes to avoid complications w/ stepping through, for example 9->10 is an hour step but market opens at 9:30
barSize = barSize == Resolution.Hour ? Resolution.Minute : barSize;
var barCount = (int)(period.Ticks / barSize.ToTimeSpan().Ticks);
var closeTimeUtc = ComputeCloseTime(exchangeHours, generatedTimeUtc, barSize, barCount);
if (closeTimeUtc == generatedTimeUtc)
{
return ComputeCloseTime(exchangeHours, generatedTimeUtc, Resolution.Second, 1);
}
var totalPeriodUsed = barSize.ToTimeSpan().Multiply(barCount);
if (totalPeriodUsed != period)
{
var delta = period - totalPeriodUsed;
// interpret the remainder as fractional trading days
if (barSize == Resolution.Daily)
{
var percentOfDay = delta.Ticks / (double) Time.OneDay.Ticks;
delta = exchangeHours.RegularMarketDuration.Multiply(percentOfDay);
}
if (delta != TimeSpan.Zero)
{
// continue stepping forward using minute resolution for the remainder
barCount = (int) (delta.Ticks / Time.OneMinute.Ticks);
if (barCount > 0)
{
closeTimeUtc = ComputeCloseTime(exchangeHours, closeTimeUtc, Resolution.Minute, barCount);
}
}
}
return closeTimeUtc;
}
/// <summary>Returns a string that represents the current object.</summary>
/// <returns>A string that represents the current object.</returns>
/// <filterpriority>2</filterpriority>
public override string ToString()
{
return Messages.Insight.ToString(this);
}
/// <summary>
/// Returns a short string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public string ShortToString()
{
return Messages.Insight.ShortToString(this);
}
/// <summary>
/// Distinguishes between the different ways an insight's period/close times can be specified
/// This was really only required since we can't properly acces certain data from within a static
/// context (such as Insight.Price) or from within a constructor w/out requiring the users to properly
/// fetch the required data and supply it as an argument.
/// </summary>
private interface IPeriodSpecification
{
void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours);
}
/// <summary>
/// User defined the insight's period using a time span
/// </summary>
private class TimeSpanPeriodSpecification : IPeriodSpecification
{
public readonly TimeSpan Period;
public TimeSpanPeriodSpecification(TimeSpan period)
{
if (period == TimeSpan.Zero)
{
period = Time.OneSecond;
}
Period = period;
}
public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
{
insight.Period = Period;
insight.CloseTimeUtc = ComputeCloseTime(exchangeHours, insight.GeneratedTimeUtc, Period);
}
}
/// <summary>
/// User defined insight's period using a resolution and bar count
/// </summary>
private class ResolutionBarCountPeriodSpecification : IPeriodSpecification
{
public readonly Resolution Resolution;
public readonly int BarCount;
public ResolutionBarCountPeriodSpecification(Resolution resolution, int barCount)
{
if (resolution == Resolution.Tick)
{
resolution = Resolution.Second;
}
if (resolution == Resolution.Hour)
{
// remap hours to minutes to avoid errors w/ half hours, for example, 9:30 open
barCount *= 60;
resolution = Resolution.Minute;
}
Resolution = resolution;
BarCount = barCount;
}
public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
{
insight.CloseTimeUtc = ComputeCloseTime(exchangeHours, insight.GeneratedTimeUtc, Resolution, BarCount);
insight.Period = insight.CloseTimeUtc - insight.GeneratedTimeUtc;
}
}
/// <summary>
/// User defined the insight's local closing time
/// </summary>
private class CloseTimePeriodSpecification : IPeriodSpecification
{
public readonly DateTime CloseTimeLocal;
public CloseTimePeriodSpecification(DateTime closeTimeLocal)
{
CloseTimeLocal = closeTimeLocal;
}
public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
{
// Prevent close time to be defined to a date/time in closed market
var closeTimeLocal = exchangeHours.IsOpen(CloseTimeLocal, false)
? CloseTimeLocal
: exchangeHours.GetNextMarketOpen(CloseTimeLocal, false);
insight.CloseTimeUtc = closeTimeLocal.ConvertToUtc(exchangeHours.TimeZone);
if (insight.GeneratedTimeUtc > insight.CloseTimeUtc)
{
throw new ArgumentOutOfRangeException(nameof(closeTimeLocal), $"Insight closeTimeLocal must not be in the past.");
}
insight.Period = insight.CloseTimeUtc - insight.GeneratedTimeUtc;
}
}
/// <summary>
/// Special case for insights which close time is defined by a function
/// and want insights to expiry with calendar rules
/// </summary>
private class FuncPeriodSpecification : IPeriodSpecification
{
public readonly Func<DateTime, DateTime> _expiryFunc;
public FuncPeriodSpecification(Func<DateTime, DateTime> expiryFunc)
{
_expiryFunc = expiryFunc;
}
public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
{
var closeTimeLocal = insight.GeneratedTimeUtc.ConvertFromUtc(exchangeHours.TimeZone);
closeTimeLocal = _expiryFunc(closeTimeLocal);
// Prevent close time to be defined to a date/time in closed market
if (!exchangeHours.IsOpen(closeTimeLocal, false))
{
closeTimeLocal = exchangeHours.GetNextMarketOpen(closeTimeLocal, false);
}
insight.CloseTimeUtc = closeTimeLocal.ConvertToUtc(exchangeHours.TimeZone);
insight.Period = insight.CloseTimeUtc - insight.GeneratedTimeUtc;
}
}
/// <summary>
/// Special case for insights where we do not know whats the
/// <see cref="Period"/> or <see cref="CloseTimeUtc"/>.
/// </summary>
private class EndOfTimeCloseTimePeriodSpecification : IPeriodSpecification
{
public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
{
insight.Period = Time.EndOfTimeTimeSpan;
insight.CloseTimeUtc = Time.EndOfTime;
}
}
}
}
@@ -0,0 +1,395 @@
/*
* 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 System;
using System.Linq;
using Python.Runtime;
using System.Collections;
using System.Collections.Generic;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Provides a collection for managing insights. This type provides collection access semantics
/// as well as dictionary access semantics through TryGetValue, ContainsKey, and this[symbol]
/// </summary>
public class InsightCollection : IEnumerable<Insight>
{
private int _totalInsightCount;
private int _openInsightCount;
private readonly List<Insight> _insightsComplete = new();
private readonly Dictionary<Symbol, List<Insight>> _insights = new();
/// <summary>
/// The open insight count
/// </summary>
public int Count
{
get
{
lock (_insights)
{
return _openInsightCount;
}
}
}
/// <summary>
/// The total insight count
/// </summary>
public int TotalCount
{
get
{
lock (_insights)
{
return _totalInsightCount;
}
}
}
/// <summary>Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1" />.</summary>
/// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1" /> is read-only.</exception>
public void Add(Insight item)
{
lock (_insights)
{
_openInsightCount++;
_totalInsightCount++;
_insightsComplete.Add(item);
if (!_insights.TryGetValue(item.Symbol, out var existingInsights))
{
_insights[item.Symbol] = existingInsights = new();
}
existingInsights.Add(item);
}
}
/// <summary>
/// Adds each item in the specified enumerable of insights to this collection
/// </summary>
/// <param name="insights">The insights to add to this collection</param>
public void AddRange(IEnumerable<Insight> insights)
{
foreach (var insight in insights)
{
Add(insight);
}
}
/// <summary>Determines whether the <see cref="T:System.Collections.Generic.ICollection`1" /> contains a specific value.</summary>
/// <returns>true if <paramref name="item" /> is found in the <see cref="T:System.Collections.Generic.ICollection`1" />; otherwise, false.</returns>
/// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
public bool Contains(Insight item)
{
lock(_insights)
{
return _insights.TryGetValue(item.Symbol, out var symbolInsights)
&& symbolInsights.Contains(item);
}
}
/// <summary>
/// Determines whether insights exist in this collection for the specified symbol
/// </summary>
/// <param name="symbol">The symbol key</param>
/// <returns>True if there are insights for the symbol in this collection</returns>
public bool ContainsKey(Symbol symbol)
{
lock (_insights)
{
return _insights.TryGetValue(symbol, out var symbolInsights)
&& symbolInsights.Count > 0;
}
}
/// <summary>Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1" />.</summary>
/// <returns>true if <paramref name="item" /> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1" />; otherwise, false. This method also returns false if <paramref name="item" /> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1" />.</returns>
/// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1" /> is read-only.</exception>
public bool Remove(Insight item)
{
lock (_insights)
{
if (_insights.TryGetValue(item.Symbol, out var symbolInsights))
{
if (symbolInsights.Remove(item))
{
_openInsightCount--;
// remove empty list from dictionary
if (symbolInsights.Count == 0)
{
_insights.Remove(item.Symbol);
}
return true;
}
}
}
return false;
}
/// <summary>
/// Dictionary accessor returns a list of insights for the specified symbol
/// </summary>
/// <param name="symbol">The symbol key</param>
/// <returns>List of insights for the symbol</returns>
public List<Insight> this[Symbol symbol]
{
get
{
lock(_insights)
{
return _insights[symbol]?.ToList();
}
}
set
{
lock (_insights)
{
if (_insights.TryGetValue(symbol, out var existingInsights))
{
_openInsightCount -= existingInsights?.Count ?? 0;
}
if (value != null)
{
_openInsightCount += value.Count;
_totalInsightCount += value.Count;
}
_insights[symbol] = value;
}
}
}
/// <summary>
/// Attempts to get the list of insights with the specified symbol key
/// </summary>
/// <param name="symbol">The symbol key</param>
/// <param name="insights">The insights for the specified symbol, or null if not found</param>
/// <returns>True if insights for the specified symbol were found, false otherwise</returns>
public bool TryGetValue(Symbol symbol, out List<Insight> insights)
{
lock (_insights)
{
var result = _insights.TryGetValue(symbol, out insights);
if (result)
{
// for thread safety we need to return a copy of the collection
insights = insights.ToList();
}
return result;
}
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>A <see cref="T:System.Collections.Generic.IEnumerator`1" /> that can be used to iterate through the collection.</returns>
/// <filterpriority>1</filterpriority>
public IEnumerator<Insight> GetEnumerator()
{
lock (_insights)
{
return _insights.SelectMany(kvp => kvp.Value).ToList().GetEnumerator();
}
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
/// <filterpriority>2</filterpriority>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Removes the symbol and its insights
/// </summary>
/// <param name="symbols">List of symbols that will be removed</param>
public void Clear(Symbol[] symbols)
{
lock (_insights)
{
foreach (var symbol in symbols)
{
if (_insights.Remove(symbol, out var existingInsights))
{
_openInsightCount -= existingInsights.Count;
}
}
}
}
/// <summary>
/// Gets the next expiry time UTC
/// </summary>
public DateTime? GetNextExpiryTime()
{
lock(_insights)
{
if (_openInsightCount == 0)
{
return null;
}
// we can't store expiration time because it can change
return _insights.Min(x => x.Value.Min(i => i.CloseTimeUtc));
}
}
/// <summary>
/// Gets the last generated active insight
/// </summary>
/// <returns>Collection of insights that are active</returns>
public ICollection<Insight> GetActiveInsights(DateTime utcTime)
{
var activeInsights = new List<Insight>();
lock (_insights)
{
foreach (var kvp in _insights)
{
foreach (var insight in kvp.Value)
{
if (insight.IsActive(utcTime))
{
activeInsights.Add(insight);
}
}
}
return activeInsights;
}
}
/// <summary>
/// Returns true if there are active insights for a given symbol and time
/// </summary>
/// <param name="symbol">The symbol key</param>
/// <param name="utcTime">Time that determines whether the insight has expired</param>
/// <returns></returns>
public bool HasActiveInsights(Symbol symbol, DateTime utcTime)
{
lock (_insights)
{
if(_insights.TryGetValue(symbol, out var existingInsights))
{
return existingInsights.Any(i => i.IsActive(utcTime));
}
}
return false;
}
/// <summary>
/// Remove all expired insights from the collection and retuns them
/// </summary>
/// <param name="utcTime">Time that determines whether the insight has expired</param>
/// <returns>Expired insights that were removed</returns>
public ICollection<Insight> RemoveExpiredInsights(DateTime utcTime)
{
var removedInsights = new List<Insight>();
lock (_insights)
{
foreach (var kvp in _insights)
{
foreach (var insight in kvp.Value)
{
if (insight.IsExpired(utcTime))
{
removedInsights.Add(insight);
}
}
}
foreach (var insight in removedInsights)
{
Remove(insight);
}
}
return removedInsights;
}
/// <summary>
/// Will remove insights from the complete insight collection
/// </summary>
/// <param name="filter">The function that will determine which insight to remove</param>
public void RemoveInsights(Func<Insight, bool> filter)
{
lock (_insights)
{
_insightsComplete.RemoveAll(insight => filter(insight));
// for consistentcy remove from open insights just in case
List<Insight> insightsToRemove = null;
foreach (var insights in _insights.Values)
{
foreach (var insight in insights)
{
if (filter(insight))
{
insightsToRemove ??= new ();
insightsToRemove.Add(insight);
}
}
}
if(insightsToRemove != null)
{
foreach (var insight in insightsToRemove)
{
Remove(insight);
}
}
}
}
/// <summary>
/// Will return insights from the complete insight collection
/// </summary>
/// <param name="filter">The function that will determine which insight to return</param>
/// <returns>A new list containing the selected insights</returns>
public List<Insight> GetInsights(Func<Insight, bool> filter = null)
{
lock (_insights)
{
if(filter == null)
{
return _insightsComplete.ToList();
}
return _insightsComplete.Where(filter).ToList();
}
}
/// <summary>
/// Will return insights from the complete insight collection
/// </summary>
/// <param name="filter">The function that will determine which insight to return</param>
/// <returns>A new list containing the selected insights</returns>
public List<Insight> GetInsights(PyObject filter)
{
Func<Insight, bool> convertedFilter;
if (filter.TrySafeAs(out convertedFilter))
{
return GetInsights(convertedFilter);
}
else
{
using (Py.GIL())
{
throw new ArgumentException($"InsightCollection.GetInsights: {filter.Repr()} is not a valid argument.");
}
}
}
}
}
@@ -0,0 +1,42 @@
/*
* 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.Converters;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Specifies the predicted direction for a insight (price/volatility)
/// </summary>
[JsonConverter(typeof(StringEnumConverter), true)]
public enum InsightDirection
{
/// <summary>
/// The value will go down (-1)
/// </summary>
Down = -1,
/// <summary>
/// The value will stay flat (0)
/// </summary>
Flat = 0,
/// <summary>
/// The value will go up (1)
/// </summary>
Up = 1
}
}
@@ -0,0 +1,136 @@
/*
* 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 System;
using Newtonsoft.Json;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Defines the scores given to a particular insight
/// </summary>
public class InsightScore
{
/// <summary>
/// Gets the time these scores were last updated
/// </summary>
[JsonProperty]
public DateTime UpdatedTimeUtc { get; private set; }
/// <summary>
/// Gets the direction score
/// </summary>
[JsonProperty]
public double Direction { get; private set; }
/// <summary>
/// Gets the magnitude score
/// </summary>
[JsonProperty]
public double Magnitude { get; private set; }
/// <summary>
/// Gets whether or not this is the insight's final score
/// </summary>
[JsonProperty]
public bool IsFinalScore { get; private set; }
/// <summary>
/// Initializes a new, default instance of the <see cref="InsightScore"/> class
/// </summary>
public InsightScore()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InsightScore"/> class
/// </summary>
/// <param name="direction">The insight direction score</param>
/// <param name="magnitude">The insight percent change score</param>
/// <param name="updatedTimeUtc">The algorithm utc time these scores were computed</param>
public InsightScore(double direction, double magnitude, DateTime updatedTimeUtc)
{
Direction = direction;
Magnitude = magnitude;
UpdatedTimeUtc = updatedTimeUtc;
}
/// <summary>
/// Sets the specified score type with the value
/// </summary>
/// <param name="type">The score type to be set, Direction/Magnitude</param>
/// <param name="value">The new value for the score</param>
/// <param name="algorithmUtcTime">The algorithm's utc time at which time the new score was computed</param>
public void SetScore(InsightScoreType type, double value, DateTime algorithmUtcTime)
{
if (IsFinalScore) return;
UpdatedTimeUtc = algorithmUtcTime;
switch (type)
{
case InsightScoreType.Direction:
Direction = Math.Max(0, Math.Min(1, value));
break;
case InsightScoreType.Magnitude:
Magnitude = Math.Max(0, Math.Min(1, value));
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
/// <summary>
/// Marks the score as finalized, preventing any further updates.
/// </summary>
/// <param name="algorithmUtcTime">The algorithm's utc time at which time these scores were finalized</param>
public void Finalize(DateTime algorithmUtcTime)
{
IsFinalScore = true;
UpdatedTimeUtc = algorithmUtcTime;
}
/// <summary>
/// Gets the specified score
/// </summary>
/// <param name="type">The type of score to get, Direction/Magnitude</param>
/// <returns>The requested score</returns>
public double GetScore(InsightScoreType type)
{
switch (type)
{
case InsightScoreType.Direction:
return Direction;
case InsightScoreType.Magnitude:
return Magnitude;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
/// <summary>Returns a string that represents the current object.</summary>
/// <returns>A string that represents the current object.</returns>
/// <filterpriority>2</filterpriority>
public override string ToString()
{
return Messages.InsightScore.ToString(this);
}
}
}
@@ -0,0 +1,45 @@
/*
* 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 System;
using Python.Runtime;
using QuantConnect.Algorithm.Framework.Alphas.Analysis;
using QuantConnect.Python;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// A python implementation insight evaluator wrapper
/// </summary>
public class InsightScoreFunctionPythonWrapper : BasePythonWrapper<IInsightScoreFunction>, IInsightScoreFunction
{
/// <summary>
/// Creates a new python wrapper instance
/// </summary>
/// <param name="insightEvaluator">The python instance to wrap</param>
public InsightScoreFunctionPythonWrapper(PyObject insightEvaluator)
: base(insightEvaluator)
{
}
/// <summary>
/// Method to evaluate and score insights for each time step
/// </summary>
public void Score(InsightManager insightManager, DateTime utcTime)
{
InvokeMethod(nameof(Score), insightManager, utcTime);
}
}
}
@@ -0,0 +1,38 @@
/*
* 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.Converters;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Defines a specific type of score for a insight
/// </summary>
[JsonConverter(typeof(StringEnumConverter), true)]
public enum InsightScoreType
{
/// <summary>
/// Directional accuracy (0)
/// </summary>
Direction,
/// <summary>
/// Magnitude accuracy (1)
/// </summary>
Magnitude
}
}
@@ -0,0 +1,37 @@
/*
* 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.Converters;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Specifies the type of insight
/// </summary>
[JsonConverter(typeof(StringEnumConverter), true)]
public enum InsightType
{
/// <summary>
/// The insight is for a security's price (0)
/// </summary>
Price,
/// <summary>
/// The insight is for a security's price volatility (1)
/// </summary>
Volatility
}
}
@@ -0,0 +1,46 @@
/*
* 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.Util;
namespace QuantConnect.Algorithm.Framework.Alphas.Serialization
{
/// <summary>
/// Defines how insights should be serialized to json
/// </summary>
public class InsightJsonConverter : TypeChangeJsonConverter<Insight, SerializedInsight>
{
/// <summary>
/// Convert the input value to a value to be serialized
/// </summary>
/// <param name="value">The input value to be converted before serialization</param>
/// <returns>A new instance of TResult that is to be serialized</returns>
protected override SerializedInsight Convert(Insight value)
{
return new SerializedInsight(value);
}
/// <summary>
/// Converts the input value to be deserialized
/// </summary>
/// <param name="value">The deserialized value that needs to be converted to T</param>
/// <returns>The converted value</returns>
protected override Insight Convert(SerializedInsight value)
{
return Insight.FromSerializedInsight(value);
}
}
}
@@ -0,0 +1,292 @@
/*
* 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.Util;
using System;
namespace QuantConnect.Algorithm.Framework.Alphas.Serialization
{
/// <summary>
/// DTO used for serializing an insight that was just generated by an algorithm.
/// This type does not contain any of the analysis dependent fields, such as scores
/// and estimated value
/// </summary>
public class SerializedInsight
{
private double _createdTime;
/// <summary>
/// See <see cref="Insight.Id"/>
/// </summary>
public string Id { get; set; }
/// <summary>
/// See <see cref="Insight.GroupId"/>
/// </summary>
public string GroupId { get; set; }
/// <summary>
/// See <see cref="Insight.SourceModel"/>
/// </summary>
public string SourceModel { get; set; }
/// <summary>
/// Pass-through for <see cref="CreatedTime"/>
/// </summary>
[Obsolete("Deprecated as of 2020-01-23. Please use the `CreatedTime` property instead.")]
public double GeneratedTime
{
get { return _createdTime; }
set { _createdTime = value; }
}
/// <summary>
/// See <see cref="Insight.GeneratedTimeUtc"/>
/// </summary>
public double CreatedTime
{
get { return _createdTime; }
set { _createdTime = value; }
}
/// <summary>
/// See <see cref="Insight.CloseTimeUtc"/>
/// </summary>
public double CloseTime { get; set; }
/// <summary>
/// See <see cref="Insight.Symbol"/>
/// The symbol's security identifier string
/// </summary>
public string Symbol { get; set; }
/// <summary>
/// See <see cref="Insight.Symbol"/>
/// The symbol's ticker at the generated time
/// </summary>
public string Ticker { get; set; }
/// <summary>
/// See <see cref="Insight.Type"/>
/// </summary>
public InsightType Type { get; set; }
/// <summary>
/// See <see cref="Insight.ReferenceValue"/>
/// </summary>
[JsonProperty("reference")]
public decimal ReferenceValue { get; set; }
/// <summary>
/// See <see cref="Insight.ReferenceValueFinal"/>
/// </summary>
public decimal ReferenceValueFinal { get; set; }
/// <summary>
/// See <see cref="Insight.Direction"/>
/// </summary>
public InsightDirection Direction { get; set; }
/// <summary>
/// See <see cref="Insight.Period"/>
/// </summary>
public double Period { get; set; }
/// <summary>
/// See <see cref="Insight.Magnitude"/>
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public double? Magnitude { get; set; }
/// <summary>
/// See <see cref="Insight.Confidence"/>
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public double? Confidence { get; set; }
/// <summary>
/// See <see cref="Insight.Weight"/>
/// </summary>
public double? Weight { get; set; }
/// <summary>
/// See <see cref="InsightScore.IsFinalScore"/>
/// </summary>
public bool ScoreIsFinal { get; set; }
/// <summary>
/// See <see cref="InsightScore.Magnitude"/>
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public double ScoreMagnitude { get; set; }
/// <summary>
/// See <see cref="InsightScore.Direction"/>
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public double ScoreDirection { get; set; }
/// <summary>
/// See <see cref="Insight.EstimatedValue"/>
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal EstimatedValue { get; set; }
/// <summary>
/// See <see cref="Insight.Tag"/>
/// </summary>
public string Tag { get; set; }
/// <summary>
/// Initializes a new default instance of the <see cref="SerializedInsight"/> class
/// </summary>
public SerializedInsight()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SerializedInsight "/> class by copying the specified insight
/// </summary>
/// <param name="insight">The insight to copy</param>
public SerializedInsight(Insight insight)
{
Id = insight.Id.ToStringInvariant("N");
SourceModel = insight.SourceModel;
GroupId = insight.GroupId?.ToStringInvariant("N");
CreatedTime = Time.DateTimeToUnixTimeStamp(insight.GeneratedTimeUtc);
CloseTime = Time.DateTimeToUnixTimeStamp(insight.CloseTimeUtc);
Symbol = insight.Symbol.ID.ToString();
Ticker = insight.Symbol.Value;
Type = insight.Type;
ReferenceValue = insight.ReferenceValue;
ReferenceValueFinal = insight.ReferenceValueFinal;
Direction = insight.Direction;
Period = insight.Period.TotalSeconds;
Magnitude = insight.Magnitude;
Confidence = insight.Confidence;
ScoreIsFinal = insight.Score.IsFinalScore;
ScoreMagnitude = insight.Score.Magnitude;
ScoreDirection = insight.Score.Direction;
EstimatedValue = insight.EstimatedValue;
Weight = insight.Weight;
Tag = insight.Tag;
}
#region BackwardsCompatibility
[JsonProperty("group-id")]
string OldGroupId
{
set
{
GroupId = value;
}
}
[JsonProperty("source-model")]
string OldSourceModel
{
set
{
SourceModel = value;
}
}
/// <summary>
/// Pass-through for <see cref="CreatedTime"/>
/// </summary>
[JsonProperty("generated-time")]
double OldGeneratedTime
{
set
{
GeneratedTime = value;
}
}
/// <summary>
/// See <see cref="Insight.GeneratedTimeUtc"/>
/// </summary>
[JsonProperty("created-time")]
public double OldCreatedTime
{
set
{
CreatedTime = value;
}
}
/// <summary>
/// See <see cref="Insight.CloseTimeUtc"/>
/// </summary>
[JsonProperty("close-time")]
public double OldCloseTime
{
set
{
CloseTime = value;
}
}
[JsonProperty("reference-final")]
decimal OldReferenceValueFinal
{
set
{
ReferenceValueFinal = value;
}
}
[JsonProperty("score-final")]
bool OldScoreIsFinal
{
set
{
ScoreIsFinal = value;
}
}
[JsonProperty("score-magnitude")]
[JsonConverter(typeof(JsonRoundingConverter))]
double OldScoreMagnitude
{
set
{
ScoreMagnitude = value;
}
}
[JsonProperty("score-direction")]
[JsonConverter(typeof(JsonRoundingConverter))]
double OldScoreDirection
{
set
{
ScoreDirection = value;
}
}
[JsonProperty("estimated-value")]
[JsonConverter(typeof(JsonRoundingConverter))]
decimal OldEstimatedValue
{
set
{
EstimatedValue = value;
}
}
#endregion
}
}
@@ -0,0 +1,39 @@
/*
* 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.
*/
namespace QuantConnect.Algorithm.Framework.Portfolio
{
/// <summary>
/// Represents a portfolio target. This may be a percentage of total portfolio value
/// or it may be a fixed number of shares.
/// </summary>
public interface IPortfolioTarget
{
/// <summary>
/// Gets the symbol of this target
/// </summary>
Symbol Symbol { get; }
/// <summary>
/// Gets the quantity of this symbol the algorithm should hold
/// </summary>
decimal Quantity { get; }
/// <summary>
/// Portfolio target tag with additional information
/// </summary>
string Tag { get; }
}
}
@@ -0,0 +1,203 @@
/*
* 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 System;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using System.Collections.Generic;
using QuantConnect.Securities.Positions;
using QuantConnect.Algorithm.Framework.Alphas;
namespace QuantConnect.Algorithm.Framework.Portfolio
{
/// <summary>
/// Provides an implementation of <see cref="IPortfolioTarget"/> that specifies a
/// specified quantity of a security to be held by the algorithm
/// </summary>
public class PortfolioTarget : IPortfolioTarget
{
/// <summary>
/// Flag to determine if the minimum order margin portfolio percentage warning should or has already been sent to the user algorithm
/// <see cref="IAlgorithmSettings.MinimumOrderMarginPortfolioPercentage"/>
/// </summary>
public static bool? MinimumOrderMarginPercentageWarningSent { get; set; }
/// <summary>
/// Gets the symbol of this target
/// </summary>
public Symbol Symbol { get; }
/// <summary>
/// Gets the target quantity for the symbol
/// </summary>
public decimal Quantity { get; }
/// <summary>
/// Portfolio target tag with additional information
/// </summary>
public string Tag { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioTarget"/> class
/// </summary>
/// <param name="symbol">The symbol this target is for</param>
/// <param name="quantity">The target quantity</param>
/// <param name="tag">The target tag with additional information</param>
public PortfolioTarget(Symbol symbol, decimal quantity, string tag = "")
{
Symbol = symbol;
Quantity = quantity;
Tag = tag;
}
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioTarget"/> class
/// </summary>
/// <param name="symbol">The symbol this target is for</param>
/// <param name="quantity">The target quantity</param>
/// <param name="tag">The target tag with additional information</param>
public PortfolioTarget(Symbol symbol, int quantity, string tag = "")
: this(symbol, (decimal)quantity, tag)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioTarget"/> class
/// </summary>
/// <param name="symbol">The symbol this target is for</param>
/// <param name="insightDirection">
/// The insight direction, which will be used to calculate the target quantity
/// (1 for Up, 0 for flat, -1 for down)
/// </param>
/// <param name="tag">The target tag with additional information</param>
public PortfolioTarget(Symbol symbol, InsightDirection insightDirection, string tag = "")
: this(symbol,
insightDirection switch
{
InsightDirection.Up => 1m,
InsightDirection.Down => -1m,
InsightDirection.Flat => 0m,
_ => throw new ArgumentOutOfRangeException(nameof(insightDirection), insightDirection,
Messages.PortfolioTarget.InvalidInsightDirection(symbol, insightDirection)),
},
tag)
{
}
/// <summary>
/// Creates a new target for the specified percent
/// </summary>
/// <param name="algorithm">The algorithm instance, used for getting total portfolio value and current security price</param>
/// <param name="symbol">The symbol the target is for</param>
/// <param name="percent">The requested target percent of total portfolio value</param>
/// <returns>A portfolio target for the specified symbol/percent</returns>
public static IPortfolioTarget Percent(IAlgorithm algorithm, Symbol symbol, double percent)
{
return Percent(algorithm, symbol, percent.SafeDecimalCast());
}
/// <summary>
/// Creates a new target for the specified percent
/// </summary>
/// <param name="algorithm">The algorithm instance, used for getting total portfolio value and current security price</param>
/// <param name="symbol">The symbol the target is for</param>
/// <param name="percent">The requested target percent of total portfolio value</param>
/// <param name="tag">The target tag with additional information</param>
/// <returns>A portfolio target for the specified symbol/percent</returns>
public static IPortfolioTarget Percent(IAlgorithm algorithm, Symbol symbol, double percent, string tag)
{
return Percent(algorithm, symbol, percent.SafeDecimalCast(), tag: tag);
}
/// <summary>
/// Creates a new target for the specified percent
/// </summary>
/// <param name="algorithm">The algorithm instance, used for getting total portfolio value and current security price</param>
/// <param name="symbol">The symbol the target is for</param>
/// <param name="percent">The requested target percent of total portfolio value</param>
/// <param name="returnDeltaQuantity">True, result quantity will be the Delta required to reach target percent.
/// False, the result quantity will be the Total quantity to reach the target percent, including current holdings</param>
/// <param name="tag">The target tag with additional information</param>
/// <returns>A portfolio target for the specified symbol/percent</returns>
public static IPortfolioTarget Percent(IAlgorithm algorithm, Symbol symbol, decimal percent, bool returnDeltaQuantity = false, string tag = "")
{
var absolutePercentage = Math.Abs(percent);
if (absolutePercentage > algorithm.Settings.MaxAbsolutePortfolioTargetPercentage
|| absolutePercentage != 0 && absolutePercentage < algorithm.Settings.MinAbsolutePortfolioTargetPercentage)
{
algorithm.Error(Messages.PortfolioTarget.InvalidTargetPercent(algorithm, percent));
return null;
}
Security security;
try
{
security = algorithm.Securities[symbol];
}
catch (KeyNotFoundException)
{
algorithm.Error(Messages.PortfolioTarget.SymbolNotFound(symbol));
return null;
}
if (security.Price == 0)
{
algorithm.Error(symbol.GetZeroPriceMessage());
return null;
}
// Factoring in FreePortfolioValuePercentage.
var adjustedPercent = percent * algorithm.Portfolio.TotalPortfolioValueLessFreeBuffer / algorithm.Portfolio.TotalPortfolioValue;
// we normalize the target buying power by the leverage so we work in the land of margin
var targetFinalMarginPercentage = adjustedPercent / security.BuyingPowerModel.GetLeverage(security);
var positionGroup = algorithm.Portfolio.Positions.GetOrCreateDefaultGroup(security);
var result = positionGroup.BuyingPowerModel.GetMaximumLotsForTargetBuyingPower(
new GetMaximumLotsForTargetBuyingPowerParameters(algorithm.Portfolio, positionGroup,
targetFinalMarginPercentage, algorithm.Settings.MinimumOrderMarginPortfolioPercentage));
if (result.IsError)
{
algorithm.Error(Messages.PortfolioTarget.UnableToComputeOrderQuantityDueToNullResult(symbol, result));
return null;
}
if (MinimumOrderMarginPercentageWarningSent.HasValue && !MinimumOrderMarginPercentageWarningSent.Value)
{
// we send the warning once
MinimumOrderMarginPercentageWarningSent = true;
algorithm.Debug(Messages.BuyingPowerModel.TargetOrderMarginNotAboveMinimum());
}
// be sure to back out existing holdings quantity since the buying power model yields
// the required delta quantity to reach a final target portfolio value for a symbol
var lotSize = security.SymbolProperties.LotSize;
var quantity = result.NumberOfLots * lotSize + (returnDeltaQuantity ? 0 : security.Holdings.Quantity);
return new PortfolioTarget(symbol, quantity, tag);
}
/// <summary>Returns a string that represents the current object.</summary>
/// <returns>A string that represents the current object.</returns>
/// <filterpriority>2</filterpriority>
public override string ToString()
{
return Messages.PortfolioTarget.ToString(this);
}
}
}
@@ -0,0 +1,435 @@
/*
* 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 System;
using System.Linq;
using System.Collections;
using QuantConnect.Interfaces;
using System.Collections.Generic;
namespace QuantConnect.Algorithm.Framework.Portfolio
{
/// <summary>
/// Provides a collection for managing <see cref="IPortfolioTarget"/>s for each symbol
/// </summary>
public class PortfolioTargetCollection : ICollection<IPortfolioTarget>, IDictionary<Symbol, IPortfolioTarget>
{
private List<IPortfolioTarget> _enumerable;
private List<KeyValuePair<Symbol, IPortfolioTarget>> _kvpEnumerable;
private readonly Dictionary<Symbol, IPortfolioTarget> _targets = new ();
/// <summary>
/// Gets the number of targets in this collection
/// </summary>
public int Count
{
get
{
lock (_targets)
{
return _targets.Count;
}
}
}
/// <summary>
/// True if there is no target in the collection
/// </summary>
public bool IsEmpty
{
get
{
lock (_targets)
{
return _targets.Count == 0;
}
}
}
/// <summary>
/// Gets `false`. This collection is not read-only.
/// </summary>
public bool IsReadOnly => false;
/// <summary>
/// Gets the symbol keys for this collection
/// </summary>
public ICollection<Symbol> Keys
{
get
{
lock (_targets)
{
return _targets.Keys.ToList();
}
}
}
/// <summary>
/// Gets all portfolio targets in this collection
/// Careful, will return targets for securities that might have no data yet.
/// </summary>
public ICollection<IPortfolioTarget> Values
{
get
{
var result = _enumerable;
if (result == null)
{
lock (_targets)
{
result = _enumerable = _targets.Values.ToList();
}
}
return result;
}
}
/// <summary>
/// Adds the specified target to the collection. If a target for the same symbol
/// already exists it wil be overwritten.
/// </summary>
/// <param name="target">The portfolio target to add</param>
public void Add(IPortfolioTarget target)
{
if (target == null)
{
return;
}
lock (_targets)
{
_enumerable = null;
_kvpEnumerable = null;
_targets[target.Symbol] = target;
}
}
/// <summary>
/// Adds the specified target to the collection. If a target for the same symbol
/// already exists it wil be overwritten.
/// </summary>
/// <param name="target">The portfolio target to add</param>
public void Add(KeyValuePair<Symbol, IPortfolioTarget> target)
{
Add(target);
}
/// <summary>
/// Adds the specified target to the collection. If a target for the same symbol
/// already exists it wil be overwritten.
/// </summary>
/// <param name="symbol">The symbol key</param>
/// <param name="target">The portfolio target to add</param>
public void Add(Symbol symbol, IPortfolioTarget target)
{
Add(target);
}
/// <summary>
/// Adds the specified targets to the collection. If a target for the same symbol
/// already exists it will be overwritten.
/// </summary>
/// <param name="targets">The portfolio targets to add</param>
public void AddRange(IEnumerable<IPortfolioTarget> targets)
{
lock (_targets)
{
_enumerable = null;
_kvpEnumerable = null;
foreach (var item in targets)
{
_targets[item.Symbol] = item;
}
}
}
/// <summary>
/// Adds the specified targets to the collection. If a target for the same symbol
/// already exists it will be overwritten.
/// </summary>
/// <param name="targets">The portfolio targets to add</param>
public void AddRange(IPortfolioTarget[] targets)
{
AddRange((IEnumerable<IPortfolioTarget>)targets);
}
/// <summary>
/// Removes all portfolio targets from this collection
/// </summary>
public void Clear()
{
lock (_targets)
{
_enumerable = null;
_kvpEnumerable = null;
_targets.Clear();
}
}
/// <summary>
/// Removes fulfilled portfolio targets from this collection.
/// Will only take into account actual holdings and ignore open orders.
/// </summary>
public void ClearFulfilled(IAlgorithm algorithm)
{
foreach (var target in this)
{
var security = algorithm.Securities[target.Symbol];
var holdings = security.Holdings.Quantity;
// check to see if we're done with this target
if (Math.Abs(target.Quantity - holdings) < security.SymbolProperties.LotSize)
{
Remove(target.Symbol);
}
}
}
/// <summary>
/// Determines whether or not the specified target exists in this collection.
/// NOTE: This checks for the exact specified target, not by symbol. Use ContainsKey
/// to check by symbol.
/// </summary>
/// <param name="target">The portfolio target to check for existence.</param>
/// <returns>True if the target exists, false otherwise</returns>
public bool Contains(IPortfolioTarget target)
{
if (target == null)
{
return false;
}
lock (_targets)
{
return _targets.ContainsKey(target.Symbol);
}
}
/// <summary>
/// Determines whether the specified symbol/target pair exists in this collection
/// </summary>
/// <param name="target">The symbol/target pair</param>
/// <returns>True if the pair exists, false otherwise</returns>
public bool Contains(KeyValuePair<Symbol, IPortfolioTarget> target)
{
return Contains(target);
}
/// <summary>
/// Determines whether the specified symbol exists as a key in this collection
/// </summary>
/// <param name="symbol">The symbol key</param>
/// <returns>True if the symbol exists in this collection, false otherwise</returns>
public bool ContainsKey(Symbol symbol)
{
lock (_targets)
{
return _targets.ContainsKey(symbol);
}
}
/// <summary>
/// Copies the targets in this collection to the specified array
/// </summary>
/// <param name="array">The destination array to copy to</param>
/// <param name="arrayIndex">The index in the array to start copying to</param>
public void CopyTo(IPortfolioTarget[] array, int arrayIndex)
{
lock (_targets)
{
_targets.Values.CopyTo(array, arrayIndex);
}
}
/// <summary>
/// Copies the targets in this collection to the specified array
/// </summary>
/// <param name="array">The destination array to copy to</param>
/// <param name="arrayIndex">The index in the array to start copying to</param>
public void CopyTo(KeyValuePair<Symbol, IPortfolioTarget>[] array, int arrayIndex)
{
WithDictionary(d => d.CopyTo(array, arrayIndex));
}
/// <summary>
/// Removes the target for the specified symbol if it exists in this collection.
/// </summary>
/// <param name="symbol">The symbol to remove</param>
/// <returns>True if the symbol's target was removed, false if it doesn't exist in the collection</returns>
public bool Remove(Symbol symbol)
{
lock (_targets)
{
if (_targets.Remove(symbol))
{
_enumerable = null;
_kvpEnumerable = null;
return true;
}
return false;
}
}
/// <summary>
/// Removes the target for the specified symbol/target pair if it exists in this collection.
/// </summary>
/// <param name="target">The symbol/target pair to remove</param>
/// <returns>True if the symbol's target was removed, false if it doesn't exist in the collection</returns>
public bool Remove(KeyValuePair<Symbol, IPortfolioTarget> target)
{
return Remove(target.Value);
}
/// <summary>
/// Removes the target if it exists in this collection.
/// </summary>
/// <param name="target">The target to remove</param>
/// <returns>True if the target was removed, false if it doesn't exist in the collection</returns>
public bool Remove(IPortfolioTarget target)
{
if (target == null)
{
return false;
}
lock (_targets)
{
IPortfolioTarget existing;
if (_targets.TryGetValue(target.Symbol, out existing))
{
// need to confirm that we're removing the requested target and not a different target w/ the same symbol key
if (existing.Equals(target))
{
return Remove(target.Symbol);
}
}
}
return false;
}
/// <summary>
/// Attempts to retrieve the target for the specified symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="target">The portfolio target for the symbol, or null if not found</param>
/// <returns>True if the symbol's target was found, false if it does not exist in this collection</returns>
public bool TryGetValue(Symbol symbol, out IPortfolioTarget target)
{
lock (_targets)
{
return _targets.TryGetValue(symbol, out target);
}
}
/// <summary>
/// Gets or sets the portfolio target for the specified symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <returns>The symbol's portfolio target if it exists in this collection, if not a <see cref="KeyNotFoundException"/> will be thrown.</returns>
public IPortfolioTarget this[Symbol symbol]
{
get
{
lock (_targets)
{
return _targets[symbol];
}
}
set
{
lock (_targets)
{
_enumerable = null;
_kvpEnumerable = null;
_targets[symbol] = value;
}
}
}
/// <summary>
/// Gets an enumerator to iterator over the symbol/target key value pairs in this collection.
/// </summary>
/// <returns>Symbol/target key value pair enumerator</returns>
IEnumerator<KeyValuePair<Symbol, IPortfolioTarget>> IEnumerable<KeyValuePair<Symbol, IPortfolioTarget>>.GetEnumerator()
{
var result = _kvpEnumerable;
if (result == null)
{
lock (_targets)
{
_kvpEnumerable = result = _targets.ToList();
}
}
return result.GetEnumerator();
}
/// <summary>
/// Gets an enumerator to iterator over all portfolio targets in this collection.
/// This is the default enumerator for this collection.
/// </summary>
/// <returns>Portfolio targets enumerator</returns>
public IEnumerator<IPortfolioTarget> GetEnumerator()
{
var result = _enumerable;
if (result == null)
{
lock (_targets)
{
_enumerable = result = _targets.Values.ToList();
}
}
return result.GetEnumerator();
}
/// <summary>
/// Gets an enumerator to iterator over all portfolio targets in this collection.
/// This is the default enumerator for this collection.
/// Careful, will return targets for securities that might have no data yet.
/// </summary>
/// <returns>Portfolio targets enumerator</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Helper function to easily access explicitly implemented interface methods against concurrent dictionary
/// </summary>
private void WithDictionary(Action<IDictionary<Symbol, IPortfolioTarget>> action)
{
lock (_targets)
{
action(_targets);
}
}
/// <summary>
/// Returned an ordered enumerable where position reducing orders are executed first
/// and the remaining orders are executed in decreasing order value.
/// Will NOT return targets for securities that have no data yet.
/// Will NOT return targets for which current holdings + open orders quantity, sum up to the target quantity
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
public IEnumerable<IPortfolioTarget> OrderByMarginImpact(IAlgorithm algorithm)
{
if (IsEmpty)
{
return Enumerable.Empty<IPortfolioTarget>();
}
return this.OrderTargetsByMarginImpact(algorithm);
}
}
}
@@ -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;
}
}
}
}