Skip to content

Commit 8e82ef4

Browse files
authored
Feature: support Fair Market Value (#35)
* feat: handle new FairMarketValue WS msg refactor: use BaseClass with repeat properties * feat: additional logs * feat: add subscription on Fair Market Value * refactor: subscribe on FMV if business always * feat: use business only fro trade * refactor: log message in ProcessMessage * test:feat: fair market value as Tick or TradeBar * feat: index value updates * fix: miss subscribe on OI updates * feat: validate OI and fair market value * feat: implement aggregator minutes * feat: use consolidator in smart way refactor: use enum eventType instead of Prefixes naming for ws channels * remove: `SetUsingAggregates` from AggrManager * refactor: use EventType in all subscription methods * refactor: reset eventType in AggregatorManager * feat: implement license type logic * fix: ParseLicenseType more implicit use Individual license type * fix: use ws endpoint url by licenseType explicitly * fix: reset `_usingEventType` by default in finally block * refactor: subscribe logic in PolygonSubscriptionManager * feat: use _subscriptionsDataConfigs with specific configs data to reuse and another part of app * feat: handle not unavailable connection exception * feat: add condition to catch(PolygonAuthenticationException) in Subscribe
1 parent d74c711 commit 8e82ef4

19 files changed

Lines changed: 682 additions & 136 deletions

QuantConnect.Polygon.Tests/PolygonDataProviderBaseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ protected static Task StartEnumeratorProcessing(
240240
}, TaskContinuationOptions.OnlyOnFaulted);
241241
}
242242

