Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading;

namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
{
Expand Down Expand Up @@ -112,33 +113,88 @@ public override bool Send(SignalExportTargetParameters parameters)
return false;
}

var csv = BuildCsv(parameters);
_requestsRateLimiter?.WaitToProceed();
return Stamp(csv, parameters.Algorithm);
try
{

var csv = BuildCsv(parameters);
_requestsRateLimiter?.WaitToProceed();
return Stamp(csv, parameters.Algorithm);
}
catch (InvalidOperationException e)
{
parameters.Algorithm.Error($"vBase signal export failed: {e.Message}");
return false;
}
}

/// <summary>
/// Builds a CSV (sym,wt) for the given targets converting percent holdings into absolute quantity using PortfolioTarget.Percent
/// Builds a CSV with header `sym,wt` that lists the normalized portfolio weights for every symbol in the
/// current portfolio unioned with the provided targets, converting quantities to value using current prices.
/// </summary>
/// <param name="parameters">Signal export parameters</param>
/// <returns>Resulting CSV string</returns>
protected virtual string BuildCsv(SignalExportTargetParameters parameters)
{
var algorithm = parameters.Algorithm;
var csv = "sym,wt\n";

var targets = parameters.Targets.Select(target =>
PortfolioTarget.Percent(algorithm, target.Symbol, target.Quantity)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @vb-vlb!
The original implementation is expecting percentages/weights to be passed in, as documented at https://www.quantconnect.com/docs/v2/writing-algorithms/live-trading/signal-exports/vbase#01-Introduction see also example algorithm https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/VBaseSignalExportDemonstrationAlgorithm.py .
If the API is expecting percentages/weights too this means this implementation shouldn't be doing any math at all but just passing through the values?
Please take a look at SignalExport.SetTargetPortfolioFromPortfolio() too which will call Send on VBaseSignalExport passing percent's too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Martin-Molinero, thanks for the prompt reply.

In the VBaseSignalExportDemonstrationAlgorithm, the portfolio targets collection consists of a single record: SPY with a quantity of 0.25.
Since we only have one position with a quantity of 0.25, the weights output should consist of a single record with SPY having a weight of 1.
like this
sym,wt
SPY,1

In fact, because PortfolioTarget.Percent creates a new target for the specified percentage, we end up receiving a quantity of SPY that needs to be bought to make it 25% of the portfolio. This results in a stamped weights file like:

sym,wt
SPY,172

the documentation states:

“Under the hood, it uses PortfolioTarget.Percent to convert your absolute target quantities into portfolio weights.”
Which implies that the expected inputs are absolute target quantities, not derived quantities.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the portfolio targets collection consists of a single record: SPY with a quantity of 0.25.
Since we only have one position with a quantity of 0.25, the weights output should consist of a single record with SPY having a weight of 1.

hm don't think this is right really, 0.25 mean 25% if the available portfolio, that shouldn't translate to 1 IMHO. The remaining 75% of the portfolio can be allocated to something else in the next minute for example

the documentation states:

The documentation could be wrong, need an update 💯, but what matters I think is what it should/expected to be by the vbase api and consumer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Martin-Molinero ,

The business objective is to create a snapshot of portfolio weights that best reflects, economically, what the portfolio actually holds.

The vBase API does not analyze the contents of the file; it only stamps the hash. Maybe @greg-vbase from vBase can comment on this to confirm.

I still see two issues with the current implementation of VBaseSignalExport:

  1. It takes quantity and passes it through PortfolioTarget.Percent, which returns a number of units. As a result, we end up stamping units instead of weights.

  2. If I understand correctly, the targets in the signal export do not always contain all positions in the portfolio. Since we want to stamp the portfolio weights, we also need to include positions that already exist in the portfolio but are not listed in the targets.

The proposed PR addresses the issues above.

)
.Where(absoluteTarget => absoluteTarget != null);
var weights = GetWeights(parameters);

foreach (var target in targets)
foreach (var weight in weights)
{
csv += $"{target.Symbol.Value},{target.Quantity.ToStringInvariant()}\n";
csv += $"{weight.Symbol},{weight.Weight.ToStringInvariant()}\n";
}
return csv;
}

private List<(Symbol Symbol, decimal Weight)> GetWeights(SignalExportTargetParameters parameters)
{
var algorithm = parameters.Algorithm;
List<(Symbol Symbol, decimal Value)> symbolValues = new();

// parameters targets contain only updates to the portfolio
// as we want to stamp weights for all positions, we need to union with current portfolio symbols
List<Symbol> allSymbols = algorithm.Portfolio.Keys.Union(parameters.Targets.Select(t => t.Symbol)).ToList();

foreach (Symbol symbol in allSymbols)
{
// if symbol is in parameters targets we take quantity from there
// otherwise we take current portfolio quantity
decimal quantity = parameters.Targets
.SingleOrDefault(t => t.Symbol == symbol)
?.Quantity ?? algorithm.Portfolio[symbol].Quantity;

if (algorithm.Securities.TryGetValue(symbol, out var security))
{
// we use current price of the security to convert quantity into value, which will be used to calculate weights
symbolValues.Add((symbol, quantity * security.Price));
}
else
{
// if we can't find the symbol in securities, we cannot calculate weights
throw new InvalidOperationException(Messages.PortfolioTarget.SymbolNotFound(symbol));
}
}

List<(Symbol Symbol, decimal Weight)> weights = new();

// get total value of the portfolio by summing values of all symbols
decimal sum = symbolValues.Sum(p => p.Value);

if (sum == 0)
{
// if sum is 0 - no positions
// we cannot calculate weights, but we can still stamp an empty portfolio
return weights;
}

foreach (var symbolValue in symbolValues)
{
weights.Add((symbolValue.Symbol, symbolValue.Value / sum));
}

return weights;
}

/// <summary>
/// Sends the CSV payload to the vBase stamping API
/// </summary>
Expand Down Expand Up @@ -179,3 +235,5 @@ private bool Stamp(string csv, IAlgorithm algorithm)
}
}
}