Skip to content

Commit f4dd763

Browse files
committed
Addresses Peer-Review
1 parent 2da8092 commit f4dd763

File tree

4 files changed

+69
-22
lines changed

4 files changed

+69
-22
lines changed

Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
*/
1515

1616
using Newtonsoft.Json;
17+
using QuantConnect.Algorithm.Framework.Portfolio;
1718
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
1819
using QuantConnect.Api;
1920
using QuantConnect.Data;
2021
using QuantConnect.Interfaces;
2122
using System;
23+
using System.Linq;
2224
using System.Net.Http;
2325
using System.Net.Http.Json;
2426
using System.Text;
@@ -43,6 +45,7 @@ public override void Initialize()
4345

4446
/// Our custom signal export accepts all asset types
4547
AddEquity("SPY", Resolution.Second);
48+
AddCrypto("BTCUSD", Resolution.Second);
4649
AddForex("EURUSD", Resolution.Second);
4750
AddFutureContract(QuantConnect.Symbol.CreateFuture("ES", Market.CME, new DateTime(2023, 12, 15), null));
4851
AddOptionContract(QuantConnect.Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 130, new DateTime(2023, 9, 1)));
@@ -57,7 +60,7 @@ public override void Initialize()
5760
/// <param name="slice"></param>
5861
public override void OnData(Slice slice)
5962
{
60-
foreach (var ticker in new[] { "SPY", "EURUSD" })
63+
foreach (var ticker in new[] { "SPY", "EURUSD", "BTCUSD" })
6164
{
6265
if (!Portfolio[ticker].Invested && Securities[ticker].HasData)
6366
{
@@ -74,10 +77,17 @@ internal class CustomSignalExport : ISignalExportTarget
7477

7578
public bool Send(SignalExportTargetParameters parameters)
7679
{
77-
var message = JsonConvert.SerializeObject(parameters.Targets);
80+
object SimplePayload(PortfolioTarget target)
81+
{
82+
var newTarget = PortfolioTarget.Percent(parameters.Algorithm, target.Symbol, target.Quantity);
83+
return new { symbol = newTarget.Symbol.Value, quantity = newTarget.Quantity };
84+
};
85+
86+
var message = JsonConvert.SerializeObject(parameters.Targets.Select(SimplePayload));
7887
using var httpMessage = new StringContent(message, Encoding.UTF8, "application/json");
7988
using HttpResponseMessage response = _httpClient.PostAsync(_requestUri, httpMessage).Result;
8089
var result = response.Content.ReadFromJsonAsync<RestResponse>().Result;
90+
parameters.Algorithm.Log($"Send #{parameters.Targets.Count} targets. Success: {result.Success}");
8191
return result.Success;
8292
}
8393

@@ -86,6 +96,7 @@ public bool Send(SignalExportTargetParameters parameters)
8696
}
8797

8898
/*
99+
# To test the algorithm, you can create a simple Python Flask application (app.py) and run flask
89100
# $ flask --app app run
90101
91102
# app.py:
@@ -95,6 +106,7 @@ from json import loads
95106
@app.post('/')
96107
def handle_positions():
97108
result = loads(request.data)
109+
print(result)
98110
return jsonify({'success': True,'message': f'{len(result)} positions received'})
99111
if __name__ == '__main__':
100112
app.run(debug=True)

Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def initialize(self) -> None:
3030

3131
# Our custom signal export accepts all asset types
3232
self.add_equity("SPY", Resolution.SECOND)
33+
self.add_crypto("BTCUSD", Resolution.SECOND)
3334
self.add_forex("EURUSD", Resolution.SECOND)
3435
self.add_future_contract(Symbol.create_future("ES", Market.CME, datetime(2023, 12, 15), None))
3536
self.add_option_contract(Symbol.create_option("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 130, datetime(2023, 9, 1)))
@@ -39,22 +40,27 @@ def initialize(self) -> None:
3940

4041
def on_data(self, data: Slice) -> None:
4142
'''Buy and hold EURUSD and SPY'''
42-
for ticker in [ "SPY", "EURUSD" ]:
43+
for ticker in [ "SPY", "EURUSD", "BTCUSD" ]:
4344
if not self.portfolio[ticker].invested and self.securities[ticker].has_data:
4445
self.set_holdings(ticker, 0.5)
4546

4647
from requests import post
4748
class CustomSignalExport:
4849
def send(self, parameters: SignalExportTargetParameters) -> bool:
49-
data = { x.symbol.value: x.quantity for x in parameters.targets }
50+
targets = [PortfolioTarget.percent(parameters.algorithm, x.symbol, x.quantity)
51+
for x in parameters.targets] ;
52+
data = [ {'symbol' : x.symbol.value, 'quantity': x.quantity} for x in targets ]
5053
response = post("http://localhost:5000/", json = data)
5154
result = response.json()
52-
return result.get('success', False)
55+
success = result.get('success', False)
56+
parameters.algorithm.log(f"Send #{len(parameters.targets)} targets. Success: {success}");
57+
return success
5358

5459
def dispose(self):
5560
pass
5661

5762
'''
63+
# To test the algorithm, you can create a simple Python Flask application (app.py) and run flask
5864
# $ flask --app app run
5965
6066
# app.py:
@@ -64,6 +70,7 @@ def dispose(self):
6470
@app.post('/')
6571
def handle_positions():
6672
result = loads(request.data)
73+
print(result)
6774
return jsonify({'success': True,'message': f'{len(result)} positions received'})
6875
if __name__ == '__main__':
6976
app.run(debug=True)

Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
using System;
2323
using System.Linq;
2424
using QuantConnect.Util;
25-
using QLNet;
2625

2726
namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
2827
{
@@ -35,7 +34,7 @@ public class SignalExportManager
3534
/// <summary>
3635
/// Records the time of the first order event of a group of events
3736
/// </summary>
38-
private DateTime _initialOrderEventTimeUtc = Time.EndOfTime;
37+
private ReferenceWrapper<DateTime> _initialOrderEventTimeUtc = new(Time.EndOfTime);
3938

4039
/// <summary>
4140
/// List of signal export providers
@@ -85,19 +84,37 @@ public void AddSignalExportProvider(ISignalExportTarget signalExport)
8584
/// <param name="signalExport">Signal export provider</param>
8685
public void AddSignalExportProvider(PyObject signalExport)
8786
{
88-
AddSignalExportProvider(new SignalExportTargetPythonWrapper(signalExport));
87+
if (!signalExport.TryConvert<ISignalExportTarget>(out var managedSignalExport))
88+
{
89+
managedSignalExport = new SignalExportTargetPythonWrapper(signalExport);
90+
}
91+
AddSignalExportProvider(managedSignalExport);
8992
}
9093

9194
/// <summary>
9295
/// Adds one or more new signal exports providers
9396
/// </summary>
9497
/// <param name="signalExports">One or more signal export provider</param>
95-
[Obsolete("This method is deprecated. Please use AddSignalExportProvider(ISignalExportTarget).")]
9698
public void AddSignalExportProviders(params ISignalExportTarget[] signalExports)
9799
{
98100
signalExports.DoForEach(AddSignalExportProvider);
99101
}
100102

103+
/// <summary>
104+
/// Adds one or more new signal exports providers
105+
/// </summary>
106+
/// <param name="signalExports">One or more signal export provider</param>
107+
public void AddSignalExportProviders(PyObject signalExports)
108+
{
109+
using var _ = Py.GIL();
110+
if (!signalExports.IsIterable())
111+
{
112+
AddSignalExportProvider(signalExports);
113+
return;
114+
}
115+
PyList.AsList(signalExports).DoForEach(AddSignalExportProvider);
116+
}
117+
101118
/// <summary>
102119
/// Sets the portfolio targets from the algorihtm's Portfolio and sends them with the
103120
/// algorithm being ran to the signal exports providers already set
@@ -215,9 +232,9 @@ private IEnumerable<PortfolioTarget> GetPortfolioTargets(decimal totalPortfolioV
215232
/// <param name="orderEvent">Event information</param>
216233
public void OnOrderEvent(OrderEvent orderEvent)
217234
{
218-
if (_initialOrderEventTimeUtc == Time.EndOfTime && orderEvent.Status.IsFill())
235+
if (_initialOrderEventTimeUtc.Value == Time.EndOfTime && orderEvent.Status.IsFill())
219236
{
220-
_initialOrderEventTimeUtc = DateTime.UtcNow;
237+
_initialOrderEventTimeUtc = new(orderEvent.UtcTime);
221238
}
222239
}
223240

@@ -227,19 +244,28 @@ public void OnOrderEvent(OrderEvent orderEvent)
227244
/// <param name="currentTimeUtc">The current time of synchronous events</param>
228245
public void Flush(DateTime currentTimeUtc)
229246
{
230-
if (_initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue)
247+
var initialOrderEventTimeUtc = _initialOrderEventTimeUtc.Value;
248+
if (initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue)
231249
{
232250
return;
233251
}
234252

235-
if (currentTimeUtc - _initialOrderEventTimeUtc < AutomaticExportTimeSpan)
253+
if (currentTimeUtc - initialOrderEventTimeUtc < AutomaticExportTimeSpan)
236254
{
237255
return;
238256
}
239-
240-
var success = SetTargetPortfolioFromPortfolio();
241-
Logging.Log.Trace($"SignalExportManager.Flush({currentTimeUtc:T}) :: Success: {success}. Initial OrderEvent Time: {_initialOrderEventTimeUtc:T}");
242-
_initialOrderEventTimeUtc = Time.EndOfTime;
257+
258+
try
259+
{
260+
SetTargetPortfolioFromPortfolio();
261+
}
262+
catch (Exception exception)
263+
{
264+
// SetTargetPortfolioFromPortfolio logs all known error on LEAN side.
265+
// Exceptions occurs in the ISignalExportTarget.Send method (user-defined).
266+
_algorithm.Error($"Failed to send portfolio target(s). Reason: {exception.Message}.{Environment.NewLine}{exception.StackTrace}");
267+
}
268+
_initialOrderEventTimeUtc = new(Time.EndOfTime);
243269
}
244270
}
245271
}

Common/Python/SignalExportTargetPythonWrapper.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ namespace QuantConnect.Python
2222
/// <summary>
2323
/// Provides an implementation of <see cref="ISignalExportTarget"/> that wraps a <see cref="PyObject"/> object
2424
/// </summary>
25-
/// <remarks>
26-
/// Constructor for initialising the <see cref="SignalExportTargetPythonWrapper"/> class with wrapped <see cref="PyObject"/> object
27-
/// </remarks>
28-
/// <param name="model">Python benchmark model</param>
29-
public class SignalExportTargetPythonWrapper(PyObject model) : BasePythonWrapper<ISignalExportTarget>(model), ISignalExportTarget
25+
public class SignalExportTargetPythonWrapper : BasePythonWrapper<ISignalExportTarget>, ISignalExportTarget
3026
{
27+
/// <summary>
28+
/// Constructor for initialising the <see cref="SignalExportTargetPythonWrapper"/> class with wrapped <see cref="PyObject"/> object
29+
/// </summary>
30+
/// <param name="instance">The underlying python instance</param>
31+
public SignalExportTargetPythonWrapper(PyObject instance) : base(instance) { }
32+
3133
/// <summary>
3234
/// Interface to send positions holdings to different 3rd party API's
3335
/// </summary>

0 commit comments

Comments
 (0)