243-
protected SubscriptionDataConfig GetSubscriptionDataConfig<T>(Symbol symbol, Resolution resolution)
243+
public static SubscriptionDataConfig GetSubscriptionDataConfig<T>(Symbol symbol, Resolution resolution)
244244
{
245245
return new SubscriptionDataConfig(
246246
typeof(T),
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
using System;
18+
using System.Linq;
19+
using NUnit.Framework;
20+
using System.Threading;
21+
using QuantConnect.Data;
22+
using QuantConnect.Tests;
23+
using QuantConnect.Logging;
24+
using QuantConnect.Data.Market;
25+
using QuantConnect.Configuration;
26+
using System.Collections.Generic;
27+
using QuantConnect.Lean.Engine.DataFeeds;
28+
using QuantConnect.Lean.Engine.DataFeeds.Enumerators;
29+
30+
namespace QuantConnect.Lean.DataSource.Polygon.Tests
31+
{
32+
[TestFixture]
33+
[Explicit("Tests are dependent on network and take long")]
34+
public class PolygonDataProviderFairMarketValueTests
35+
{
36+
private List<SubscriptionDataConfig> GetConfigs(Resolution resolution)
37+
{
38+
var symbols = new Symbol[]
39+
{
40+
Symbols.AAPL,
41+
Symbols.MSFT,
42+
Symbol.CreateOption(Symbols.AAPL, Symbols.AAPL.ID.Market, SecurityType.Option.DefaultOptionStyle(), OptionRight.Call, 227.5m, new(2025, 08, 29)),
43+
Symbol.CreateOption(Symbols.MSFT, Symbols.MSFT.ID.Market, SecurityType.Option.DefaultOptionStyle(), OptionRight.Call, 502.5m, new(2025, 08, 29)),
44+
//Symbols.SPX,
45+
Symbol.CreateOption(Symbols.SPX, "SPXW", Symbols.SPX.ID.Market, SecurityType.IndexOption.DefaultOptionStyle(), OptionRight.Call, 6485m, new(2025, 08, 29))
46+
};
47+
48+
return [.. symbols.SelectMany(symbol => GetSubscriptionDataConfigs(symbol, resolution))];
49+
}
50+
51+
[TestCase(Resolution.Tick, 10)]
52+
[TestCase(Resolution.Second, 5)]
53+
[TestCase(Resolution.Minute, 1)]
54+
public void StreamDataOnDifferentSecuritiesTypes(Resolution resolution, int expectedReceiveAmountData)
55+
{
56+
var mockDateTimeAfterOpenExchange = DateTime.UtcNow.Date.AddHours(9).AddMinutes(30).AddSeconds(59).ConvertToUtc(TimeZones.NewYork);
57+
TestablePolygonDataProvider.TimeProviderInstance = new ManualTimeProvider(mockDateTimeAfterOpenExchange);
58+
using var polygon = new TestablePolygonDataProvider(Config.Get("polygon-api-key"), Config.Get("polygon-license-type"));
59+
60+
var configs = GetConfigs(resolution);
61+
62+
var receivedData = new Dictionary<Symbol, List<BaseData>>();
63+
64+
var resetEvent = new AutoResetEvent(false);
65+
var receivedAllData = default(bool);
66+
67+
foreach (var config in configs)
68+
{
69+
receivedData[config.Symbol] = [];
70+
71+
polygon.Subscribe(config, (sender, args) =>
72+
{
73+
if (args is NewDataAvailableEventArgs newData && newData.DataPoint is BaseData bd)
74+
{
75+
Log.Trace($"{bd}. Time span: {bd.Time} - {bd.EndTime}");
76+
lock (receivedData)
77+
{
78+
receivedData[bd.Symbol].Add(bd);
79+
80+
if ((bd is Tick t && t.TickType == TickType.OpenInterest) || bd is OpenInterest)
81+
{
82+
// Prevent from repeating request
83+
TestablePolygonDataProvider.TimeProviderInstance.SetCurrentTimeUtc(DateTime.UtcNow);
84+
}
85+
86+
if (!receivedAllData && receivedData.Values.All(x => x.Count >= expectedReceiveAmountData))
87+
{
88+
receivedAllData = true;
89+
resetEvent.Set();
90+
}
91+
}
92+
}
93+
});
94+
}
95+
96+
Assert.IsTrue(resetEvent.WaitOne(TimeSpan.FromSeconds(120)));
97+
98+
Log.Trace("Unsubscribing symbols");
99+
foreach (var config in configs)
100+
{
101+
polygon.Unsubscribe(config);
102+
}
103+
104+
resetEvent.WaitOne(TimeSpan.FromSeconds(10));
105+
106+
var dataSummary = receivedData.ToDictionary(pair => pair.Key, pair => pair.Value.Count);
107+
Log.Trace($"Received {receivedData.Count} symbols with data points -> {string.Join(", ", dataSummary.Select(kvp => $"{kvp.Key}: {kvp.Value}"))}");
108+
109+
Assert.IsTrue(receivedData.Values.All(d => d.Count >= expectedReceiveAmountData));
110+
111+
foreach (var (symbol, baseData) in receivedData.Where(x => x.Key.SecurityType.IsOption()))
112+
{
113+
var openInterestDetected = default(bool);
114+
foreach (var data in baseData)
115+
{
116+
if (data is Tick t && t.TickType == TickType.OpenInterest)
117+
{
118+
openInterestDetected = true;
119+
break;
120+
}
121+
}
122+
Assert.IsTrue(openInterestDetected);
123+
}
124+
125+
foreach (var (symbol, baseData) in receivedData)
126+
{
127+
foreach (var data in baseData)
128+
{
129+
switch (data)
130+
{
131+
case OpenInterest oi:
132+
Assert.Greater(oi.Value, 0m);
133+
break;
134+
case Tick t:
135+
switch (t.TickType)
136+
{
137+
case TickType.OpenInterest:
138+
Assert.Greater(t.Value, 0m);
139+
break;
140+
case TickType.Trade:
141+
Assert.Greater(t.LastPrice, 0m);
142+
Assert.AreEqual(t.Quantity, 0m);
143+
break;
144+
default:
145+
throw new NotSupportedException();
146+
}
147+
break;
148+
case TradeBar tb:
149+
Assert.Greater(tb.Open, 0m);
150+
Assert.Greater(tb.High, 0m);
151+
Assert.Greater(tb.Low, 0m);
152+
Assert.Greater(tb.Close, 0m);
153+
if (resolution >= Resolution.Minute)
154+
{
155+
// For 1-minute or higher aggregates, volume is expected (may be zero).
156+
// Example: {"ev":"AM","sym":"O:MSFT250829C00502500","v":0,"av":358,...}
157+
Assert.GreaterOrEqual(tb.Volume, 0m);
158+
}
159+
else
160+
{
161+
// For tick/fmv data, volume is not reported and should remain zero
162+
Assert.AreEqual(tb.Volume, 0m);
163+
}
164+
break;
165+
default:
166+
throw new NotSupportedException();
167+
}
168+
}
169+
}
170+
}
171+
172+
private static List<SubscriptionDataConfig> GetSubscriptionDataConfigs(Symbol symbol, Resolution resolution)
173+
{
174+
var subs = new List<SubscriptionDataConfig>();
175+
if (resolution == Resolution.Tick)
176+
{
177+
subs.Add(new SubscriptionDataConfig(PolygonDataProviderBaseTests.GetSubscriptionDataConfig<Tick>(symbol, resolution), tickType: TickType.Trade));
178+
179+
if (symbol.SecurityType.IsOption())
180+
{
181+
subs.Add(new SubscriptionDataConfig(PolygonDataProviderBaseTests.GetSubscriptionDataConfig<Tick>(symbol, resolution), tickType: TickType.OpenInterest));
182+
}
183+
184+
return subs;
185+
}
186+
187+
subs.Add(PolygonDataProviderBaseTests.GetSubscriptionDataConfig<TradeBar>(symbol, resolution));
188+
189+
if (symbol.SecurityType.IsOption())
190+
{
191+
subs.Add(PolygonDataProviderBaseTests.GetSubscriptionDataConfig<OpenInterest>(symbol, resolution));
192+
}
193+
194+
return subs;
195+
}
196+
}
197+
}

QuantConnect.Polygon.Tests/PolygonOpenInterestProcessorManagerTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,9 @@ public class PolygonOpenInterestProcessorManagerTests : PolygonDataProviderBaseT
3333

3434
private readonly PolygonSymbolMapper symbolMapper = new();
3535

36-
private readonly PolygonAggregationManager dataAggregator = new();
37-
3836
private readonly ManualTimeProvider _timeProviderInstance = new();
3937

40-
private readonly EventBasedDataQueueHandlerSubscriptionManager _subscriptionManager = new()
38+
private readonly PolygonSubscriptionManager _subscriptionManager = new([], 0, null)
4139
{
4240
SubscribeImpl = (symbols, _) => { return true; },
4341
UnsubscribeImpl = (symbols, _) => { return true; }
@@ -53,6 +51,7 @@ public void GetOpenInterestWithHugeAmountSymbols()
5351
var resetEvent = new AutoResetEvent(false);
5452
var cancellationTokenSource = new CancellationTokenSource();
5553
var _activeEnumerators = new ConcurrentDictionary<Symbol, IEnumerator<BaseData>>();
54+
var dataAggregator = new PolygonAggregationManager(_subscriptionManager);
5655

5756
using var polygon = new PolygonDataProvider(ApiKey);
5857

QuantConnect.Polygon.Tests/TestablePolygonDataProvider.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,27 @@
1414
* limitations under the License.
1515
*/
1616

17+
using QuantConnect.Lean.Engine.DataFeeds;
18+
using System;
19+
1720
namespace QuantConnect.Lean.DataSource.Polygon.Tests
1821
{
1922
public class TestablePolygonDataProvider : PolygonDataProvider
2023
{
24+
public static ManualTimeProvider TimeProviderInstance = new(DateTime.UtcNow);
25+
26+
protected override ITimeProvider TimeProvider => TimeProviderInstance;
27+
2128
public PolygonSubscriptionManager SubscriptionManager => _subscriptionManager;
2229

2330
public TestablePolygonDataProvider(string apiKey, int maxSubscriptionsPerWebSocket)
2431
: base(apiKey, maxSubscriptionsPerWebSocket)
2532
{
2633
}
34+
35+
public TestablePolygonDataProvider(string apiKey, string licenseType, bool streamingEnabled = true)
36+
: base(apiKey, streamingEnabled, licenseType)
37+
{
38+
}
2739
}
2840
}

QuantConnect.Polygon.Tests/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"job-organization-id": "",
88

99
"polygon-api-key": "",
10-
"polygon-subscription-plan": "Advanced",
10+
"polygon-license-type": "Individual",
1111
"polygon-api-url": "https://api.polygon.io",
1212
"polygon-ws-url": "wss://socket.polygon.io",
1313
// For launchpad:
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace QuantConnect.Lean.DataSource.Polygon;
17+
18+
/// <summary>
19+
/// Defines the types of market data events that can be streamed via WebSocket,
20+
/// covering various levels of aggregation and security types.
21+
/// </summary>
22+
public enum EventType
23+
{
24+
/// <summary>
25+
/// No event type. Used when no subscription is active or the event is undefined.
26+
/// </summary>
27+
None = 0,
28+
29+
/// <summary>
30+
/// Minute-level aggregated OHLC (Open, High, Low, Close) and volume data.
31+
/// </summary>
32+
AM = 1,
33+
34+
/// <summary>
35+
/// Second-level aggregated OHLC (Open, High, Low, Close) and volume data.
36+
/// </summary>
37+
A = 2,
38+
39+
/// <summary>
40+
/// Tick-by-tick trade data for supported securities.
41+
/// </summary>
42+
T = 3,
43+
44+
/// <summary>
45+
/// National Best Bid and Offer (NBBO) quote data for supported securities.
46+
/// </summary>
47+
Q = 4,
48+
49+
/// <summary>
50+
/// Real-time Fair Market Value (FMV) data for supported securities.
51+
/// </summary>
52+
FMV = 5,
53+
54+
/// <summary>
55+
/// Real-time index value updates for specified indexes.
56+
/// </summary>
57+
V = 6
58+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace QuantConnect.Lean.DataSource.Polygon;
17+
18+
/// <summary>
19+
/// Defines the available license types for a Polygon subscription.
20+
/// </summary>
21+
public enum LicenseType
22+
{
23+
/// <summary>
24+
/// Represents an individual subscription plan, intended for single users.
25+
/// </summary>
26+
Individual = 0,
27+
28+
/// <summary>
29+
/// Represents a business subscription plan, intended for organizations or commercial use.
30+
/// </summary>
31+
Business = 1
32+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
namespace QuantConnect.Lean.DataSource.Polygon;
18+
19+
/// <summary>
20+
/// Exception thrown when a tick type is not supported for the current Polygon license.
21+
/// </summary>
22+
public class UnsupportedTickTypeForLicenseException : Exception
23+
{
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="UnsupportedTickTypeForLicenseException"/> class
26+
/// with the specified tick type and license type.
27+
/// </summary>
28+
/// <param name="tickType">The tick type that was requested (e.g., Trade, Quote).</param>
29+
/// <param name="licenseType">The license type of the current Polygon subscription (Individual or Business).</param>
30+
public UnsupportedTickTypeForLicenseException(string tickType, LicenseType licenseType)
31+
: base($"TickType '{tickType}' is not supported for license type '{licenseType}'.")
32+
{
33+
}
34+
}

0 commit comments

Comments
 (0)