From bfb96fa857e723a4c384b9052b68f4a1b4aa5d5c Mon Sep 17 00:00:00 2001 From: Roman Yavnikov <45608740+Romazes@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:39:29 +0300 Subject: [PATCH] Develop of Theta Data Provider (#1) * update: .gitignore * update: ignore `Data/*` folder in .gitignore * feat: prepare structure of data folders * feat: workflow and GH's templates * feat: DataQueueHandler for Option * feat: prepare Test project * feat: test for DQH * feat: symbolMapper * feat: Contract WS model * feat: Handle Trade/Quote response with aggregator * feat: convert string to Date extension * feat: IsConnected flag from ws connection * feat: ITimeProvider in DataProvider * feat: ThetaData incapsulate Rest API Client logic * feat: IDataQueueUniverseProvider && IOptionChainProvider feat: GetLean in SymbolMapper feat: generic entity for REST responses feat: test for IDataQueueUniverseProvider and IOptionChainProvider * remove: not used usings * refactor: SymbolMapper + tests * rename: extension converting date * rename: RestResponse to BaseResponse * refactor: RestApi BaseUrl * feat: free space command run in workflow file * move: BaseResponse to Common folder * feat: log of requested url * refactor: global variable of restClient and add comment * feat: first impl GetHistory Option * refactor: symbolMapper + new tests * refactor: symbolMapper use Market USA * feat: create symbol extension in test * feat: DQH additional test cases * feat: exception msg in converter classes * refactor: short type to byte in EOD entity * fix: validation of OpenInterest tickType * refactor: GetHistory of Daily TickType data feat: GetHistory Tests * feat: validation of correct status code response * feat: add validation subscription on IndexOption symbol * fix: missed double quotes * feat: support of muliple pages requests/responses feat: base entity for Header of Responses feat: json null string converter cuz API return "null" like a a string * feat: Exchanges' code collection remove: condition collection refactor: some property in entities * feat: exchange's code to WS responses * fix: convert time in history request * feat: GetHistory tests * feat: DataDownloader refactor: history test refactor: DataQueueUniverse provider * refactor: skip empty Response for Trade of Quote Daily * feat: xml description RestApiClient * feat: Trade(Tick,Min,Hour) DataConsolidator refactor: test's GetHistory * refactor: IOptionChainProvider and IDataQueueUniverseProvider * feat: add json description file * refactor: use Lean consolidator for trade ticks * fix: OptionStyle in GetOptionContractList * refactor: LookupSymbols remove: GetTickTime() remove: not use variables * remove: CachingOptionChainProvider for OptionChainProvider instance * revert: not use param to explicit in LookupSymbols * remove: duplicate validation in OptionChain feat: test option future chains * feat: ValidateSubscription() * feat: additional test case with invalid param of GetHIstory * feat: implement different subscription price plan * fix: wrong validation of available subscription process * refactor:test: DQH multiple subscription * refactor: use custom convert for Date From API * refactor: use generic format for date ThetaData extension * feat: Custom Json convert For ThetaData DateTime format from WS --- .github/issue_template.md | 27 ++ .github/pull_request_template.md | 42 ++ .github/workflows/gh-actions.yml | 49 +++ .gitignore | 268 ++++++++++++- Lean.DataSource.ThetaData.sln | 37 ++ ...tConnect.DataSource.ThetaData.Tests.csproj | 37 ++ QuantConnect.ThetaData.Tests/TestHelpers.cs | 165 ++++++++ QuantConnect.ThetaData.Tests/TestSetup.cs | 66 +++ .../ThetaDataDownloaderTests.cs | 77 ++++ .../ThetaDataHistoryProviderTests..cs | 63 +++ .../ThetaDataOptionChainProviderTests.cs | 80 ++++ .../ThetaDataProviderTests.cs | 270 +++++++++++++ .../ThetaDataQueueUniverseProviderTests.cs | 70 ++++ .../ThetaDataSymbolMapperTests.cs | 89 +++++ QuantConnect.ThetaData.Tests/config.json | 6 + .../Converters/DateTimeIntJsonConverter.cs | 73 ++++ .../Converters/ThetaDataEndOfDayConverter.cs | 69 ++++ .../ThetaDataNullStringConverter.cs | 64 +++ .../ThetaDataOpenInterestConverter.cs | 68 ++++ .../Converters/ThetaDataQuoteConverter.cs | 77 ++++ .../Converters/ThetaDataTradeConverter.cs | 71 ++++ .../Models/Common/BaseHeaderResponse.cs | 42 ++ .../Models/Common/BaseResponse.cs | 50 +++ .../Models/Enums/ContractSecurityType.cs | 30 ++ .../Models/Enums/SubscriptionPlanType.cs | 43 ++ .../Models/Enums/WebSocketHeaderType.cs | 36 ++ .../Models/Interfaces/IBaseResponse.cs | 29 ++ .../Models/Interfaces/ISubscriptionPlan.cs | 47 +++ .../Models/Rest/EndOfDayReportResponse.cs | 141 +++++++ .../Models/Rest/OpenInterestResponse.cs | 62 +++ .../Models/Rest/QuoteResponse.cs | 112 ++++++ .../Models/Rest/TradeResponse.cs | 83 ++++ .../SubscriptionPlans/FreeSubscriptionPlan.cs | 30 ++ .../SubscriptionPlans/ProSubscriptionPlan.cs | 30 ++ .../StandardSubscriptionPlan.cs | 30 ++ .../ValueSubscriptionPlan.cs | 31 ++ .../Models/WebSocket/WebSocketContract.cs | 49 +++ .../Models/WebSocket/WebSocketHeader.cs | 37 ++ .../Models/WebSocket/WebSocketQuote.cs | 78 ++++ .../Models/WebSocket/WebSocketResponse.cs | 44 ++ .../Models/WebSocket/WebSocketTrade.cs | 64 +++ .../QuantConnect.DataSource.ThetaData.csproj | 39 ++ QuantConnect.ThetaData/ThetaDataDownloader.cs | 146 +++++++ QuantConnect.ThetaData/ThetaDataExtensions.cs | 120 ++++++ .../ThetaDataHistoryProvider.cs | 290 ++++++++++++++ .../ThetaDataOptionChainProvider.cs | 118 ++++++ QuantConnect.ThetaData/ThetaDataProvider.cs | 376 ++++++++++++++++++ .../ThetaDataQueueUniverseProvider.cs | 76 ++++ .../ThetaDataRestApiClient.cs | 90 +++++ .../ThetaDataSymbolMapper.cs | 251 ++++++++++++ .../ThetaDataWebSocketClientWrapper.cs | 211 ++++++++++ thetadata.json | 15 + 52 files changed, 4547 insertions(+), 21 deletions(-) create mode 100644 .github/issue_template.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/gh-actions.yml create mode 100644 Lean.DataSource.ThetaData.sln create mode 100644 QuantConnect.ThetaData.Tests/QuantConnect.DataSource.ThetaData.Tests.csproj create mode 100644 QuantConnect.ThetaData.Tests/TestHelpers.cs create mode 100644 QuantConnect.ThetaData.Tests/TestSetup.cs create mode 100644 QuantConnect.ThetaData.Tests/ThetaDataDownloaderTests.cs create mode 100644 QuantConnect.ThetaData.Tests/ThetaDataHistoryProviderTests..cs create mode 100644 QuantConnect.ThetaData.Tests/ThetaDataOptionChainProviderTests.cs create mode 100644 QuantConnect.ThetaData.Tests/ThetaDataProviderTests.cs create mode 100644 QuantConnect.ThetaData.Tests/ThetaDataQueueUniverseProviderTests.cs create mode 100644 QuantConnect.ThetaData.Tests/ThetaDataSymbolMapperTests.cs create mode 100644 QuantConnect.ThetaData.Tests/config.json create mode 100644 QuantConnect.ThetaData/Converters/DateTimeIntJsonConverter.cs create mode 100644 QuantConnect.ThetaData/Converters/ThetaDataEndOfDayConverter.cs create mode 100644 QuantConnect.ThetaData/Converters/ThetaDataNullStringConverter.cs create mode 100644 QuantConnect.ThetaData/Converters/ThetaDataOpenInterestConverter.cs create mode 100644 QuantConnect.ThetaData/Converters/ThetaDataQuoteConverter.cs create mode 100644 QuantConnect.ThetaData/Converters/ThetaDataTradeConverter.cs create mode 100644 QuantConnect.ThetaData/Models/Common/BaseHeaderResponse.cs create mode 100644 QuantConnect.ThetaData/Models/Common/BaseResponse.cs create mode 100644 QuantConnect.ThetaData/Models/Enums/ContractSecurityType.cs create mode 100644 QuantConnect.ThetaData/Models/Enums/SubscriptionPlanType.cs create mode 100644 QuantConnect.ThetaData/Models/Enums/WebSocketHeaderType.cs create mode 100644 QuantConnect.ThetaData/Models/Interfaces/IBaseResponse.cs create mode 100644 QuantConnect.ThetaData/Models/Interfaces/ISubscriptionPlan.cs create mode 100644 QuantConnect.ThetaData/Models/Rest/EndOfDayReportResponse.cs create mode 100644 QuantConnect.ThetaData/Models/Rest/OpenInterestResponse.cs create mode 100644 QuantConnect.ThetaData/Models/Rest/QuoteResponse.cs create mode 100644 QuantConnect.ThetaData/Models/Rest/TradeResponse.cs create mode 100644 QuantConnect.ThetaData/Models/SubscriptionPlans/FreeSubscriptionPlan.cs create mode 100644 QuantConnect.ThetaData/Models/SubscriptionPlans/ProSubscriptionPlan.cs create mode 100644 QuantConnect.ThetaData/Models/SubscriptionPlans/StandardSubscriptionPlan.cs create mode 100644 QuantConnect.ThetaData/Models/SubscriptionPlans/ValueSubscriptionPlan.cs create mode 100644 QuantConnect.ThetaData/Models/WebSocket/WebSocketContract.cs create mode 100644 QuantConnect.ThetaData/Models/WebSocket/WebSocketHeader.cs create mode 100644 QuantConnect.ThetaData/Models/WebSocket/WebSocketQuote.cs create mode 100644 QuantConnect.ThetaData/Models/WebSocket/WebSocketResponse.cs create mode 100644 QuantConnect.ThetaData/Models/WebSocket/WebSocketTrade.cs create mode 100644 QuantConnect.ThetaData/QuantConnect.DataSource.ThetaData.csproj create mode 100644 QuantConnect.ThetaData/ThetaDataDownloader.cs create mode 100644 QuantConnect.ThetaData/ThetaDataExtensions.cs create mode 100644 QuantConnect.ThetaData/ThetaDataHistoryProvider.cs create mode 100644 QuantConnect.ThetaData/ThetaDataOptionChainProvider.cs create mode 100644 QuantConnect.ThetaData/ThetaDataProvider.cs create mode 100644 QuantConnect.ThetaData/ThetaDataQueueUniverseProvider.cs create mode 100644 QuantConnect.ThetaData/ThetaDataRestApiClient.cs create mode 100644 QuantConnect.ThetaData/ThetaDataSymbolMapper.cs create mode 100644 QuantConnect.ThetaData/ThetaDataWebSocketClientWrapper.cs create mode 100644 thetadata.json diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..c50f3e3 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,27 @@ + + +#### Expected Behavior + + +#### Actual Behavior + + +#### Potential Solution + + +#### Reproducing the Problem + + +#### System Information + + +#### Checklist + + +- [ ] I have completely filled out this template +- [ ] I have confirmed that this issue exists on the current `master` branch +- [ ] I have confirmed that this is not a duplicate issue by searching [issues](https://github.com/QuantConnect/Lean/issues) + +- [ ] I have provided detailed steps to reproduce the issue + + \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1418a96 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,42 @@ + + + +#### Description + + +#### Related Issue + + + + + +#### Motivation and Context + + +#### Requires Documentation Change + + +#### How Has This Been Tested? + + + + +#### Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] Refactor (non-breaking change which improves implementation) +- [ ] Performance (non-breaking change which improves performance. Please add associated performance test and results) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Non-functional change (xml comments/documentation/etc) + +#### Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] I have read the **CONTRIBUTING** [document](https://github.com/QuantConnect/Lean/blob/master/CONTRIBUTING.md). +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. +- [ ] My branch follows the naming convention `bug--` or `feature--` + + diff --git a/.github/workflows/gh-actions.yml b/.github/workflows/gh-actions.yml new file mode 100644 index 0000000..0d4de7e --- /dev/null +++ b/.github/workflows/gh-actions.yml @@ -0,0 +1,49 @@ +name: Build & Test + +on: + push: + branches: ["*"] + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Free space + run: df -h && sudo rm -rf /usr/local/lib/android && sudo rm -rf /opt/ghc && rm -rf /opt/hostedtoolcache* && df -h + + - name: Checkout Lean Same Branch + id: lean-same-branch + uses: actions/checkout@v2 + continue-on-error: true + with: + ref: ${{ github.ref }} + repository: QuantConnect/Lean + path: Lean + + - name: Checkout Lean Master + if: steps.lean-same-branch.outcome != 'success' + uses: actions/checkout@v2 + with: + repository: QuantConnect/Lean + path: Lean + + - name: Move Lean + run: mv Lean ../Lean + + - name: Run Image + uses: addnab/docker-run-action@v3 + with: + image: quantconnect/lean:foundation + options: -v /home/runner/work:/__w --workdir /__w/Lean.DataSource.ThetaData/Lean.DataSource.ThetaData -e QC_THETADATA_USERNAME=${{ secrets.THETADATA_USERNAME }} -e QC_THETADATA_PASSWORD=${{ secrets.THETADATA_PASSWORD }} -e QC_JOB_USER_ID=${{ secrets.JOB_USER_ID }} -e QC_API_ACCESS_TOKEN=${{ secrets.API_ACCESS_TOKEN }} -e QC_JOB_ORGANIZATION_ID=${{ secrets.JOB_ORGANIZATION_ID }} + + - name: Build QuantConnect.ThetaData + run: dotnet build ./QuantConnect.ThetaData/QuantConnect.DataSource.ThetaData.csproj /p:Configuration=Release /v:quiet /p:WarningLevel=1 + + - name: Build QuantConnect.ThetaData.Tests + run: dotnet build ./QuantConnect.ThetaData.Tests/QuantConnect.DataSource.ThetaData.Tests.csproj /p:Configuration=Release /v:quiet /p:WarningLevel=1 + + - name: Run QuantConnect.ThetaData.Tests + run: dotnet test ./QuantConnect.ThetaData.Tests/bin/Release/QuantConnect.Lean.DataSource.ThetaData.dll diff --git a/.gitignore b/.gitignore index c6127b3..5c909cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,12 @@ -# Prerequisites -*.d - # Object files *.o *.ko *.obj *.elf +*.pyc -# Linker output -*.ilk -*.map -*.exp +# Visual Studio Project Items: +*.suo # Precompiled Headers *.gch @@ -23,30 +19,260 @@ *.lo # Shared objects (inc. Windows DLLs) -*.dll +#*.dll *.so *.so.* *.dylib # Executables -*.exe +*/bin/*.exe *.out *.app *.i*86 *.x86_64 *.hex -# Debug files -*.dSYM/ -*.su -*.idb +# QC Cloud Setup Bash Files +*.sh +# Include docker launch scripts for Mac/Linux +!run_docker.sh +!research/run_docker_notebook.sh + +# QC Config Files: +# config.json + +# QC-C-Specific +*Engine/bin/Debug/cache/data/*.zip +*/obj/* +*/bin/* +*Docker/* +*/Docker/* +*Algorithm.Python/Lib/* +*/[Ee]xtensions/* +!**/Libraries/* + +# C Debug Binaries *.pdb -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +.vs/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# JetBrains Rider +.idea/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +!LocalPackages/* +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# If using the old MSBuild-Integrated Package Restore, uncomment this: +#!**/packages/repositories.config +# ignore sln level nuget +.nuget/ +!.nuget/NuGet.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Test Runner +testrunner/ + +# Meld original diff files +*.orig + +# Output chart data +Charts/ + +# NCrunch files +*.ncrunchsolution +*.ncrunchproject + +# QuantConnect plugin files +QuantConnectProjects.xml +Launcher/Plugins/* +/ApiPython/dist +/ApiPython/quantconnect.egg-info +/ApiPython/quantconnect.egg-info/* + +QuantConnect.Lean.sln.DotSettings* + +#User notebook files +Research/Notebooks + +#Docker result files +Results/ \ No newline at end of file diff --git a/Lean.DataSource.ThetaData.sln b/Lean.DataSource.ThetaData.sln new file mode 100644 index 0000000..648315d --- /dev/null +++ b/Lean.DataSource.ThetaData.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.DataSource.ThetaData", "QuantConnect.ThetaData\QuantConnect.DataSource.ThetaData.csproj", "{BB6FBD65-4AB0-4DF8-8BFE-731BFEC5F33D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.DataSource.ThetaData.Tests", "QuantConnect.ThetaData.Tests\QuantConnect.DataSource.ThetaData.Tests.csproj", "{F93E08F9-9961-4DBC-9959-EFA00775252A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.Tests", "..\Lean\Tests\QuantConnect.Tests.csproj", "{9A666189-E7DD-43DA-95DF-419E3E5363F3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BB6FBD65-4AB0-4DF8-8BFE-731BFEC5F33D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB6FBD65-4AB0-4DF8-8BFE-731BFEC5F33D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB6FBD65-4AB0-4DF8-8BFE-731BFEC5F33D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB6FBD65-4AB0-4DF8-8BFE-731BFEC5F33D}.Release|Any CPU.Build.0 = Release|Any CPU + {F93E08F9-9961-4DBC-9959-EFA00775252A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F93E08F9-9961-4DBC-9959-EFA00775252A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F93E08F9-9961-4DBC-9959-EFA00775252A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F93E08F9-9961-4DBC-9959-EFA00775252A}.Release|Any CPU.Build.0 = Release|Any CPU + {9A666189-E7DD-43DA-95DF-419E3E5363F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A666189-E7DD-43DA-95DF-419E3E5363F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A666189-E7DD-43DA-95DF-419E3E5363F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A666189-E7DD-43DA-95DF-419E3E5363F3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {155C3765-EC51-49A5-99BF-856714A46247} + EndGlobalSection +EndGlobal diff --git a/QuantConnect.ThetaData.Tests/QuantConnect.DataSource.ThetaData.Tests.csproj b/QuantConnect.ThetaData.Tests/QuantConnect.DataSource.ThetaData.Tests.csproj new file mode 100644 index 0000000..390fd94 --- /dev/null +++ b/QuantConnect.ThetaData.Tests/QuantConnect.DataSource.ThetaData.Tests.csproj @@ -0,0 +1,37 @@ + + + Release + AnyCPU + net6.0 + false + UnitTest + bin\$(Configuration)\ + QuantConnect.Lean.DataSource.ThetaData.Tests + QuantConnect.Lean.DataSource.ThetaData.Tests + QuantConnect.Lean.DataSource.ThetaData.Tests + QuantConnect.Lean.DataSource.ThetaData.Tests + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/QuantConnect.ThetaData.Tests/TestHelpers.cs b/QuantConnect.ThetaData.Tests/TestHelpers.cs new file mode 100644 index 0000000..6c9ee1c --- /dev/null +++ b/QuantConnect.ThetaData.Tests/TestHelpers.cs @@ -0,0 +1,165 @@ +/* + * 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 NodaTime; +using System.Linq; +using NUnit.Framework; +using QuantConnect.Data; +using QuantConnect.Util; +using QuantConnect.Tests; +using Microsoft.CodeAnalysis; +using QuantConnect.Securities; +using QuantConnect.Data.Market; +using System.Collections.Generic; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + public static class TestHelpers + { + public static void ValidateHistoricalBaseData(IEnumerable history, Resolution resolution, TickType tickType, DateTime startDate, DateTime endDate, Symbol requestedSymbol = null) + { + Assert.IsNotNull(history); + Assert.IsNotEmpty(history); + + if (resolution < Resolution.Daily) + { + Assert.That(history.First().Time.Date, Is.EqualTo(startDate.ConvertFromUtc(TimeZones.EasternStandard).Date)); + Assert.That(history.Last().Time.Date, Is.EqualTo(endDate.ConvertFromUtc(TimeZones.EasternStandard).Date)); + } + else + { + Assert.That(history.First().Time.Date, Is.GreaterThanOrEqualTo(startDate.ConvertFromUtc(TimeZones.EasternStandard).Date)); + Assert.That(history.Last().Time.Date, Is.LessThanOrEqualTo(endDate.ConvertFromUtc(TimeZones.EasternStandard).Date)); + } + + switch (tickType) + { + case TickType.Trade when resolution != Resolution.Tick: + AssertTradeBars(history.Select(x => x as TradeBar), requestedSymbol, resolution.ToTimeSpan()); + break; + case TickType.Trade: + AssertTradeTickBars(history.Select(x => x as Tick), requestedSymbol); + break; + case TickType.Quote: + AssertTickQuoteBars(history.Select(t => t as Tick), requestedSymbol); + break; + } + } + + public static void AssertTradeTickBars(IEnumerable ticks, Symbol symbol = null) + { + foreach (var tick in ticks) + { + if (symbol != null) + { + Assert.That(tick.Symbol, Is.EqualTo(symbol)); + } + + Assert.That(tick.Price, Is.GreaterThan(0)); + Assert.That(tick.Value, Is.GreaterThan(0)); + Assert.IsNotEmpty(tick.SaleCondition); + } + } + + public static void AssertTickQuoteBars(IEnumerable ticks, Symbol symbol = null) + { + foreach (var tick in ticks) + { + if (symbol != null) + { + Assert.That(tick.Symbol, Is.EqualTo(symbol)); + } + + Assert.That(tick.AskPrice, Is.GreaterThan(0)); + Assert.That(tick.AskSize, Is.GreaterThan(0)); + Assert.That(tick.BidPrice, Is.GreaterThan(0)); + Assert.That(tick.BidSize, Is.GreaterThan(0)); + Assert.That(tick.DataType, Is.EqualTo(MarketDataType.Tick)); + Assert.That(tick.Time, Is.GreaterThan(default(DateTime))); + Assert.That(tick.EndTime, Is.GreaterThan(default(DateTime))); + Assert.IsNotEmpty(tick.SaleCondition); + } + } + + public static void AssertTradeBars(IEnumerable tradeBars, Symbol symbol, TimeSpan period) + { + foreach (var tradeBar in tradeBars) + { + Assert.That(tradeBar.Symbol, Is.EqualTo(symbol)); + Assert.That(tradeBar.Period, Is.EqualTo(period)); + Assert.That(tradeBar.Open, Is.GreaterThan(0)); + Assert.That(tradeBar.High, Is.GreaterThan(0)); + Assert.That(tradeBar.Low, Is.GreaterThan(0)); + Assert.That(tradeBar.Close, Is.GreaterThan(0)); + Assert.That(tradeBar.Price, Is.GreaterThan(0)); + Assert.That(tradeBar.Volume, Is.GreaterThan(0)); + Assert.That(tradeBar.Time, Is.GreaterThan(default(DateTime))); + Assert.That(tradeBar.EndTime, Is.GreaterThan(default(DateTime))); + } + } + + public static HistoryRequest CreateHistoryRequest(Symbol symbol, Resolution resolution, TickType tickType, DateTime startDateTime, DateTime endDateTime, + SecurityExchangeHours exchangeHours = null, DateTimeZone dataTimeZone = null) + { + if (exchangeHours == null) + { + exchangeHours = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork); + } + + if (dataTimeZone == null) + { + dataTimeZone = TimeZones.NewYork; + } + + var dataType = LeanData.GetDataType(resolution, tickType); + return new HistoryRequest( + startDateTime, + endDateTime, + dataType, + symbol, + resolution, + exchangeHours, + dataTimeZone, + null, + true, + false, + DataNormalizationMode.Adjusted, + tickType + ); + } + + public static Symbol CreateSymbol(string ticker, SecurityType securityType, OptionRight? optionRight = null, decimal? strikePrice = null, DateTime? expirationDate = null, string market = Market.USA) + { + switch (securityType) + { + case SecurityType.Equity: + case SecurityType.Index: + return Symbol.Create(ticker, securityType, market); + case SecurityType.Option: + var underlyingEquitySymbol = Symbol.Create(ticker, SecurityType.Equity, market); + return Symbol.CreateOption(underlyingEquitySymbol, market, OptionStyle.American, optionRight.Value, strikePrice.Value, expirationDate.Value); + case SecurityType.IndexOption: + var underlyingIndexSymbol = Symbol.Create(ticker, SecurityType.Index, market); + return Symbol.CreateOption(underlyingIndexSymbol, market, OptionStyle.American, optionRight.Value, strikePrice.Value, expirationDate.Value); + case SecurityType.FutureOption: + var underlyingFuture = Symbols.CreateFutureSymbol(ticker, expirationDate.Value); + return Symbols.CreateFutureOptionSymbol(underlyingFuture, optionRight.Value, strikePrice.Value, expirationDate.Value); + default: + throw new NotSupportedException($"The security type '{securityType}' is not supported."); + } + } + } +} diff --git a/QuantConnect.ThetaData.Tests/TestSetup.cs b/QuantConnect.ThetaData.Tests/TestSetup.cs new file mode 100644 index 0000000..026422d --- /dev/null +++ b/QuantConnect.ThetaData.Tests/TestSetup.cs @@ -0,0 +1,66 @@ +/* + * 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.IO; +using NUnit.Framework; +using System.Collections; +using QuantConnect.Logging; +using QuantConnect.Configuration; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + [SetUpFixture] + public class TestSetup + { + [OneTimeSetUp] + public void GlobalSetup() + { + Log.DebuggingEnabled = true; + Log.LogHandler = new CompositeLogHandler(); + Log.Trace("TestSetup(): starting..."); + ReloadConfiguration(); + } + + private static void ReloadConfiguration() + { + // nunit 3 sets the current folder to a temp folder we need it to be the test bin output folder + var dir = TestContext.CurrentContext.TestDirectory; + Environment.CurrentDirectory = dir; + Directory.SetCurrentDirectory(dir); + // reload config from current path + Config.Reset(); + + var environment = Environment.GetEnvironmentVariables(); + foreach (DictionaryEntry entry in environment) + { + var envKey = entry.Key.ToString(); + var value = entry.Value.ToString(); + + if (envKey.StartsWith("QC_")) + { + var key = envKey.Substring(3).Replace("_", "-").ToLower(); + + Log.Trace($"TestSetup(): Updating config setting '{key}' from environment var '{envKey}'"); + Config.Set(key, value); + } + } + + // resets the version among other things + Globals.Reset(); + } + } +} diff --git a/QuantConnect.ThetaData.Tests/ThetaDataDownloaderTests.cs b/QuantConnect.ThetaData.Tests/ThetaDataDownloaderTests.cs new file mode 100644 index 0000000..1143b9d --- /dev/null +++ b/QuantConnect.ThetaData.Tests/ThetaDataDownloaderTests.cs @@ -0,0 +1,77 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Util; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + + [TestFixture] + public class ThetaDataDownloaderTests + { + private ThetaDataDownloader _dataDownloader; + + [SetUp] + public void SetUp() + { + _dataDownloader = new(); + } + + [TearDown] + public void TearDown() + { + if (_dataDownloader != null) + { + _dataDownloader.DisposeSafely(); + } + } + + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Tick, TickType.Quote, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Tick, TickType.Trade, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Second, TickType.Quote, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Second, TickType.Trade, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Hour, TickType.Quote, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Hour, TickType.Trade, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Daily, TickType.Quote, "2024/01/18", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Daily, TickType.Trade, "2024/01/18", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Daily, TickType.OpenInterest, "2024/01/18", "2024/03/28")] + public void DownloadsOptionHistoricalData(string ticker, OptionRight optionRight, decimal strikePrice, DateTime expirationDate, Resolution resolution, TickType tickType, DateTime startDate, DateTime endDate) + { + var symbol = TestHelpers.CreateSymbol(ticker, SecurityType.Option, optionRight, strikePrice, expirationDate); + + var parameters = new DataDownloaderGetParameters(symbol, resolution, startDate, endDate, tickType); + + var downloadedHistoricalData = _dataDownloader.Get(parameters); + + TestHelpers.ValidateHistoricalBaseData(downloadedHistoricalData, resolution, tickType, startDate, endDate, symbol); + } + + [TestCase("AAPL", Resolution.Daily, TickType.Quote, "2024/01/18", "2024/03/28")] + public void DownloadsCanonicalOptionHistoricalData(string ticker, Resolution resolution, TickType tickType, DateTime startDate, DateTime endDate) + { + var symbol = Symbol.CreateCanonicalOption(TestHelpers.CreateSymbol(ticker, SecurityType.Equity)); + + var parameters = new DataDownloaderGetParameters(symbol, resolution, startDate, endDate, tickType); + + var downloadedData = _dataDownloader.Get(parameters); + + TestHelpers.ValidateHistoricalBaseData(downloadedData, resolution, tickType, startDate, endDate); + } + } +} diff --git a/QuantConnect.ThetaData.Tests/ThetaDataHistoryProviderTests..cs b/QuantConnect.ThetaData.Tests/ThetaDataHistoryProviderTests..cs new file mode 100644 index 0000000..3dc46ff --- /dev/null +++ b/QuantConnect.ThetaData.Tests/ThetaDataHistoryProviderTests..cs @@ -0,0 +1,63 @@ +/* + * 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 NUnit.Framework; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + [TestFixture] + public class ThetaDataHistoryProviderTests + { + ThetaDataProvider _thetaDataProvider = new(); + + + [TestCase("AAPL", SecurityType.Option, Resolution.Hour, TickType.OpenInterest, "2024/03/18", "2024/03/28", Description = "Wrong Resolution for OpenInterest")] + [TestCase("AAPL", SecurityType.Option, Resolution.Hour, TickType.OpenInterest, "2024/03/28", "2024/03/18", Description = "StartDate > EndDate")] + [TestCase("AAPL", SecurityType.Equity, Resolution.Hour, TickType.OpenInterest, "2024/03/28", "2024/03/18", Description = "Wrong SecurityType")] + [TestCase("AAPL", SecurityType.FutureOption, Resolution.Hour, TickType.Trade, "2024/03/28", "2024/03/18", Description = "Wrong SecurityType")] + public void TryGetHistoryDataWithInvalidRequestedParameters(string ticker, SecurityType securityType, Resolution resolution, TickType tickType, DateTime startDate, DateTime endDate) + { + var symbol = TestHelpers.CreateSymbol(ticker, securityType, OptionRight.Call, 170, new DateTime(2024, 03, 28)); + + var historyRequest = TestHelpers.CreateHistoryRequest(symbol, resolution, tickType, startDate, endDate); + + var history = _thetaDataProvider.GetHistory(historyRequest); + + Assert.IsNull(history); + } + + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Daily, TickType.Trade, "2024/01/18", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Daily, TickType.OpenInterest, "2024/01/18", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Daily, TickType.Quote, "2024/01/18", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Tick, TickType.Quote, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Tick, TickType.Trade, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Second, TickType.Quote, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Put, 170, "2024/03/28", Resolution.Second, TickType.Trade, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Hour, TickType.Quote, "2024/03/19", "2024/03/28")] + [TestCase("AAPL", OptionRight.Call, 170, "2024/03/28", Resolution.Hour, TickType.Trade, "2024/03/19", "2024/03/28")] + public void GetHistoryOptionData(string ticker, OptionRight optionRight, decimal strikePrice, DateTime expirationDate, Resolution resolution, TickType tickType, DateTime startDate, DateTime endDate) + { + var symbol = TestHelpers.CreateSymbol(ticker, SecurityType.Option, optionRight, strikePrice, expirationDate); + + var historyRequest = TestHelpers.CreateHistoryRequest(symbol, resolution, tickType, startDate, endDate); + + var history = _thetaDataProvider.GetHistory(historyRequest).ToList(); + + TestHelpers.ValidateHistoricalBaseData(history, resolution, tickType, startDate, endDate, symbol); + } + } +} diff --git a/QuantConnect.ThetaData.Tests/ThetaDataOptionChainProviderTests.cs b/QuantConnect.ThetaData.Tests/ThetaDataOptionChainProviderTests.cs new file mode 100644 index 0000000..9cce510 --- /dev/null +++ b/QuantConnect.ThetaData.Tests/ThetaDataOptionChainProviderTests.cs @@ -0,0 +1,80 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Util; +using QuantConnect.Tests; +using QuantConnect.Securities; +using System.Collections.Generic; +using QuantConnect.Lean.DataSource.ThetaData.Models.SubscriptionPlans; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + [TestFixture] + public class ThetaDataOptionChainProviderTests + { + private ThetaDataOptionChainProvider _thetaDataOptionChainProvider; + + [SetUp] + public void SetUp() + { + var userSubscription = new ProSubscriptionPlan(); + _thetaDataOptionChainProvider = new(new ThetaDataSymbolMapper(), new ThetaDataRestApiClient(userSubscription.RateGate)); + } + + private static IEnumerable UnderlyingSymbols + { + get + { + TestGlobals.Initialize(); + yield return Symbol.Create("XEO", SecurityType.Index, Market.USA); + yield return Symbol.Create("DJX", SecurityType.Index, Market.USA); + } + } + + [TestCaseSource(nameof(UnderlyingSymbols))] + public void GetOptionContractList(Symbol symbol) + { + var referenceDate = new DateTime(2024, 03, 28); + var optionChain = _thetaDataOptionChainProvider.GetOptionContractList(symbol, referenceDate).ToList(); + + Assert.That(optionChain, Is.Not.Null.And.Not.Empty); + + // Multiple strikes + var strikes = optionChain.Select(x => x.ID.StrikePrice).Distinct().ToList(); + Assert.That(strikes, Has.Count.GreaterThan(1).And.All.GreaterThan(0)); + + // Multiple expirations + var expirations = optionChain.Select(x => x.ID.Date).Distinct().ToList(); + Assert.That(expirations, Has.Count.GreaterThan(1)); + + // All contracts have the same underlying + var underlying = symbol.Underlying ?? symbol; + Assert.That(optionChain.Select(x => x.Underlying), Is.All.EqualTo(underlying)); + } + + [TestCase(Futures.Indices.SP500EMini, OptionRight.Call, 100, "2024/06/21")] + public void GetFutureOptionContractListShouldReturnNothing(string ticker, OptionRight optionRight, decimal strikePrice, DateTime expiryDate) + { + var symbol = TestHelpers.CreateSymbol(ticker, SecurityType.FutureOption, optionRight, strikePrice, expiryDate); + + var optionChain = _thetaDataOptionChainProvider.GetOptionContractList(symbol, expiryDate).ToList(); + + Assert.IsEmpty(optionChain); + } + } +} diff --git a/QuantConnect.ThetaData.Tests/ThetaDataProviderTests.cs b/QuantConnect.ThetaData.Tests/ThetaDataProviderTests.cs new file mode 100644 index 0000000..463cddd --- /dev/null +++ b/QuantConnect.ThetaData.Tests/ThetaDataProviderTests.cs @@ -0,0 +1,270 @@ +/* + * 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 NUnit.Framework; +using System.Threading; +using QuantConnect.Data; +using QuantConnect.Tests; +using QuantConnect.Logging; +using System.Threading.Tasks; +using QuantConnect.Data.Market; +using System.Collections.Generic; +using System.Collections.Concurrent; +using QuantConnect.Lean.Engine.DataFeeds.Enumerators; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + [TestFixture] + public class ThetaDataProviderTests + { + private ThetaDataProvider _thetaDataProvider; + private CancellationTokenSource _cancellationTokenSource; + + [SetUp] + public void SetUp() + { + TestGlobals.Initialize(); + _thetaDataProvider = new(); + _cancellationTokenSource = new(); + } + + [TearDown] + public void TearDown() + { + _cancellationTokenSource.Dispose(); + + if (_thetaDataProvider != null) + { + _thetaDataProvider.Dispose(); + } + } + + [TestCase("AAPL", SecurityType.Equity)] + [TestCase("VIX", SecurityType.Index)] + public void SubscribeWithWrongInputParameters(string ticker, SecurityType securityType) + { + var symbol = TestHelpers.CreateSymbol(ticker, securityType); + var configs = GetSubscriptionDataConfigs(symbol, Resolution.Minute).ToList(); + + var isNotSubscribed = new List(); + foreach (var config in configs) + { + if (_thetaDataProvider.Subscribe(config, (sender, args) => { }) == null) + { + isNotSubscribed.Add(false); + } + } + + Assert.That(configs.Count, Is.EqualTo(isNotSubscribed.Count)); + Assert.IsFalse(isNotSubscribed.Contains(true), "One of config is subscribed successfully."); + } + + [TestCase("AAPL", Resolution.Second, 170, "2024/04/19")] + [TestCase("NVDA", Resolution.Second, 890, "2024/04/12")] + public void CanSubscribeAndUnsubscribeOnSecondResolution(string ticker, Resolution resolution, decimal strikePrice, DateTime expiryDate) + { + var configs = GetSubscriptionDataConfigs(ticker, resolution, strikePrice, expiryDate); + + Assert.That(configs, Is.Not.Empty); + + var dataFromEnumerator = new Dictionary() { { typeof(TradeBar), 0 }, { typeof(QuoteBar), 0 } }; + + Action callback = (dataPoint) => + { + if (dataPoint == null) + { + return; + } + + switch (dataPoint) + { + case TradeBar _: + dataFromEnumerator[typeof(TradeBar)] += 1; + break; + case QuoteBar _: + dataFromEnumerator[typeof(QuoteBar)] += 1; + break; + }; + }; + + foreach (var config in configs) + { + ProcessFeed(_thetaDataProvider.Subscribe(config, (sender, args) => + { + var dataPoint = ((NewDataAvailableEventArgs)args).DataPoint; + Log.Trace($"{dataPoint}. Time span: {dataPoint.Time} - {dataPoint.EndTime}"); + }), _cancellationTokenSource.Token, callback: callback); + } + + Thread.Sleep(TimeSpan.FromSeconds(10)); + + Log.Trace("Unsubscribing symbols"); + foreach (var config in configs) + { + _thetaDataProvider.Unsubscribe(config); + } + + Thread.Sleep(TimeSpan.FromSeconds(5)); + + Assert.Greater(dataFromEnumerator[typeof(QuoteBar)], 0); + // The ThetaData returns TradeBar seldom. Perhaps should find more relevant ticker. + Assert.GreaterOrEqual(dataFromEnumerator[typeof(TradeBar)], 0); + } + + [TestCase("AAPL", SecurityType.Equity)] + [TestCase("VIX", SecurityType.Index)] + public void MultipleSubscriptionOnOptionContractsTickResolution(string ticker, SecurityType securityType) + { + var minReturnResponse = 5; + var obj = new object(); + var cancellationTokenSource = new CancellationTokenSource(); + var resetEvent = new AutoResetEvent(false); + var underlyingSymbol = TestHelpers.CreateSymbol(ticker, securityType); + var configs = _thetaDataProvider.LookupSymbols(underlyingSymbol, false).SelectMany(x => GetSubscriptionTickDataConfigs(x)).Take(500).ToList(); + + var incomingSymbolDataByTickType = new ConcurrentDictionary<(Symbol, TickType), int>(); + + Action callback = (dataPoint) => + { + if (dataPoint == null) + { + return; + } + + var tick = dataPoint as Tick; + + lock (obj) + { + switch (tick.TickType) + { + case TickType.Trade: + incomingSymbolDataByTickType[(tick.Symbol, tick.TickType)] += 1; + break; + case TickType.Quote: + incomingSymbolDataByTickType[(tick.Symbol, tick.TickType)] += 1; + break; + }; + } + }; + + foreach (var config in configs) + { + incomingSymbolDataByTickType.TryAdd((config.Symbol, config.TickType), 0); + ProcessFeed(_thetaDataProvider.Subscribe(config, (sender, args) => + { + var dataPoint = ((NewDataAvailableEventArgs)args).DataPoint; + Log.Trace($"{dataPoint}. Time span: {dataPoint.Time} - {dataPoint.EndTime}"); + }), + cancellationTokenSource.Token, + 300, + callback: callback, + throwExceptionCallback: () => cancellationTokenSource.Cancel()); + } + + resetEvent.WaitOne(TimeSpan.FromMinutes(1), cancellationTokenSource.Token); + + Log.Trace("Unsubscribing symbols"); + foreach (var config in configs) + { + _thetaDataProvider.Unsubscribe(config); + } + + resetEvent.WaitOne(TimeSpan.FromSeconds(20), cancellationTokenSource.Token); + + var symbolVolatilities = incomingSymbolDataByTickType.Where(kv => kv.Value > 0).ToList(); + + Log.Debug($"CancellationToken: {_cancellationTokenSource.Token.IsCancellationRequested}"); + + Assert.IsNotEmpty(symbolVolatilities); + Assert.That(symbolVolatilities.Count, Is.GreaterThan(minReturnResponse)); + + cancellationTokenSource.Cancel(); + } + + private static IEnumerable GetSubscriptionDataConfigs(string ticker, Resolution resolution, decimal strikePrice, DateTime expiry, + OptionRight optionRight = OptionRight.Call, string market = Market.USA) + { + var symbol = TestHelpers.CreateSymbol(ticker, SecurityType.Option, optionRight, strikePrice, expiry, market); + foreach (var subscription in GetSubscriptionDataConfigs(symbol, resolution)) + { + yield return subscription; + } + } + private static IEnumerable GetSubscriptionDataConfigs(Symbol symbol, Resolution resolution) + { + yield return GetSubscriptionDataConfig(symbol, resolution); + yield return GetSubscriptionDataConfig(symbol, resolution); + } + + public static IEnumerable GetSubscriptionTickDataConfigs(Symbol symbol) + { + yield return new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, Resolution.Tick), tickType: TickType.Trade); + yield return new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, Resolution.Tick), tickType: TickType.Quote); + } + + private static SubscriptionDataConfig GetSubscriptionDataConfig(Symbol symbol, Resolution resolution) + { + return new SubscriptionDataConfig( + typeof(T), + symbol, + resolution, + TimeZones.Utc, + TimeZones.Utc, + true, + extendedHours: false, + false); + } + + private Task ProcessFeed( + IEnumerator enumerator, + CancellationToken cancellationToken, + int cancellationTokenDelayMilliseconds = 100, + Action callback = null, + Action throwExceptionCallback = null) + { + return Task.Factory.StartNew(() => + { + try + { + while (enumerator.MoveNext() && !cancellationToken.IsCancellationRequested) + { + BaseData tick = enumerator.Current; + + if (tick != null) + { + callback?.Invoke(tick); + } + + cancellationToken.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(cancellationTokenDelayMilliseconds)); + } + } + catch (Exception ex) + { + Log.Debug($"{nameof(ThetaDataProviderTests)}.{nameof(ProcessFeed)}.Exception: {ex.Message}"); + throw; + } + }, cancellationToken).ContinueWith(task => + { + if (throwExceptionCallback != null) + { + throwExceptionCallback(); + } + Log.Debug("The throwExceptionCallback is null."); + }, TaskContinuationOptions.OnlyOnFaulted); + } + } +} diff --git a/QuantConnect.ThetaData.Tests/ThetaDataQueueUniverseProviderTests.cs b/QuantConnect.ThetaData.Tests/ThetaDataQueueUniverseProviderTests.cs new file mode 100644 index 0000000..b8c3e86 --- /dev/null +++ b/QuantConnect.ThetaData.Tests/ThetaDataQueueUniverseProviderTests.cs @@ -0,0 +1,70 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Lean.Engine.DataFeeds; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + [TestFixture] + public class ThetaDataQueueUniverseProviderTests + { + private TestableThetaDataProvider _thetaDataProvider; + + [SetUp] + public void SetUp() + { + _thetaDataProvider = new TestableThetaDataProvider(); + } + + private static Symbol[] OptionChainTestCases => + new[] + { + Symbol.Create("SPY", SecurityType.Equity, Market.USA), + Symbol.Create("SPX", SecurityType.Index, Market.USA), + } + .Select(underlying => new[] { underlying, Symbol.CreateCanonicalOption(underlying) }) + .SelectMany(x => x) + .ToArray(); + + [TestCaseSource(nameof(OptionChainTestCases))] + public void GetsOptionChain(Symbol symbol) + { + var date = new DateTime(2014, 10, 7); + _thetaDataProvider.TimeProviderInstance.SetCurrentTimeUtc(date); + var optionChain = _thetaDataProvider.LookupSymbols(symbol, true).ToList(); + + Assert.That(optionChain, Is.Not.Null.And.Not.Empty); + + var expectedOptionType = symbol.SecurityType; + if (!expectedOptionType.IsOption()) + { + expectedOptionType = expectedOptionType == SecurityType.Equity ? SecurityType.Option : SecurityType.IndexOption; + } + Assert.IsTrue(optionChain.All(x => x.SecurityType == expectedOptionType)); + Assert.IsTrue(optionChain.All(x => x.ID.Date.Date >= date)); + } + + + private class TestableThetaDataProvider : ThetaDataProvider + { + public ManualTimeProvider TimeProviderInstance = new ManualTimeProvider(); + + protected override ITimeProvider TimeProvider => TimeProviderInstance; + } + } +} diff --git a/QuantConnect.ThetaData.Tests/ThetaDataSymbolMapperTests.cs b/QuantConnect.ThetaData.Tests/ThetaDataSymbolMapperTests.cs new file mode 100644 index 0000000..46b6d7e --- /dev/null +++ b/QuantConnect.ThetaData.Tests/ThetaDataSymbolMapperTests.cs @@ -0,0 +1,89 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Brokerages; +using QuantConnect.Lean.DataSource.ThetaData.Models.Enums; + +namespace QuantConnect.Lean.DataSource.ThetaData.Tests +{ + [TestFixture] + public class ThetaDataSymbolMapperTests + { + private ISymbolMapper _symbolMapper = new ThetaDataSymbolMapper(); + + [TestCase("JD", SecurityType.Equity, null, null, null, "JD", null, null, null)] + [TestCase("SPEUFP2I", SecurityType.Index, null, null, null, "SPEUFP2I", null, null, null)] + [TestCase("AAPL", SecurityType.Option, OptionRight.Call, 22, "2024/03/08", "AAPL", "C", "22000", "20240308")] + [TestCase("9QE", SecurityType.Option, OptionRight.Put, 100, "2026/02/02", "9QE", "P", "100000", "20260202")] + [TestCase("9QE", SecurityType.Option, OptionRight.Put, 123, "2026/02/02", "9QE", "P", "123000", "20260202")] + [TestCase("SPXW", SecurityType.IndexOption, OptionRight.Call, 6700, "2022/09/30", "SPXW", "C", "6700000", "20220930")] + [TestCase("NDX", SecurityType.IndexOption, OptionRight.Call, 1650, "2013/01/19", "NDX", "C", "1650000", "20130119")] + public void GetDataProviderOptionTicker(string ticker, SecurityType securityType, OptionRight? optionRight, decimal? strikePrice, DateTime? expirationDate, + string expectedTicker, string expectedOptionRight, string expectedStrike, string expectedExpirationDate) + { + var leanSymbol = TestHelpers.CreateSymbol(ticker, securityType, optionRight, strikePrice, expirationDate); + + var dataProviderContract = _symbolMapper.GetBrokerageSymbol(leanSymbol).Split(','); + + Assert.That(dataProviderContract[0], Is.EqualTo(expectedTicker)); + + if (securityType.IsOption()) + { + Assert.That(dataProviderContract[1], Is.EqualTo(expectedExpirationDate)); + Assert.That(dataProviderContract[2], Is.EqualTo(expectedStrike)); + Assert.That(dataProviderContract[3], Is.EqualTo(expectedOptionRight)); + } + } + + [TestCase("AAPL", ContractSecurityType.Option, "C", 22000, "20240308", Market.USA, OptionRight.Call, 22, "2024/03/08")] + [TestCase("AAPL", ContractSecurityType.Option, "P", 1000000, "20240303", Market.USA, OptionRight.Put, 1000, "2024/03/03")] + [TestCase("AAPL", ContractSecurityType.Equity, "", 0, "", Market.USA, null, null, null)] + [TestCase("INTL", ContractSecurityType.Equity, "", 0, "", Market.USA, null, null, null)] + public void GetLeanSymbol( + string dataProviderTicker, + ContractSecurityType dataProviderContractSecurityType, + string dataProviderOptionRight, + decimal dataProviderStrike, + string dataProviderExpirationDate, + string expectedMarket, + OptionRight? expectedOptionRight = null, + decimal? expectedStrikePrice = null, + DateTime? expectedExpiryDateTime = null) + { + var leanSymbol = (_symbolMapper as ThetaDataSymbolMapper) + .GetLeanSymbol(dataProviderTicker, dataProviderContractSecurityType, dataProviderExpirationDate, dataProviderStrike, dataProviderOptionRight); + + var expectedLeanSymbol = + CreateSymbol(dataProviderContractSecurityType, dataProviderTicker, expectedOptionRight, expectedStrikePrice, expectedExpiryDateTime, expectedMarket); + + Assert.That(leanSymbol, Is.EqualTo(expectedLeanSymbol)); + } + + private Symbol CreateSymbol(ContractSecurityType contractSecurityType, string ticker, OptionRight? optionRight, decimal? strikePrice, DateTime? expirationDate, string market = Market.USA) + { + switch (contractSecurityType) + { + case ContractSecurityType.Option: + return TestHelpers.CreateSymbol(ticker, SecurityType.Option, optionRight, strikePrice, expirationDate, market); + case ContractSecurityType.Equity: + return TestHelpers.CreateSymbol(ticker, SecurityType.Equity, market: market); + default: + throw new NotSupportedException($"The contract security type '{contractSecurityType}' is not supported."); + } + } + } +} diff --git a/QuantConnect.ThetaData.Tests/config.json b/QuantConnect.ThetaData.Tests/config.json new file mode 100644 index 0000000..644cd1f --- /dev/null +++ b/QuantConnect.ThetaData.Tests/config.json @@ -0,0 +1,6 @@ +{ + "data-folder": "../../../../Lean/Data/", + "data-directory": "../../../../Lean/Data/", + + "thetadata-subscription-plan": "Free" +} \ No newline at end of file diff --git a/QuantConnect.ThetaData/Converters/DateTimeIntJsonConverter.cs b/QuantConnect.ThetaData/Converters/DateTimeIntJsonConverter.cs new file mode 100644 index 0000000..b2dedd2 --- /dev/null +++ b/QuantConnect.ThetaData/Converters/DateTimeIntJsonConverter.cs @@ -0,0 +1,73 @@ +/* + * 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 System.Globalization; +using Newtonsoft.Json.Linq; + +namespace QuantConnect.Lean.DataSource.ThetaData.Converters; + +/// +/// Converts a ThetaData Int DateTime presention to +/// +public class DateTimeIntJsonConverter : JsonConverter +{ + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + if (objectType == typeof(DateTime) || objectType == typeof(DateTime?)) + { + return true; + } + + return false; + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + if (token.Type != JTokenType.Integer) + return null; + + string? dateText = reader.Value?.ToString(); + + return DateTime.ParseExact(dateText!, "yyyyMMdd", CultureInfo.InvariantCulture); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} diff --git a/QuantConnect.ThetaData/Converters/ThetaDataEndOfDayConverter.cs b/QuantConnect.ThetaData/Converters/ThetaDataEndOfDayConverter.cs new file mode 100644 index 0000000..7ff0400 --- /dev/null +++ b/QuantConnect.ThetaData/Converters/ThetaDataEndOfDayConverter.cs @@ -0,0 +1,69 @@ +/* + * 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 System.Globalization; +using QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +namespace QuantConnect.Lean.DataSource.ThetaData.Converters; + +public class ThetaDataEndOfDayConverter : JsonConverter +{ + /// + /// Gets a value indicating whether this can write JSON. + /// + /// true if this can write JSON; otherwise, false. + public override bool CanWrite => false; + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// true if this can read JSON; otherwise, false. + public override bool CanRead => true; + + public override EndOfDayReportResponse ReadJson(JsonReader reader, Type objectType, EndOfDayReportResponse existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + if (token.Type != JTokenType.Array || token.Count() != 17) throw new Exception($"{nameof(ThetaDataEndOfDayConverter)}.{nameof(ReadJson)}: Invalid token type or count. Expected a JSON array with exactly four elements."); + + existingValue = new EndOfDayReportResponse( + reportGeneratedTimeMilliseconds: token[0]!.Value(), + lastTradeTimeMilliseconds: token[1]!.Value(), + open: token[2]!.Value(), + high: token[3]!.Value(), + low: token[4]!.Value(), + close: token[5]!.Value(), + volume: token[6]!.Value(), + amountTrades: token[7]!.Value(), + bidSize: token[8]!.Value(), + bidExchange: token[9]!.Value(), + bidPrice: token[10]!.Value(), + bidCondition: token[11]!.Value() ?? string.Empty, + askSize: token[12]!.Value(), + askExchange: token[13]!.Value(), + askPrice: token[14]!.Value(), + askCondition: token[15]!.Value() ?? string.Empty, + date: DateTime.ParseExact(token[16]!.ToString(), "yyyyMMdd", CultureInfo.InvariantCulture) + ); + + return existingValue; + } + + public override void WriteJson(JsonWriter writer, EndOfDayReportResponse value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} diff --git a/QuantConnect.ThetaData/Converters/ThetaDataNullStringConverter.cs b/QuantConnect.ThetaData/Converters/ThetaDataNullStringConverter.cs new file mode 100644 index 0000000..91516e0 --- /dev/null +++ b/QuantConnect.ThetaData/Converters/ThetaDataNullStringConverter.cs @@ -0,0 +1,64 @@ +/* + * 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; + +namespace QuantConnect.Lean.DataSource.ThetaData.Converters; + +/// +/// Converts the string value "null" to null during JSON deserialization. +/// +public class ThetaDataNullStringConverter : JsonConverter +{ + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(string); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The Newtonsoft.Json.JsonReader to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + if (token.Type == JTokenType.String && (string?)token == "null") + return null; + return token.ToObject(); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The Newtonsoft.Json.JsonWriter to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + writer.WriteValue(value); + } +} diff --git a/QuantConnect.ThetaData/Converters/ThetaDataOpenInterestConverter.cs b/QuantConnect.ThetaData/Converters/ThetaDataOpenInterestConverter.cs new file mode 100644 index 0000000..1839ddb --- /dev/null +++ b/QuantConnect.ThetaData/Converters/ThetaDataOpenInterestConverter.cs @@ -0,0 +1,68 @@ +/* + * 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 System.Globalization; +using QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +namespace QuantConnect.Lean.DataSource.ThetaData.Converters; + +public class ThetaDataOpenInterestConverter : JsonConverter +{ + /// + /// Gets a value indicating whether this can write JSON. + /// + /// true if this can write JSON; otherwise, false. + public override bool CanWrite => false; + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// true if this can read JSON; otherwise, false. + public override bool CanRead => true; + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + public override OpenInterestResponse ReadJson(JsonReader reader, Type objectType, OpenInterestResponse existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + if (token.Type != JTokenType.Array || token.Count() != 3) throw new Exception($"{nameof(ThetaDataOpenInterestConverter)}.{nameof(ReadJson)}: Invalid token type or count. Expected a JSON array with exactly four elements."); + + return new OpenInterestResponse( + timeMilliseconds: token[0]!.Value(), + openInterest: token[1]!.Value(), + date: DateTime.ParseExact(token[2]!.ToString(), "yyyyMMdd", CultureInfo.InvariantCulture) + ); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, OpenInterestResponse value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} diff --git a/QuantConnect.ThetaData/Converters/ThetaDataQuoteConverter.cs b/QuantConnect.ThetaData/Converters/ThetaDataQuoteConverter.cs new file mode 100644 index 0000000..3a49ae8 --- /dev/null +++ b/QuantConnect.ThetaData/Converters/ThetaDataQuoteConverter.cs @@ -0,0 +1,77 @@ +/* + * 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 System.Globalization; +using QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +namespace QuantConnect.Lean.DataSource.ThetaData.Converters; + +/// +/// JSON converter to convert ThetaData Quote +/// +public class ThetaDataQuoteConverter : JsonConverter +{ + /// + /// Gets a value indicating whether this can write JSON. + /// + /// true if this can write JSON; otherwise, false. + public override bool CanWrite => false; + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// true if this can read JSON; otherwise, false. + public override bool CanRead => true; + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, QuoteResponse value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + public override QuoteResponse ReadJson(JsonReader reader, Type objectType, QuoteResponse existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + if (token.Type != JTokenType.Array || token.Count() != 10) throw new Exception($"{nameof(ThetaDataQuoteConverter)}.{nameof(ReadJson)}: Invalid token type or count. Expected a JSON array with exactly four elements."); + + return new QuoteResponse( + timeMilliseconds: token[0]!.Value(), + bidSize: token[1]!.Value(), + bidExchange: token[2]!.Value(), + bidPrice: token[3]!.Value(), + bidCondition: token[4]!.Value() ?? string.Empty, + askSize: token[5]!.Value(), + askExchange: token[6]!.Value(), + askPrice: token[7]!.Value(), + askCondition: token[8]!.Value() ?? string.Empty, + date: DateTime.ParseExact(token[9]!.ToString(), "yyyyMMdd", CultureInfo.InvariantCulture)); + } +} diff --git a/QuantConnect.ThetaData/Converters/ThetaDataTradeConverter.cs b/QuantConnect.ThetaData/Converters/ThetaDataTradeConverter.cs new file mode 100644 index 0000000..37c0b30 --- /dev/null +++ b/QuantConnect.ThetaData/Converters/ThetaDataTradeConverter.cs @@ -0,0 +1,71 @@ +/* + * 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 System.Globalization; +using QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +namespace QuantConnect.Lean.DataSource.ThetaData.Converters; + +public class ThetaDataTradeConverter : JsonConverter +{ + /// + /// Gets a value indicating whether this can write JSON. + /// + /// true if this can write JSON; otherwise, false. + public override bool CanWrite => false; + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// true if this can read JSON; otherwise, false. + public override bool CanRead => true; + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, TradeResponse value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + public override TradeResponse ReadJson(JsonReader reader, Type objectType, TradeResponse existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + if (token.Type != JTokenType.Array || token.Count() != 15) throw new Exception($"{nameof(ThetaDataTradeConverter)}.{nameof(ReadJson)}: Invalid token type or count. Expected a JSON array with exactly four elements."); + + return new TradeResponse( + timeMilliseconds: token[0]!.Value(), + condition: token[6]!.Value() ?? string.Empty, + size: token[7]!.Value(), + exchange: token[8]!.Value(), + price: token[9]!.Value(), + date: DateTime.ParseExact(token[14]!.ToString(), "yyyyMMdd", CultureInfo.InvariantCulture) + ); + } +} diff --git a/QuantConnect.ThetaData/Models/Common/BaseHeaderResponse.cs b/QuantConnect.ThetaData/Models/Common/BaseHeaderResponse.cs new file mode 100644 index 0000000..e4af976 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Common/BaseHeaderResponse.cs @@ -0,0 +1,42 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using Newtonsoft.Json; +using QuantConnect.Lean.DataSource.ThetaData.Converters; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Common; + +/// +/// Represents the base header response. +/// +public readonly struct BaseHeaderResponse +{ + /// + /// Gets the next page value. + /// + [JsonProperty("next_page")] + [JsonConverter(typeof(ThetaDataNullStringConverter))] + public string NextPage { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The next page value. + [JsonConstructor] + public BaseHeaderResponse(string nextPage) + { + NextPage = nextPage; + } +} diff --git a/QuantConnect.ThetaData/Models/Common/BaseResponse.cs b/QuantConnect.ThetaData/Models/Common/BaseResponse.cs new file mode 100644 index 0000000..6ad8e45 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Common/BaseResponse.cs @@ -0,0 +1,50 @@ +/* + * 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.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Common; + +/// +/// Represents a base response containing a header and a collection of items of type T. +/// +/// The type of items in the response. +public readonly struct BaseResponse : IBaseResponse +{ + /// + /// Gets the header of the response. + /// + [JsonProperty("header")] + public BaseHeaderResponse Header { get; } + + /// + /// Gets the collection of items in the response. + /// + [JsonProperty("response")] + public IEnumerable Response { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The collection of items in the response. + /// The header of the response. + [JsonConstructor] + public BaseResponse(IEnumerable response, BaseHeaderResponse header) + { + Response = response; + Header = header; + } +} \ No newline at end of file diff --git a/QuantConnect.ThetaData/Models/Enums/ContractSecurityType.cs b/QuantConnect.ThetaData/Models/Enums/ContractSecurityType.cs new file mode 100644 index 0000000..ec8d4f6 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Enums/ContractSecurityType.cs @@ -0,0 +1,30 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Enums; + +[JsonConverter(typeof(StringEnumConverter))] +public enum ContractSecurityType +{ + [EnumMember(Value = "OPTION")] + Option = 0, + + [EnumMember(Value = "EQUITY")] + Equity = 1, +} diff --git a/QuantConnect.ThetaData/Models/Enums/SubscriptionPlanType.cs b/QuantConnect.ThetaData/Models/Enums/SubscriptionPlanType.cs new file mode 100644 index 0000000..473209f --- /dev/null +++ b/QuantConnect.ThetaData/Models/Enums/SubscriptionPlanType.cs @@ -0,0 +1,43 @@ +/* + * 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.Lean.DataSource.ThetaData.Models.Enums; + +/// +/// Enum representing different subscription plan types. +/// following link: +/// +public enum SubscriptionPlanType +{ + /// + /// Free subscription plan. + /// + Free = 0, + + /// + /// Value subscription plan. + /// + Value = 1, + + /// + /// Standard subscription plan. + /// + Standard = 2, + + /// + /// Pro subscription plan. + /// + Pro = 3 +} diff --git a/QuantConnect.ThetaData/Models/Enums/WebSocketHeaderType.cs b/QuantConnect.ThetaData/Models/Enums/WebSocketHeaderType.cs new file mode 100644 index 0000000..2dd6927 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Enums/WebSocketHeaderType.cs @@ -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 Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Enums; + +[JsonConverter(typeof(StringEnumConverter))] +public enum WebSocketHeaderType +{ + [EnumMember(Value = "STATUS")] + Status = 0, + + [EnumMember(Value = "QUOTE")] + Quote = 1, + + [EnumMember(Value = "TRADE")] + Trade = 2, + + [EnumMember(Value = "OHLC")] + Ohlc = 3, +} diff --git a/QuantConnect.ThetaData/Models/Interfaces/IBaseResponse.cs b/QuantConnect.ThetaData/Models/Interfaces/IBaseResponse.cs new file mode 100644 index 0000000..edd70b9 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Interfaces/IBaseResponse.cs @@ -0,0 +1,29 @@ +/* + * 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.Lean.DataSource.ThetaData.Models.Common; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +/// +/// Represents the base interface for response objects. +/// +public interface IBaseResponse +{ + /// + /// Gets the header of the response. + /// + public BaseHeaderResponse Header { get; } +} diff --git a/QuantConnect.ThetaData/Models/Interfaces/ISubscriptionPlan.cs b/QuantConnect.ThetaData/Models/Interfaces/ISubscriptionPlan.cs new file mode 100644 index 0000000..de63398 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Interfaces/ISubscriptionPlan.cs @@ -0,0 +1,47 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Util; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +/// +/// The ISubscriptionPlan interface defines the base structure for different price plans offered by ThetaData for users. +/// For detailed documentation on ThetaData subscription plans, refer to the following links: +/// +/// +/// +public interface ISubscriptionPlan +{ + /// + /// Gets the set of resolutions accessible under the subscription plan. + /// + public HashSet AccessibleResolutions { get; } + + /// + /// Gets the date when the user first accessed the subscription plan. + /// + public DateTime FirstAccessDate { get; } + + /// + /// Gets the maximum number of contracts that can be streamed simultaneously under the subscription plan. + /// + public uint MaxStreamingContracts { get; } + + /// + /// Represents a rate limiting mechanism that controls the rate of access to a resource. + /// + public RateGate? RateGate { get; } +} diff --git a/QuantConnect.ThetaData/Models/Rest/EndOfDayReportResponse.cs b/QuantConnect.ThetaData/Models/Rest/EndOfDayReportResponse.cs new file mode 100644 index 0000000..22471f1 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Rest/EndOfDayReportResponse.cs @@ -0,0 +1,141 @@ +/* + * 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.Lean.DataSource.ThetaData.Converters; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +[JsonConverter(typeof(ThetaDataEndOfDayConverter))] +public readonly struct EndOfDayReportResponse +{ + /// + /// Represents the time of day the report was generated + /// + public uint ReportGeneratedTimeMilliseconds { get; } + + /// + /// Represents the time of the last trade + /// + public uint LastTradeTimeMilliseconds { get; } + + /// + /// The opening trade price. + /// + public decimal Open { get; } + + /// + /// The highest traded price. + /// + public decimal High { get; } + + /// + /// The lowest traded price. + /// + public decimal Low { get; } + + /// + /// The closing traded price. + /// + public decimal Close { get; } + + /// + /// The amount of contracts traded. + /// + public decimal Volume { get; } + + /// + /// The amount of trades. + /// + public uint AmountTrades { get; } + + /// + /// The last NBBO bid size. + /// + public decimal BidSize { get; } + + /// + /// The last NBBO bid exchange. + /// + public byte BidExchange { get; } + + /// + /// The last NBBO bid price. + /// + public decimal BidPrice { get; } + + /// + /// The last NBBO bid condition. + /// + public string BidCondition { get; } + + /// + /// The last NBBO ask size. + /// + //[JsonProperty("ask_size")] + public decimal AskSize { get; } + + /// + /// The last NBBO ask exchange. + /// + public byte AskExchange { get; } + + /// + /// The last NBBO ask price. + /// + public decimal AskPrice { get; } + + /// + /// The last NBBO ask condition. + /// + public string AskCondition { get; } + + /// + /// The date formated as YYYYMMDD. + /// + public DateTime Date { get; } + + /// + /// Gets the DateTime representation of the last trade time in milliseconds. DateTime is New York Time (EST) Time Zone! + /// + /// + /// This property calculates the by adding the to the Date property. + /// + public DateTime LastTradeDateTimeMilliseconds { get => Date.AddMilliseconds(LastTradeTimeMilliseconds); } + + //[JsonConstructor] + public EndOfDayReportResponse(uint reportGeneratedTimeMilliseconds, uint lastTradeTimeMilliseconds, decimal open, decimal high, decimal low, decimal close, + decimal volume, uint amountTrades, decimal bidSize, byte bidExchange, decimal bidPrice, string bidCondition, decimal askSize, byte askExchange, + decimal askPrice, string askCondition, DateTime date) + { + ReportGeneratedTimeMilliseconds = reportGeneratedTimeMilliseconds; + LastTradeTimeMilliseconds = lastTradeTimeMilliseconds; + Open = open; + High = high; + Low = low; + Close = close; + Volume = volume; + AmountTrades = amountTrades; + BidSize = bidSize; + BidExchange = bidExchange; + BidPrice = bidPrice; + BidCondition = bidCondition; + AskSize = askSize; + AskExchange = askExchange; + AskPrice = askPrice; + AskCondition = askCondition; + Date = date; + } +} diff --git a/QuantConnect.ThetaData/Models/Rest/OpenInterestResponse.cs b/QuantConnect.ThetaData/Models/Rest/OpenInterestResponse.cs new file mode 100644 index 0000000..453d32a --- /dev/null +++ b/QuantConnect.ThetaData/Models/Rest/OpenInterestResponse.cs @@ -0,0 +1,62 @@ +/* + * 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.Lean.DataSource.ThetaData.Converters; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +/// +/// Represents a response containing open interest data. +/// +[JsonConverter(typeof(ThetaDataOpenInterestConverter))] +public readonly struct OpenInterestResponse +{ + /// + /// Gets the time at which open interest was reported, represented in milliseconds since 00:00:00.000 (midnight) Eastern Time (ET). + /// + public uint TimeMilliseconds { get; } + + /// + /// Gets the total amount of outstanding contracts. + /// + public decimal OpenInterest { get; } + + /// + /// Gets the date of the open interest data in the format YYYYMMDD. For example, "20240328" represents March 28, 2024. + /// + public DateTime Date { get; } + + /// + /// Gets the DateTime representation of the last Open Interest time. DateTime is New York Time (EST) Time Zone! + /// + /// + /// This property calculates the by adding the to the Date property. + /// + public DateTime DateTimeMilliseconds { get => Date.AddMilliseconds(TimeMilliseconds); } + + /// + /// Initializes a new instance of the struct with the specified time, open interest, and date. + /// + /// The time in milliseconds since midnight Eastern Time (ET). + /// The total amount of outstanding contracts. + /// The date of the data in the format YYYYMMDD. + public OpenInterestResponse(uint timeMilliseconds, decimal openInterest, DateTime date) + { + TimeMilliseconds = timeMilliseconds; + OpenInterest = openInterest; + Date = date; + } +} diff --git a/QuantConnect.ThetaData/Models/Rest/QuoteResponse.cs b/QuantConnect.ThetaData/Models/Rest/QuoteResponse.cs new file mode 100644 index 0000000..cdcecc8 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Rest/QuoteResponse.cs @@ -0,0 +1,112 @@ +/* + * 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.Lean.DataSource.ThetaData.Converters; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +/// +/// Represents a Quote containing information about the last NBBO (National Best Bid and Offer) for a financial instrument. +/// +[JsonConverter(typeof(ThetaDataQuoteConverter))] +public readonly struct QuoteResponse +{ + /// + /// The milliseconds since 00:00:00.000 (midnight) Eastern Time (ET). + /// + public uint TimeMilliseconds { get; } + + /// + /// The last NBBO bid size. + /// + public decimal BidSize { get; } + + /// + /// The last NBBO bid exchange. + /// + public byte BidExchange { get; } + + /// + /// The last NBBO bid price. + /// + public decimal BidPrice { get; } + + /// + /// The last NBBO bid condition. + /// + public string BidCondition { get; } + + /// + /// The last NBBO ask size. + /// + public decimal AskSize { get; } + + /// + /// The last NBBO ask exchange. + /// + public byte AskExchange { get; } + + /// + /// The last NBBO ask price. + /// + public decimal AskPrice { get; } + + /// + /// The last NBBO ask condition. + /// + public string AskCondition { get; } + + /// + /// The date formatted as YYYYMMDD. (e.g. "20240328" -> 2024/03/28) + /// + public DateTime Date { get; } + + /// + /// Gets the DateTime representation of the last quote time. DateTime is New York Time (EST) Time Zone! + /// + /// + /// This property calculates the by adding the to the Date property. + /// + public DateTime DateTimeMilliseconds { get => Date.AddMilliseconds(TimeMilliseconds); } + + /// + /// Initializes a new instance of the struct. + /// + /// Milliseconds since 00:00:00.000 (midnight) Eastern Time (ET). + /// The last NBBO bid size. + /// The last NBBO bid exchange. + /// The last NBBO bid price. + /// The last NBBO bid condition. + /// The last NBBO ask size. + /// The last NBBO ask exchange. + /// The last NBBO ask price. + /// The last NBBO ask condition. + /// The date formatted as YYYYMMDD. + public QuoteResponse(uint timeMilliseconds, decimal bidSize, byte bidExchange, decimal bidPrice, string bidCondition, + decimal askSize, byte askExchange, decimal askPrice, string askCondition, DateTime date) + { + TimeMilliseconds = timeMilliseconds; + BidSize = bidSize; + BidExchange = bidExchange; + BidPrice = bidPrice; + BidCondition = bidCondition; + AskSize = askSize; + AskExchange = askExchange; + AskPrice = askPrice; + AskCondition = askCondition; + Date = date; + } +} diff --git a/QuantConnect.ThetaData/Models/Rest/TradeResponse.cs b/QuantConnect.ThetaData/Models/Rest/TradeResponse.cs new file mode 100644 index 0000000..c255928 --- /dev/null +++ b/QuantConnect.ThetaData/Models/Rest/TradeResponse.cs @@ -0,0 +1,83 @@ +/* + * 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.Lean.DataSource.ThetaData.Converters; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.Rest; + +/// +/// Represents a Trade response containing information about the last NBBO Trade for a financial instrument. +/// +[JsonConverter(typeof(ThetaDataTradeConverter))] +public readonly struct TradeResponse +{ + /// + /// The milliseconds since 00:00:00.000 (midnight) Eastern Time (ET). + /// + public uint TimeMilliseconds { get; } + + /// + /// The trade condition. + /// + public string Condition { get; } + + /// + /// The amount of contracts traded. + /// + public decimal Size { get; } + + /// + /// The exchange the trade was executed. + /// + public byte Exchange { get; } + + /// + /// The price of the trade. + /// + public decimal Price { get; } + + /// + /// The date formatted as YYYYMMDD. (e.g. "20240328" -> 2024/03/28) + /// + public DateTime Date { get; } + + /// + /// Gets the DateTime representation of the last trade time. DateTime is New York Time (EST) Time Zone! + /// + /// + /// This property calculates the by adding the to the Date property. + /// + public DateTime DateTimeMilliseconds { get => Date.AddMilliseconds(TimeMilliseconds); } + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The milliseconds since midnight ET. + /// The trade condition. + /// The amount of contracts traded. + /// The exchange where the trade was executed. + /// The price of the trade. + /// The date formatted as YYYYMMDD. + public TradeResponse(uint timeMilliseconds, string condition, decimal size, byte exchange, decimal price, DateTime date) + { + TimeMilliseconds = timeMilliseconds; + Condition = condition; + Size = size; + Exchange = exchange; + Price = price; + Date = date; + } +} diff --git a/QuantConnect.ThetaData/Models/SubscriptionPlans/FreeSubscriptionPlan.cs b/QuantConnect.ThetaData/Models/SubscriptionPlans/FreeSubscriptionPlan.cs new file mode 100644 index 0000000..21cdac4 --- /dev/null +++ b/QuantConnect.ThetaData/Models/SubscriptionPlans/FreeSubscriptionPlan.cs @@ -0,0 +1,30 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Util; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.SubscriptionPlans; + +public class FreeSubscriptionPlan : ISubscriptionPlan +{ + public HashSet AccessibleResolutions => new() { Resolution.Daily }; + + public DateTime FirstAccessDate => new DateTime(2023, 06, 01); + + public uint MaxStreamingContracts => 0; + + public RateGate RateGate => new(30, TimeSpan.FromMinutes(1)); +} diff --git a/QuantConnect.ThetaData/Models/SubscriptionPlans/ProSubscriptionPlan.cs b/QuantConnect.ThetaData/Models/SubscriptionPlans/ProSubscriptionPlan.cs new file mode 100644 index 0000000..a092f68 --- /dev/null +++ b/QuantConnect.ThetaData/Models/SubscriptionPlans/ProSubscriptionPlan.cs @@ -0,0 +1,30 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Util; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.SubscriptionPlans; + +public class ProSubscriptionPlan : ISubscriptionPlan +{ + public HashSet AccessibleResolutions => new() { Resolution.Tick, Resolution.Second, Resolution.Minute, Resolution.Hour, Resolution.Daily }; + + public DateTime FirstAccessDate => new DateTime(2012, 06, 01); + + public uint MaxStreamingContracts => 15_000; + + public RateGate? RateGate => null; +} diff --git a/QuantConnect.ThetaData/Models/SubscriptionPlans/StandardSubscriptionPlan.cs b/QuantConnect.ThetaData/Models/SubscriptionPlans/StandardSubscriptionPlan.cs new file mode 100644 index 0000000..1f63ff0 --- /dev/null +++ b/QuantConnect.ThetaData/Models/SubscriptionPlans/StandardSubscriptionPlan.cs @@ -0,0 +1,30 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Util; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.SubscriptionPlans; + +public class StandardSubscriptionPlan : ISubscriptionPlan +{ + public HashSet AccessibleResolutions => new() { Resolution.Tick, Resolution.Second, Resolution.Minute, Resolution.Hour, Resolution.Daily }; + + public DateTime FirstAccessDate => new DateTime(2016, 01, 01); + + public uint MaxStreamingContracts => 10_000; + + public RateGate? RateGate => null; +} diff --git a/QuantConnect.ThetaData/Models/SubscriptionPlans/ValueSubscriptionPlan.cs b/QuantConnect.ThetaData/Models/SubscriptionPlans/ValueSubscriptionPlan.cs new file mode 100644 index 0000000..6a08c5b --- /dev/null +++ b/QuantConnect.ThetaData/Models/SubscriptionPlans/ValueSubscriptionPlan.cs @@ -0,0 +1,31 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Util; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.SubscriptionPlans; + +public class ValueSubscriptionPlan : ISubscriptionPlan +{ + /// + public HashSet AccessibleResolutions => new() { Resolution.Minute, Resolution.Hour, Resolution.Daily }; + + public DateTime FirstAccessDate => new DateTime(2020, 01, 01); + + public uint MaxStreamingContracts => 0; + + public RateGate? RateGate => null; +} diff --git a/QuantConnect.ThetaData/Models/WebSocket/WebSocketContract.cs b/QuantConnect.ThetaData/Models/WebSocket/WebSocketContract.cs new file mode 100644 index 0000000..c8b7ce4 --- /dev/null +++ b/QuantConnect.ThetaData/Models/WebSocket/WebSocketContract.cs @@ -0,0 +1,49 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using QuantConnect.Lean.DataSource.ThetaData.Models.Enums; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.WebSocket; + +public readonly struct WebSocketContract +{ + [JsonProperty("security_type")] + [JsonConverter(typeof(StringEnumConverter))] + public ContractSecurityType SecurityType { get; } + + [JsonProperty("root")] + public string Root { get; } + + [JsonProperty("expiration")] + public string Expiration { get; } + + [JsonProperty("strike")] + public decimal Strike { get; } + + [JsonProperty("right")] + public string Right { get; } + + [JsonConstructor] + public WebSocketContract(ContractSecurityType securityType, string root, string expiration, decimal strike, string right) + { + SecurityType = securityType; + Root = root; + Expiration = expiration; + Strike = strike; + Right = right; + } +} diff --git a/QuantConnect.ThetaData/Models/WebSocket/WebSocketHeader.cs b/QuantConnect.ThetaData/Models/WebSocket/WebSocketHeader.cs new file mode 100644 index 0000000..b2b86f3 --- /dev/null +++ b/QuantConnect.ThetaData/Models/WebSocket/WebSocketHeader.cs @@ -0,0 +1,37 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using QuantConnect.Lean.DataSource.ThetaData.Models.Enums; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.WebSocket; + +public readonly struct WebSocketHeader +{ + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + public WebSocketHeaderType Type { get; } + + [JsonProperty("status")] + public string Status { get; } + + [JsonConstructor] + public WebSocketHeader(WebSocketHeaderType type, string status) + { + Type = type; + Status = status; + } +} diff --git a/QuantConnect.ThetaData/Models/WebSocket/WebSocketQuote.cs b/QuantConnect.ThetaData/Models/WebSocket/WebSocketQuote.cs new file mode 100644 index 0000000..b865c31 --- /dev/null +++ b/QuantConnect.ThetaData/Models/WebSocket/WebSocketQuote.cs @@ -0,0 +1,78 @@ +/* + * 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.Lean.DataSource.ThetaData.Converters; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.WebSocket; + +public readonly struct WebSocketQuote +{ + [JsonProperty("ms_of_day")] + public int TimeMilliseconds { get; } + + [JsonProperty("bid_size")] + public int BidSize { get; } + + [JsonProperty("bid_exchange")] + public byte BidExchange { get; } + + [JsonProperty("bid")] + public decimal BidPrice { get; } + + [JsonProperty("bid_condition")] + public int BidCondition { get; } + + [JsonProperty("ask_size")] + public int AskSize { get; } + + [JsonProperty("ask_exchange")] + public byte AskExchange { get; } + + [JsonProperty("ask")] + public decimal AskPrice { get; } + + [JsonProperty("ask_condition")] + public int AskCondition { get; } + + [JsonProperty("date")] + [JsonConverter(typeof(DateTimeIntJsonConverter))] + public DateTime Date { get; } + + /// + /// Gets the DateTime representation of the last quote time. DateTime is New York Time (EST) Time Zone! + /// + /// + /// This property calculates the by adding the to the Date property. + /// + public DateTime DateTimeMilliseconds { get => Date.AddMilliseconds(TimeMilliseconds); } + + [JsonConstructor] + public WebSocketQuote( + int timeMilliseconds, + int bidSize, byte bidExchange, decimal bidPrice, int bidCondition, int askSize, byte askExchange, decimal askPrice, int askCondition, DateTime date) + { + TimeMilliseconds = timeMilliseconds; + BidSize = bidSize; + BidExchange = bidExchange; + BidPrice = bidPrice; + BidCondition = bidCondition; + AskSize = askSize; + AskExchange = askExchange; + AskPrice = askPrice; + AskCondition = askCondition; + Date = date; + } +} diff --git a/QuantConnect.ThetaData/Models/WebSocket/WebSocketResponse.cs b/QuantConnect.ThetaData/Models/WebSocket/WebSocketResponse.cs new file mode 100644 index 0000000..8c2ae65 --- /dev/null +++ b/QuantConnect.ThetaData/Models/WebSocket/WebSocketResponse.cs @@ -0,0 +1,44 @@ +/* + * 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; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.WebSocket +{ + + public class WebSocketResponse + { + [JsonProperty("header")] + public WebSocketHeader Header { get; } + + [JsonProperty("contract")] + public WebSocketContract? Contract { get; } + + [JsonProperty("trade")] + public WebSocketTrade? Trade { get; } + + [JsonProperty("quote")] + public WebSocketQuote? Quote { get; } + + public WebSocketResponse(WebSocketHeader header, WebSocketContract? contract, WebSocketTrade? trade, WebSocketQuote? quote) + { + Header = header; + Contract = contract; + Trade = trade; + Quote = quote; + + } + } +} diff --git a/QuantConnect.ThetaData/Models/WebSocket/WebSocketTrade.cs b/QuantConnect.ThetaData/Models/WebSocket/WebSocketTrade.cs new file mode 100644 index 0000000..e1764d5 --- /dev/null +++ b/QuantConnect.ThetaData/Models/WebSocket/WebSocketTrade.cs @@ -0,0 +1,64 @@ +/* + * 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.Lean.DataSource.ThetaData.Converters; + +namespace QuantConnect.Lean.DataSource.ThetaData.Models.WebSocket; + +public readonly struct WebSocketTrade +{ + [JsonProperty("ms_of_day")] + public int TimeMilliseconds { get; } + + [JsonProperty("sequence")] + public int Sequence { get; } + + [JsonProperty("size")] + public int Size { get; } + + [JsonProperty("condition")] + public int Condition { get; } + + [JsonProperty("price")] + public decimal Price { get; } + + [JsonProperty("exchange")] + public byte Exchange { get; } + + [JsonProperty("date")] + [JsonConverter(typeof(DateTimeIntJsonConverter))] + public DateTime Date { get; } + + /// + /// Gets the DateTime representation of the last trade time. DateTime is New York Time (EST) Time Zone! + /// + /// + /// This property calculates the by adding the to the Date property. + /// + public DateTime DateTimeMilliseconds { get => Date.AddMilliseconds(TimeMilliseconds); } + + [JsonConstructor] + public WebSocketTrade(int dayTimeMilliseconds, int sequence, int size, int condition, decimal price, byte exchange, DateTime date) + { + TimeMilliseconds = dayTimeMilliseconds; + Sequence = sequence; + Size = size; + Condition = condition; + Price = price; + Exchange = exchange; + Date = date; + } +} diff --git a/QuantConnect.ThetaData/QuantConnect.DataSource.ThetaData.csproj b/QuantConnect.ThetaData/QuantConnect.DataSource.ThetaData.csproj new file mode 100644 index 0000000..7d83ecd --- /dev/null +++ b/QuantConnect.ThetaData/QuantConnect.DataSource.ThetaData.csproj @@ -0,0 +1,39 @@ + + + Release + AnyCPU + net6.0 + QuantConnect.Lean.DataSource.ThetaData + QuantConnect.Lean.DataSource.ThetaData + QuantConnect.Lean.DataSource.ThetaData + QuantConnect.Lean.DataSource.ThetaData + Library + bin\$(Configuration)\ + false + true + false + QuantConnect LEAN ThetaData Data Source: ThetaData Data Source plugin for Lean + + enable + enable + + + + full + bin\Debug\ + + + + pdbonly + bin\Release\ + + + + + + + + + + + \ No newline at end of file diff --git a/QuantConnect.ThetaData/ThetaDataDownloader.cs b/QuantConnect.ThetaData/ThetaDataDownloader.cs new file mode 100644 index 0000000..f420009 --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataDownloader.cs @@ -0,0 +1,146 @@ +/* + * 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 NodaTime; +using QuantConnect.Data; +using QuantConnect.Util; +using QuantConnect.Securities; +using System.Collections.Concurrent; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + public class ThetaDataDownloader : IDataDownloader, IDisposable + { + /// + /// + /// + private readonly ThetaDataProvider _historyProvider; + + /// + private readonly MarketHoursDatabase _marketHoursDatabase; + + /// + /// Initializes a new instance of the + /// + public ThetaDataDownloader() + { + _historyProvider = new(); + _marketHoursDatabase = MarketHoursDatabase.FromDataFolder(); + } + + public IEnumerable? Get(DataDownloaderGetParameters downloadParameters) + { + var symbol = downloadParameters.Symbol; + + var dataType = LeanData.GetDataType(downloadParameters.Resolution, downloadParameters.TickType); + var exchangeHours = _marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); + var dataTimeZone = _marketHoursDatabase.GetDataTimeZone(symbol.ID.Market, symbol, symbol.SecurityType); + + if (symbol.IsCanonical()) + { + return GetCanonicalOptionHistory( + symbol, + downloadParameters.StartUtc, + downloadParameters.EndUtc, + dataType, + downloadParameters.Resolution, + exchangeHours, + dataTimeZone, + downloadParameters.TickType); + } + else + { + var historyRequest = new HistoryRequest( + startTimeUtc: downloadParameters.StartUtc, + endTimeUtc: downloadParameters.EndUtc, dataType, + symbol: symbol, + resolution: downloadParameters.Resolution, + exchangeHours: exchangeHours, + dataTimeZone: dataTimeZone, + fillForwardResolution: downloadParameters.Resolution, + includeExtendedMarketHours: true, + isCustomData: false, + dataNormalizationMode: DataNormalizationMode.Raw, + tickType: downloadParameters.TickType); + + var historyData = _historyProvider.GetHistory(historyRequest); + + if (historyData == null) + { + return null; + } + + return historyData; + } + } + + private IEnumerable? GetCanonicalOptionHistory(Symbol symbol, DateTime startUtc, DateTime endUtc, Type dataType, + Resolution resolution, SecurityExchangeHours exchangeHours, DateTimeZone dataTimeZone, TickType tickType) + { + var blockingOptionCollection = new BlockingCollection(); + var symbols = GetOptions(symbol, startUtc, endUtc); + + // Symbol can have a lot of Option parameters + Task.Run(() => Parallel.ForEach(symbols, targetSymbol => + { + var historyRequest = new HistoryRequest(startUtc, endUtc, dataType, targetSymbol, resolution, exchangeHours, dataTimeZone, + resolution, true, false, DataNormalizationMode.Raw, tickType); + + var history = _historyProvider.GetHistory(historyRequest); + + // If history is null, it indicates an incorrect or missing request for historical data, + // so we skip processing for this symbol and move to the next one. + if (history == null) + { + return; + } + + foreach (var data in history) + { + blockingOptionCollection.Add(data); + } + })).ContinueWith(_ => + { + blockingOptionCollection.CompleteAdding(); + }); + + var options = blockingOptionCollection.GetConsumingEnumerable(); + + // Validate if the collection contains at least one successful response from history. + if (!options.Any()) + { + return null; + } + + return options; + } + + protected virtual IEnumerable GetOptions(Symbol symbol, DateTime startUtc, DateTime endUtc) + { + foreach (var option in _historyProvider.GetOptionChain(symbol, startUtc, endUtc)) + { + yield return option; + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _historyProvider.DisposeSafely(); + } + } +} diff --git a/QuantConnect.ThetaData/ThetaDataExtensions.cs b/QuantConnect.ThetaData/ThetaDataExtensions.cs new file mode 100644 index 0000000..ab7f9b2 --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataExtensions.cs @@ -0,0 +1,120 @@ +/* + * 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.Globalization; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + public static class ThetaDataExtensions + { + /// + /// Converts a date string from Theta data format (yyyyMMdd) to a DateTime object. + /// + /// The date string in Theta data format (e.g., "20240303" for March 3, 2024). + /// The equivalent DateTime object. + public static DateTime ConvertFromThetaDataDateFormat(this string date) => DateTime.ParseExact(date, DateFormat.EightCharacter, CultureInfo.InvariantCulture); + + /// + /// Converts a DateTime object to Theta data format (yyyyMMdd) to a string. + /// + /// The DateTime object (e.g., new DateTime(2024, 03, 03)) + /// The equivalent Theta Date string date. + public static string ConvertToThetaDataDateFormat(this DateTime date) => date.ToStringInvariant(DateFormat.EightCharacter); + + /// + /// Represents a collection of Exchanges with their corresponding numerical codes. + /// + public static Dictionary Exchanges = new() + { + { 1, "NQEX" }, + { 2, "NQAD" }, + { 3, "NYSE" }, + { 4, "AMEX" }, + { 5, "CBOE" }, + { 6, "ISEX" }, + { 7, "PACF" }, + { 8, "CINC" }, + { 9, "PHIL" }, + { 10, "OPRA" }, + { 11, "BOST" }, + { 12, "NQNM" }, + { 13, "NQSC" }, + { 14, "NQBB" }, + { 15, "NQPK" }, + { 16, "NQIX" }, + { 17, "CHIC" }, + { 18, "TSE" }, + { 19, "CDNX" }, + { 20, "CME" }, + { 21, "NYBT" }, + { 22, "MRCY" }, + { 23, "COMX" }, + { 24, "CBOT" }, + { 25, "NYMX" }, + { 26, "KCBT" }, + { 27, "MGEX" }, + { 28, "NYBO" }, + { 29, "NQBS" }, + { 30, "DOWJ" }, + { 31, "GEMI" }, + { 32, "SIMX" }, + { 33, "FTSE" }, + { 34, "EURX" }, + { 35, "IMPL" }, + { 36, "DTN" }, + { 37, "LMT" }, + { 38, "LME" }, + { 39, "IPEX" }, + { 40, "NQMF" }, + { 41, "FCEC" }, + { 42, "C2" }, + { 43, "MIAX" }, + { 44, "CLRP" }, + { 45, "BARK" }, + { 46, "EMLD" }, + { 47, "NQBX" }, + { 48, "HOTS" }, + { 49, "EUUS" }, + { 50, "EUEU" }, + { 51, "ENCM" }, + { 52, "ENID" }, + { 53, "ENIR" }, + { 54, "CFE" }, + { 55, "PBOT" }, + { 56, "CMEFloor" }, + { 57, "NQNX" }, + { 58, "BTRF" }, + { 59, "NTRF" }, + { 60, "BATS" }, + { 61, "FCBT" }, + { 62, "PINK" }, + { 63, "BATY" }, + { 64, "EDGE" }, + { 65, "EDGX" }, + { 66, "RUSL" }, + { 67, "CMEX" }, + { 68, "IEX" }, + { 69, "PERL" }, + { 70, "LSE" }, + { 71, "GIF" }, + { 72, "TSIX" }, + { 73, "MEMX" }, + { 74, "EMPT" }, + { 75, "LTSE" }, + { 76, "EMPT" }, + { 77, "EMPT" }, + }; + } +} diff --git a/QuantConnect.ThetaData/ThetaDataHistoryProvider.cs b/QuantConnect.ThetaData/ThetaDataHistoryProvider.cs new file mode 100644 index 0000000..6a34911 --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataHistoryProvider.cs @@ -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 NodaTime; +using RestSharp; +using QuantConnect.Data; +using QuantConnect.Util; +using QuantConnect.Logging; +using QuantConnect.Interfaces; +using QuantConnect.Data.Market; +using QuantConnect.Lean.Engine.DataFeeds; +using QuantConnect.Lean.Engine.HistoricalData; +using QuantConnect.Lean.DataSource.ThetaData.Models.Rest; +using QuantConnect.Lean.DataSource.ThetaData.Models.Common; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + /// + /// ThetaData.net implementation of + /// + public partial class ThetaDataProvider : SynchronizingHistoryProvider + { + /// + /// Indicates whether the warning for invalid has been fired. + /// + private volatile bool _invalidSecurityTypeWarningFired; + + /// + /// Indicates whether the warning for invalid has been fired. + /// + private volatile bool _invalidSubscriptionResolutionRequestWarningFired; + + /// + /// Indicates whether the warning indicating that the requested date is greater than the has been triggered. + /// + private volatile bool _invalidStartDateInCurrentSubscriptionWarningFired; + + /// + /// Indicates whether a warning for an invalid start time has been fired, where the start time is greater than or equal to the end time in UTC. + /// + private volatile bool _invalidStartTimeWarningFired; + + /// + /// Indicates whether an warning should be raised when encountering invalid open interest data for an option security type at daily resolution. + /// + /// + /// This flag is set to true when an error is detected for invalid open interest data for options at daily resolution. + /// + private volatile bool _invalidOpenInterestWarningFired; + + /// + public override void Initialize(HistoryProviderInitializeParameters parameters) + { } + + /// + public override IEnumerable? GetHistory(IEnumerable requests, DateTimeZone sliceTimeZone) + { + var subscriptions = new List(); + foreach (var request in requests) + { + var history = GetHistory(request); + + var subscription = CreateSubscription(request, history); + if (!subscription.MoveNext()) + { + continue; + } + + subscriptions.Add(subscription); + } + + if (subscriptions.Count == 0) + { + return null; + } + return CreateSliceEnumerableFromSubscriptions(subscriptions, sliceTimeZone); + } + + public IEnumerable? GetHistory(HistoryRequest historyRequest) + { + if (!_userSubscriptionPlan.AccessibleResolutions.Contains(historyRequest.Resolution)) + { + if (!_invalidSubscriptionResolutionRequestWarningFired) + { + _invalidSubscriptionResolutionRequestWarningFired = true; + Log.Trace($"{nameof(ThetaDataProvider)}.{nameof(GetHistory)}: The current user's subscription plan does not support the requested resolution: {historyRequest.Resolution}"); + } + return null; + } + + if (_userSubscriptionPlan.FirstAccessDate.Date > historyRequest.StartTimeUtc.Date) + { + if (!_invalidStartDateInCurrentSubscriptionWarningFired) + { + _invalidStartDateInCurrentSubscriptionWarningFired = true; + Log.Trace($"{nameof(ThetaDataProvider)}.{nameof(GetHistory)}: The requested start time ({historyRequest.StartTimeUtc.Date}) exceeds the maximum available date ({_userSubscriptionPlan.FirstAccessDate.Date}) allowed by the user's subscription."); + } + } + + if (!CanSubscribe(historyRequest.Symbol)) + { + if (!_invalidSecurityTypeWarningFired) + { + _invalidSecurityTypeWarningFired = true; + Log.Trace($"{nameof(ThetaDataProvider)}.{nameof(GetHistory)}: Unsupported SecurityType '{historyRequest.Symbol.SecurityType}' for symbol '{historyRequest.Symbol}'"); + } + return null; + } + + if (historyRequest.StartTimeUtc >= historyRequest.EndTimeUtc) + { + if (!_invalidStartTimeWarningFired) + { + _invalidStartTimeWarningFired = true; + Log.Error($"{nameof(ThetaDataProvider)}.{nameof(GetHistory)}: Error - The start date in the history request must come before the end date. No historical data will be returned."); + } + return null; + } + + if (historyRequest.Symbol.SecurityType == SecurityType.Option && historyRequest.TickType == TickType.OpenInterest && historyRequest.Resolution != Resolution.Daily) + { + if (!_invalidOpenInterestWarningFired) + { + _invalidOpenInterestWarningFired = true; + Log.Trace($"Invalid data request: TickType 'OpenInterest' only supports Resolution 'Daily' and SecurityType 'Option'. Requested: Resolution '{historyRequest.Resolution}', SecurityType '{historyRequest.Symbol.SecurityType}'."); + } + return null; + } + + var restRequest = new RestRequest(Method.GET); + + var startDate = historyRequest.StartTimeUtc.ConvertFromUtc(TimeZones.EasternStandard).ConvertToThetaDataDateFormat(); + var endDate = historyRequest.EndTimeUtc.ConvertFromUtc(TimeZones.EasternStandard).ConvertToThetaDataDateFormat(); + + restRequest.AddQueryParameter("start_date", startDate); + restRequest.AddQueryParameter("end_date", endDate); + + switch (historyRequest.Symbol.SecurityType) + { + case SecurityType.Option: + return GetOptionHistoryData(restRequest, historyRequest.Symbol, historyRequest.Resolution, historyRequest.TickType); + } + + return null; + } + + public IEnumerable? GetOptionHistoryData(RestRequest optionRequest, Symbol symbol, Resolution resolution, TickType tickType) + { + var ticker = _symbolMapper.GetBrokerageSymbol(symbol).Split(','); + + optionRequest.AddQueryParameter("root", ticker[0]); + optionRequest.AddQueryParameter("exp", ticker[1]); + optionRequest.AddQueryParameter("strike", ticker[2]); + optionRequest.AddQueryParameter("right", ticker[3]); + + if (resolution == Resolution.Daily) + { + switch (tickType) + { + case TickType.Trade: + optionRequest.Resource = "/hist/option/eod"; + var period = resolution.ToTimeSpan(); + return GetOptionEndOfDay(optionRequest, + // If OHLC prices zero, low trading activity, empty result, low volatility. + (eof) => eof.Open == 0 || eof.High == 0 || eof.Low == 0 || eof.Close == 0, + (tradeDateTime, eof) => new TradeBar(tradeDateTime, symbol, eof.Open, eof.High, eof.Low, eof.Close, eof.Volume, period)); + case TickType.Quote: + optionRequest.Resource = "/hist/option/eod"; + return GetOptionEndOfDay(optionRequest, + // If Ask/Bid - prices/sizes zero, low quote activity, empty result, low volatility. + (eof) => eof.AskPrice == 0 || eof.AskSize == 0 || eof.BidPrice == 0 || eof.BidSize == 0, + (quoteDateTime, eof) => new Tick(quoteDateTime, symbol, eof.AskCondition, ThetaDataExtensions.Exchanges[eof.AskExchange], eof.BidSize, eof.BidPrice, eof.AskSize, eof.AskPrice)); + case TickType.OpenInterest: + optionRequest.Resource = "/hist/option/open_interest"; + return GetHistoricalOpenInterestData(optionRequest, symbol); + default: + throw new ArgumentException($"Invalid tick type: {tickType}."); + } + } + else + { + switch (tickType) + { + case TickType.Trade: + optionRequest.Resource = "/hist/option/trade"; + var tickTradeBars = GetHistoricalTickTradeData(optionRequest, symbol); + if (resolution != Resolution.Tick) + { + return LeanData.AggregateTicksToTradeBars(tickTradeBars, symbol, resolution.ToTimeSpan()); + } + return tickTradeBars; + case TickType.Quote: + optionRequest.AddQueryParameter("ivl", GetIntervalsInMilliseconds(resolution)); + optionRequest.Resource = "/hist/option/quote"; + return GetHistoricalQuoteData(optionRequest, symbol); + default: + throw new ArgumentException($"Invalid tick type: {tickType}."); + } + } + } + + private IEnumerable GetHistoricalOpenInterestData(RestRequest request, Symbol symbol) + { + foreach (var openInterests in _restApiClient.ExecuteRequest>(request)) + { + foreach (var openInterest in openInterests.Response) + { + yield return new OpenInterest(openInterest.DateTimeMilliseconds, symbol, openInterest.OpenInterest); + } + } + } + + private IEnumerable GetHistoricalTickTradeData(RestRequest request, Symbol symbol) + { + foreach (var trades in _restApiClient.ExecuteRequest>(request)) + { + foreach (var trade in trades.Response) + { + yield return new Tick(trade.DateTimeMilliseconds, symbol, trade.Condition.ToStringInvariant(), ThetaDataExtensions.Exchanges[trade.Exchange], trade.Size, trade.Price); + } + } + } + + private IEnumerable GetHistoricalQuoteData(RestRequest request, Symbol symbol) + { + foreach (var quotes in _restApiClient.ExecuteRequest>(request)) + { + foreach (var quote in quotes.Response) + { + // If Ask/Bid - prices/sizes zero, low quote activity, empty result, low volatility. + if (quote.AskPrice == 0 || quote.AskSize == 0 || quote.BidPrice == 0 || quote.BidSize == 0) + { + continue; + } + + yield return new Tick(quote.DateTimeMilliseconds, symbol, quote.AskCondition, ThetaDataExtensions.Exchanges[quote.AskExchange], quote.BidSize, quote.BidPrice, quote.AskSize, quote.AskPrice); + } + } + } + + private IEnumerable? GetOptionEndOfDay(RestRequest request, Func validateEmptyResponse, Func res) + { + foreach (var endOfDays in _restApiClient.ExecuteRequest>(request)) + { + foreach (var endOfDay in endOfDays.Response) + { + if (validateEmptyResponse(endOfDay)) + { + continue; + } + yield return res(endOfDay.LastTradeDateTimeMilliseconds, endOfDay); + } + } + } + + /// + /// Returns the interval in milliseconds corresponding to the specified resolution. + /// + /// The for which to retrieve the interval. + /// + /// The interval in milliseconds as a string. + /// For , returns "0". + /// For , returns "1000". + /// For , returns "60000". + /// For , returns "3600000". + /// + /// Thrown when the specified resolution is not supported. + private string GetIntervalsInMilliseconds(Resolution resolution) => resolution switch + { + Resolution.Tick => "0", + Resolution.Second => "1000", + Resolution.Minute => "60000", + Resolution.Hour => "3600000", + _ => throw new NotSupportedException($"The resolution type '{resolution}' is not supported.") + }; + } +} \ No newline at end of file diff --git a/QuantConnect.ThetaData/ThetaDataOptionChainProvider.cs b/QuantConnect.ThetaData/ThetaDataOptionChainProvider.cs new file mode 100644 index 0000000..0824017 --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataOptionChainProvider.cs @@ -0,0 +1,118 @@ +/* + * 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 RestSharp; +using QuantConnect.Logging; +using QuantConnect.Interfaces; +using QuantConnect.Lean.DataSource.ThetaData.Models.Common; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + /// + /// ThetaData.net implementation of + /// + public class ThetaDataOptionChainProvider : IOptionChainProvider + { + /// + /// Provides the TheData mapping between Lean symbols and brokerage specific symbols. + /// + private readonly ThetaDataSymbolMapper _symbolMapper; + + /// + /// Provider The TheData Rest Api client instance. + /// + private readonly ThetaDataRestApiClient _restApiClient; + + /// + /// Collection of pre-defined option rights. + /// Initialized for performance optimization as the API only returns strike price without indicating the right. + /// + private readonly IEnumerable optionRights = new[] { OptionRight.Call, OptionRight.Put }; + + /// + /// Indicates whether the warning for invalid has been fired. + /// + private volatile bool _unsupportedSecurityTypeWarningFired; + + /// + /// Initializes a new instance of the + /// + /// The TheData mapping between Lean symbols and brokerage specific symbols. + /// The client for interacting with the Theta Data REST API by sending HTTP requests + public ThetaDataOptionChainProvider(ThetaDataSymbolMapper symbolMapper, ThetaDataRestApiClient restApiClient) + { + _symbolMapper = symbolMapper; + _restApiClient = restApiClient; + } + + /// + public IEnumerable GetOptionContractList(Symbol symbol, DateTime date) + { + if ((symbol.SecurityType.IsOption() && symbol.SecurityType == SecurityType.FutureOption) || + (symbol.HasUnderlying && symbol.Underlying.SecurityType != SecurityType.Equity && symbol.Underlying.SecurityType != SecurityType.Index)) + { + if (!_unsupportedSecurityTypeWarningFired) + { + _unsupportedSecurityTypeWarningFired = true; + Log.Trace($"{nameof(ThetaDataOptionChainProvider)}.{nameof(GetOptionContractList)}: Unsupported security type {symbol.SecurityType}"); + } + yield break; + } + + var underlying = symbol.SecurityType.IsOption() ? symbol.Underlying : symbol; + var optionsSecurityType = underlying.SecurityType == SecurityType.Index ? SecurityType.IndexOption : SecurityType.Option; + var optionStyle = optionsSecurityType.DefaultOptionStyle(); + + var strikeRequest = new RestRequest("/list/strikes", Method.GET); + strikeRequest.AddQueryParameter("root", underlying.Value); + foreach (var expiryDateStr in GetExpirationDates(underlying.Value)) + { + var expirationDate = expiryDateStr.ConvertFromThetaDataDateFormat(); + + if (expirationDate < date) + { + continue; + } + + strikeRequest.AddOrUpdateParameter("exp", expirationDate.ConvertToThetaDataDateFormat()); + + foreach (var strike in _restApiClient.ExecuteRequest>(strikeRequest).SelectMany(strikes => strikes.Response)) + { + foreach (var right in optionRights) + { + yield return _symbolMapper.GetLeanSymbol(underlying.Value, optionsSecurityType, underlying.ID.Market, optionStyle, + expirationDate, strike, right, underlying); + } + } + } + } + + /// + /// Returns all expirations date for a ticker. + /// + /// The underlying symbol value to list expirations for. + /// An enumerable collection of expiration dates in string format (e.g., "20240303" for March 3, 2024). + public IEnumerable GetExpirationDates(string ticker) + { + var request = new RestRequest("/list/expirations", Method.GET); + request.AddQueryParameter("root", ticker); + + foreach (var expirationDate in _restApiClient.ExecuteRequest>(request).SelectMany(x => x.Response)) + { + yield return expirationDate; + } + } + } +} diff --git a/QuantConnect.ThetaData/ThetaDataProvider.cs b/QuantConnect.ThetaData/ThetaDataProvider.cs new file mode 100644 index 0000000..bf720ea --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataProvider.cs @@ -0,0 +1,376 @@ +/* + * 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 RestSharp; +using System.Net; +using System.Text; +using Newtonsoft.Json; +using QuantConnect.Data; +using QuantConnect.Api; +using QuantConnect.Util; +using QuantConnect.Packets; +using QuantConnect.Logging; +using Newtonsoft.Json.Linq; +using QuantConnect.Interfaces; +using QuantConnect.Data.Market; +using QuantConnect.Configuration; +using System.Security.Cryptography; +using System.Net.NetworkInformation; +using QuantConnect.Lean.DataSource.ThetaData.Models.Enums; +using QuantConnect.Lean.DataSource.ThetaData.Models.WebSocket; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; +using QuantConnect.Lean.DataSource.ThetaData.Models.SubscriptionPlans; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + /// + /// ThetaData.net implementation of + /// + public partial class ThetaDataProvider : IDataQueueHandler + { + /// + /// Aggregates ticks and bars based on given subscriptions. + /// + private readonly IDataAggregator _dataAggregator; + + /// + /// Provides the TheData mapping between Lean symbols and brokerage specific symbols. + /// + private readonly ThetaDataSymbolMapper _symbolMapper; + + /// + /// Helper class is doing to subscribe / unsubscribe process. + /// + private readonly EventBasedDataQueueHandlerSubscriptionManager _subscriptionManager; + + /// + /// Represents an instance of a WebSocket client wrapper for ThetaData.net. + /// + private readonly ThetaDataWebSocketClientWrapper _webSocketClient; + + /// + /// Represents a client for interacting with the Theta Data REST API by sending HTTP requests. + /// + private readonly ThetaDataRestApiClient _restApiClient; + + /// + /// Ensures thread-safe synchronization when updating aggregation tick data, such as quotes or trades. + /// + private object _lock = new object(); + + /// + /// Represents the subscription plan assigned to the user. + /// + private ISubscriptionPlan _userSubscriptionPlan; + + /// + /// The time provider instance. Used for improved testability + /// + protected virtual ITimeProvider TimeProvider { get; } = RealTimeProvider.Instance; + + /// + public bool IsConnected => _webSocketClient.IsOpen; + + /// + /// Initializes a new instance of the + /// + public ThetaDataProvider() + { + _dataAggregator = Composer.Instance.GetPart(); + if (_dataAggregator == null) + { + _dataAggregator = + Composer.Instance.GetExportedValueByTypeName(Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"), forceTypeNameOnExisting: false); + } + + _userSubscriptionPlan = GetUserSubscriptionPlan(); + + _restApiClient = new ThetaDataRestApiClient(_userSubscriptionPlan.RateGate!); + _symbolMapper = new ThetaDataSymbolMapper(); + + _optionChainProvider = new ThetaDataOptionChainProvider(_symbolMapper, _restApiClient); + + _webSocketClient = new ThetaDataWebSocketClientWrapper(_symbolMapper, _userSubscriptionPlan.MaxStreamingContracts, OnMessage); + _subscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager(); + _subscriptionManager.SubscribeImpl += (symbols, _) => _webSocketClient.Subscribe(symbols); + _subscriptionManager.UnsubscribeImpl += (symbols, _) => _webSocketClient.Unsubscribe(symbols); + + ValidateSubscription(); + } + + public void Dispose() + { + _dataAggregator?.DisposeSafely(); + } + + /// + public IEnumerator? Subscribe(SubscriptionDataConfig dataConfig, EventHandler newDataAvailableHandler) + { + if (!CanSubscribe(dataConfig.Symbol) || _userSubscriptionPlan.MaxStreamingContracts == 0) + { + return null; + } + + var enumerator = _dataAggregator.Add(dataConfig, newDataAvailableHandler); + _subscriptionManager.Subscribe(dataConfig); + + return enumerator; + } + + /// + public void Unsubscribe(SubscriptionDataConfig dataConfig) + { + _subscriptionManager.Unsubscribe(dataConfig); + _dataAggregator.Remove(dataConfig); + } + + /// + public void SetJob(LiveNodePacket job) + { + } + + private void OnMessage(string message) + { + var json = JsonConvert.DeserializeObject(message); + + var leanSymbol = default(Symbol); + if (json != null && json.Header.Type != WebSocketHeaderType.Status && json.Contract.HasValue) + { + leanSymbol = _symbolMapper.GetLeanSymbol( + json.Contract.Value.Root, + json.Contract.Value.SecurityType, + json.Contract.Value.Expiration, + json.Contract.Value.Strike, + json.Contract.Value.Right); + } + + switch (json?.Header.Type) + { + case WebSocketHeaderType.Quote when leanSymbol != null && json.Quote != null: + HandleQuoteMessage(leanSymbol, json.Quote.Value); + break; + case WebSocketHeaderType.Trade when leanSymbol != null && json.Trade != null: + HandleTradeMessage(leanSymbol, json.Trade.Value); + break; + case WebSocketHeaderType.Status: + break; + default: + Log.Debug(message); + break; + } + } + + private void HandleQuoteMessage(Symbol symbol, WebSocketQuote webSocketQuote) + { + var tick = new Tick(webSocketQuote.DateTimeMilliseconds, symbol, webSocketQuote.BidCondition.ToStringInvariant(), ThetaDataExtensions.Exchanges[webSocketQuote.BidExchange], + bidSize: webSocketQuote.BidSize, bidPrice: webSocketQuote.BidPrice, + askSize: webSocketQuote.AskSize, askPrice: webSocketQuote.AskPrice); + + lock (_lock) + { + _dataAggregator.Update(tick); + } + } + + private void HandleTradeMessage(Symbol symbol, WebSocketTrade webSocketTrade) + { + var tick = new Tick(webSocketTrade.DateTimeMilliseconds, symbol, webSocketTrade.Condition.ToStringInvariant(), ThetaDataExtensions.Exchanges[webSocketTrade.Exchange], webSocketTrade.Size, webSocketTrade.Price); + lock (_lock) + { + _dataAggregator.Update(tick); + } + } + + /// + /// Checks if this brokerage supports the specified symbol + /// + /// The symbol + /// returns true if brokerage supports the specified symbol; otherwise false + private static bool CanSubscribe(Symbol symbol) + { + return + symbol.Value.IndexOfInvariant("universe", true) == -1 && + !symbol.IsCanonical() && + symbol.SecurityType == SecurityType.Option || symbol.SecurityType == SecurityType.IndexOption; + } + + /// + /// Retrieves the subscription plan associated with the current user. + /// + /// + /// An instance of the interface representing the subscription plan of the user. + /// + private ISubscriptionPlan GetUserSubscriptionPlan() + { + if (!Config.TryGetValue("thetadata-subscription-plan", out var pricePlan) || string.IsNullOrEmpty(pricePlan)) + { + pricePlan = "Free"; + } + + if (!Enum.TryParse(pricePlan, out var parsedPricePlan) || !Enum.IsDefined(typeof(SubscriptionPlanType), parsedPricePlan)) + { + throw new ArgumentException($"An error occurred while parsing the price plan '{pricePlan}'. Please ensure that the provided price plan is valid and supported by the system."); + } + + return parsedPricePlan switch + { + SubscriptionPlanType.Free => new FreeSubscriptionPlan(), + SubscriptionPlanType.Value => new ValueSubscriptionPlan(), + SubscriptionPlanType.Standard => new StandardSubscriptionPlan(), + SubscriptionPlanType.Pro => new ProSubscriptionPlan(), + _ => throw new ArgumentException($"{nameof(ThetaDataProvider)}.{nameof(GetUserSubscriptionPlan)}: Invalid subscription plan type.") + }; + } + + private class ModulesReadLicenseRead : Api.RestResponse + { + [JsonProperty(PropertyName = "license")] + public string License; + + [JsonProperty(PropertyName = "organizationId")] + public string OrganizationId; + } + + /// + /// Validate the user of this project has permission to be using it via our web API. + /// + private static void ValidateSubscription() + { + try + { + const int productId = 344; + var userId = Globals.UserId; + var token = Globals.UserToken; + var organizationId = Globals.OrganizationID; + // Verify we can authenticate with this user and token + var api = new ApiConnection(userId, token); + if (!api.Connected) + { + throw new ArgumentException("Invalid api user id or token, cannot authenticate subscription."); + } + // Compile the information we want to send when validating + var information = new Dictionary() + { + {"productId", productId}, + {"machineName", Environment.MachineName}, + {"userName", Environment.UserName}, + {"domainName", Environment.UserDomainName}, + {"os", Environment.OSVersion} + }; + // IP and Mac Address Information + try + { + var interfaceDictionary = new List>(); + foreach (var nic in NetworkInterface.GetAllNetworkInterfaces().Where(nic => nic.OperationalStatus == OperationalStatus.Up)) + { + var interfaceInformation = new Dictionary(); + // Get UnicastAddresses + var addresses = nic.GetIPProperties().UnicastAddresses + .Select(uniAddress => uniAddress.Address) + .Where(address => !IPAddress.IsLoopback(address)).Select(x => x.ToString()); + // If this interface has non-loopback addresses, we will include it + if (!addresses.IsNullOrEmpty()) + { + interfaceInformation.Add("unicastAddresses", addresses); + // Get MAC address + interfaceInformation.Add("MAC", nic.GetPhysicalAddress().ToString()); + // Add Interface name + interfaceInformation.Add("name", nic.Name); + // Add these to our dictionary + interfaceDictionary.Add(interfaceInformation); + } + } + information.Add("networkInterfaces", interfaceDictionary); + } + catch (Exception) + { + // NOP, not necessary to crash if fails to extract and add this information + } + // Include our OrganizationId if specified + if (!string.IsNullOrEmpty(organizationId)) + { + information.Add("organizationId", organizationId); + } + var request = new RestRequest("modules/license/read", Method.POST) { RequestFormat = DataFormat.Json }; + request.AddParameter("application/json", JsonConvert.SerializeObject(information), ParameterType.RequestBody); + api.TryRequest(request, out ModulesReadLicenseRead result); + if (!result.Success) + { + throw new InvalidOperationException($"Request for subscriptions from web failed, Response Errors : {string.Join(',', result.Errors)}"); + } + + var encryptedData = result.License; + // Decrypt the data we received + DateTime? expirationDate = null; + long? stamp = null; + bool? isValid = null; + if (encryptedData != null) + { + // Fetch the org id from the response if it was not set, we need it to generate our validation key + if (string.IsNullOrEmpty(organizationId)) + { + organizationId = result.OrganizationId; + } + // Create our combination key + var password = $"{token}-{organizationId}"; + var key = SHA256.HashData(Encoding.UTF8.GetBytes(password)); + // Split the data + var info = encryptedData.Split("::"); + var buffer = Convert.FromBase64String(info[0]); + var iv = Convert.FromBase64String(info[1]); + // Decrypt our information + using var aes = new AesManaged(); + var decryptor = aes.CreateDecryptor(key, iv); + using var memoryStream = new MemoryStream(buffer); + using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read); + using var streamReader = new StreamReader(cryptoStream); + var decryptedData = streamReader.ReadToEnd(); + if (!decryptedData.IsNullOrEmpty()) + { + var jsonInfo = JsonConvert.DeserializeObject(decryptedData); + expirationDate = jsonInfo["expiration"]?.Value(); + isValid = jsonInfo["isValid"]?.Value(); + stamp = jsonInfo["stamped"]?.Value(); + } + } + // Validate our conditions + if (!expirationDate.HasValue || !isValid.HasValue || !stamp.HasValue) + { + throw new InvalidOperationException("Failed to validate subscription."); + } + + var nowUtc = DateTime.UtcNow; + var timeSpan = nowUtc - Time.UnixTimeStampToDateTime(stamp.Value); + if (timeSpan > TimeSpan.FromHours(12)) + { + throw new InvalidOperationException("Invalid API response."); + } + if (!isValid.Value) + { + throw new ArgumentException($"Your subscription is not valid, please check your product subscriptions on our website."); + } + if (expirationDate < nowUtc) + { + throw new ArgumentException($"Your subscription expired {expirationDate}, please renew in order to use this product."); + } + } + catch (Exception e) + { + Log.Error($"{nameof(ThetaDataProvider)}.{nameof(ValidateSubscription)}: Failed during validation, shutting down. Error : {e.Message}"); + throw; + } + } + } +} diff --git a/QuantConnect.ThetaData/ThetaDataQueueUniverseProvider.cs b/QuantConnect.ThetaData/ThetaDataQueueUniverseProvider.cs new file mode 100644 index 0000000..6745bdd --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataQueueUniverseProvider.cs @@ -0,0 +1,76 @@ +/* + * 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; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + /// + /// ThetaData.net implementation of + /// + public partial class ThetaDataProvider : IDataQueueUniverseProvider + { + /// + /// Provides the full option chain for a given underlying. + /// + private IOptionChainProvider _optionChainProvider; + + /// + public bool CanPerformSelection() + { + return IsConnected; + } + + /// + public IEnumerable LookupSymbols(Symbol symbol, bool includeExpired, string? securityCurrency = null) + { + var utcNow = TimeProvider.GetUtcNow(); + var symbols = GetOptionChain(symbol, utcNow.Date); + + foreach (var optionSymbol in symbols) + { + yield return optionSymbol; + } + } + + /// + /// Retrieves a collection of option contracts for a given security symbol and requested date. + /// We have returned option contracts from to a future date, excluding expired contracts. + /// + /// The unique security identifier for which option contracts are to be retrieved. + /// The date from which to find option contracts. + /// A collection of option contracts. + public IEnumerable GetOptionChain(Symbol symbol, DateTime requestedDate) => _optionChainProvider.GetOptionContractList(symbol, requestedDate); + + /// + /// Retrieves a collection of option contracts for a given security symbol and requested date. + /// We have returned option contracts from to a + /// + /// The unique security identifier for which option contracts are to be retrieved. + /// The start date from which to find option contracts. + /// The end date from which to find option contracts + /// A collection of option contracts. + public IEnumerable GetOptionChain(Symbol requestedSymbol, DateTime startDate, DateTime endDate) + { + foreach (var symbol in _optionChainProvider.GetOptionContractList(requestedSymbol, startDate)) + { + if (symbol.ID.Date >= startDate && symbol.ID.Date <= endDate) + { + yield return symbol; + } + } + } + } +} diff --git a/QuantConnect.ThetaData/ThetaDataRestApiClient.cs b/QuantConnect.ThetaData/ThetaDataRestApiClient.cs new file mode 100644 index 0000000..c8a2bf6 --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataRestApiClient.cs @@ -0,0 +1,90 @@ +/* + * 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 RestSharp; +using Newtonsoft.Json; +using QuantConnect.Util; +using QuantConnect.Logging; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + /// + /// Represents a client for interacting with the Theta Data REST API by sending HTTP requests. + /// + public class ThetaDataRestApiClient + { + /// + /// Represents the base URL for the REST API. + /// + private const string RestApiBaseUrl = "http://127.0.0.1:25510/v2"; + + /// + /// Represents a client for making RESTFul API requests. + /// + private readonly RestClient _restClient; + + /// + /// Represents a RateGate instance used to control the rate of certain operations. + /// + private readonly RateGate? _rateGate; + + /// + /// Initializes a new instance of the + /// + /// User's ThetaData subscription price plan. + public ThetaDataRestApiClient(RateGate rateGate) + { + _restClient = new RestClient(RestApiBaseUrl); + _rateGate = rateGate; + } + + /// + /// Executes a REST request and deserializes the response content into an object. + /// + /// The type of objects that implement the base response interface. + /// The REST request to execute. + /// An enumerable collection of objects that implement the specified base response interface. + /// Thrown when an error occurs during the execution of the request or when the response is invalid. + public IEnumerable ExecuteRequest(RestRequest? request) where T : IBaseResponse + { + while (request != null) + { + Log.Debug($"{nameof(ThetaDataRestApiClient)}.{nameof(ExecuteRequest)}: URI: {_restClient.BuildUri(request)}"); + + _rateGate?.WaitToProceed(); + + var response = _restClient.Execute(request); + + if (response == null || response.StatusCode == 0 || response.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new Exception($"{nameof(ThetaDataRestApiClient)}.{nameof(ExecuteRequest)}: No response received for request to {request.Resource}. Error message: {response?.ErrorMessage ?? "No error message available."}"); + } + + // docs: https://http-docs.thetadata.us/docs/theta-data-rest-api-v2/3ucp87xxgy8d3-error-codes + if ((int)response.StatusCode == 472) + { + Log.Trace($"{nameof(ThetaDataRestApiClient)}.{nameof(ExecuteRequest)}:NO_DATA There was no data found for the specified request."); + } + + var res = JsonConvert.DeserializeObject(response.Content); + + yield return res; + + request = res?.Header.NextPage == null ? null : new RestRequest(res.Header.NextPage, Method.GET); + }; + } + } +} diff --git a/QuantConnect.ThetaData/ThetaDataSymbolMapper.cs b/QuantConnect.ThetaData/ThetaDataSymbolMapper.cs new file mode 100644 index 0000000..a3a0f62 --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataSymbolMapper.cs @@ -0,0 +1,251 @@ +/* + * 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.Brokerages; +using QuantConnect.Lean.DataSource.ThetaData.Models.Enums; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + /// + /// Index Option Tickers: https://http-docs.thetadata.us/docs/theta-data-rest-api-v2/s1ezbyfni6rw0-index-option-tickers + /// + public class ThetaDataSymbolMapper : ISymbolMapper + { + /// + /// docs: https://http-docs.thetadata.us/docs/theta-data-rest-api-v2/1872cab32381d-the-si-ps#options-opra + /// + private const string MARKET = Market.USA; + + /// + /// Cache mapping data provider ticker strings to symbols. + /// + private Dictionary _dataProviderSymbolCache = new(); + + /// + /// Cache mapping symbols to their corresponding data provider ticker strings. + /// + private Dictionary _leanSymbolCache = new(); + + /// + /// Converts a Lean symbol instance to a brokerage symbol. + /// + /// The Lean symbol instance to be converted. + /// + /// For Equity or Index symbols, returns the actual symbol's ticker. + /// For Option or IndexOption symbols, returns a formatted string: "Ticker,yyyyMMdd,strikePrice,optionRight". + /// Example: For symbol AAPL expiry DateTime(2024, 03, 28), strikePrice = 100m, OptionRight.Call => "AAPL,20240328,100000,C". + /// + /// Thrown when the specified securityType is not supported. + public string GetBrokerageSymbol(Symbol symbol) + { + if (!_leanSymbolCache.TryGetValue(symbol, out var dataProviderTicker)) + { + switch (symbol.SecurityType) + { + case SecurityType.Equity: + case SecurityType.Index: + dataProviderTicker = GetDataProviderTicker(ContractSecurityType.Equity, symbol.Value); + break; + case SecurityType.Option: + case SecurityType.IndexOption: + dataProviderTicker = GetDataProviderTicker( + ContractSecurityType.Option, + symbol.ID.Symbol, + symbol.ID.Date.ConvertToThetaDataDateFormat(), + ConvertStrikePriceToThetaDataFormat(symbol.ID.StrikePrice), + symbol.ID.OptionRight == OptionRight.Call ? "C" : "P"); + break; + default: + throw new NotSupportedException($"{nameof(ThetaDataSymbolMapper)}.{nameof(GetBrokerageSymbol)}: The security type '{symbol.SecurityType}' is not supported by {nameof(ThetaDataProvider)}."); + } + + _dataProviderSymbolCache[dataProviderTicker] = symbol; + _leanSymbolCache[symbol] = dataProviderTicker; + } + + return dataProviderTicker; + } + + /// + /// Constructs a Lean symbol based on the provided parameters. + /// + /// The root symbol for the Lean symbol. + /// The type of contract security (e.g., Option, Equity). + /// The date string formatted according to the data provider's requirements. + /// The strike price for options contracts. + /// The option right for options contracts (Call or Put). + /// + /// A Lean symbol constructed using the provided parameters. + /// + public Symbol GetLeanSymbol(string root, ContractSecurityType contractSecurityType, string dataProviderDate, decimal strike, string right) + { + if (!_dataProviderSymbolCache.TryGetValue( + GetDataProviderTicker(contractSecurityType, root, dataProviderDate, strike.ToStringInvariant(), right), out var symbol)) + { + switch (contractSecurityType) + { + case ContractSecurityType.Option: + return GetLeanSymbol(root, SecurityType.Option, MARKET, dataProviderDate.ConvertFromThetaDataDateFormat(), strike, ConvertContractOptionRightFromThetaDataFormat(right)); + case ContractSecurityType.Equity: + return GetLeanSymbol(root, SecurityType.Equity, MARKET); + default: + throw new NotImplementedException(""); + } + } + return symbol; + } + + /// + /// Constructs a Lean symbol based on the provided parameters. + /// + /// The brokerage symbol representing the security. + /// The type of security (e.g., Equity, Option). + /// The market/exchange where the security is traded. + /// The expiration date for options contracts. Default is DateTime.MinValue. + /// The strike price for options contracts. Default is 0. + /// The option right for options contracts (Call or Put). Default is Call. + /// + /// A Lean symbol constructed using the provided parameters. + /// + public Symbol GetLeanSymbol(string brokerageSymbol, SecurityType securityType, string market, DateTime expirationDate = default, decimal strike = 0, OptionRight optionRight = OptionRight.Call) + { + return GetLeanSymbol(brokerageSymbol, securityType, market, OptionStyle.American, expirationDate, strike, optionRight); + } + + /// + /// Constructs a Lean symbol based on the provided parameters. + /// + /// The ticker symbol formatted according to the data provider's requirements. + /// The type of security (e.g., Equity, Option). + /// The market/exchange where the security is traded. + /// The option style for options contracts (e.g., American, European). + /// The expiration date for options contracts. Default is DateTime.MinValue. + /// The strike price for options contracts. Default is 0. + /// The option right for options contracts (Call or Put). Default is Call. + /// The underlying symbol for options contracts. Default is null. + /// + /// A Lean symbol constructed using the provided parameters. + /// + public Symbol GetLeanSymbol(string dataProviderTicker, SecurityType securityType, string market, OptionStyle optionStyle, + DateTime expirationDate = new DateTime(), decimal strike = 0, OptionRight optionRight = OptionRight.Call, + Symbol? underlying = null) + { + if (string.IsNullOrWhiteSpace(dataProviderTicker)) + { + throw new ArgumentException("Invalid symbol: " + dataProviderTicker); + } + + var underlyingSymbolStr = underlying?.Value ?? dataProviderTicker; + var leanSymbol = default(Symbol); + + if (strike != 0m) + { + strike = ConvertStrikePriceFromThetaDataFormat(strike); + } + + switch (securityType) + { + case SecurityType.Option: + leanSymbol = Symbol.CreateOption(underlyingSymbolStr, market, optionStyle, optionRight, strike, expirationDate); + break; + + case SecurityType.IndexOption: + underlying ??= Symbol.Create(underlyingSymbolStr, SecurityType.Index, market); + leanSymbol = Symbol.CreateOption(underlying, dataProviderTicker, market, optionStyle, optionRight, strike, expirationDate); + break; + + case SecurityType.Equity: + leanSymbol = Symbol.Create(dataProviderTicker, securityType, market); + break; + + case SecurityType.Index: + leanSymbol = Symbol.Create(dataProviderTicker, securityType, market); + break; + + default: + throw new Exception($"{nameof(ThetaDataSymbolMapper)}.{nameof(GetLeanSymbol)}: unsupported security type: {securityType}"); + } + + return leanSymbol; + } + + /// + /// Gets the ticker for the data provider based on the contract security type. + /// + /// The type of contract security (e.g., Option, Equity). + /// The ticker symbol. + /// The expiration date for options contracts. Default is null. + /// The strike price for options contracts. Default is null. + /// The option right for options contracts. Default is null. + /// + /// The ticker string formatted according to the data provider's requirements. + /// For options contracts, the format is "Ticker,ExpirationDate,StrikePrice,OptionRight". + /// For equity contracts, the ticker is returned directly. + /// + /// + /// Thrown when the provided contractSecurityType is not supported. + /// + private string GetDataProviderTicker(ContractSecurityType contractSecurityType, string ticker, string? expirationDate = null, string? strikePrice = null, string? optionRight = null) + { + switch (contractSecurityType) + { + case ContractSecurityType.Option: + return $"{ticker},{expirationDate},{strikePrice},{optionRight}"; + case ContractSecurityType.Equity: + return ticker; + default: + throw new NotSupportedException(); + } + + } + + /// + /// Converts an option right from ThetaData format to the corresponding Lean format. + /// + /// The option right in ThetaData format ("C" for Call, "P" for Put). + /// + /// The corresponding Lean OptionRight enum value. + /// + /// + /// Thrown when the provided contractOptionRight is not a recognized ThetaData option right ("C" for Call or "P" for Put). + /// + private OptionRight ConvertContractOptionRightFromThetaDataFormat(string contractOptionRight) => contractOptionRight switch + { + "C" => OptionRight.Call, + "P" => OptionRight.Put, + _ => throw new ArgumentException($"{nameof(ThetaDataSymbolMapper)}.{nameof(ConvertContractOptionRightFromThetaDataFormat)}:The provided contractOptionRight is not a recognized ThetaData option right. Expected values are 'C' for Call or 'P' for Put.") + }; + + /// + /// Converts an option strike price to ThetaData format, where strike prices are formatted in 10ths of a cent. + /// + /// The option strike price. + /// + /// The strike price in ThetaData format. + /// For example, if the input strike price is 100.00m, the returned value would be "100_000". + /// + private string ConvertStrikePriceToThetaDataFormat(decimal value) => Math.Truncate(value * 1000m).ToStringInvariant(); + + /// + /// Converts an option strike price from ThetaData format to Lean format, where strike prices are formatted in 10ths of a cent. + /// + /// The option strike price in ThetaData format. + /// + /// The strike price in Lean format. + /// For example, if the input strike price is "100000", the returned value would be 100m. + /// + private decimal ConvertStrikePriceFromThetaDataFormat(decimal value) => value / 1000m; + } +} diff --git a/QuantConnect.ThetaData/ThetaDataWebSocketClientWrapper.cs b/QuantConnect.ThetaData/ThetaDataWebSocketClientWrapper.cs new file mode 100644 index 0000000..10d0f1d --- /dev/null +++ b/QuantConnect.ThetaData/ThetaDataWebSocketClientWrapper.cs @@ -0,0 +1,211 @@ +/* + * 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.Logging; +using QuantConnect.Brokerages; +using QuantConnect.Configuration; +using QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces; + +namespace QuantConnect.Lean.DataSource.ThetaData +{ + /// + /// Provides a WebSocket client wrapper for ThetaData.net. + /// + public class ThetaDataWebSocketClientWrapper : WebSocketClientWrapper + { + /// + /// Represents the base URL endpoint for receiving stream messages from Theta Data. + /// A single connection to this endpoint is required for both sending streaming requests and receiving messages. + /// + private static readonly string BaseUrl = Config.Get("thetadata-ws-url", "ws://127.0.0.1:25520/v1/events"); + + /// + /// Represents the array of required subscription channels for receiving real-time market data. + /// Subscribing to these channels allows access to specific types of data streams from the Options Price Reporting Authority (OPRA) feed. + /// + /// + /// Available Channels: + /// - TRADE: This channel provides every trade executed for a specified contract reported on the OPRA feed. + /// - QUOTE: This channel provides every National Best Bid and Offer (NBBO) quote for US Options reported on the OPRA feed for the specified contract. + /// + private static readonly string[] Channels = { "TRADE", "QUOTE" }; + + /// + /// Represents a method that handles messages received from a WebSocket. + /// + private readonly Action _messageHandler; + + /// + /// Provides the ThetaData mapping between Lean symbols and brokerage specific symbols. + /// + private readonly ISymbolMapper _symbolMapper; + + /// + /// The maximum number of contracts that can be streamed simultaneously under the subscription plan. + /// + /// + private readonly uint _maxStreamingContracts; + + /// + /// Ensures thread-safe synchronization when updating + /// + private readonly object _lock = new(); + + /// + /// Represents the current amount of subscribed symbols. + /// + private volatile uint _subscribedSymbolCount; + + /// + /// Represents a way of tracking streaming requests made. + /// The field should be increased for each new stream request made. + /// + private int _idRequestCount = 0; + + /// + /// Initializes a new instance of the + /// + /// Provides the mapping between Lean symbols and brokerage specific symbols. + /// The maximum number of contracts that can be streamed simultaneously under the subscription plan. + /// The method that handles messages received from the WebSocket client. + public ThetaDataWebSocketClientWrapper(ISymbolMapper symbolMapper, uint maxStreamingContracts, Action messageHandler) + { + Initialize(BaseUrl); + + _symbolMapper = symbolMapper; + _messageHandler = messageHandler; + _maxStreamingContracts = maxStreamingContracts; + + Closed += OnClosed; + Message += OnMessage; + } + + /// + /// Adds the specified symbols to the subscription + /// + /// The symbols to be added keyed by SecurityType + public bool Subscribe(IEnumerable symbols) + { + if (!IsOpen) + { + Connect(); + } + + foreach (var symbol in symbols) + { + lock (_lock) + { + // constantly following of current amount of subscribed symbols (post increment!) + if (++_subscribedSymbolCount > _maxStreamingContracts) + { + throw new ArgumentException($"{nameof(ThetaDataWebSocketClientWrapper)}.{nameof(Subscribe)}: Subscription Limit Exceeded. The number of symbols you're trying to subscribe to exceeds the maximum allowed limit of {_maxStreamingContracts}. Please adjust your subscription quantity or upgrade your plan accordingly. Current subscription count: {_subscribedSymbolCount}"); + } + } + + foreach (var jsonMessage in GetContractSubscriptionMessage(true, symbol)) + { + Send(jsonMessage); + Interlocked.Increment(ref _idRequestCount); + } + } + + return true; + } + + private IEnumerable GetContractSubscriptionMessage(bool isSubscribe, Symbol symbol) + { + var brokerageSymbol = _symbolMapper.GetBrokerageSymbol(symbol).Split(','); + foreach (var channel in Channels) + { + yield return GetMessage(isSubscribe, channel, brokerageSymbol[0], brokerageSymbol[1], brokerageSymbol[2], brokerageSymbol[3]); + } + } + + /// + /// Removes the specified symbols to the subscription + /// + /// The symbols to be removed keyed by SecurityType + public bool Unsubscribe(IEnumerable symbols) + { + foreach (var symbol in symbols) + { + lock (_lock) + { + _subscribedSymbolCount--; + } + + foreach (var jsonMessage in GetContractSubscriptionMessage(false, symbol)) + { + Send(jsonMessage); + Interlocked.Increment(ref _idRequestCount); + } + } + return true; + } + + /// + /// Constructs a message for subscribing or unsubscribing to a financial instrument on a specified channel. + /// + /// A boolean value indicating whether to subscribe (true) or unsubscribe (false). + /// The name of the channel to subscribe or unsubscribe from. + /// The ticker symbol of the financial instrument. + /// The expiration date of the option contract. + /// The strike price of the option contract. + /// The option type, either "C" for call or "P" for put. + /// A json string representing the constructed message. + private string GetMessage(bool isSubscribe, string channelName, string ticker, string expirationDate, string strikePrice, string optionRight) + { + return JsonConvert.SerializeObject(new + { + msg_type = "STREAM", + sec_type = "OPTION", + req_type = channelName, + add = isSubscribe, + id = _idRequestCount, + contract = new + { + root = ticker, + expiration = expirationDate, + strike = strikePrice, + right = optionRight + } + }); + } + + + /// + /// Event handler for processing WebSocket messages. + /// + /// The object that raised the event. + /// The WebSocket message received. + private void OnMessage(object? sender, WebSocketMessage webSocketMessage) + { + var e = (TextMessage)webSocketMessage.Data; + + _messageHandler?.Invoke(e.Message); + } + + /// + /// Event handler for processing WebSocket close data. + /// + /// The object that raised the event. + /// The WebSocket Close Data received. + private void OnClosed(object? sender, WebSocketCloseData webSocketCloseData) + { + Log.Trace($"{nameof(ThetaDataWebSocketClientWrapper)}.{nameof(OnClosed)}: {webSocketCloseData.Reason}"); + } + } +} diff --git a/thetadata.json b/thetadata.json new file mode 100644 index 0000000..a09d6f9 --- /dev/null +++ b/thetadata.json @@ -0,0 +1,15 @@ +{ + "description": "ThetaData provides a high-performance desktop interface to dozens of market data feeds. The QuantConnect integration enables research, backtesting, optimization, and live trading across multiple asset classes. ThetaData utilizes a specialized terminal built on the Java SDK.", + "platforms-features": [ + { + "Platform Support": ["Cloud Platform", "Local Platform", "LEAN CLI"], + "Download Data": [0, 1, 1], + "Backtesting": [0, 1, 1], + "Optimization": [0, 1, 1], + "Live Trading": [0, 1, 1] + } + ], + "data-supported": ["Equity", "Equity Options", "Index", "Index Options"], + "documentation": "/docs/v2/lean-cli/datasets/thetadata", + "more-information": "https://www.thetadata.net/" +}