chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user