├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── dotnet-core-branches.yml │ └── dotnet-core.yml ├── .gitignore ├── Crypto.Websocket.Extensions.sln ├── Crypto.Websocket.Extensions.sln.DotSettings ├── Directory.Build.props ├── LICENSE ├── README.md ├── bitcoin.png ├── cwe_logo.png ├── src ├── Crypto.Websocket.Extensions.Core │ ├── Crypto.Websocket.Extensions.Core.csproj │ ├── Exceptions │ │ ├── CryptoBadInputException.cs │ │ └── CryptoException.cs │ ├── Models │ │ ├── CryptoChangeInfo.cs │ │ ├── CryptoOrderSide.cs │ │ ├── CryptoPositionSide.cs │ │ ├── CryptoQuotes.cs │ │ ├── CryptoTradeSide.cs │ │ ├── ICryptoChangeInfo.cs │ │ └── ICryptoQuotes.cs │ ├── OrderBooks │ │ ├── CryptoOrderBook.cs │ │ ├── CryptoOrderBookBase.cs │ │ ├── CryptoOrderBookL2.cs │ │ ├── CryptoOrderBookType.cs │ │ ├── CryptoQuote.cs │ │ ├── EnumerableExtensions.cs │ │ ├── ICryptoOrderBook.cs │ │ ├── L2Snapshot.cs │ │ ├── Models │ │ │ ├── IOrderBookChangeInfo.cs │ │ │ ├── ITopNLevelsChangeInfo.cs │ │ │ ├── OrderBookAction.cs │ │ │ ├── OrderBookChangeInfo.cs │ │ │ ├── OrderBookLevel.cs │ │ │ ├── OrderBookLevelBulk.cs │ │ │ ├── OrderBookLevelOrdering.cs │ │ │ ├── OrderBookLevelsById.cs │ │ │ └── TopNLevelsChangeInfo.cs │ │ └── Sources │ │ │ ├── IOrderBookSource.cs │ │ │ └── OrderBookSourceBase.cs │ ├── Orders │ │ ├── CryptoOrderCollection.cs │ │ ├── CryptoOrderCollectionReadonly.cs │ │ ├── CryptoOrders.cs │ │ ├── ICryptoOrders.cs │ │ ├── Models │ │ │ ├── CryptoOrder.cs │ │ │ ├── CryptoOrderStatus.cs │ │ │ └── CryptoOrderType.cs │ │ └── Sources │ │ │ ├── IOrderSource.cs │ │ │ └── OrderSourceBase.cs │ ├── Positions │ │ ├── Models │ │ │ └── CryptoPosition.cs │ │ └── Sources │ │ │ ├── IPositionSource.cs │ │ │ └── PositionSourceBase.cs │ ├── Threading │ │ └── CryptoAsyncLock.cs │ ├── Trades │ │ ├── Models │ │ │ └── CryptoTrade.cs │ │ └── Sources │ │ │ ├── ITradeSource.cs │ │ │ └── TradeSourceBase.cs │ ├── Utils │ │ ├── CryptoDateUtils.cs │ │ ├── CryptoMathUtils.cs │ │ └── CryptoPairsHelper.cs │ ├── Validations │ │ └── CryptoValidations.cs │ ├── Wallets │ │ ├── Models │ │ │ └── CryptoWallet.cs │ │ └── Sources │ │ │ ├── IWalletSource.cs │ │ │ └── WalletSourceBase.cs │ └── icon.png └── Crypto.Websocket.Extensions │ ├── Crypto.Websocket.Extensions.csproj │ ├── OrderBooks │ ├── Sources │ │ ├── BinanceOrderBookSource.cs │ │ ├── BitfinexOrderBookSource.cs │ │ ├── BitmexOrderBookSource.cs │ │ ├── BitstampOrderBookSource.cs │ │ ├── BybitOrderBookSource.cs │ │ ├── CoinbaseOrderBookSource.cs │ │ ├── HuobiOrderBookSource.cs │ │ └── ValrOrderBookSource.cs │ └── SourcesL3 │ │ ├── BitfinexOrderBookL3Source.cs │ │ └── LunoOrderBookL3Source.cs │ ├── Orders │ └── Sources │ │ ├── BinanceOrderSource.cs │ │ └── BitmexOrderSource.cs │ ├── Positions │ └── Sources │ │ └── BitmexPositionSource.cs │ ├── Trades │ └── Sources │ │ ├── BinanceTradeSource.cs │ │ ├── BitfinexTradeSource.cs │ │ ├── BitmexTradeSource.cs │ │ ├── BitstampTradeSource.cs │ │ ├── CoinbaseTradeSource.cs │ │ └── HuobiTradeSource.cs │ ├── Wallets │ └── Sources │ │ └── BitmexWalletSource.cs │ └── icon.png ├── test └── Crypto.Websocket.Extensions.Tests │ ├── Crypto.Websocket.Extensions.Tests.csproj │ ├── CryptoDateTests.cs │ ├── CryptoMathTests.cs │ ├── CryptoOrderBookL2PerformanceTests.cs │ ├── CryptoOrderBookL2Tests.cs │ ├── CryptoOrderBookL3PerformanceTests.cs │ ├── CryptoOrderBookL3Tests.cs │ ├── CryptoOrderBookPerformanceTests.cs │ ├── CryptoOrderBookTests.cs │ ├── CryptoOrdersTests.cs │ ├── Helpers │ ├── OrderBookSourceMock.cs │ ├── OrderBookTestUtils.cs │ └── OrderSourceMock.cs │ ├── NonParallelCollectionDefinitionClass.cs │ ├── OrderBookLevel2SourceTests.cs │ └── data │ ├── RawFileCommunicator.cs │ └── bitmex_raw_xbtusd_2018-11-13.txt.gz └── test_integration ├── Crypto.Websocket.Extensions.Sample ├── Crypto.Websocket.Extensions.Sample.csproj ├── OrderBookExample.cs ├── OrderBookL3Example.cs ├── OrdersExample.cs ├── Program.cs └── TradesExample.cs └── Crypto.Websocket.Extensions.Tests.Integration ├── BinanceOrderBookSourceTests.cs ├── BitfinexOrderBookSourceTests.cs ├── BitmexOrderBookSourceTests.cs ├── CoinbaseOrderBookSourceTests.cs ├── Crypto.Websocket.Extensions.Tests.Integration.csproj ├── Luno-XBTZAR-Websocket-Log.txt └── LunoOrderBookSourceTests.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core-branches.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core (branch) 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 9.x 18 | - name: Install dependencies 19 | run: dotnet restore 20 | - name: Build 21 | run: dotnet build --configuration Release --no-restore 22 | - name: Test 23 | run: dotnet test --no-restore --verbosity normal 24 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 9.x 18 | 19 | - name: Install dependencies 20 | run: dotnet restore 21 | - name: Build 22 | run: dotnet build --configuration Release --no-restore 23 | - name: Test 24 | run: dotnet test --no-restore --logger "trx;LogFileName=tests.trx" 25 | - name: Test Report 26 | uses: dorny/test-reporter@v1 27 | if: success() || failure() # run this step even if previous step failed 28 | with: 29 | name: tests 30 | path: '**/*.trx' 31 | reporter: dotnet-trx 32 | - name: Pack library project (core) 33 | run: dotnet pack src/Crypto.Websocket.Extensions.Core/Crypto.Websocket.Extensions.Core.csproj --no-build --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg -o . 34 | - name: Pack library project 35 | run: dotnet pack src/Crypto.Websocket.Extensions/Crypto.Websocket.Extensions.csproj --no-build --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg -o . 36 | - name: Publish library (NuGet) 37 | run: dotnet nuget push *.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source "https://api.nuget.org/v3/index.json" --skip-duplicate 38 | #- name: Publish library (Github) 39 | # run: dotnet nuget push *.nupkg --api-key ${{secrets.PUBLISH_TO_GITHUB_TOKEN}} --source "https://nuget.pkg.github.com/marfusios/index.json" --skip-duplicate 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | .idea/ 5 | .DS_Store 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | 113 | # MightyMoose 114 | *.mm.* 115 | AutoTest.Net/ 116 | 117 | # Web workbench (sass) 118 | .sass-cache/ 119 | 120 | # Installshield output folder 121 | [Ee]xpress/ 122 | 123 | # DocProject is a documentation generator add-in 124 | DocProject/buildhelp/ 125 | DocProject/Help/*.HxT 126 | DocProject/Help/*.HxC 127 | DocProject/Help/*.hhc 128 | DocProject/Help/*.hhk 129 | DocProject/Help/*.hhp 130 | DocProject/Help/Html2 131 | DocProject/Help/html 132 | 133 | # Click-Once directory 134 | publish/ 135 | 136 | # Publish Web Output 137 | *.[Pp]ublish.xml 138 | *.azurePubxml 139 | ## TODO: Comment the next line if you want to checkin your 140 | ## web deploy settings but do note that will include unencrypted 141 | ## passwords 142 | #*.pubxml 143 | 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Windows Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Windows Store app package directory 160 | AppPackages/ 161 | 162 | # Visual Studio cache files 163 | # files ending in .cache can be ignored 164 | *.[Cc]ache 165 | # but keep track of directories ending in .cache 166 | !*.[Cc]ache/ 167 | 168 | # Others 169 | ClientBin/ 170 | [Ss]tyle[Cc]op.* 171 | ~$* 172 | *~ 173 | *.dbmdl 174 | *.dbproj.schemaview 175 | *.publishsettings 176 | node_modules/ 177 | orleans.codegen.cs 178 | 179 | # RIA/Silverlight projects 180 | Generated_Code/ 181 | 182 | # Backup & report files from converting an old project file 183 | # to a newer Visual Studio version. Backup files are not needed, 184 | # because we have git ;-) 185 | _UpgradeReport_Files/ 186 | Backup*/ 187 | UpgradeLog*.XML 188 | UpgradeLog*.htm 189 | 190 | # SQL Server files 191 | *.mdf 192 | *.ldf 193 | 194 | # Business Intelligence projects 195 | *.rdl.data 196 | *.bim.layout 197 | *.bim_*.settings 198 | 199 | # Microsoft Fakes 200 | FakesAssemblies/ 201 | 202 | # Node.js Tools for Visual Studio 203 | .ntvs_analysis.dat 204 | 205 | # Visual Studio 6 build log 206 | *.plg 207 | 208 | # Visual Studio 6 workspace options file 209 | *.opt 210 | 211 | # LightSwitch generated files 212 | GeneratedArtifacts/ 213 | _Pvt_Extensions/ 214 | ModelManifest.xml 215 | -------------------------------------------------------------------------------- /Crypto.Websocket.Extensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34601.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{360B18BD-653A-4824-9261-C88167EEBAD2}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0544BC01-B569-4729-86B0-5AFDBF7EED46}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test_integration", "test_integration", "{42E4078C-4FBE-4B03-B889-1573332945A9}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B771F515-0677-409B-BEC1-B84BC88E7392}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | Directory.Build.props = Directory.Build.props 16 | .github\workflows\dotnet-core-branches.yml = .github\workflows\dotnet-core-branches.yml 17 | .github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml 18 | LICENSE = LICENSE 19 | README.md = README.md 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crypto.Websocket.Extensions", "src\Crypto.Websocket.Extensions\Crypto.Websocket.Extensions.csproj", "{17A9A7BA-8387-4689-A581-A9C2CAC5B3E2}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crypto.Websocket.Extensions.Tests", "test\Crypto.Websocket.Extensions.Tests\Crypto.Websocket.Extensions.Tests.csproj", "{17E08CC0-D8ED-4DDD-88E1-9DADB5C8AA06}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crypto.Websocket.Extensions.Sample", "test_integration\Crypto.Websocket.Extensions.Sample\Crypto.Websocket.Extensions.Sample.csproj", "{081FD24C-6264-41D5-8444-C47C7331CCDE}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crypto.Websocket.Extensions.Tests.Integration", "test_integration\Crypto.Websocket.Extensions.Tests.Integration\Crypto.Websocket.Extensions.Tests.Integration.csproj", "{2AFCA5EA-E46D-40FF-A79A-310B60E23632}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crypto.Websocket.Extensions.Core", "src\Crypto.Websocket.Extensions.Core\Crypto.Websocket.Extensions.Core.csproj", "{BDE39220-9DBC-4799-A5DC-89D631A8F4A0}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {17A9A7BA-8387-4689-A581-A9C2CAC5B3E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {17A9A7BA-8387-4689-A581-A9C2CAC5B3E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {17A9A7BA-8387-4689-A581-A9C2CAC5B3E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {17A9A7BA-8387-4689-A581-A9C2CAC5B3E2}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {17E08CC0-D8ED-4DDD-88E1-9DADB5C8AA06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {17E08CC0-D8ED-4DDD-88E1-9DADB5C8AA06}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {17E08CC0-D8ED-4DDD-88E1-9DADB5C8AA06}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {17E08CC0-D8ED-4DDD-88E1-9DADB5C8AA06}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {081FD24C-6264-41D5-8444-C47C7331CCDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {081FD24C-6264-41D5-8444-C47C7331CCDE}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {081FD24C-6264-41D5-8444-C47C7331CCDE}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {081FD24C-6264-41D5-8444-C47C7331CCDE}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {2AFCA5EA-E46D-40FF-A79A-310B60E23632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {2AFCA5EA-E46D-40FF-A79A-310B60E23632}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {2AFCA5EA-E46D-40FF-A79A-310B60E23632}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {2AFCA5EA-E46D-40FF-A79A-310B60E23632}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {BDE39220-9DBC-4799-A5DC-89D631A8F4A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {BDE39220-9DBC-4799-A5DC-89D631A8F4A0}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {BDE39220-9DBC-4799-A5DC-89D631A8F4A0}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {BDE39220-9DBC-4799-A5DC-89D631A8F4A0}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {17A9A7BA-8387-4689-A581-A9C2CAC5B3E2} = {360B18BD-653A-4824-9261-C88167EEBAD2} 64 | {17E08CC0-D8ED-4DDD-88E1-9DADB5C8AA06} = {0544BC01-B569-4729-86B0-5AFDBF7EED46} 65 | {081FD24C-6264-41D5-8444-C47C7331CCDE} = {42E4078C-4FBE-4B03-B889-1573332945A9} 66 | {2AFCA5EA-E46D-40FF-A79A-310B60E23632} = {42E4078C-4FBE-4B03-B889-1573332945A9} 67 | {BDE39220-9DBC-4799-A5DC-89D631A8F4A0} = {360B18BD-653A-4824-9261-C88167EEBAD2} 68 | EndGlobalSection 69 | GlobalSection(ExtensibilityGlobals) = postSolution 70 | SolutionGuid = {EE3E0B94-CF20-4C22-B50B-E720E8DFD675} 71 | EndGlobalSection 72 | EndGlobal 73 | -------------------------------------------------------------------------------- /Crypto.Websocket.Extensions.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> 3 | <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | True 14 | True 15 | True 16 | True 17 | True 18 | True 19 | True 20 | True 21 | True 22 | True 23 | True 24 | True 25 | True 26 | True 27 | True 28 | True 29 | True 30 | True 31 | True 32 | True 33 | True 34 | True 35 | True 36 | True 37 | True 38 | True 39 | True -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | latest 7 | 2.10.0 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /bitcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/crypto-websocket-extensions/a039ba974fcfaf9887c696c4da59910d0a820868/bitcoin.png -------------------------------------------------------------------------------- /cwe_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/crypto-websocket-extensions/a039ba974fcfaf9887c696c4da59910d0a820868/cwe_logo.png -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Crypto.Websocket.Extensions.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1;net6;net7;net8;net9 5 | Crypto.Websocket.Extensions.Core 6 | Mariusz Kotas 7 | Extensions to cryptocurrency websocket clients (core library, only interfaces and feature classes) 8 | false 9 | Enhancements 10 | Copyright 2024 Mariusz Kotas. All rights reserved. 11 | Crypto websockets websocket client cryptocurrency exchange bitcoin extensions 12 | MIT 13 | https://github.com/Marfusios/crypto-websocket-extensions 14 | https://raw.githubusercontent.com/Marfusios/crypto-websocket-extensions/master/bitcoin.png 15 | icon.png 16 | https://github.com/Marfusios/crypto-websocket-extensions 17 | README.md 18 | Git 19 | true 20 | true 21 | 22 | true 23 | snupkg 24 | enable 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Exceptions/CryptoBadInputException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Exceptions 4 | { 5 | /// 6 | /// Exception to cover bad user input 7 | /// 8 | public class CryptoBadInputException : CryptoException 9 | { 10 | /// 11 | public CryptoBadInputException() 12 | { 13 | } 14 | 15 | /// 16 | public CryptoBadInputException(string message) : base(message) 17 | { 18 | } 19 | 20 | /// 21 | public CryptoBadInputException(string message, Exception innerException) : base(message, innerException) 22 | { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Exceptions/CryptoException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Exceptions 4 | { 5 | /// 6 | /// Base exception for this library 7 | /// 8 | public class CryptoException : Exception 9 | { 10 | /// 11 | public CryptoException() 12 | { 13 | } 14 | 15 | /// 16 | public CryptoException(string message) 17 | : base(message) 18 | { 19 | } 20 | 21 | /// 22 | public CryptoException(string message, Exception innerException) : base(message, innerException) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Models/CryptoChangeInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Models 4 | { 5 | /// 6 | /// Base class for every change/update info 7 | /// 8 | public class CryptoChangeInfo : ICryptoChangeInfo 9 | { 10 | /// 11 | /// Origin exchange name 12 | /// 13 | public string? ExchangeName { get; set; } 14 | 15 | /// 16 | /// Server timestamp when available (only few exchanges support it) 17 | /// 18 | public DateTime? ServerTimestamp { get; set; } 19 | 20 | /// 21 | /// Server message unique sequence when available (only few exchanges support it) 22 | /// 23 | public long? ServerSequence { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Models/CryptoOrderSide.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.Models 2 | { 3 | /// 4 | /// Order side - bid or ask 5 | /// 6 | public enum CryptoOrderSide 7 | { 8 | /// 9 | /// Unknown side 10 | /// 11 | Undefined = 0, 12 | 13 | /// 14 | /// Buy side 15 | /// 16 | Bid = 1, 17 | 18 | /// 19 | /// Sell side 20 | /// 21 | Ask = 2 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Models/CryptoPositionSide.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.Models 2 | { 3 | /// 4 | /// Position side - long or short 5 | /// 6 | public enum CryptoPositionSide 7 | { 8 | /// 9 | /// Unknown side 10 | /// 11 | Undefined = 0, 12 | 13 | /// 14 | /// Long (buy) side 15 | /// 16 | Long = 1, 17 | 18 | /// 19 | /// Short (sell) side 20 | /// 21 | Short = 2 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Models/CryptoQuotes.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Models 4 | { 5 | /// 6 | /// Price quotes 7 | /// 8 | [DebuggerDisplay("CryptoQuotes bid: {Bid}/{BidAmount}, ask: {Ask}/{AskAmount}")] 9 | public class CryptoQuotes : ICryptoQuotes 10 | { 11 | /// 12 | /// Price quotes 13 | /// 14 | public CryptoQuotes(double bid, double ask, double bidAmount, double askAmount) 15 | { 16 | Bid = bid; 17 | Ask = ask; 18 | BidAmount = bidAmount; 19 | AskAmount = askAmount; 20 | Mid = (bid + ask) / 2; 21 | } 22 | 23 | /// 24 | /// Top level bid price 25 | /// 26 | public double Bid { get; protected set; } 27 | 28 | /// 29 | /// Top level ask price 30 | /// 31 | public double Ask { get; protected set; } 32 | 33 | /// 34 | /// Current mid price 35 | /// 36 | public double Mid { get; protected set; } 37 | 38 | /// 39 | /// Top level bid amount 40 | /// 41 | public double BidAmount { get; protected set; } 42 | 43 | /// 44 | /// Top level ask amount 45 | /// 46 | public double AskAmount { get; protected set; } 47 | 48 | /// 49 | /// Returns true if quotes are in valid state 50 | /// 51 | public bool IsValid() 52 | { 53 | var isPriceValid = Bid <= Ask; 54 | return isPriceValid; 55 | } 56 | 57 | /// 58 | /// Format quotes to readable form 59 | /// 60 | public override string ToString() 61 | { 62 | return $"bid: {Bid}/{BidAmount}, ask: {Ask}/{AskAmount}"; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Models/CryptoTradeSide.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.Models 2 | { 3 | /// 4 | /// Trade side - buy or sell 5 | /// 6 | public enum CryptoTradeSide 7 | { 8 | /// 9 | /// Unknown side 10 | /// 11 | Undefined = 0, 12 | 13 | /// 14 | /// Buy side 15 | /// 16 | Buy = 1, 17 | 18 | /// 19 | /// Sell side 20 | /// 21 | Sell = 2 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Models/ICryptoChangeInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Models 4 | { 5 | /// 6 | /// Generic interface for every change/update info 7 | /// 8 | public interface ICryptoChangeInfo 9 | { 10 | /// 11 | /// Origin exchange name 12 | /// 13 | string? ExchangeName { get; } 14 | 15 | /// 16 | /// Server timestamp when available (only few exchanges support it) 17 | /// 18 | DateTime? ServerTimestamp { get; } 19 | 20 | /// 21 | /// Server message unique sequence when available (only few exchanges support it) 22 | /// 23 | long? ServerSequence { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Models/ICryptoQuotes.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.Models 2 | { 3 | /// 4 | /// Price quotes 5 | /// 6 | public interface ICryptoQuotes 7 | { 8 | /// 9 | /// Top level bid price 10 | /// 11 | double Bid { get; } 12 | 13 | /// 14 | /// Top level ask price 15 | /// 16 | double Ask { get; } 17 | 18 | /// 19 | /// Current mid price 20 | /// 21 | double Mid { get; } 22 | 23 | /// 24 | /// Top level bid amount 25 | /// 26 | double BidAmount { get; } 27 | 28 | /// 29 | /// Top level ask amount 30 | /// 31 | double AskAmount { get; } 32 | 33 | /// 34 | /// Returns true if quotes are in valid state 35 | /// 36 | bool IsValid(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/CryptoOrderBookType.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.OrderBooks 2 | { 3 | /// 4 | /// Order book type 5 | /// 6 | public enum CryptoOrderBookType 7 | { 8 | /// 9 | /// Unspecified 10 | /// 11 | Undefined, 12 | 13 | /// 14 | /// Only best bid/ask, quotes 15 | /// 16 | L1, 17 | 18 | /// 19 | /// Grouped by price 20 | /// 21 | L2, 22 | 23 | /// 24 | /// Raw, every single order 25 | /// 26 | L3, 27 | 28 | /// 29 | /// Everything 30 | /// 31 | All 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/CryptoQuote.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.OrderBooks 4 | { 5 | /// 6 | /// A price and amount. 7 | /// 8 | [DebuggerDisplay("Price: {Price}, Amount: {Amount}")] 9 | public class CryptoQuote 10 | { 11 | /// 12 | /// Creates a new quote. 13 | /// 14 | /// The price. 15 | /// The amount. 16 | public CryptoQuote(double price, double amount) 17 | { 18 | Price = price; 19 | Amount = amount; 20 | } 21 | 22 | /// 23 | /// The price. 24 | /// 25 | public double Price { get; set; } 26 | 27 | /// 28 | /// The amount. 29 | /// 30 | public double Amount { get; set; } 31 | 32 | internal bool IsValid => Price != 0 && Amount != 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.OrderBooks 5 | { 6 | internal static class EnumerableExtensions 7 | { 8 | public static List ToList(this IEnumerable items, int length) 9 | { 10 | var list = new List(length); 11 | list.AddRange(items.Take(length)); 12 | return list; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/L2Snapshot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using Crypto.Websocket.Extensions.Core.Models; 4 | 5 | namespace Crypto.Websocket.Extensions.Core.OrderBooks 6 | { 7 | /// 8 | /// A snapshot of bid and ask levels. 9 | /// 10 | [DebuggerDisplay("Bid: {Bid}/{BidAmount}, Ask: {Ask}/{AskAmount}, Bids: {Bids.Count}, Asks: {Asks.Count}")] 11 | public class L2Snapshot : CryptoQuotes 12 | { 13 | /// 14 | /// Creates a new snapshot. 15 | /// 16 | /// The source. 17 | /// The bids. 18 | /// The asks. 19 | public L2Snapshot(ICryptoOrderBook cryptoOrderBook, IReadOnlyList bids, IReadOnlyList asks) 20 | : base(cryptoOrderBook.BidPrice, cryptoOrderBook.AskPrice, cryptoOrderBook.BidAmount, cryptoOrderBook.AskAmount) 21 | { 22 | Bids = bids; 23 | Asks = asks; 24 | } 25 | 26 | /// 27 | /// Updates the snapshot. 28 | /// 29 | /// The bid price. 30 | /// The ask price. 31 | /// The bid amount. 32 | /// The ask amount. 33 | internal void Update(double bid, double ask, double bidAmount, double askAmount) 34 | { 35 | Bid = bid; 36 | Ask = ask; 37 | BidAmount = bidAmount; 38 | AskAmount = askAmount; 39 | Mid = (bid + ask) / 2; 40 | } 41 | 42 | /// 43 | /// Bid levels. 44 | /// 45 | public IReadOnlyList Bids { get; } 46 | 47 | /// 48 | /// Ask levels. 49 | /// 50 | public IReadOnlyList Asks { get; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/IOrderBookChangeInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Crypto.Websocket.Extensions.Core.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models 5 | { 6 | /// 7 | /// Info about changed order book 8 | /// 9 | public interface IOrderBookChangeInfo : ICryptoChangeInfo 10 | { 11 | /// 12 | /// Target pair for this quotes 13 | /// 14 | string Pair { get; } 15 | 16 | /// 17 | /// Unmodified target pair for this quotes 18 | /// 19 | string PairOriginal { get; } 20 | 21 | /// 22 | /// Current quotes 23 | /// 24 | ICryptoQuotes Quotes { get; } 25 | 26 | /// 27 | /// Order book levels that caused the change. 28 | /// Streamed only when debug mode is enabled. 29 | /// 30 | IReadOnlyList Levels { get; } 31 | 32 | /// 33 | /// Source bulks that caused this update (all levels) 34 | /// 35 | IReadOnlyList Sources { get; } 36 | 37 | /// 38 | /// Whenever this order book change update comes from snapshot or diffs 39 | /// 40 | bool IsSnapshot { get; } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/ITopNLevelsChangeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models; 2 | 3 | /// 4 | public interface ITopNLevelsChangeInfo : IOrderBookChangeInfo 5 | { 6 | /// 7 | /// A snapshot of the orderbook. 8 | /// 9 | L2Snapshot Snapshot { get; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/OrderBookAction.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models 2 | { 3 | /// 4 | /// Action type of the order book data 5 | /// 6 | public enum OrderBookAction 7 | { 8 | /// 9 | /// Unknown action 10 | /// 11 | Undefined, 12 | 13 | /// 14 | /// Insert a new order book level 15 | /// 16 | Insert, 17 | 18 | /// 19 | /// Update order book level 20 | /// 21 | Update, 22 | 23 | /// 24 | /// Delete order book level 25 | /// 26 | Delete, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/OrderBookChangeInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using Crypto.Websocket.Extensions.Core.Models; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models 7 | { 8 | /// 9 | /// Info about changed order book 10 | /// 11 | [DebuggerDisplay("OrderBookChangeInfo [{PairOriginal}] {Quotes} sources: {Sources.Count}")] 12 | public class OrderBookChangeInfo : CryptoChangeInfo, IOrderBookChangeInfo 13 | { 14 | /// 15 | public OrderBookChangeInfo(string pair, string pairOriginal, 16 | ICryptoQuotes quotes, IReadOnlyList? levels, IReadOnlyList sources, 17 | bool isSnapshot) 18 | { 19 | Pair = pair; 20 | PairOriginal = pairOriginal; 21 | Quotes = quotes; 22 | Sources = sources; 23 | IsSnapshot = isSnapshot; 24 | Levels = levels ?? Array.Empty(); 25 | } 26 | 27 | /// 28 | /// Target pair for this quotes 29 | /// 30 | public string Pair { get; } 31 | 32 | /// 33 | /// Unmodified target pair for this quotes 34 | /// 35 | public string PairOriginal { get; } 36 | 37 | /// 38 | /// Current quotes 39 | /// 40 | public ICryptoQuotes Quotes { get; } 41 | 42 | /// 43 | /// Order book levels that caused the change. 44 | /// Streamed only when debug mode is enabled. 45 | /// 46 | public IReadOnlyList Levels { get; } 47 | 48 | /// 49 | /// Source bulks that caused this update (all levels) 50 | /// 51 | public IReadOnlyList Sources { get; } 52 | 53 | /// 54 | /// Whenever this order book change update comes from snapshot or diffs 55 | /// 56 | public bool IsSnapshot { get; } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/OrderBookLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Crypto.Websocket.Extensions.Core.Models; 4 | using Crypto.Websocket.Extensions.Core.Utils; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models 7 | { 8 | /// 9 | /// One level of the order book 10 | /// 11 | [DebuggerDisplay("OrderBookLevel [{Pair}] {Id} {Amount} @ {Price}")] 12 | public class OrderBookLevel 13 | { 14 | /// 15 | /// Level constructor 16 | /// 17 | public OrderBookLevel(string id, CryptoOrderSide side, double? price, double? amount, int? count, string? pair) 18 | { 19 | Id = id; 20 | Side = side; 21 | Price = price; 22 | Count = count; 23 | Amount = amount; 24 | Pair = pair == null ? null : CryptoPairsHelper.Clean(pair); 25 | 26 | Price = Price; 27 | Amount = Abs(Amount); 28 | Count = Abs(Count); 29 | } 30 | 31 | /// 32 | /// Unique identification of this level or order id 33 | /// 34 | public string Id { get; private set; } 35 | 36 | /// 37 | /// Side of this order book level 38 | /// 39 | public CryptoOrderSide Side { get; private set; } 40 | 41 | /// 42 | /// Price level 43 | /// 44 | public double? Price { get; internal set; } 45 | 46 | /// 47 | /// Number of orders at that price level or order 48 | /// 49 | public int? Count { get; internal set; } 50 | 51 | /// 52 | /// Total amount available at that price level or order. 53 | /// 54 | public double? Amount { get; internal set; } 55 | 56 | /// 57 | /// Pair to which this level or order belongs 58 | /// 59 | public string? Pair { get; internal set; } 60 | 61 | /// 62 | /// Ordering index to determine position in the queue 63 | /// 64 | public int Ordering { get; internal set; } 65 | 66 | /// 67 | /// How many times price was updated for this order book level/order (makes sense only for L3) 68 | /// 69 | public int PriceUpdatedCount { get; internal set; } 70 | 71 | /// 72 | /// How many times amount was updated for this order book level/order (makes sense for both L3 and L2) 73 | /// 74 | public int AmountUpdatedCount { get; internal set; } 75 | 76 | /// 77 | /// Difference between previous level amount and current one, negative means that current level amount is smaller than it was before (makes sense for both L3 and L2) 78 | /// 79 | public double AmountDifference { get; internal set; } 80 | 81 | /// 82 | /// Difference between first level amount and current one, negative means that current level amount is smaller than it was at the beginning (makes sense for both L3 and L2) 83 | /// 84 | public double AmountDifferenceAggregated { get; internal set; } 85 | 86 | /// 87 | /// Difference between previous level order count and current one, negative means that there are fewer orders on that level (makes sense only for L2) 88 | /// 89 | public int CountDifference { get; internal set; } 90 | 91 | /// 92 | /// Difference between first level order count and current one, negative means that there are fewer orders on that level than at the beginning (makes sense only for L2) 93 | /// 94 | public int CountDifferenceAggregated { get; internal set; } 95 | 96 | /// 97 | /// Level index (position) in the order book. 98 | /// Beware not updated after first set! 99 | /// 100 | public int? Index { get; internal set; } 101 | 102 | /// 103 | /// Create a new clone 104 | /// 105 | public OrderBookLevel Clone() 106 | { 107 | return new OrderBookLevel( 108 | Id, 109 | Side, 110 | Price, 111 | Amount, 112 | Count, 113 | Pair 114 | ) 115 | { 116 | Ordering = Ordering, 117 | PriceUpdatedCount = PriceUpdatedCount, 118 | AmountUpdatedCount = AmountUpdatedCount, 119 | AmountDifference = AmountDifference, 120 | CountDifference = CountDifference, 121 | AmountDifferenceAggregated = AmountDifferenceAggregated, 122 | CountDifferenceAggregated = CountDifferenceAggregated 123 | }; 124 | } 125 | 126 | private static double? Abs(double? value) 127 | { 128 | if (value.HasValue) 129 | return Math.Abs(value.Value); 130 | return null; 131 | } 132 | 133 | private static int? Abs(int? value) 134 | { 135 | if (value.HasValue) 136 | return Math.Abs(value.Value); 137 | return null; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/OrderBookLevelBulk.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Crypto.Websocket.Extensions.Core.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models 5 | { 6 | /// 7 | /// Groups together order book levels that are coming from server 8 | /// 9 | [DebuggerDisplay("OrderBookLevelBulk [{Action}] type: {OrderBookType} count: {Levels.Length}")] 10 | public class OrderBookLevelBulk : CryptoChangeInfo 11 | { 12 | /// 13 | public OrderBookLevelBulk(OrderBookAction action, OrderBookLevel[] levels, CryptoOrderBookType orderBookType) 14 | { 15 | Action = action; 16 | OrderBookType = orderBookType; 17 | Levels = levels ?? new OrderBookLevel[0]; 18 | } 19 | 20 | /// 21 | /// Action of this bulk 22 | /// 23 | public OrderBookAction Action { get; } 24 | 25 | /// 26 | /// Order book levels for this bulk 27 | /// 28 | public OrderBookLevel[] Levels { get; } 29 | 30 | /// 31 | /// Type of the order book levels 32 | /// 33 | public CryptoOrderBookType OrderBookType { get; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/OrderBookLevelOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models 4 | { 5 | /// 6 | /// Order book price to order index 7 | /// 8 | public class OrderBookLevelsOrderPerPrice : Dictionary 9 | { 10 | /// 11 | public OrderBookLevelsOrderPerPrice() 12 | { 13 | } 14 | 15 | /// 16 | public OrderBookLevelsOrderPerPrice(int capacity) : base(capacity) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/OrderBookLevelsById.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models 4 | { 5 | /// 6 | /// Order book levels together, indexed by id 7 | /// 8 | public class OrderBookLevelsById : Dictionary 9 | { 10 | /// 11 | public OrderBookLevelsById() 12 | { 13 | } 14 | 15 | /// 16 | public OrderBookLevelsById(int capacity) : base(capacity) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Models/TopNLevelsChangeInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Crypto.Websocket.Extensions.Core.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Models; 5 | 6 | /// 7 | /// Info about changed order book 8 | /// 9 | public class TopNLevelsChangeInfo : OrderBookChangeInfo, ITopNLevelsChangeInfo 10 | { 11 | /// 12 | public TopNLevelsChangeInfo(string pair, string pairOriginal, 13 | ICryptoQuotes quotes, IReadOnlyList levels, IReadOnlyList sources, 14 | bool isSnapshot) 15 | : base(pair, pairOriginal, quotes, levels, sources, isSnapshot) 16 | { 17 | } 18 | 19 | /// 20 | /// The L2 snapshot of the top N bid/ask levels 21 | /// 22 | public L2Snapshot Snapshot { get; internal set; } 23 | } 24 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/OrderBooks/Sources/IOrderBookSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.OrderBooks.Sources 7 | { 8 | /// 9 | /// Order book source that provides level 2 data 10 | /// 11 | public interface IOrderBookSource : IDisposable 12 | { 13 | /// 14 | /// Origin exchange name 15 | /// 16 | string ExchangeName { get; } 17 | 18 | /// 19 | /// Enable or disable snapshot loading (used by auto snapshot reload feature on OrderBook). 20 | /// Disabled by default. 21 | /// 22 | bool LoadSnapshotEnabled { get; set; } 23 | 24 | /// 25 | /// Whenever messages should be buffered before processing. 26 | /// Use property `BufferInterval` to configure buffering interval. 27 | /// Enabled by default. 28 | /// 29 | bool BufferEnabled { get; set; } 30 | 31 | /// 32 | /// Time interval for buffering received order book data updates. 33 | /// Higher it for data intensive sources (Bitmex, etc.) 34 | /// Lower - more realtime data, high CPU load. 35 | /// Higher - less realtime data, less CPU intensive. 36 | /// Default: 10 ms 37 | /// 38 | TimeSpan BufferInterval { get; set; } 39 | 40 | /// 41 | /// Exposed logger 42 | /// 43 | ILogger Logger { get; } 44 | 45 | /// 46 | /// Streams initial snapshot of the order book 47 | /// 48 | IObservable OrderBookSnapshotStream { get; } 49 | 50 | /// 51 | /// Streams every update to the order book 52 | /// 53 | IObservable OrderBookStream { get; } 54 | 55 | /// 56 | /// Request a new order book snapshot, will be streamed via 'OrderBookSnapshotStream'. 57 | /// Method doesn't throw exception, just logs it 58 | /// 59 | /// Target pair 60 | /// Max level count 61 | Task LoadSnapshot(string pair, int count = 1000); 62 | 63 | /// 64 | /// Returns true if order book is in valid state 65 | /// 66 | bool IsValid(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Orders/CryptoOrderCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Crypto.Websocket.Extensions.Core.Orders.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.Orders 5 | { 6 | /// 7 | /// Orders collection (thread-safe) 8 | /// 9 | public class CryptoOrderCollection : ConcurrentDictionary 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Orders/CryptoOrderCollectionReadonly.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using Crypto.Websocket.Extensions.Core.Orders.Models; 4 | 5 | namespace Crypto.Websocket.Extensions.Core.Orders 6 | { 7 | /// 8 | /// Orders collection (readonly) 9 | /// 10 | public class CryptoOrderCollectionReadonly : ReadOnlyDictionary 11 | { 12 | /// 13 | public CryptoOrderCollectionReadonly(IDictionary dictionary) : base(dictionary) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Orders/ICryptoOrders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Crypto.Websocket.Extensions.Core.Orders.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.Orders 5 | { 6 | /// 7 | /// Orders manager 8 | /// 9 | public interface ICryptoOrders 10 | { 11 | /// 12 | /// Order was changed stream 13 | /// 14 | IObservable OrderChangedStream { get; } 15 | 16 | /// 17 | /// Order was changed stream (only ours, based on client id prefix) 18 | /// 19 | IObservable OurOrderChangedStream { get; } 20 | 21 | /// 22 | /// Selected client id prefix 23 | /// 24 | long? ClientIdPrefix { get; } 25 | 26 | /// 27 | /// Selected client id prefix as string 28 | /// 29 | string ClientIdPrefixString { get; } 30 | 31 | /// 32 | /// Client id exponent when prefix is selected. 33 | /// For example: 34 | /// prefix = 333 35 | /// exponent = 1000000 36 | /// generated client id = 333000001 37 | /// 38 | long ClientIdPrefixExponent { get; set; } 39 | 40 | /// 41 | /// Target pair for this orders data (other orders will be filtered out) 42 | /// 43 | string TargetPair { get; } 44 | 45 | /// 46 | /// Originally provided target pair for this orders data 47 | /// 48 | string? TargetPairOriginal { get; } 49 | 50 | /// 51 | /// Last executed (or partially filled) buy order 52 | /// 53 | CryptoOrder? LastExecutedBuyOrder { get; } 54 | 55 | /// 56 | /// Last executed (or partially filled) sell order 57 | /// 58 | CryptoOrder? LastExecutedSellOrder { get; } 59 | 60 | /// 61 | /// Generate a new client id (with prefix) 62 | /// 63 | long GenerateClientId(); 64 | 65 | /// 66 | /// Returns only our active orders (based on client id prefix) 67 | /// 68 | CryptoOrderCollectionReadonly GetActiveOrders(); 69 | 70 | /// 71 | /// Returns only our orders (based on client id prefix) 72 | /// 73 | CryptoOrderCollectionReadonly GetOrders(); 74 | 75 | /// 76 | /// Returns all orders (ignore prefix for client id) 77 | /// 78 | CryptoOrderCollectionReadonly GetAllOrders(); 79 | 80 | /// 81 | /// Find active order by provided unique id 82 | /// 83 | CryptoOrder? FindActiveOrder(string id); 84 | 85 | /// 86 | /// Find order by provided unique id 87 | /// 88 | CryptoOrder? FindOrder(string id); 89 | 90 | /// 91 | /// Find active order by provided client id 92 | /// 93 | CryptoOrder? FindActiveOrderByClientId(string clientId); 94 | 95 | /// 96 | /// Find order by provided client id 97 | /// 98 | CryptoOrder? FindOrderByClientId(string clientId); 99 | 100 | /// 101 | /// Returns true if client id matches prefix 102 | /// 103 | bool IsOurOrder(CryptoOrder order); 104 | 105 | /// 106 | /// Returns true if client id matches prefix 107 | /// 108 | bool IsOurOrder(string clientId); 109 | 110 | /// 111 | /// Track selected order (use immediately after placing an order via REST call) 112 | /// 113 | void TrackOrder(CryptoOrder order); 114 | 115 | /// 116 | /// Clean internal orders cache, remove canceled orders 117 | /// 118 | void RemoveCanceled(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Orders/Models/CryptoOrderStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.Orders.Models 2 | { 3 | /// 4 | /// Current order status 5 | /// 6 | public enum CryptoOrderStatus 7 | { 8 | Undefined, 9 | New, 10 | Active, 11 | Executed, 12 | PartiallyFilled, 13 | Canceled 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Orders/Models/CryptoOrderType.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.Orders.Models 2 | { 3 | /// 4 | /// Order type - limit, market, etc. 5 | /// 6 | public enum CryptoOrderType 7 | { 8 | Undefined, 9 | Limit, 10 | Market, 11 | Stop, 12 | TrailingStop, 13 | Fok, 14 | StopLimit, 15 | TakeProfitLimit, 16 | TakeProfitMarket 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Orders/Sources/IOrderSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Crypto.Websocket.Extensions.Core.Orders.Models; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Crypto.Websocket.Extensions.Core.Orders.Sources 6 | { 7 | /// 8 | /// Source that provides current orders info 9 | /// 10 | public interface IOrderSource 11 | { 12 | /// 13 | /// Origin exchange name 14 | /// 15 | string ExchangeName { get; } 16 | 17 | /// 18 | /// Stream snapshot of currently active orders 19 | /// 20 | IObservable OrdersInitialStream { get; } 21 | 22 | /// 23 | /// Stream info about new active order 24 | /// 25 | IObservable OrderCreatedStream { get; } 26 | 27 | /// 28 | /// Stream on every status change of the order 29 | /// 30 | IObservable OrderUpdatedStream { get; } 31 | 32 | /// 33 | /// Exposed logger 34 | /// 35 | ILogger Logger { get; } 36 | 37 | /// 38 | /// Set collection of existing orders (to correctly handle orders state) 39 | /// 40 | void SetExistingOrders(CryptoOrderCollection orders); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Orders/Sources/OrderSourceBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using Crypto.Websocket.Extensions.Core.Orders.Models; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Crypto.Websocket.Extensions.Core.Orders.Sources 8 | { 9 | /// 10 | /// Source that provides current orders info 11 | /// 12 | public abstract class OrderSourceBase : IOrderSource 13 | { 14 | protected readonly Subject OrderSnapshotSubject = new Subject(); 15 | protected readonly Subject OrderCreatedSubject = new Subject(); 16 | protected readonly Subject OrderUpdatedSubject = new Subject(); 17 | 18 | protected CryptoOrderCollection ExistingOrders = new CryptoOrderCollection(); 19 | 20 | protected OrderSourceBase(ILogger logger) 21 | { 22 | Logger = logger; 23 | } 24 | 25 | /// 26 | /// Origin exchange name 27 | /// 28 | public abstract string ExchangeName { get; } 29 | 30 | /// 31 | /// Exposed logger 32 | /// 33 | public ILogger Logger { get; } 34 | 35 | /// 36 | /// Stream snapshot of currently active orders 37 | /// 38 | public virtual IObservable OrdersInitialStream => OrderSnapshotSubject.AsObservable(); 39 | 40 | /// 41 | /// Stream info about new active order 42 | /// 43 | public virtual IObservable OrderCreatedStream => OrderCreatedSubject.AsObservable(); 44 | 45 | /// 46 | /// Stream on every status change of the order 47 | /// 48 | public virtual IObservable OrderUpdatedStream => OrderUpdatedSubject.AsObservable(); 49 | 50 | /// 51 | /// Set collection of existing orders (to correctly handle orders state) 52 | /// 53 | public void SetExistingOrders(CryptoOrderCollection? orders) 54 | { 55 | ExistingOrders = orders ?? new CryptoOrderCollection(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Positions/Models/CryptoPosition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Crypto.Websocket.Extensions.Core.Models; 4 | using Crypto.Websocket.Extensions.Core.Utils; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.Positions.Models 7 | { 8 | /// 9 | /// Information about currently open position 10 | /// 11 | [DebuggerDisplay("Position: {Pair} - {EntryPrice} {Amount}/{AmountQuote} - pnl: {UnrealizedPnl}")] 12 | public class CryptoPosition 13 | { 14 | private CryptoPositionSide _side; 15 | private double _amount; 16 | private double _amountQuote; 17 | 18 | /// 19 | /// Pair to which this position belongs 20 | /// 21 | public string Pair { get; set; } 22 | 23 | /// 24 | /// Pair to which this position belongs (cleaned) 25 | /// 26 | public string PairClean => CryptoPairsHelper.Clean(Pair); 27 | 28 | /// 29 | /// Position's opening timestamp 30 | /// 31 | public DateTime? OpeningTimestamp { get; set; } 32 | 33 | /// 34 | /// Position's current timestamp 35 | /// 36 | public DateTime? CurrentTimestamp { get; set; } 37 | 38 | /// 39 | /// Position's entry price 40 | /// 41 | public double EntryPrice { get; set; } 42 | 43 | /// 44 | /// Market's last price 45 | /// 46 | public double LastPrice { get; set; } 47 | 48 | /// 49 | /// Market's mark price - used for liquidation 50 | /// 51 | public double MarkPrice { get; set; } 52 | 53 | /// 54 | /// Position's liquidation price 55 | /// 56 | public double LiquidationPrice { get; set; } 57 | 58 | /// 59 | /// Original position amount (stable) in base currency 60 | /// 61 | public double Amount 62 | { 63 | get => (_amount); 64 | set => _amount = WithCorrectSign(value); 65 | } 66 | 67 | /// 68 | /// Original order amount (stable) in quote currency 69 | /// 70 | public double AmountQuote 71 | { 72 | get => (_amountQuote); 73 | set => _amountQuote = WithCorrectSign(value); 74 | } 75 | 76 | /// 77 | /// Position's side 78 | /// 79 | public CryptoPositionSide Side 80 | { 81 | get => _side; 82 | set 83 | { 84 | _side = value; 85 | 86 | _amount = WithCorrectSign(_amount); 87 | _amountQuote = WithCorrectSign(_amountQuote); 88 | } 89 | } 90 | 91 | /// 92 | /// Current leverage (supported only by few exchanges) 93 | /// 94 | public double? Leverage { get; set; } 95 | 96 | /// 97 | /// Realized position profit (supported only by few exchanges) 98 | /// 99 | public double? RealizedPnl { get; set; } 100 | 101 | /// 102 | /// Unrealized position profit (supported only by few exchanges) 103 | /// 104 | public double? UnrealizedPnl { get; set; } 105 | 106 | 107 | private double WithCorrectSign(double value) 108 | { 109 | if (_side == CryptoPositionSide.Undefined) 110 | return value; 111 | return Math.Abs(value) * (_side == CryptoPositionSide.Long ? 1 : -1); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Positions/Sources/IPositionSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Crypto.Websocket.Extensions.Core.Positions.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.Positions.Sources 5 | { 6 | /// 7 | /// Source that provides info about currently opened positions 8 | /// 9 | public interface IPositionSource 10 | { 11 | /// 12 | /// Origin exchange name 13 | /// 14 | string ExchangeName { get; } 15 | 16 | /// 17 | /// Stream info about current positions 18 | /// 19 | IObservable PositionsStream { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Positions/Sources/PositionSourceBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using Crypto.Websocket.Extensions.Core.Positions.Models; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.Positions.Sources 7 | { 8 | /// 9 | /// Source that provides info about currently opened positions 10 | /// 11 | public abstract class PositionSourceBase : IPositionSource 12 | { 13 | /// 14 | /// Position subject 15 | /// 16 | protected readonly Subject PositionsSubject = new Subject(); 17 | 18 | 19 | /// 20 | /// Origin exchange name 21 | /// 22 | public abstract string ExchangeName { get; } 23 | 24 | /// 25 | /// Stream info about current positions 26 | /// 27 | public virtual IObservable PositionsStream => PositionsSubject.AsObservable(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Threading/CryptoAsyncLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Crypto.Websocket.Extensions.Core.Threading 6 | { 7 | /// 8 | /// Class that wraps SemaphoreSlim and enables to use locking inside 'using' blocks easily 9 | /// Don't need to bother with releasing and handling SemaphoreSlim correctly 10 | /// Example: 11 | /// 12 | /// using(await _asyncLock.LockAsync()) 13 | /// { 14 | /// // do your synchronized work 15 | /// } 16 | /// 17 | /// 18 | public class CryptoAsyncLock 19 | { 20 | private readonly Task _releaserTask; 21 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); 22 | private readonly IDisposable _releaser; 23 | 24 | /// 25 | /// Class that wraps SemaphoreSlim and enables to use locking inside 'using' blocks easily 26 | /// Don't need to bother with releasing and handling SemaphoreSlim correctly 27 | /// 28 | public CryptoAsyncLock() 29 | { 30 | _releaser = new Releaser(_semaphore); 31 | _releaserTask = Task.FromResult(_releaser); 32 | } 33 | 34 | /// 35 | /// Dispose to release the lock 36 | /// 37 | /// 38 | public IDisposable Lock() 39 | { 40 | _semaphore.Wait(); 41 | return _releaser; 42 | } 43 | 44 | /// 45 | /// Dispose to release the lock 46 | /// 47 | /// 48 | public Task LockAsync() 49 | { 50 | var waitTask = _semaphore.WaitAsync(); 51 | return waitTask.IsCompleted 52 | ? _releaserTask 53 | : waitTask.ContinueWith( 54 | (_, releaser) => (IDisposable) releaser, 55 | _releaser, 56 | CancellationToken.None, 57 | TaskContinuationOptions.ExecuteSynchronously, 58 | TaskScheduler.Default); 59 | } 60 | 61 | private class Releaser : IDisposable 62 | { 63 | private readonly SemaphoreSlim _semaphore; 64 | 65 | public Releaser(SemaphoreSlim semaphore) 66 | { 67 | _semaphore = semaphore; 68 | } 69 | 70 | public void Dispose() 71 | { 72 | _semaphore.Release(); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Trades/Models/CryptoTrade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Crypto.Websocket.Extensions.Core.Models; 4 | using Crypto.Websocket.Extensions.Core.Utils; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.Trades.Models 7 | { 8 | /// 9 | /// Executed trade info 10 | /// 11 | [DebuggerDisplay("Trade: {Id} - {Pair} - {Price} {Amount}/{AmountQuote}")] 12 | public class CryptoTrade : CryptoChangeInfo 13 | { 14 | private CryptoTradeSide _side; 15 | private double _amount; 16 | private double _amountQuote; 17 | 18 | /// 19 | /// Unique trade id (provided by exchange) 20 | /// 21 | public string Id { get; set; } 22 | 23 | /// 24 | /// Unique related order id from maker side - liquidity provider (provided only by few exchanges) 25 | /// 26 | public string MakerOrderId { get; set; } 27 | 28 | /// 29 | /// Unique related order id from taker side - liquidity taker (provided only by few exchanges) 30 | /// 31 | public string TakerOrderId { get; set; } 32 | 33 | /// 34 | /// Pair to which this trade belongs 35 | /// 36 | public string Pair { get; set; } 37 | 38 | /// 39 | /// Pair to which this trade belongs (cleaned) 40 | /// 41 | public string PairClean => CryptoPairsHelper.Clean(Pair); 42 | 43 | /// 44 | /// Trade's executed timestamp 45 | /// 46 | public DateTime? Timestamp { get; set; } 47 | 48 | /// 49 | /// Trade's price 50 | /// 51 | public double Price { get; set; } 52 | 53 | /// 54 | /// Original trade amount (stable) in base currency 55 | /// 56 | public double Amount 57 | { 58 | get => (_amount); 59 | set => _amount = WithCorrectSign(value); 60 | } 61 | 62 | /// 63 | /// Original trade amount (stable) in quote currency 64 | /// 65 | public double AmountQuote 66 | { 67 | get => (_amountQuote); 68 | set => _amountQuote = WithCorrectSign(value); 69 | } 70 | 71 | /// 72 | /// Trade's side 73 | /// 74 | public CryptoTradeSide Side 75 | { 76 | get => _side; 77 | set 78 | { 79 | _side = value; 80 | 81 | _amount = WithCorrectSign(_amount); 82 | _amountQuote = WithCorrectSign(_amountQuote); 83 | } 84 | } 85 | 86 | 87 | 88 | private double WithCorrectSign(double value) 89 | { 90 | if (_side == CryptoTradeSide.Undefined) 91 | return value; 92 | return Math.Abs(value) * (_side == CryptoTradeSide.Buy ? 1 : -1); 93 | } 94 | 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Trades/Sources/ITradeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Crypto.Websocket.Extensions.Core.Trades.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.Trades.Sources 5 | { 6 | /// 7 | /// Source that provides info about executed trades 8 | /// 9 | public interface ITradeSource 10 | { 11 | /// 12 | /// Origin exchange name 13 | /// 14 | string ExchangeName { get; } 15 | 16 | /// 17 | /// Stream info about executed trades 18 | /// 19 | IObservable TradesStream { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Trades/Sources/TradeSourceBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using Crypto.Websocket.Extensions.Core.Trades.Models; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.Trades.Sources 7 | { 8 | /// 9 | /// Source that provides info about executed trades 10 | /// 11 | public abstract class TradeSourceBase : ITradeSource 12 | { 13 | /// 14 | /// Trades subject 15 | /// 16 | protected readonly Subject TradesSubject = new Subject(); 17 | 18 | 19 | /// 20 | /// Origin exchange name 21 | /// 22 | public abstract string ExchangeName { get; } 23 | 24 | /// 25 | /// Stream info about executed trades 26 | /// 27 | public virtual IObservable TradesStream => TradesSubject.AsObservable(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Utils/CryptoDateUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Utils 4 | { 5 | /// 6 | /// DateTime utils to support high resolution 7 | /// 8 | public static class CryptoDateUtils 9 | { 10 | /// 11 | /// Unix base datetime (1.1. 1970) 12 | /// 13 | public static readonly DateTime UnixBase = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 14 | 15 | /// 16 | /// Convert from unix seconds into DateTime with high resolution (6 decimal places for milliseconds) 17 | /// 18 | public static DateTime ConvertFromUnixSeconds(double timeInSec) 19 | { 20 | var unixTimeStampInTicks = (long)(timeInSec * TimeSpan.TicksPerSecond); 21 | return new DateTime(UnixBase.Ticks + unixTimeStampInTicks, DateTimeKind.Utc); 22 | } 23 | 24 | /// 25 | /// Convert from unix seconds into DateTime with high resolution (6 decimal places for milliseconds) 26 | /// 27 | public static DateTime? ConvertFromUnixSeconds(double? timeInSec) 28 | { 29 | if (!timeInSec.HasValue) 30 | return null; 31 | return ConvertFromUnixSeconds(timeInSec.Value); 32 | } 33 | 34 | /// 35 | /// Convert from unix seconds into DateTime with high resolution (6 decimal places for milliseconds) 36 | /// 37 | public static DateTime ConvertFromUnixSeconds(decimal timeInSec) 38 | { 39 | var unixTimeStampInTicks = (long)(timeInSec * TimeSpan.TicksPerSecond); 40 | return new DateTime(UnixBase.Ticks + unixTimeStampInTicks, DateTimeKind.Utc); 41 | } 42 | 43 | /// 44 | /// Convert from unix seconds into DateTime with high resolution (6 decimal places for milliseconds) 45 | /// 46 | public static DateTime? ConvertFromUnixSeconds(decimal? timeInSec) 47 | { 48 | if (!timeInSec.HasValue) 49 | return null; 50 | return ConvertFromUnixSeconds(timeInSec.Value); 51 | } 52 | 53 | 54 | /// 55 | /// Convert DateTime into unix seconds with high resolution (6 decimal places for milliseconds) 56 | /// 57 | public static double ToUnixSeconds(this DateTime date) 58 | { 59 | var unixTimeStampInTicks = (date.ToUniversalTime() - UnixBase).Ticks; 60 | return (double)unixTimeStampInTicks / TimeSpan.TicksPerSecond; 61 | } 62 | 63 | /// 64 | /// Convert DateTime into unix seconds with high resolution (6 decimal places for milliseconds) 65 | /// 66 | public static double? ToUnixSeconds(this DateTime? date) 67 | { 68 | if (!date.HasValue) 69 | return null; 70 | return ToUnixSeconds(date.Value); 71 | } 72 | 73 | /// 74 | /// Convert DateTime into unix seconds with high resolution (6 decimal places for milliseconds) 75 | /// 76 | public static decimal ToUnixSecondsDecimal(this DateTime date) 77 | { 78 | var unixTimeStampInTicks = Convert.ToDecimal((date.ToUniversalTime() - UnixBase).Ticks); 79 | return unixTimeStampInTicks / TimeSpan.TicksPerSecond; 80 | } 81 | 82 | /// 83 | /// Convert DateTime into unix seconds with high resolution (6 decimal places for milliseconds) 84 | /// 85 | public static decimal? ToUnixSecondsDecimal(this DateTime? date) 86 | { 87 | if (!date.HasValue) 88 | return null; 89 | return ToUnixSecondsDecimal(date.Value); 90 | } 91 | 92 | /// 93 | /// Convert DateTime into unix seconds string with high resolution (6 decimal places for milliseconds) 94 | /// 95 | public static string? ToUnixSecondsString(this DateTime? value) 96 | { 97 | return value?.ToUnixSecondsString(); 98 | } 99 | 100 | /// 101 | /// Convert DateTime into unix seconds string with high resolution (6 decimal places for milliseconds) 102 | /// 103 | public static string ToUnixSecondsString(this DateTime value) 104 | { 105 | var seconds = value.ToUnixSecondsDecimal(); 106 | var str = seconds.ToString("0.000000"); 107 | return str; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Utils/CryptoMathUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Utils 4 | { 5 | /// 6 | /// Math utils 7 | /// 8 | public static class CryptoMathUtils 9 | { 10 | /// 11 | /// Compare two double numbers correctly 12 | /// 13 | public static bool IsSame(double first, double second) 14 | { 15 | return Math.Abs(first - second) < EqualTolerance; 16 | } 17 | 18 | /// 19 | /// Compare two double numbers correctly 20 | /// 21 | public static bool IsSame(double? first, double? second) 22 | { 23 | if (!first.HasValue && !second.HasValue) 24 | return true; 25 | if (!first.HasValue || !second.HasValue) 26 | return false; 27 | 28 | return IsSame(first.Value, second.Value); 29 | } 30 | 31 | /// 32 | /// Tolerance used for comparing float numbers 33 | /// 34 | public static double EqualTolerance => 1E-8; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Utils/CryptoPairsHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Crypto.Websocket.Extensions.Core.Utils 2 | { 3 | /// 4 | /// Helper class for working with pair identifications 5 | /// 6 | public static class CryptoPairsHelper 7 | { 8 | /// 9 | /// Clean pair from any unnecessary characters and make lowercase 10 | /// 11 | public static string Clean(string? pair) 12 | { 13 | return (pair ?? string.Empty) 14 | .Trim() 15 | .ToLower() 16 | .Replace("/", "") 17 | .Replace("-", "") 18 | .Replace("\\", ""); 19 | } 20 | 21 | /// 22 | /// Compare two pairs, clean them before 23 | /// 24 | public static bool AreSame(string? firstPair, string? secondPair) 25 | { 26 | var first = Clean(firstPair); 27 | var second = Clean(secondPair); 28 | return first.Equals(second); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Validations/CryptoValidations.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Crypto.Websocket.Extensions.Core.Exceptions; 4 | 5 | namespace Crypto.Websocket.Extensions.Core.Validations 6 | { 7 | /// 8 | /// Validations utils 9 | /// 10 | public static class CryptoValidations 11 | { 12 | /// 13 | /// It throws if value is null or empty/white spaces 14 | /// 15 | /// The value to be validated 16 | /// Input parameter name 17 | public static void ValidateInput(string value, string name) 18 | { 19 | if (string.IsNullOrWhiteSpace(value)) 20 | { 21 | throw new CryptoBadInputException($"Input string parameter '{name}' is null or empty. Please correct it."); 22 | } 23 | } 24 | 25 | /// 26 | /// It throws if value is null 27 | /// 28 | /// The value to be validated 29 | /// Input parameter name 30 | public static void ValidateInput(T value, string name) 31 | { 32 | if (Equals(value, default(T))) 33 | { 34 | throw new CryptoBadInputException($"Input parameter '{name}' is null. Please correct it."); 35 | } 36 | } 37 | 38 | /// 39 | /// It throws if collection is null or collection is empty 40 | /// 41 | /// The collection to be validated 42 | /// Input parameter name 43 | public static void ValidateInputCollection(IEnumerable collection, string name) 44 | { 45 | // ReSharper disable once PossibleMultipleEnumeration 46 | ValidateInput(collection, name); 47 | 48 | // ReSharper disable once PossibleMultipleEnumeration 49 | if (!collection.Any()) 50 | { 51 | throw new CryptoBadInputException($"Input collection '{name}' is empty. Please correct it."); 52 | } 53 | } 54 | 55 | /// 56 | /// It throws if value is not in specified range 57 | /// 58 | /// The value to be validated 59 | /// Input parameter name 60 | /// Minimal value of input 61 | /// Maximum value of input 62 | public static void ValidateInput(int value, string name, int minValue = int.MinValue, int maxValue = int.MaxValue) 63 | { 64 | if (value < minValue) 65 | { 66 | throw new CryptoBadInputException($"Input parameter '{name}' is lower than {minValue}. Please correct it."); 67 | } 68 | if (value > maxValue) 69 | { 70 | throw new CryptoBadInputException($"Input parameter '{name}' is higher than {maxValue}. Please correct it."); 71 | } 72 | } 73 | 74 | /// 75 | /// It throws if value is not in specified range 76 | /// 77 | /// The value to be validated 78 | /// Input parameter name 79 | /// Minimal value of input 80 | /// Maximum value of input 81 | public static void ValidateInput(long value, string name, long minValue = long.MinValue, long maxValue = long.MaxValue) 82 | { 83 | if (value < minValue) 84 | { 85 | throw new CryptoBadInputException($"Input parameter '{name}' is lower than {minValue}. Please correct it."); 86 | } 87 | if (value > maxValue) 88 | { 89 | throw new CryptoBadInputException($"Input parameter '{name}' is higher than {maxValue}. Please correct it."); 90 | } 91 | } 92 | 93 | /// 94 | /// It throws if value is not in specified range 95 | /// 96 | /// The value to be validated 97 | /// Input parameter name 98 | /// Minimal value of input 99 | /// Maximum value of input 100 | public static void ValidateInput(double value, string name, double minValue = double.MinValue, double maxValue = double.MaxValue) 101 | { 102 | if (value < minValue) 103 | { 104 | throw new CryptoBadInputException($"Input parameter '{name}' is lower than {minValue}. Please correct it."); 105 | } 106 | if (value > maxValue) 107 | { 108 | throw new CryptoBadInputException($"Input parameter '{name}' is higher than {maxValue}. Please correct it."); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Wallets/Models/CryptoWallet.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Crypto.Websocket.Extensions.Core.Wallets.Models 4 | { 5 | /// 6 | /// Wallet info - single currency balance 7 | /// 8 | [DebuggerDisplay("Wallet: {Currency} - {Balance}")] 9 | public class CryptoWallet 10 | { 11 | /// 12 | /// Wallet type (supported only by few exchanges) 13 | /// 14 | public string Type { get; set; } 15 | 16 | /// 17 | /// Target currency 18 | /// 19 | public string Currency { get; set; } 20 | 21 | /// 22 | /// Balance in target currency 23 | /// 24 | public double Balance { get; set; } 25 | 26 | /// 27 | /// Available balance (supported only by few exchanges) 28 | /// 29 | public double? BalanceAvailable { get; set; } 30 | 31 | /// 32 | /// Current leverage (supported only by few exchanges) 33 | /// 34 | public double? Leverage { get; set; } 35 | 36 | /// 37 | /// Realized profit (supported only by few exchanges) 38 | /// 39 | public double? RealizedPnl { get; set; } 40 | 41 | /// 42 | /// Unrealized profit (supported only by few exchanges) 43 | /// 44 | public double? UnrealizedPnl { get; set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Wallets/Sources/IWalletSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Crypto.Websocket.Extensions.Core.Wallets.Models; 3 | 4 | namespace Crypto.Websocket.Extensions.Core.Wallets.Sources 5 | { 6 | /// 7 | /// Source that provides current wallet info 8 | /// 9 | public interface IWalletSource 10 | { 11 | /// 12 | /// Origin exchange name 13 | /// 14 | string ExchangeName { get; } 15 | 16 | /// 17 | /// Stream info about wallet changes (balance, transactions, etc) 18 | /// 19 | IObservable WalletChangedStream { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/Wallets/Sources/WalletSourceBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using Crypto.Websocket.Extensions.Core.Wallets.Models; 5 | 6 | namespace Crypto.Websocket.Extensions.Core.Wallets.Sources 7 | { 8 | /// 9 | /// Source that provides current wallet info 10 | /// 11 | public abstract class WalletSourceBase : IWalletSource 12 | { 13 | /// 14 | /// Wallet subject 15 | /// 16 | protected readonly Subject WalletChangedSubject = new Subject(); 17 | 18 | 19 | /// 20 | /// Origin exchange name 21 | /// 22 | public abstract string ExchangeName { get; } 23 | 24 | /// 25 | /// Stream info about wallet changes (balance, transactions, etc) 26 | /// 27 | public virtual IObservable WalletChangedStream => WalletChangedSubject.AsObservable(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions.Core/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/crypto-websocket-extensions/a039ba974fcfaf9887c696c4da59910d0a820868/src/Crypto.Websocket.Extensions.Core/icon.png -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Crypto.Websocket.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1;net6;net7;net8;net9 5 | Crypto.Websocket.Extensions 6 | Mariusz Kotas 7 | Extensions to cryptocurrency websocket clients 8 | false 9 | Enhancements 10 | Copyright 2024 Mariusz Kotas. All rights reserved. 11 | Crypto websockets websocket client cryptocurrency exchange bitcoin extensions 12 | MIT 13 | https://github.com/Marfusios/crypto-websocket-extensions 14 | https://raw.githubusercontent.com/Marfusios/crypto-websocket-extensions/master/bitcoin.png 15 | icon.png 16 | https://github.com/Marfusios/crypto-websocket-extensions 17 | README.md 18 | Git 19 | true 20 | true 21 | 22 | true 23 | snupkg 24 | enable 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/OrderBooks/Sources/BitfinexOrderBookSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Bitfinex.Client.Websocket.Client; 8 | using Bitfinex.Client.Websocket.Responses; 9 | using Bitfinex.Client.Websocket.Responses.Books; 10 | using Crypto.Websocket.Extensions.Core.Models; 11 | using Crypto.Websocket.Extensions.Core.OrderBooks; 12 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 13 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 14 | using Crypto.Websocket.Extensions.Core.Validations; 15 | using Microsoft.Extensions.Logging; 16 | using Newtonsoft.Json; 17 | 18 | namespace Crypto.Websocket.Extensions.OrderBooks.Sources 19 | { 20 | /// 21 | public class BitfinexOrderBookSource : OrderBookSourceBase 22 | { 23 | private readonly HttpClient _httpClient = new HttpClient(); 24 | private BitfinexWebsocketClient _client = null!; 25 | private IDisposable? _subscription; 26 | private IDisposable? _subscriptionSnapshot; 27 | 28 | 29 | /// 30 | public BitfinexOrderBookSource(BitfinexWebsocketClient client) : base(client.Logger) 31 | { 32 | _httpClient.BaseAddress = new Uri("https://api-pub.bitfinex.com"); 33 | 34 | ChangeClient(client); 35 | } 36 | 37 | /// 38 | public override string ExchangeName => "bitfinex"; 39 | 40 | /// 41 | /// Change client and resubscribe to the new streams 42 | /// 43 | public void ChangeClient(BitfinexWebsocketClient client) 44 | { 45 | CryptoValidations.ValidateInput(client, nameof(client)); 46 | 47 | _client = client; 48 | _subscriptionSnapshot?.Dispose(); 49 | _subscription?.Dispose(); 50 | Subscribe(); 51 | } 52 | 53 | private void Subscribe() 54 | { 55 | _subscriptionSnapshot = _client.Streams.BookSnapshotStream.Subscribe(HandleSnapshot); 56 | _subscription = _client.Streams.BookStream.Subscribe(HandleBook); 57 | } 58 | 59 | private void HandleSnapshot(Book[] books) 60 | { 61 | // received snapshot, convert and stream 62 | var levels = ConvertLevels(books); 63 | var last = books.LastOrDefault(); 64 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 65 | FillBulk(last, bulk); 66 | StreamSnapshot(bulk); 67 | } 68 | 69 | private void HandleBook(Book book) 70 | { 71 | BufferData(book); 72 | } 73 | 74 | private OrderBookLevel[] ConvertLevels(Book[] data) 75 | { 76 | return data 77 | .Select(ConvertLevel) 78 | .ToArray(); 79 | } 80 | 81 | private OrderBookLevel ConvertLevel(Book x) 82 | { 83 | return new OrderBookLevel 84 | ( 85 | x.Price.ToString(CultureInfo.InvariantCulture), 86 | ConvertSide(x.Amount), 87 | x.Price, 88 | x.Amount, 89 | x.Count, 90 | x.Pair 91 | ); 92 | } 93 | 94 | private CryptoOrderSide ConvertSide(double amount) 95 | { 96 | if (amount > 0) 97 | return CryptoOrderSide.Bid; 98 | if (amount < 0) 99 | return CryptoOrderSide.Ask; 100 | return CryptoOrderSide.Undefined; 101 | } 102 | 103 | private OrderBookAction RecognizeAction(Book book) 104 | { 105 | if (book.Count > 0) 106 | return OrderBookAction.Update; 107 | return OrderBookAction.Delete; 108 | } 109 | 110 | /// 111 | protected override async Task LoadSnapshotInternal(string? pair, int count = 1000) 112 | { 113 | Book[]? parsed = null; 114 | var pairSafe = (pair ?? string.Empty).Trim().ToUpper(); 115 | pairSafe = $"t{pairSafe}"; 116 | var countSafe = count > 100 ? 100 : count; 117 | var result = string.Empty; 118 | 119 | try 120 | { 121 | var url = $"/v2/book/{pairSafe}/P0?len={countSafe}"; 122 | using HttpResponseMessage response = await _httpClient.GetAsync(url); 123 | using HttpContent content = response.Content; 124 | 125 | result = await content.ReadAsStringAsync(); 126 | parsed = JsonConvert.DeserializeObject(result); 127 | if (parsed == null || !parsed.Any()) 128 | return null; 129 | 130 | foreach (var book in parsed) 131 | { 132 | book.Pair = pair ?? string.Empty; 133 | } 134 | } 135 | catch (Exception e) 136 | { 137 | _client.Logger.LogDebug("[ORDER BOOK {exchangeName}] Failed to load L2 orderbook snapshot for pair '{pair}'. " + 138 | "Error: '{error}'. Content: '{content}'", ExchangeName, pairSafe, e.Message, result); 139 | return null; 140 | } 141 | 142 | var levels = ConvertLevels(parsed); 143 | var last = parsed.LastOrDefault(); 144 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 145 | FillBulk(last, bulk); 146 | return bulk; 147 | } 148 | 149 | private OrderBookLevelBulk ConvertDiff(Book book) 150 | { 151 | var converted = ConvertLevel(book); 152 | var action = RecognizeAction(book); 153 | var bulk = new OrderBookLevelBulk(action, new[] { converted }, CryptoOrderBookType.L2); 154 | FillBulk(book, bulk); 155 | return bulk; 156 | } 157 | 158 | private void FillBulk(ResponseBase? response, OrderBookLevelBulk bulk) 159 | { 160 | if (response == null) 161 | return; 162 | 163 | bulk.ExchangeName = ExchangeName; 164 | bulk.ServerTimestamp = response.ServerTimestamp; 165 | bulk.ServerSequence = response.ServerSequence; 166 | } 167 | 168 | /// 169 | protected override OrderBookLevelBulk[] ConvertData(object[] data) 170 | { 171 | var result = new List(); 172 | foreach (var response in data) 173 | { 174 | var responseSafe = response as Book; 175 | if (responseSafe == null) 176 | continue; 177 | 178 | var converted = ConvertDiff(responseSafe); 179 | result.Add(converted); 180 | } 181 | 182 | return result.ToArray(); 183 | } 184 | 185 | 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/OrderBooks/Sources/BitmexOrderBookSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Bitmex.Client.Websocket.Client; 7 | using Bitmex.Client.Websocket.Responses; 8 | using Bitmex.Client.Websocket.Responses.Books; 9 | using Crypto.Websocket.Extensions.Core.Models; 10 | using Crypto.Websocket.Extensions.Core.OrderBooks; 11 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 12 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 13 | using Crypto.Websocket.Extensions.Core.Validations; 14 | using Microsoft.Extensions.Logging; 15 | using Newtonsoft.Json; 16 | using OrderBookLevel = Crypto.Websocket.Extensions.Core.OrderBooks.Models.OrderBookLevel; 17 | 18 | namespace Crypto.Websocket.Extensions.OrderBooks.Sources 19 | { 20 | /// 21 | public class BitmexOrderBookSource : OrderBookSourceBase 22 | { 23 | private readonly HttpClient _httpClient = new HttpClient(); 24 | private BitmexWebsocketClient _client = null!; 25 | private IDisposable? _subscription; 26 | 27 | /// 28 | public BitmexOrderBookSource(BitmexWebsocketClient client, bool isTestnet = false) : base(client.Logger) 29 | { 30 | _httpClient.BaseAddress = isTestnet ? 31 | new Uri("https://testnet.bitmex.com") : 32 | new Uri("https://www.bitmex.com"); 33 | 34 | ChangeClient(client); 35 | } 36 | 37 | /// 38 | public override string ExchangeName => "bitmex"; 39 | 40 | /// 41 | /// Change client and resubscribe to the new streams 42 | /// 43 | public void ChangeClient(BitmexWebsocketClient client) 44 | { 45 | CryptoValidations.ValidateInput(client, nameof(client)); 46 | 47 | _client = client; 48 | _subscription?.Dispose(); 49 | Subscribe(); 50 | } 51 | 52 | private void Subscribe() 53 | { 54 | _subscription = _client.Streams.BookStream.Subscribe(HandleBookResponse); 55 | } 56 | 57 | private void HandleBookResponse(BookResponse bookResponse) 58 | { 59 | if (bookResponse.Action == BitmexAction.Undefined) 60 | { 61 | // weird state, do nothing 62 | return; 63 | } 64 | 65 | if (bookResponse.Action == BitmexAction.Partial) 66 | { 67 | // received snapshot, convert and stream 68 | var levels = ConvertLevels(bookResponse.Data); 69 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2) 70 | { 71 | ExchangeName = ExchangeName 72 | }; 73 | StreamSnapshot(bulk); 74 | return; 75 | } 76 | 77 | // received difference, buffer it 78 | BufferData(bookResponse); 79 | } 80 | 81 | private OrderBookLevel[] ConvertLevels(BookLevel[] data) 82 | { 83 | return data 84 | .Select(x => new OrderBookLevel 85 | ( 86 | x.Id.ToString(), 87 | ConvertSide(x.Side), 88 | x.Price, 89 | x.Size, 90 | null, 91 | x.Symbol 92 | )) 93 | .ToArray(); 94 | } 95 | 96 | private CryptoOrderSide ConvertSide(BitmexSide side) 97 | { 98 | switch (side) 99 | { 100 | case BitmexSide.Buy: 101 | return CryptoOrderSide.Bid; 102 | case BitmexSide.Sell: 103 | return CryptoOrderSide.Ask; 104 | default: 105 | return CryptoOrderSide.Undefined; 106 | } 107 | } 108 | 109 | private OrderBookAction ConvertAction(BitmexAction action) 110 | { 111 | switch (action) 112 | { 113 | case BitmexAction.Insert: 114 | return OrderBookAction.Insert; 115 | case BitmexAction.Update: 116 | return OrderBookAction.Update; 117 | case BitmexAction.Delete: 118 | return OrderBookAction.Delete; 119 | default: 120 | return OrderBookAction.Undefined; 121 | } 122 | } 123 | 124 | /// 125 | protected override async Task LoadSnapshotInternal(string? pair, int count = 1000) 126 | { 127 | BookLevel[]? parsed = null; 128 | var pairSafe = (pair ?? string.Empty).Trim().ToUpper(); 129 | var countSafe = count > 1000 ? 0 : count; 130 | var result = string.Empty; 131 | 132 | try 133 | { 134 | var url = $"/api/v1/orderBook/L2?symbol={pairSafe}&depth={countSafe}"; 135 | using HttpResponseMessage response = await _httpClient.GetAsync(url); 136 | using HttpContent content = response.Content; 137 | 138 | result = await content.ReadAsStringAsync(); 139 | parsed = JsonConvert.DeserializeObject(result); 140 | if (parsed == null || !parsed.Any()) 141 | return null; 142 | } 143 | catch (Exception e) 144 | { 145 | _client.Logger.LogDebug("[ORDER BOOK {exchangeName}] Failed to load orderbook snapshot for pair '{pair}'. " + 146 | "Error: '{error}'. Content: '{content}'", ExchangeName, pairSafe, e.Message, result); 147 | return null; 148 | } 149 | 150 | // received snapshot, convert and stream 151 | var levels = ConvertLevels(parsed); 152 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2) 153 | { 154 | ExchangeName = ExchangeName 155 | }; 156 | return bulk; 157 | } 158 | 159 | private OrderBookLevelBulk ConvertDiff(BookResponse response) 160 | { 161 | var action = ConvertAction(response.Action); 162 | var bulk = new OrderBookLevelBulk(action, ConvertLevels(response.Data), CryptoOrderBookType.L2) 163 | { 164 | ExchangeName = ExchangeName 165 | }; 166 | return bulk; 167 | } 168 | 169 | /// 170 | protected override OrderBookLevelBulk[] ConvertData(object[] data) 171 | { 172 | var result = new List(); 173 | foreach (var response in data) 174 | { 175 | var responseSafe = response as BookResponse; 176 | if (responseSafe == null) 177 | continue; 178 | 179 | var converted = ConvertDiff(responseSafe); 180 | result.Add(converted); 181 | } 182 | 183 | return result.ToArray(); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/OrderBooks/Sources/BitstampOrderBookSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Bitstamp.Client.Websocket.Client; 6 | using Bitstamp.Client.Websocket.Responses.Books; 7 | using Crypto.Websocket.Extensions.Core.Models; 8 | using Crypto.Websocket.Extensions.Core.OrderBooks; 9 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 10 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 11 | using Crypto.Websocket.Extensions.Core.Validations; 12 | 13 | namespace Crypto.Websocket.Extensions.OrderBooks.Sources 14 | { 15 | /// 16 | /// Bitstamp order book source - based only on 100 level snapshots (not diffs) 17 | /// 18 | public class BitstampOrderBookSource : OrderBookSourceBase 19 | { 20 | private BitstampWebsocketClient _client = null!; 21 | private IDisposable? _subscriptionSnapshot; 22 | 23 | /// 24 | public BitstampOrderBookSource(BitstampWebsocketClient client) : base(client.Logger) 25 | { 26 | ChangeClient(client); 27 | } 28 | 29 | /// 30 | public override string ExchangeName => "bitstamp"; 31 | 32 | /// 33 | /// Change client and resubscribe to the new streams 34 | /// 35 | public void ChangeClient(BitstampWebsocketClient client) 36 | { 37 | CryptoValidations.ValidateInput(client, nameof(client)); 38 | 39 | _client = client; 40 | _subscriptionSnapshot?.Dispose(); 41 | Subscribe(); 42 | } 43 | 44 | private void Subscribe() 45 | { 46 | _subscriptionSnapshot = _client.Streams.OrderBookStream.Subscribe(HandleSnapshot); 47 | } 48 | 49 | private void HandleSnapshot(OrderBookResponse response) 50 | { 51 | // received snapshot, convert and stream 52 | var levels = ConvertLevels(response); 53 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 54 | FillBulk(response, bulk); 55 | StreamSnapshot(bulk); 56 | } 57 | 58 | 59 | 60 | private OrderBookLevel[] ConvertLevels(OrderBookResponse response) 61 | { 62 | var bids = response.Data?.Bids 63 | .Select(x => ConvertLevel(x, CryptoOrderSide.Bid, response.Symbol)) 64 | .ToArray() ?? Array.Empty(); 65 | var asks = response.Data?.Asks 66 | .Select(x => ConvertLevel(x, CryptoOrderSide.Ask, response.Symbol)) 67 | .ToArray() ?? Array.Empty(); 68 | return bids.Concat(asks).ToArray(); 69 | } 70 | 71 | private OrderBookLevel ConvertLevel(BookLevel x, CryptoOrderSide side, string pair) 72 | { 73 | return new OrderBookLevel 74 | ( 75 | x.OrderId > 0 ? 76 | x.OrderId.ToString(CultureInfo.InvariantCulture) : 77 | x.Price.ToString(CultureInfo.InvariantCulture), 78 | side, 79 | x.Price, 80 | x.Amount, 81 | null, 82 | pair 83 | ); 84 | } 85 | 86 | /// 87 | protected override Task LoadSnapshotInternal(string? pair, int count = 1000) 88 | { 89 | return Task.FromResult(null); 90 | } 91 | 92 | private void FillBulk(OrderBookResponse? response, OrderBookLevelBulk bulk) 93 | { 94 | if (response == null) 95 | return; 96 | 97 | bulk.ExchangeName = ExchangeName; 98 | bulk.ServerTimestamp = response.Data?.Microtimestamp; 99 | } 100 | 101 | /// 102 | protected override OrderBookLevelBulk[] ConvertData(object[] data) 103 | { 104 | return Array.Empty(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/OrderBooks/Sources/BybitOrderBookSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Bybit.Client.Websocket.Client; 7 | using Bybit.Client.Websocket.Responses; 8 | using Crypto.Websocket.Extensions.Core.Models; 9 | using Crypto.Websocket.Extensions.Core.OrderBooks; 10 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 11 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 12 | using Crypto.Websocket.Extensions.Core.Validations; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace Crypto.Websocket.Extensions.OrderBooks.Sources 16 | { 17 | /// 18 | /// Bybit order book source - based on snapshot plus diffs 19 | /// 20 | public class BybitOrderBookSource : OrderBookSourceBase 21 | { 22 | private IBybitPublicWebsocketClient _client = null!; 23 | private IDisposable? _snapshotSubscription; 24 | private IDisposable? _updateSubscription; 25 | 26 | /// 27 | public BybitOrderBookSource(IBybitPublicWebsocketClient client, ILogger logger) : base(logger) 28 | { 29 | ChangeClient(client); 30 | } 31 | 32 | /// 33 | public override string ExchangeName => "bybit"; 34 | 35 | /// 36 | /// Change client and resubscribe to the new streams 37 | /// 38 | public void ChangeClient(IBybitPublicWebsocketClient client) 39 | { 40 | CryptoValidations.ValidateInput(client, nameof(client)); 41 | 42 | _client = client; 43 | _snapshotSubscription?.Dispose(); 44 | _updateSubscription?.Dispose(); 45 | Subscribe(); 46 | } 47 | 48 | /// 49 | public override void Dispose() 50 | { 51 | base.Dispose(); 52 | _snapshotSubscription?.Dispose(); 53 | } 54 | 55 | private void Subscribe() 56 | { 57 | _snapshotSubscription = _client.Streams.OrderBookSnapshotStream.Subscribe(HandleSnapshot); 58 | _updateSubscription = _client.Streams.OrderBookDeltaStream.Subscribe(HandleDelta); 59 | } 60 | 61 | private void HandleSnapshot(OrderBookSnapshotResponse response) 62 | { 63 | // received snapshot, convert and stream 64 | var levels = ConvertLevels(response); 65 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 66 | FillBulk(response, bulk); 67 | StreamSnapshot(bulk); 68 | } 69 | 70 | private void HandleDelta(OrderBookDeltaResponse response) 71 | { 72 | BufferData(response); 73 | } 74 | 75 | private static OrderBookLevel[] ConvertLevels(OrderBookResponse response) 76 | { 77 | var bids = response.Data.Bids 78 | .Select(x => ConvertLevel(x, CryptoOrderSide.Bid, response.Data.Symbol)) 79 | .ToArray(); 80 | var asks = response.Data.Asks 81 | .Select(x => ConvertLevel(x, CryptoOrderSide.Ask, response.Data.Symbol)) 82 | .ToArray(); 83 | return bids.Concat(asks).ToArray(); 84 | } 85 | 86 | private static OrderBookLevel ConvertLevel(double[] x, CryptoOrderSide side, string pair) 87 | { 88 | return new 89 | ( 90 | x[0].ToString(CultureInfo.InvariantCulture), 91 | side, 92 | x[0], 93 | x[1], 94 | null, 95 | pair 96 | ); 97 | } 98 | 99 | private static OrderBookAction RecognizeAction(OrderBookLevel level) => level.Amount > 0 ? OrderBookAction.Update : OrderBookAction.Delete; 100 | 101 | /// 102 | protected override Task LoadSnapshotInternal(string? pair, int count = 1000) => Task.FromResult(null); 103 | 104 | private IEnumerable ConvertDiff(OrderBookDeltaResponse response) 105 | { 106 | var levels = ConvertLevels(response); 107 | var group = levels.GroupBy(RecognizeAction).ToArray(); 108 | foreach (var actionGroup in group) 109 | { 110 | var bulk = new OrderBookLevelBulk(actionGroup.Key, actionGroup.ToArray(), CryptoOrderBookType.L2); 111 | FillBulk(response, bulk); 112 | yield return bulk; 113 | } 114 | } 115 | 116 | private void FillBulk(OrderBookResponse response, OrderBookLevelBulk bulk) 117 | { 118 | bulk.ExchangeName = ExchangeName; 119 | bulk.ServerTimestamp = response.CreatedTimestamp; 120 | bulk.ServerSequence = response.Data.Sequence; 121 | } 122 | 123 | /// 124 | protected override OrderBookLevelBulk[] ConvertData(object[] data) 125 | { 126 | var result = new List(); 127 | foreach (var response in data) 128 | { 129 | if (response is not OrderBookDeltaResponse orderBookDeltaResponse) 130 | continue; 131 | 132 | var converted = ConvertDiff(orderBookDeltaResponse); 133 | result.AddRange(converted); 134 | } 135 | 136 | return result.ToArray(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/OrderBooks/Sources/HuobiOrderBookSource.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Collections.Generic; 3 | //using System.Globalization; 4 | //using System.Linq; 5 | //using System.Threading.Tasks; 6 | //using Crypto.Websocket.Extensions.Core.Models; 7 | //using Crypto.Websocket.Extensions.Core.OrderBooks; 8 | //using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 9 | //using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 10 | //using Crypto.Websocket.Extensions.Core.Validations; 11 | //using Crypto.Websocket.Extensions.Logging; 12 | //using Huobi.Client.Websocket.Clients; 13 | //using Huobi.Client.Websocket.Messages.MarketData; 14 | //using Huobi.Client.Websocket.Messages.MarketData.MarketByPrice; 15 | //using Huobi.Client.Websocket.Messages.MarketData.Values; 16 | 17 | //namespace Crypto.Websocket.Extensions.OrderBooks.Sources 18 | //{ 19 | // /// 20 | // /// Bitstamp order book source - based only on 100 level snapshots (not diffs) 21 | // /// 22 | // public class HuobiOrderBookSource : OrderBookSourceBase 23 | // { 24 | // private static readonly ILog Log = LogProvider.GetCurrentClassLogger(); 25 | 26 | // private IHuobiMarketByPriceWebsocketClient _client; 27 | // private IDisposable _subscription; 28 | // private IDisposable _subscriptionSnapshot; 29 | 30 | // /// 31 | // public HuobiOrderBookSource(IHuobiMarketByPriceWebsocketClient client) 32 | // { 33 | // ChangeClient(client); 34 | // } 35 | 36 | // /// 37 | // public override string ExchangeName => "huobi"; 38 | 39 | // /// 40 | // /// Change client and resubscribe to the new streams 41 | // /// 42 | // public void ChangeClient(IHuobiMarketByPriceWebsocketClient client) 43 | // { 44 | // CryptoValidations.ValidateInput(client, nameof(client)); 45 | 46 | // _client = client; 47 | // _subscriptionSnapshot?.Dispose(); 48 | // _subscription?.Dispose(); 49 | // Subscribe(); 50 | // } 51 | 52 | // protected override OrderBookLevelBulk[] ConvertData(object[] data) 53 | // { 54 | // var result = new List(); 55 | 56 | // foreach (var item in data) 57 | // { 58 | // if (item is MarketByPriceUpdateMessage message) 59 | // { 60 | // var levels = ConvertLevels(message); 61 | // var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 62 | // FillBulk(message, bulk); 63 | // result.Add(bulk); 64 | // } 65 | // } 66 | 67 | // return result.ToArray(); 68 | // } 69 | 70 | // protected override Task LoadSnapshotInternal(string pair, int count) 71 | // { 72 | // // TODO: how to simulate and what to do with count? 73 | 74 | // // request a snapshot which will be received by Handle method 75 | // var request = new MarketByPricePullRequest("pX", pair, 20); 76 | // _client.Send(request); 77 | 78 | // // nothing to stream here... 79 | // return Task.FromResult(default(OrderBookLevelBulk)); 80 | // } 81 | 82 | // private void Subscribe() 83 | // { 84 | // _subscription = _client.Streams.MarketByPriceUpdateStream.Subscribe(HandleUpdate); 85 | // _subscriptionSnapshot = _client.Streams.MarketByPricePullStream.Subscribe(HandleSnapshot); 86 | // } 87 | 88 | // private void HandleUpdate(MarketByPriceUpdateMessage response) 89 | // { 90 | // // TODO: how to force refresh when sequence number does not match? 91 | // BufferData(response); 92 | // } 93 | 94 | // private void HandleSnapshot(MarketByPricePullResponse response) 95 | // { 96 | // // received snapshot, convert and stream 97 | // var levels = ConvertLevels(response); 98 | // var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 99 | // FillBulk(response, bulk); 100 | // StreamSnapshot(bulk); 101 | // } 102 | 103 | // private OrderBookLevel[] ConvertLevels(MarketByPriceUpdateMessage response) 104 | // { 105 | // var symbol = response.ParseSymbolFromTopic(); 106 | // return ConvertLevels(symbol, response.Tick?.Bids, response.Tick?.Asks); 107 | // } 108 | 109 | // private OrderBookLevel[] ConvertLevels(MarketByPricePullResponse response) 110 | // { 111 | // var symbol = response.ParseSymbolFromTopic(); 112 | // return ConvertLevels(symbol, response.Data?.Bids, response.Data?.Asks); 113 | // } 114 | 115 | // private OrderBookLevel[] ConvertLevels(string symbol, BookLevel[] bids, BookLevel[] asks) 116 | // { 117 | // var convertedBids = bids?.Select(x => ConvertLevel(x, CryptoOrderSide.Bid, symbol)).ToArray() 118 | // ?? Array.Empty(); 119 | // var convertedAsks = asks?.Select(x => ConvertLevel(x, CryptoOrderSide.Ask, symbol)).ToArray() 120 | // ?? Array.Empty(); 121 | // return convertedBids.Concat(convertedAsks).ToArray(); 122 | // } 123 | 124 | // private OrderBookLevel ConvertLevel(BookLevel x, CryptoOrderSide side, string pair) 125 | // { 126 | // return new OrderBookLevel( 127 | // x.Price.ToString(CultureInfo.InvariantCulture), 128 | // side, 129 | // (double)x.Price, 130 | // (double)x.Size, 131 | // null, // TODO: really null? 132 | // pair); 133 | // } 134 | 135 | // private void FillBulk(MarketByPriceUpdateMessage message, OrderBookLevelBulk bulk) 136 | // { 137 | // if (message == null) 138 | // { 139 | // return; 140 | // } 141 | 142 | // bulk.ExchangeName = ExchangeName; 143 | // bulk.ServerTimestamp = message.Timestamp.DateTime; 144 | // } 145 | 146 | // private void FillBulk(MarketByPricePullResponse response, OrderBookLevelBulk bulk) 147 | // { 148 | // if (response == null) 149 | // { 150 | // return; 151 | // } 152 | 153 | // bulk.ExchangeName = ExchangeName; 154 | // bulk.ServerTimestamp = response.Timestamp.DateTime; 155 | // } 156 | // } 157 | //} -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/OrderBooks/Sources/ValrOrderBookSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Crypto.Websocket.Extensions.Core.Models; 7 | using Crypto.Websocket.Extensions.Core.OrderBooks; 8 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 9 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 10 | using Crypto.Websocket.Extensions.Core.Validations; 11 | using Microsoft.Extensions.Logging; 12 | using Valr.Client.Websocket.Client; 13 | using Valr.Client.Websocket.Responses; 14 | 15 | namespace Crypto.Websocket.Extensions.OrderBooks.Sources 16 | { 17 | /// 18 | /// Valr order book source - based on snapshot plus diffs 19 | /// 20 | public class ValrOrderBookSource : OrderBookSourceBase 21 | { 22 | private IValrTradeWebsocketClient _client = null!; 23 | private IDisposable? _snapshotSubscription; 24 | private IDisposable? _updateSubscription; 25 | 26 | /// 27 | public ValrOrderBookSource(IValrTradeWebsocketClient client, ILogger logger) : base(logger) 28 | { 29 | ChangeClient(client); 30 | } 31 | 32 | /// 33 | public override string ExchangeName => "valr"; 34 | 35 | /// 36 | /// Change client and resubscribe to the new streams 37 | /// 38 | public void ChangeClient(IValrTradeWebsocketClient client) 39 | { 40 | CryptoValidations.ValidateInput(client, nameof(client)); 41 | 42 | _client = client; 43 | _snapshotSubscription?.Dispose(); 44 | _updateSubscription?.Dispose(); 45 | Subscribe(); 46 | } 47 | 48 | /// 49 | public override void Dispose() 50 | { 51 | base.Dispose(); 52 | _snapshotSubscription?.Dispose(); 53 | } 54 | 55 | private void Subscribe() 56 | { 57 | _snapshotSubscription = _client.Streams.L1OrderBookSnapshotStream.Subscribe(HandleSnapshot); 58 | _updateSubscription = _client.Streams.L1OrderBookUpdateStream.Subscribe(HandleUpdate); 59 | } 60 | 61 | private void HandleSnapshot(L1TrackedOrderBookResponse response) 62 | { 63 | // received snapshot, convert and stream 64 | var levels = ConvertLevels(response); 65 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 66 | FillBulk(response, bulk); 67 | StreamSnapshot(bulk); 68 | } 69 | 70 | private void HandleUpdate(L1TrackedOrderBookResponse response) 71 | { 72 | BufferData(response); 73 | } 74 | 75 | private static OrderBookLevel[] ConvertLevels(L1TrackedOrderBookResponse response) 76 | { 77 | var bids = response.Data.Bids 78 | .Select(x => ConvertLevel(x, CryptoOrderSide.Bid, response.CurrencyPairSymbol)) 79 | .ToArray(); 80 | var asks = response.Data.Asks 81 | .Select(x => ConvertLevel(x, CryptoOrderSide.Ask, response.CurrencyPairSymbol)) 82 | .ToArray(); 83 | return bids.Concat(asks).ToArray(); 84 | } 85 | 86 | private static OrderBookLevel ConvertLevel(L1Quote x, CryptoOrderSide side, string pair) 87 | { 88 | return new 89 | ( 90 | x.Price.ToString(CultureInfo.InvariantCulture), 91 | side, 92 | x.Price, 93 | x.Quantity, 94 | null, 95 | pair 96 | ); 97 | } 98 | 99 | private OrderBookAction RecognizeAction(OrderBookLevel level) 100 | { 101 | if (level.Amount > 0) 102 | return OrderBookAction.Update; 103 | return OrderBookAction.Delete; 104 | } 105 | 106 | /// 107 | protected override Task LoadSnapshotInternal(string? pair, int count = 1000) 108 | { 109 | return Task.FromResult(null); 110 | } 111 | 112 | private IEnumerable ConvertDiff(L1TrackedOrderBookResponse response) 113 | { 114 | var levels = ConvertLevels(response); 115 | var group = levels.GroupBy(RecognizeAction).ToArray(); 116 | foreach (var actionGroup in group) 117 | { 118 | var bulk = new OrderBookLevelBulk(actionGroup.Key, actionGroup.ToArray(), CryptoOrderBookType.L2); 119 | FillBulk(response, bulk); 120 | yield return bulk; 121 | } 122 | } 123 | 124 | private void FillBulk(L1TrackedOrderBookResponse response, OrderBookLevelBulk bulk) 125 | { 126 | bulk.ExchangeName = ExchangeName; 127 | bulk.ServerTimestamp = response.Data.LastChange; 128 | bulk.ServerSequence = response.Data.SequenceNumber; 129 | } 130 | 131 | /// 132 | protected override OrderBookLevelBulk[] ConvertData(object[] data) 133 | { 134 | var result = new List(); 135 | foreach (var response in data) 136 | { 137 | var responseSafe = response as L1TrackedOrderBookResponse; 138 | if (responseSafe == null) 139 | continue; 140 | 141 | var converted = ConvertDiff(responseSafe); 142 | result.AddRange(converted); 143 | } 144 | 145 | return result.ToArray(); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/OrderBooks/SourcesL3/BitfinexOrderBookL3Source.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Bitfinex.Client.Websocket.Client; 8 | using Bitfinex.Client.Websocket.Responses; 9 | using Bitfinex.Client.Websocket.Responses.Books; 10 | using Crypto.Websocket.Extensions.Core.Models; 11 | using Crypto.Websocket.Extensions.Core.OrderBooks; 12 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 13 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 14 | using Crypto.Websocket.Extensions.Core.Validations; 15 | using Microsoft.Extensions.Logging; 16 | using Newtonsoft.Json; 17 | 18 | namespace Crypto.Websocket.Extensions.OrderBooks.SourcesL3 19 | { 20 | /// 21 | public class BitfinexOrderBookL3Source : OrderBookSourceBase 22 | { 23 | private readonly HttpClient _httpClient = new HttpClient(); 24 | private BitfinexWebsocketClient _client = null!; 25 | private IDisposable? _subscription; 26 | private IDisposable? _subscriptionSnapshot; 27 | 28 | 29 | /// 30 | public BitfinexOrderBookL3Source(BitfinexWebsocketClient client) : base(client.Logger) 31 | { 32 | _httpClient.BaseAddress = new Uri("https://api-pub.bitfinex.com"); 33 | 34 | ChangeClient(client); 35 | } 36 | 37 | /// 38 | public override string ExchangeName => "bitfinex"; 39 | 40 | /// 41 | /// Change client and resubscribe to the new streams 42 | /// 43 | public void ChangeClient(BitfinexWebsocketClient client) 44 | { 45 | CryptoValidations.ValidateInput(client, nameof(client)); 46 | 47 | _client = client; 48 | _subscriptionSnapshot?.Dispose(); 49 | _subscription?.Dispose(); 50 | Subscribe(); 51 | } 52 | 53 | private void Subscribe() 54 | { 55 | _subscriptionSnapshot = _client.Streams.RawBookSnapshotStream.Subscribe(HandleSnapshot); 56 | _subscription = _client.Streams.RawBookStream.Subscribe(HandleBook); 57 | } 58 | 59 | private void HandleSnapshot(RawBook[] books) 60 | { 61 | // received snapshot, convert and stream 62 | var levels = ConvertLevels(books); 63 | var last = books.LastOrDefault(); 64 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L3); 65 | FillBulk(last, bulk); 66 | StreamSnapshot(bulk); 67 | } 68 | 69 | private void HandleBook(RawBook book) 70 | { 71 | BufferData(book); 72 | } 73 | 74 | private OrderBookLevel[] ConvertLevels(RawBook[] data) 75 | { 76 | return data 77 | .Select(ConvertLevel) 78 | .ToArray(); 79 | } 80 | 81 | private OrderBookLevel ConvertLevel(RawBook x) 82 | { 83 | return new OrderBookLevel 84 | ( 85 | x.OrderId.ToString(CultureInfo.InvariantCulture), 86 | ConvertSide(x.Amount), 87 | x.Price, 88 | x.Amount, 89 | null, 90 | x.Pair 91 | ); 92 | } 93 | 94 | private CryptoOrderSide ConvertSide(double amount) 95 | { 96 | if (amount > 0) 97 | return CryptoOrderSide.Bid; 98 | if (amount < 0) 99 | return CryptoOrderSide.Ask; 100 | return CryptoOrderSide.Undefined; 101 | } 102 | 103 | private OrderBookAction RecognizeAction(RawBook book) 104 | { 105 | if (book.Price > 0) 106 | return OrderBookAction.Update; 107 | return OrderBookAction.Delete; 108 | } 109 | 110 | /// 111 | protected override async Task LoadSnapshotInternal(string? pair, int count = 1000) 112 | { 113 | RawBook[]? parsed = null; 114 | var pairSafe = (pair ?? string.Empty).Trim().ToUpper(); 115 | pairSafe = $"t{pairSafe}"; 116 | var countSafe = count > 100 ? 100 : count; 117 | var result = string.Empty; 118 | 119 | try 120 | { 121 | var url = $"/v2/book/{pairSafe}/R0?len={countSafe}"; 122 | using HttpResponseMessage response = await _httpClient.GetAsync(url); 123 | using HttpContent content = response.Content; 124 | 125 | result = await content.ReadAsStringAsync(); 126 | parsed = JsonConvert.DeserializeObject(result); 127 | if (parsed == null || !parsed.Any()) 128 | return null; 129 | 130 | foreach (var book in parsed) 131 | { 132 | book.Pair = pair ?? string.Empty; 133 | } 134 | } 135 | catch (Exception e) 136 | { 137 | _client.Logger.LogDebug("[ORDER BOOK {exchangeName}] Failed to load L3 orderbook snapshot for pair '{pair}'. " + 138 | "Error: '{error}'. Content: '{content}'", ExchangeName, pairSafe, e.Message, result); 139 | return null; 140 | } 141 | 142 | var levels = ConvertLevels(parsed); 143 | var last = parsed.LastOrDefault(); 144 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L3); 145 | FillBulk(last, bulk); 146 | return bulk; 147 | } 148 | 149 | private OrderBookLevelBulk ConvertDiff(RawBook book) 150 | { 151 | var converted = ConvertLevel(book); 152 | var action = RecognizeAction(book); 153 | var bulk = new OrderBookLevelBulk(action, new[] { converted }, CryptoOrderBookType.L3); 154 | FillBulk(book, bulk); 155 | return bulk; 156 | } 157 | 158 | private void FillBulk(ResponseBase? response, OrderBookLevelBulk bulk) 159 | { 160 | if (response == null) 161 | return; 162 | 163 | bulk.ExchangeName = ExchangeName; 164 | bulk.ServerTimestamp = response.ServerTimestamp; 165 | bulk.ServerSequence = response.ServerSequence; 166 | } 167 | 168 | /// 169 | protected override OrderBookLevelBulk[] ConvertData(object[] data) 170 | { 171 | var result = new List(); 172 | foreach (var response in data) 173 | { 174 | var responseSafe = response as RawBook; 175 | if (responseSafe == null) 176 | continue; 177 | 178 | var converted = ConvertDiff(responseSafe); 179 | result.Add(converted); 180 | } 181 | 182 | return result.ToArray(); 183 | } 184 | 185 | 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Positions/Sources/BitmexPositionSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using Bitmex.Client.Websocket.Client; 6 | using Bitmex.Client.Websocket.Responses.Positions; 7 | using Bitmex.Client.Websocket.Utils; 8 | using Crypto.Websocket.Extensions.Core.Models; 9 | using Crypto.Websocket.Extensions.Core.Positions.Models; 10 | using Crypto.Websocket.Extensions.Core.Positions.Sources; 11 | using Crypto.Websocket.Extensions.Core.Utils; 12 | using Crypto.Websocket.Extensions.Core.Validations; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace Crypto.Websocket.Extensions.Positions.Sources 16 | { 17 | /// 18 | /// Bitmex positions source 19 | /// 20 | public class BitmexPositionSource : PositionSourceBase 21 | { 22 | private readonly ConcurrentDictionary _positions = new ConcurrentDictionary(); 23 | 24 | private BitmexWebsocketClient _client = null!; 25 | private IDisposable? _subscription; 26 | 27 | /// 28 | public BitmexPositionSource(BitmexWebsocketClient client) 29 | { 30 | ChangeClient(client); 31 | } 32 | 33 | /// 34 | public override string ExchangeName => "bitmex"; 35 | 36 | /// 37 | /// Change client and resubscribe to the new streams 38 | /// 39 | public void ChangeClient(BitmexWebsocketClient client) 40 | { 41 | CryptoValidations.ValidateInput(client, nameof(client)); 42 | 43 | _client = client; 44 | _subscription?.Dispose(); 45 | Subscribe(); 46 | } 47 | 48 | private void Subscribe() 49 | { 50 | _subscription = _client.Streams.PositionStream 51 | .Where(x => x?.Data != null && x.Data.Any()) 52 | .Subscribe(HandleSafe); 53 | } 54 | 55 | private void HandleSafe(PositionResponse response) 56 | { 57 | try 58 | { 59 | Handle(response); 60 | } 61 | catch (Exception e) 62 | { 63 | _client.Logger.LogError(e, "[Bitmex] Failed to handle position info, error: '{error}'", e.Message); 64 | } 65 | } 66 | 67 | private void Handle(PositionResponse response) 68 | { 69 | PositionsSubject.OnNext(Convert(response.Data)); 70 | } 71 | 72 | private CryptoPosition[] Convert(Position[] positions) 73 | { 74 | return positions.Select(Convert).ToArray(); 75 | } 76 | 77 | private CryptoPosition Convert(Position position) 78 | { 79 | var key = GetPositionKey(position); 80 | var existing = _positions.ContainsKey(key) ? _positions[key] : null; 81 | 82 | var currency = position.Currency ?? "XBt"; 83 | 84 | var current = new CryptoPosition() 85 | { 86 | Pair = position.Symbol ?? existing?.Pair, 87 | CurrentTimestamp = position.CurrentTimestamp ?? existing?.CurrentTimestamp, 88 | OpeningTimestamp = position.OpeningTimestamp ?? existing?.OpeningTimestamp, 89 | 90 | EntryPrice = position.AvgEntryPrice ?? existing?.EntryPrice ?? 0, 91 | LastPrice = position.LastPrice ?? existing?.LastPrice ?? 0, 92 | MarkPrice = position.MarkPrice ?? existing?.MarkPrice ?? 0, 93 | LiquidationPrice = position.LiquidationPrice ?? existing?.LiquidationPrice ?? 0, 94 | 95 | Amount = position.HomeNotional ?? existing?.Amount ?? 0, 96 | AmountQuote = position.CurrentQty ?? existing?.AmountQuote ?? 0, 97 | 98 | Side = ConvertSide(position.CurrentQty ?? existing?.AmountQuote), 99 | 100 | Leverage = position.Leverage ?? existing?.Leverage, 101 | RealizedPnl = ConvertToBtc(currency, position.RealisedPnl) ?? existing?.RealizedPnl, 102 | UnrealizedPnl = ConvertToBtc(currency, position.UnrealisedPnl) ?? existing?.UnrealizedPnl, 103 | }; 104 | 105 | _positions[key] = current; 106 | return current; 107 | } 108 | 109 | private CryptoPositionSide ConvertSide(double? amount) 110 | { 111 | if (!amount.HasValue || CryptoMathUtils.IsSame(amount.Value, 0)) 112 | return CryptoPositionSide.Undefined; 113 | return amount.Value >= 0 ? CryptoPositionSide.Long : CryptoPositionSide.Short; 114 | } 115 | 116 | private double? ConvertToBtc(string currency, double? value) 117 | { 118 | if (!value.HasValue) 119 | return null; 120 | 121 | return BitmexConverter.ConvertToBtc(currency, value.Value); 122 | } 123 | 124 | private string GetPositionKey(Position position) 125 | { 126 | return $"{position.Symbol}-{position.Account}"; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Trades/Sources/BinanceTradeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using Binance.Client.Websocket.Client; 4 | using Binance.Client.Websocket.Responses.Trades; 5 | using Crypto.Websocket.Extensions.Core.Models; 6 | using Crypto.Websocket.Extensions.Core.Trades.Models; 7 | using Crypto.Websocket.Extensions.Core.Trades.Sources; 8 | using Crypto.Websocket.Extensions.Core.Validations; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Crypto.Websocket.Extensions.Trades.Sources 12 | { 13 | /// 14 | /// Binance trades source 15 | /// 16 | public class BinanceTradeSource : TradeSourceBase 17 | { 18 | private BinanceWebsocketClient _client = null!; 19 | private IDisposable? _subscription; 20 | 21 | /// 22 | public BinanceTradeSource(BinanceWebsocketClient client) 23 | { 24 | ChangeClient(client); 25 | } 26 | 27 | /// 28 | public override string ExchangeName => "binance"; 29 | 30 | /// 31 | /// Change client and resubscribe to the new streams 32 | /// 33 | public void ChangeClient(BinanceWebsocketClient client) 34 | { 35 | CryptoValidations.ValidateInput(client, nameof(client)); 36 | 37 | _client = client; 38 | _subscription?.Dispose(); 39 | Subscribe(); 40 | } 41 | 42 | private void Subscribe() 43 | { 44 | _subscription = _client.Streams.TradesStream 45 | .Where(x => x?.Data != null) 46 | .Subscribe(HandleTradeSafe); 47 | } 48 | 49 | private void HandleTradeSafe(TradeResponse response) 50 | { 51 | try 52 | { 53 | HandleTrade(response); 54 | } 55 | catch (Exception e) 56 | { 57 | _client.Logger.LogError(e, "[Binance] Failed to handle trade info, error: '{error}'", e.Message); 58 | } 59 | } 60 | 61 | private void HandleTrade(TradeResponse response) 62 | { 63 | TradesSubject.OnNext(new[] { ConvertTrade(response.Data) }); 64 | } 65 | 66 | private CryptoTrade ConvertTrade(Trade trade) 67 | { 68 | var data = new CryptoTrade() 69 | { 70 | Amount = trade.Quantity, 71 | AmountQuote = trade.Quantity * trade.Price, 72 | Side = ConvertSide(trade.Side), 73 | Id = trade.TradeId.ToString(), 74 | Price = trade.Price, 75 | Timestamp = trade.TradeTime, 76 | Pair = trade.Symbol, 77 | MakerOrderId = trade.IsBuyerMaker ? trade.BuyerOrderId.ToString() : trade.SellerOrderId.ToString(), 78 | TakerOrderId = trade.IsBuyerMaker ? trade.SellerOrderId.ToString() : trade.BuyerOrderId.ToString(), 79 | 80 | ExchangeName = ExchangeName, 81 | ServerTimestamp = trade.EventTime 82 | }; 83 | return data; 84 | } 85 | 86 | private CryptoTradeSide ConvertSide(TradeSide tradeSide) 87 | { 88 | return tradeSide == TradeSide.Buy ? CryptoTradeSide.Buy : CryptoTradeSide.Sell; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Trades/Sources/BitfinexTradeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using Bitfinex.Client.Websocket.Client; 4 | using Bitfinex.Client.Websocket.Responses.Trades; 5 | using Crypto.Websocket.Extensions.Core.Models; 6 | using Crypto.Websocket.Extensions.Core.Trades.Models; 7 | using Crypto.Websocket.Extensions.Core.Trades.Sources; 8 | using Crypto.Websocket.Extensions.Core.Validations; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Crypto.Websocket.Extensions.Trades.Sources 12 | { 13 | /// 14 | /// Bitfinex trades source 15 | /// 16 | public class BitfinexTradeSource : TradeSourceBase 17 | { 18 | private BitfinexWebsocketClient _client = null!; 19 | private IDisposable? _subscription; 20 | 21 | /// 22 | public BitfinexTradeSource(BitfinexWebsocketClient client) 23 | { 24 | ChangeClient(client); 25 | } 26 | 27 | /// 28 | public override string ExchangeName => "bitfinex"; 29 | 30 | /// 31 | /// Change client and resubscribe to the new streams 32 | /// 33 | public void ChangeClient(BitfinexWebsocketClient client) 34 | { 35 | CryptoValidations.ValidateInput(client, nameof(client)); 36 | 37 | _client = client; 38 | _subscription?.Dispose(); 39 | Subscribe(); 40 | } 41 | 42 | private void Subscribe() 43 | { 44 | _subscription = _client.Streams.TradesStream 45 | .Where(x => x != null && x.Type == TradeType.Executed) 46 | .Subscribe(HandleTradeSafe); 47 | } 48 | 49 | private void HandleTradeSafe(Trade response) 50 | { 51 | try 52 | { 53 | HandleTrade(response); 54 | } 55 | catch (Exception e) 56 | { 57 | _client.Logger.LogError(e, "[Bitfinex] Failed to handle trade info, error: '{error}'", e.Message); 58 | } 59 | } 60 | 61 | private void HandleTrade(Trade response) 62 | { 63 | TradesSubject.OnNext(new[] { ConvertTrade(response) }); 64 | } 65 | 66 | private CryptoTrade ConvertTrade(Trade trade) 67 | { 68 | var data = new CryptoTrade() 69 | { 70 | Amount = trade.Amount, 71 | AmountQuote = trade.Amount * trade.Price, 72 | Side = ConvertSide(trade.Amount), 73 | Id = trade.Id.ToString(), 74 | Price = trade.Price, 75 | Timestamp = trade.Mts, 76 | Pair = trade.Pair, 77 | 78 | ExchangeName = ExchangeName, 79 | ServerSequence = trade.ServerSequence, 80 | ServerTimestamp = trade.ServerTimestamp 81 | }; 82 | return data; 83 | } 84 | 85 | private CryptoTradeSide ConvertSide(double amount) 86 | { 87 | return amount >= 0 ? CryptoTradeSide.Buy : CryptoTradeSide.Sell; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Trades/Sources/BitmexTradeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using Bitmex.Client.Websocket.Client; 5 | using Bitmex.Client.Websocket.Responses; 6 | using Bitmex.Client.Websocket.Responses.Trades; 7 | using Crypto.Websocket.Extensions.Core.Models; 8 | using Crypto.Websocket.Extensions.Core.Trades.Models; 9 | using Crypto.Websocket.Extensions.Core.Trades.Sources; 10 | using Crypto.Websocket.Extensions.Core.Validations; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Crypto.Websocket.Extensions.Trades.Sources 14 | { 15 | /// 16 | /// Bitmex trades source 17 | /// 18 | public class BitmexTradeSource : TradeSourceBase 19 | { 20 | private BitmexWebsocketClient _client; 21 | private IDisposable _subscription; 22 | 23 | /// 24 | public BitmexTradeSource(BitmexWebsocketClient client) 25 | { 26 | ChangeClient(client); 27 | } 28 | 29 | /// 30 | public override string ExchangeName => "bitmex"; 31 | 32 | /// 33 | /// Change client and resubscribe to the new streams 34 | /// 35 | public void ChangeClient(BitmexWebsocketClient client) 36 | { 37 | CryptoValidations.ValidateInput(client, nameof(client)); 38 | 39 | _client = client; 40 | _subscription?.Dispose(); 41 | Subscribe(); 42 | } 43 | 44 | private void Subscribe() 45 | { 46 | _subscription = _client.Streams.TradesStream 47 | .Where(x => x?.Data != null && x.Data.Any()) 48 | .Subscribe(HandleTradeSafe); 49 | } 50 | 51 | private void HandleTradeSafe(TradeResponse response) 52 | { 53 | try 54 | { 55 | HandleTrade(response); 56 | } 57 | catch (Exception e) 58 | { 59 | _client.Logger.LogError(e, "[Bitmex] Failed to handle trade info, error: '{error}'", e.Message); 60 | } 61 | } 62 | 63 | private void HandleTrade(TradeResponse response) 64 | { 65 | TradesSubject.OnNext(ConvertTrades(response.Data)); 66 | } 67 | 68 | private CryptoTrade[] ConvertTrades(Trade[] trades) 69 | { 70 | return trades.Select(ConvertTrade).ToArray(); 71 | } 72 | 73 | private CryptoTrade ConvertTrade(Trade trade) 74 | { 75 | var data = new CryptoTrade() 76 | { 77 | Amount = trade.Size / trade.Price, 78 | AmountQuote = trade.Size, 79 | Side = ConvertSide(trade.Side), 80 | Id = trade.TrdMatchId, 81 | Price = trade.Price, 82 | Timestamp = trade.Timestamp, 83 | Pair = trade.Symbol, 84 | 85 | ExchangeName = ExchangeName 86 | }; 87 | return data; 88 | } 89 | 90 | private CryptoTradeSide ConvertSide(BitmexSide tradeSide) 91 | { 92 | if (tradeSide == BitmexSide.Undefined) 93 | return CryptoTradeSide.Undefined; 94 | return tradeSide == BitmexSide.Buy ? CryptoTradeSide.Buy : CryptoTradeSide.Sell; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Trades/Sources/BitstampTradeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Reactive.Linq; 4 | using Bitstamp.Client.Websocket.Client; 5 | using Bitstamp.Client.Websocket.Responses; 6 | using Bitstamp.Client.Websocket.Responses.Trades; 7 | using Crypto.Websocket.Extensions.Core.Models; 8 | using Crypto.Websocket.Extensions.Core.Trades.Models; 9 | using Crypto.Websocket.Extensions.Core.Trades.Sources; 10 | using Crypto.Websocket.Extensions.Core.Validations; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Crypto.Websocket.Extensions.Trades.Sources 14 | { 15 | /// 16 | /// Bitstamp trade source 17 | /// 18 | public class BitstampTradeSource : TradeSourceBase 19 | { 20 | private BitstampWebsocketClient _client = null!; 21 | private IDisposable? _subscription; 22 | 23 | /// 24 | public BitstampTradeSource(BitstampWebsocketClient client) 25 | { 26 | ChangeClient(client); 27 | } 28 | 29 | /// 30 | public override string ExchangeName => "bitstamp"; 31 | 32 | /// 33 | /// Change client and resubscribe to the new streams 34 | /// 35 | public void ChangeClient(BitstampWebsocketClient client) 36 | { 37 | CryptoValidations.ValidateInput(client, nameof(client)); 38 | 39 | _client = client; 40 | _subscription?.Dispose(); 41 | Subscribe(); 42 | } 43 | 44 | private void Subscribe() 45 | { 46 | _subscription = _client.Streams.TickerStream 47 | .Where(x => x?.Data != null && x.Data.Side != TradeSide.Undefined) 48 | .Subscribe(HandleTradeSafe); 49 | } 50 | 51 | private void HandleTradeSafe(TradeResponse response) 52 | { 53 | try 54 | { 55 | HandleTrade(response); 56 | } 57 | catch (Exception e) 58 | { 59 | _client.Logger.LogError(e, "[Bitstamp] Failed to handle trade info, error: '{error}'", e.Message); 60 | } 61 | } 62 | 63 | private void HandleTrade(TradeResponse response) 64 | { 65 | TradesSubject.OnNext(new[] { ConvertTrade(response) }); 66 | } 67 | 68 | private CryptoTrade ConvertTrade(TradeResponse trade) 69 | { 70 | var data = trade.Data; 71 | 72 | var buyId = data.BuyOrderId.ToString(CultureInfo.InvariantCulture); 73 | var sellId = data.SellOrderId.ToString(CultureInfo.InvariantCulture); 74 | 75 | var result = new CryptoTrade() 76 | { 77 | Amount = data.Amount, 78 | AmountQuote = data.Amount * data.Price, 79 | Side = ConvertSide(data.Side), 80 | Id = data.Id.ToString(CultureInfo.InvariantCulture), 81 | Price = data.Price, 82 | Timestamp = data.Microtimestamp, 83 | Pair = trade.Symbol, 84 | 85 | ExchangeName = ExchangeName, 86 | ServerTimestamp = data.Microtimestamp, 87 | 88 | MakerOrderId = data.Side == TradeSide.Buy ? sellId : buyId, 89 | TakerOrderId = data.Side == TradeSide.Sell ? sellId : buyId 90 | }; 91 | return result; 92 | } 93 | 94 | private CryptoTradeSide ConvertSide(TradeSide side) 95 | { 96 | return side == TradeSide.Buy ? CryptoTradeSide.Buy : CryptoTradeSide.Sell; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Trades/Sources/CoinbaseTradeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using Coinbase.Client.Websocket.Client; 4 | using Coinbase.Client.Websocket.Responses.Trades; 5 | using Crypto.Websocket.Extensions.Core.Models; 6 | using Crypto.Websocket.Extensions.Core.Trades.Models; 7 | using Crypto.Websocket.Extensions.Core.Trades.Sources; 8 | using Crypto.Websocket.Extensions.Core.Validations; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Crypto.Websocket.Extensions.Trades.Sources 12 | { 13 | /// 14 | /// Coinbase trades source 15 | /// 16 | public class CoinbaseTradeSource : TradeSourceBase 17 | { 18 | private CoinbaseWebsocketClient _client = null!; 19 | private IDisposable? _subscription; 20 | 21 | /// 22 | public CoinbaseTradeSource(CoinbaseWebsocketClient client) 23 | { 24 | ChangeClient(client); 25 | } 26 | 27 | /// 28 | public override string ExchangeName => "coinbase"; 29 | 30 | /// 31 | /// Change client and resubscribe to the new streams 32 | /// 33 | public void ChangeClient(CoinbaseWebsocketClient client) 34 | { 35 | CryptoValidations.ValidateInput(client, nameof(client)); 36 | 37 | _client = client; 38 | _subscription?.Dispose(); 39 | Subscribe(); 40 | } 41 | 42 | private void Subscribe() 43 | { 44 | _subscription = _client.Streams.TradesStream 45 | .Where(x => x != null) 46 | .Subscribe(HandleTradeSafe); 47 | } 48 | 49 | private void HandleTradeSafe(TradeResponse response) 50 | { 51 | try 52 | { 53 | HandleTrade(response); 54 | } 55 | catch (Exception e) 56 | { 57 | _client.Logger.LogError(e, "[Coinbase] Failed to handle trade info, error: '{error}'", e.Message); 58 | } 59 | } 60 | 61 | private void HandleTrade(TradeResponse response) 62 | { 63 | TradesSubject.OnNext(new[] { ConvertTrade(response) }); 64 | } 65 | 66 | private CryptoTrade ConvertTrade(TradeResponse trade) 67 | { 68 | var data = new CryptoTrade() 69 | { 70 | Amount = trade.Size, 71 | AmountQuote = trade.Size * trade.Price, 72 | Side = ConvertSide(trade.TradeSide), 73 | Id = trade.TradeId.ToString(), 74 | Price = trade.Price, 75 | Timestamp = trade.Time, 76 | Pair = trade.ProductId, 77 | MakerOrderId = trade.MakerOrderId, 78 | TakerOrderId = trade.TakerOrderId, 79 | 80 | ExchangeName = ExchangeName, 81 | ServerSequence = trade.Sequence, 82 | ServerTimestamp = trade.Time 83 | }; 84 | return data; 85 | } 86 | 87 | private CryptoTradeSide ConvertSide(TradeSide tradeSide) 88 | { 89 | if (tradeSide == TradeSide.Undefined) 90 | return CryptoTradeSide.Undefined; 91 | return tradeSide == TradeSide.Buy ? CryptoTradeSide.Buy : CryptoTradeSide.Sell; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Trades/Sources/HuobiTradeSource.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Linq; 3 | //using Crypto.Websocket.Extensions.Core.Models; 4 | //using Crypto.Websocket.Extensions.Core.Trades.Models; 5 | //using Crypto.Websocket.Extensions.Core.Trades.Sources; 6 | //using Crypto.Websocket.Extensions.Core.Validations; 7 | //using Crypto.Websocket.Extensions.Logging; 8 | //using Huobi.Client.Websocket.Clients; 9 | //using Huobi.Client.Websocket.Messages.MarketData; 10 | //using Huobi.Client.Websocket.Messages.MarketData.MarketTradeDetail; 11 | //using Huobi.Client.Websocket.Messages.MarketData.Values; 12 | 13 | //namespace Crypto.Websocket.Extensions.Trades.Sources 14 | //{ 15 | // public class HuobiTradeSource : TradeSourceBase 16 | // { 17 | // private static readonly ILog Log = LogProvider.GetCurrentClassLogger(); 18 | 19 | // private IHuobiMarketWebsocketClient _client; 20 | // private IDisposable _subscription; 21 | 22 | // public HuobiTradeSource(IHuobiMarketWebsocketClient client) 23 | // { 24 | // ChangeClient(client); 25 | // } 26 | 27 | // public override string ExchangeName => "huobi"; 28 | 29 | // public void ChangeClient(IHuobiMarketWebsocketClient client) 30 | // { 31 | // CryptoValidations.ValidateInput(client, nameof(client)); 32 | 33 | // _client = client; 34 | // _subscription?.Dispose(); 35 | // Subscribe(); 36 | // } 37 | 38 | // private void Subscribe() 39 | // { 40 | // _subscription = _client 41 | // .Streams 42 | // .TradeDetailUpdateStream 43 | // .Subscribe(HandleTradeSafe); 44 | // } 45 | 46 | // private void HandleTradeSafe(MarketTradeDetailUpdateMessage response) 47 | // { 48 | // try 49 | // { 50 | // HandleTrade(response); 51 | // } 52 | // catch (Exception e) 53 | // { 54 | // Log.Error(e, $"[Huobi] Failed to handle trade info, error: '{e.Message}'"); 55 | // } 56 | // } 57 | 58 | // private void HandleTrade(MarketTradeDetailUpdateMessage response) 59 | // { 60 | // var converted = response 61 | // .Tick 62 | // .Data 63 | // .Select( 64 | // trade => ConvertTrade( 65 | // response.ParseSymbolFromTopic(), 66 | // trade, 67 | // response.Timestamp.UtcDateTime)) 68 | // .ToArray(); 69 | 70 | // TradesSubject.OnNext(converted); 71 | // } 72 | 73 | // private CryptoTrade ConvertTrade(string symbol, MarketTradeDetailTickDataItem trade, DateTime serverTimestamp) 74 | // { 75 | // var data = new CryptoTrade 76 | // { 77 | // Amount = (double)trade.Amount, 78 | // AmountQuote = (double)(trade.Amount * trade.Price), 79 | // Side = ConvertSide(trade.Direction), 80 | // Id = trade.TradeId.ToString(), 81 | // Price = (double)trade.Price, 82 | // Timestamp = trade.Timestamp.UtcDateTime, 83 | // Pair = symbol, 84 | 85 | // ExchangeName = ExchangeName, 86 | // ServerTimestamp = serverTimestamp 87 | // }; 88 | // return data; 89 | // } 90 | 91 | // private CryptoTradeSide ConvertSide(TradeSide tradeSide) 92 | // { 93 | // return tradeSide == TradeSide.Buy 94 | // ? CryptoTradeSide.Buy 95 | // : CryptoTradeSide.Sell; 96 | // } 97 | // } 98 | //} -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/Wallets/Sources/BitmexWalletSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using Bitmex.Client.Websocket.Client; 5 | using Bitmex.Client.Websocket.Responses.Margins; 6 | using Bitmex.Client.Websocket.Utils; 7 | using Crypto.Websocket.Extensions.Core.Validations; 8 | using Crypto.Websocket.Extensions.Core.Wallets.Models; 9 | using Crypto.Websocket.Extensions.Core.Wallets.Sources; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Logging.Abstractions; 12 | 13 | namespace Crypto.Websocket.Extensions.Wallets.Sources 14 | { 15 | /// 16 | /// Bitmex wallet source 17 | /// 18 | public class BitmexWalletSource : WalletSourceBase 19 | { 20 | private readonly ILogger _logger; 21 | private BitmexWebsocketClient _client = null!; 22 | private IDisposable? _subscription; 23 | private CryptoWallet? _lastWallet; 24 | 25 | /// 26 | public BitmexWalletSource(BitmexWebsocketClient client, ILogger? logger = null) 27 | { 28 | _logger = logger ?? NullLogger.Instance; 29 | ChangeClient(client); 30 | } 31 | 32 | /// 33 | public override string ExchangeName => "bitmex"; 34 | 35 | /// 36 | /// Change client and resubscribe to the new streams 37 | /// 38 | public void ChangeClient(BitmexWebsocketClient client) 39 | { 40 | CryptoValidations.ValidateInput(client, nameof(client)); 41 | 42 | _client = client; 43 | _subscription?.Dispose(); 44 | Subscribe(); 45 | } 46 | 47 | private void Subscribe() 48 | { 49 | _subscription = _client.Streams.MarginStream 50 | .Where(x => x?.Data != null && x.Data.Any()) 51 | .Subscribe(HandleWalletSafe); 52 | } 53 | 54 | private void HandleWalletSafe(MarginResponse response) 55 | { 56 | try 57 | { 58 | HandleWallet(response); 59 | } 60 | catch (Exception e) 61 | { 62 | _logger.LogError(e, "[Bitmex] Failed to handle wallet info, error: '{error}'", e.Message); 63 | } 64 | } 65 | 66 | private void HandleWallet(MarginResponse response) 67 | { 68 | WalletChangedSubject.OnNext(response.Data.Select(ConvertWallet).ToArray()); 69 | } 70 | 71 | private CryptoWallet ConvertWallet(Margin margin) 72 | { 73 | var currency = margin.Currency ?? "XBt"; 74 | 75 | var wallet = new CryptoWallet() 76 | { 77 | Currency = "BTC", 78 | Balance = ConvertToBtc(currency, margin.WalletBalance) ?? _lastWallet?.Balance ?? 0, 79 | BalanceAvailable = ConvertToBtc(currency, margin.AvailableMargin) ?? _lastWallet?.BalanceAvailable ?? 0, 80 | Leverage = margin.MarginLeverage ?? _lastWallet?.Leverage, 81 | RealizedPnl = ConvertToBtc(currency, margin.RealisedPnl) ?? _lastWallet?.RealizedPnl, 82 | UnrealizedPnl = ConvertToBtc(currency, margin.UnrealisedPnl) ?? _lastWallet?.UnrealizedPnl, 83 | Type = margin.Account?.ToString() ?? string.Empty 84 | }; 85 | _lastWallet = wallet; 86 | return wallet; 87 | } 88 | 89 | private double? ConvertToBtc(string currency, long? value) 90 | { 91 | if (!value.HasValue) 92 | return null; 93 | 94 | return BitmexConverter.ConvertToBtc(currency, value.Value); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Crypto.Websocket.Extensions/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/crypto-websocket-extensions/a039ba974fcfaf9887c696c4da59910d0a820868/src/Crypto.Websocket.Extensions/icon.png -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/Crypto.Websocket.Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/CryptoDateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Crypto.Websocket.Extensions.Core.Utils; 3 | using Xunit; 4 | 5 | namespace Crypto.Websocket.Extensions.Tests 6 | { 7 | public class CryptoDateTests 8 | { 9 | [Theory] 10 | [InlineData(1577573034.123456, "1577573034.123456")] 11 | [InlineData(1577573034.123451, "1577573034.123451")] 12 | [InlineData(0000000000.123456, "0.123456")] 13 | [InlineData(0.0, "0.000000")] 14 | public void UnixTimeConversion_ShouldSupportSixDecimalMilliseconds(double? timeInSec, string result) 15 | { 16 | var converted = CryptoDateUtils.ConvertFromUnixSeconds(timeInSec); 17 | var convertedBack = converted.ToUnixSeconds(); 18 | var convertedString = converted.ToUnixSecondsString(); 19 | 20 | Assert.Equal(timeInSec, convertedBack); 21 | Assert.Equal(result, convertedString); 22 | } 23 | 24 | [Fact] 25 | public void UnixTimeConversionDecimal_ShouldSupportSixDecimalMilliseconds() 26 | { 27 | TestDecimal(1577573034.123456m, "1577573034.123456"); 28 | TestDecimal(1577573034.123451m, "1577573034.123451"); 29 | TestDecimal(0000000000.123456m, "0.123456"); 30 | TestDecimal(0m, "0.000000"); 31 | } 32 | 33 | private static void TestDecimal(decimal? timeInSec, string result) 34 | { 35 | var converted = CryptoDateUtils.ConvertFromUnixSeconds(timeInSec); 36 | var convertedBack = converted.ToUnixSecondsDecimal(); 37 | var convertedString = converted.ToUnixSecondsString(); 38 | 39 | Assert.Equal(timeInSec, convertedBack); 40 | Assert.Equal(result, convertedString); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/CryptoMathTests.cs: -------------------------------------------------------------------------------- 1 | using Crypto.Websocket.Extensions.Core.Utils; 2 | using Xunit; 3 | 4 | namespace Crypto.Websocket.Extensions.Tests 5 | { 6 | public class CryptoMathTests 7 | { 8 | [Theory] 9 | [InlineData(10.0, 10.0)] 10 | [InlineData(0.0, 0.0)] 11 | [InlineData(-33.0, -33.0)] 12 | [InlineData(978.654321, 978.654321)] 13 | [InlineData(978.654321001, 978.654321)] 14 | [InlineData(978.654321001, 978.654321003)] 15 | [InlineData(111222333.000000001, 111222333.000000003)] 16 | public void IsSame_SameValues_ShouldReturnTrue(double first, double second) 17 | { 18 | Assert.True(CryptoMathUtils.IsSame(first, second)); 19 | } 20 | 21 | [Theory] 22 | [InlineData(10.0, 20.0)] 23 | [InlineData(0.0, 1.0)] 24 | [InlineData(-33.0, 33.0)] 25 | [InlineData(-111222333.654321, -778.654321)] 26 | [InlineData(978.65432101, 978.654321)] 27 | [InlineData(978.65432101, 978.65432103)] 28 | [InlineData(111222333.00000001, 111222333.00000003)] 29 | [InlineData(5.00000001, 5.00000003)] 30 | public void IsSame_DifferentValues_ShouldReturnFalse(double first, double second) 31 | { 32 | Assert.False(CryptoMathUtils.IsSame(first, second)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/CryptoOrderBookL3PerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime; 3 | using System.Threading; 4 | using Crypto.Websocket.Extensions.Core.Models; 5 | using Crypto.Websocket.Extensions.Core.OrderBooks; 6 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 7 | using Crypto.Websocket.Extensions.Tests.Helpers; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | using static Crypto.Websocket.Extensions.Tests.Helpers.OrderBookTestUtils; 11 | 12 | namespace Crypto.Websocket.Extensions.Tests 13 | { 14 | [Collection("Non-Parallel Collection")] 15 | public class CryptoOrderBookL3PerformanceTests 16 | { 17 | private readonly ITestOutputHelper _output; 18 | 19 | public CryptoOrderBookL3PerformanceTests(ITestOutputHelper output) 20 | { 21 | _output = output; 22 | 23 | GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; 24 | } 25 | 26 | [Fact] 27 | public void StreamPriceChanges_10kIterations_ShouldBeFast() 28 | { 29 | var pair = "BTC/USD"; 30 | var data = GetOrderBookSnapshotMockDataL3(pair, 1000); 31 | var snapshot = new OrderBookLevelBulk(OrderBookAction.Insert, data, CryptoOrderBookType.L3); 32 | var source = new OrderBookSourceMock(snapshot); 33 | 34 | ICryptoOrderBook orderBook = new CryptoOrderBook(pair, source, CryptoOrderBookType.L3) 35 | { 36 | NotifyForLevelAndAbove = 30 37 | }; 38 | 39 | source.BufferEnabled = false; 40 | source.LoadSnapshotEnabled = false; 41 | orderBook.SnapshotReloadEnabled = false; 42 | orderBook.ValidityCheckEnabled = false; 43 | source.StreamSnapshot(); 44 | 45 | var elapsedMs = StreamLevels(pair, source, orderBook, 10000, 100, 100); 46 | var msg = $"Elapsed time was: {elapsedMs} ms"; 47 | _output.WriteLine(msg); 48 | 49 | Assert.True(elapsedMs < 7000, msg); 50 | } 51 | 52 | [Fact] 53 | public void StreamPriceChanges_20kIterations_ShouldBeFast() 54 | { 55 | var pair = "BTC/USD"; 56 | var data = GetOrderBookSnapshotMockDataL3(pair, 1000); 57 | var snapshot = new OrderBookLevelBulk(OrderBookAction.Insert, data, CryptoOrderBookType.L3); 58 | var source = new OrderBookSourceMock(snapshot); 59 | 60 | ICryptoOrderBook orderBook = new CryptoOrderBook(pair, source, CryptoOrderBookType.L3) 61 | { 62 | NotifyForLevelAndAbove = 10 63 | }; 64 | 65 | source.BufferEnabled = false; 66 | source.LoadSnapshotEnabled = false; 67 | orderBook.SnapshotReloadEnabled = false; 68 | orderBook.ValidityCheckEnabled = false; 69 | source.StreamSnapshot(); 70 | 71 | var elapsedMs = StreamLevels(pair, source, orderBook, 20000, 100, 100); 72 | var msg = $"Elapsed time was: {elapsedMs} ms"; 73 | _output.WriteLine(msg); 74 | 75 | Assert.True(elapsedMs < 7000, msg); 76 | } 77 | 78 | [Fact] 79 | public void StreamPriceChanges_100kIterations_ShouldBeFast() 80 | { 81 | var pair = "BTC/USD"; 82 | var data = GetOrderBookSnapshotMockDataL3(pair, 1000); 83 | var snapshot = new OrderBookLevelBulk(OrderBookAction.Insert, data, CryptoOrderBookType.L3); 84 | var source = new OrderBookSourceMock(snapshot); 85 | 86 | ICryptoOrderBook orderBook = new CryptoOrderBook(pair, source, CryptoOrderBookType.L3) 87 | { 88 | NotifyForLevelAndAbove = 3 89 | }; 90 | 91 | source.BufferEnabled = false; 92 | source.LoadSnapshotEnabled = false; 93 | orderBook.SnapshotReloadEnabled = false; 94 | orderBook.ValidityCheckEnabled = false; 95 | source.StreamSnapshot(); 96 | 97 | var elapsedMs = StreamLevels(pair, source, orderBook, 100000, 100, 100); 98 | var msg = $"Elapsed time was: {elapsedMs} ms"; 99 | _output.WriteLine(msg); 100 | 101 | Assert.True(elapsedMs < 7000, msg); 102 | } 103 | 104 | [Fact] 105 | public void StreamPriceChanges_1mIterations_ShouldBeFast() 106 | { 107 | var pair = "BTC/USD"; 108 | var data = GetOrderBookSnapshotMockDataL3(pair, 1000); 109 | var snapshot = new OrderBookLevelBulk(OrderBookAction.Insert, data, CryptoOrderBookType.L3); 110 | var source = new OrderBookSourceMock(snapshot); 111 | 112 | ICryptoOrderBook orderBook = new CryptoOrderBook(pair, source, CryptoOrderBookType.L3); 113 | 114 | source.BufferEnabled = false; 115 | source.LoadSnapshotEnabled = false; 116 | orderBook.SnapshotReloadEnabled = false; 117 | orderBook.ValidityCheckEnabled = false; 118 | source.StreamSnapshot(); 119 | 120 | var elapsedMs = StreamLevels(pair, source, orderBook, 1000000, 100, 100); 121 | var msg = $"Elapsed time was: {elapsedMs} ms"; 122 | _output.WriteLine(msg); 123 | 124 | Assert.True(elapsedMs < 7000, msg); 125 | } 126 | 127 | private static long StreamLevels(string pair, OrderBookSourceMock source, ICryptoOrderBook book, int iterations, int maxBidPrice, int maxAskPrice, bool slowDown = false) 128 | { 129 | var bid = book.BidLevels[0]; 130 | var ask = book.AskLevels[0]; 131 | 132 | var sw = new Stopwatch(); 133 | var iterationsSafe = iterations * 13; 134 | for (int i = 0; i < iterationsSafe; i += 13) 135 | { 136 | var newBidPrice = i % maxBidPrice; 137 | var newAskPrice = maxBidPrice + (i % maxAskPrice); 138 | 139 | // update levels 140 | var bulk = GetUpdateBulk(CryptoOrderBookType.L3, 141 | CreateLevel(pair, newBidPrice, CryptoOrderSide.Bid, bid.Id), 142 | CreateLevel(pair, newAskPrice, CryptoOrderSide.Ask, ask.Id) 143 | ); 144 | sw.Start(); 145 | source.StreamBulk(bulk); 146 | sw.Stop(); 147 | 148 | if (slowDown && i % 1000 == 0) 149 | { 150 | Thread.Sleep(10); 151 | } 152 | } 153 | 154 | return sw.ElapsedMilliseconds; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/Helpers/OrderBookSourceMock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Crypto.Websocket.Extensions.Core.OrderBooks; 5 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 6 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 7 | using Microsoft.Extensions.Logging.Abstractions; 8 | 9 | namespace Crypto.Websocket.Extensions.Tests.Helpers 10 | { 11 | public class OrderBookSourceMock : OrderBookSourceBase 12 | { 13 | private readonly OrderBookLevelBulk _snapshot; 14 | private readonly OrderBookLevelBulk[] _bulks; 15 | 16 | public int SnapshotCalledCount { get; private set; } 17 | public string SnapshotLastPair { get; private set; } 18 | 19 | public OrderBookSourceMock() : base(NullLogger.Instance) 20 | { 21 | BufferInterval = TimeSpan.FromMilliseconds(10); 22 | } 23 | 24 | public OrderBookSourceMock(OrderBookLevelBulk snapshot) : this() 25 | { 26 | BufferInterval = TimeSpan.FromMilliseconds(10); 27 | _snapshot = snapshot; 28 | } 29 | 30 | public OrderBookSourceMock(params OrderBookLevelBulk[] bulks) : this() 31 | { 32 | BufferInterval = TimeSpan.FromMilliseconds(10); 33 | _bulks = bulks; 34 | } 35 | 36 | public void StreamSnapshot() 37 | { 38 | StreamSnapshot(_snapshot); 39 | } 40 | 41 | public void StreamSnapshotRaw(OrderBookLevelBulk snapshot) 42 | { 43 | StreamSnapshot(snapshot); 44 | } 45 | 46 | public void StreamBulks() 47 | { 48 | foreach (var bulk in _bulks) 49 | { 50 | BufferData(bulk); 51 | } 52 | } 53 | 54 | public void StreamBulk(OrderBookLevelBulk bulk) 55 | { 56 | BufferData(bulk); 57 | } 58 | 59 | public override string ExchangeName => "mock"; 60 | 61 | protected override Task LoadSnapshotInternal(string pair, int count = 1000) 62 | { 63 | SnapshotCalledCount++; 64 | SnapshotLastPair = pair; 65 | 66 | var bulk = new OrderBookLevelBulk(OrderBookAction.Insert, new OrderBookLevel[0], CryptoOrderBookType.L2); 67 | 68 | return Task.FromResult(bulk); 69 | } 70 | 71 | protected override OrderBookLevelBulk[] ConvertData(object[] data) 72 | { 73 | return data.Cast().ToArray(); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/Helpers/OrderBookTestUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Crypto.Websocket.Extensions.Core.Models; 4 | using Crypto.Websocket.Extensions.Core.OrderBooks; 5 | using Crypto.Websocket.Extensions.Core.OrderBooks.Models; 6 | using Crypto.Websocket.Extensions.Core.Utils; 7 | 8 | namespace Crypto.Websocket.Extensions.Tests.Helpers 9 | { 10 | public static class OrderBookTestUtils 11 | { 12 | public static OrderBookLevel[] GetOrderBookSnapshotMockData(string pair, int count) 13 | { 14 | var result = new List(); 15 | 16 | for (int i = 0; i < count; i++) 17 | { 18 | var bid = CreateLevel(pair, i, count * 2 + i, CryptoOrderSide.Bid); 19 | result.Add(bid); 20 | } 21 | 22 | 23 | for (int i = count*2; i > count; i--) 24 | { 25 | var ask = CreateLevel(pair, i, count * 4 + i, CryptoOrderSide.Ask); 26 | result.Add(ask); 27 | } 28 | 29 | return result.ToArray(); 30 | } 31 | 32 | public static OrderBookLevel[] GetOrderBookSnapshotMockDataL3(string pair, int count) 33 | { 34 | var result = new List(); 35 | 36 | for (int i = 0; i < count; i++) 37 | { 38 | var bid = CreateLevel(pair, i/10, count * 2 + i, CryptoOrderSide.Bid, null, 39 | Guid.NewGuid().ToString("N").Substring(0, 8)); 40 | result.Add(bid); 41 | } 42 | 43 | 44 | for (int i = count*2; i > count; i--) 45 | { 46 | var ask = CreateLevel(pair, i/10, count * 4 + i, CryptoOrderSide.Ask, null, 47 | Guid.NewGuid().ToString("N").Substring(0, 8)); 48 | result.Add(ask); 49 | } 50 | 51 | return result.ToArray(); 52 | } 53 | 54 | public static OrderBookLevelBulk GetInsertBulkL2(params OrderBookLevel[] levels) 55 | { 56 | return new OrderBookLevelBulk(OrderBookAction.Insert, levels, CryptoOrderBookType.L2); 57 | } 58 | 59 | public static OrderBookLevelBulk GetUpdateBulkL2(params OrderBookLevel[] levels) 60 | { 61 | return new OrderBookLevelBulk(OrderBookAction.Update, levels, CryptoOrderBookType.L2); 62 | } 63 | 64 | public static OrderBookLevelBulk GetDeleteBulkL2(params OrderBookLevel[] levels) 65 | { 66 | return new OrderBookLevelBulk(OrderBookAction.Delete, levels, CryptoOrderBookType.L2); 67 | } 68 | 69 | public static OrderBookLevelBulk GetInsertBulk(CryptoOrderBookType type, params OrderBookLevel[] levels) 70 | { 71 | return new OrderBookLevelBulk(OrderBookAction.Insert, levels, type); 72 | } 73 | 74 | public static OrderBookLevelBulk GetUpdateBulk(CryptoOrderBookType type, params OrderBookLevel[] levels) 75 | { 76 | return new OrderBookLevelBulk(OrderBookAction.Update, levels, type); 77 | } 78 | 79 | public static OrderBookLevelBulk GetDeleteBulk(CryptoOrderBookType type, params OrderBookLevel[] levels) 80 | { 81 | return new OrderBookLevelBulk(OrderBookAction.Delete, levels, type); 82 | } 83 | 84 | public static OrderBookLevel CreateLevel(string pair, double? price, double? amount, CryptoOrderSide side, int? count = 3, string key = null) 85 | { 86 | return new OrderBookLevel( 87 | key ?? CreateKey(price,side), 88 | side, 89 | price, 90 | amount, 91 | count, 92 | pair == null ? null : CryptoPairsHelper.Clean(pair) 93 | ); 94 | } 95 | 96 | public static OrderBookLevel CreateLevelById(string pair, double? price, double? amount, CryptoOrderSide side, int? count = 3, string key = null) 97 | { 98 | return new OrderBookLevel( 99 | key ?? CreateKey(price,side), 100 | side, 101 | null, 102 | amount, 103 | count, 104 | pair == null ? null : CryptoPairsHelper.Clean(pair) 105 | ); 106 | } 107 | 108 | public static OrderBookLevel CreateLevel(string pair, double? price, CryptoOrderSide side, string key = null) 109 | { 110 | return new OrderBookLevel( 111 | key ?? CreateKey(price,side), 112 | side, 113 | null, 114 | null, 115 | null, 116 | pair == null ? null : CryptoPairsHelper.Clean(pair) 117 | ); 118 | } 119 | 120 | public static string CreateKey(double? price, CryptoOrderSide side) 121 | { 122 | var sideSafe = side == CryptoOrderSide.Bid ? "bid" : "ask"; 123 | return $"{price}-{sideSafe}"; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/Helpers/OrderSourceMock.cs: -------------------------------------------------------------------------------- 1 | using Crypto.Websocket.Extensions.Core.Orders.Models; 2 | using Crypto.Websocket.Extensions.Core.Orders.Sources; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | 5 | namespace Crypto.Websocket.Extensions.Tests.Helpers 6 | { 7 | public class OrderSourceMock : OrderSourceBase 8 | { 9 | public OrderSourceMock() : base(NullLogger.Instance) 10 | { 11 | } 12 | 13 | public override string ExchangeName => "mock"; 14 | 15 | public void StreamOrder(CryptoOrder order) 16 | { 17 | OrderUpdatedSubject.OnNext(order); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/NonParallelCollectionDefinitionClass.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Crypto.Websocket.Extensions.Tests 4 | { 5 | [CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)] 6 | public class NonParallelCollectionDefinitionClass 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/data/RawFileCommunicator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Net.WebSockets; 6 | using System.Reactive.Linq; 7 | using System.Reactive.Subjects; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Binance.Client.Websocket.Communicator; 11 | using Bitfinex.Client.Websocket.Communicator; 12 | using Bitmex.Client.Websocket.Communicator; 13 | using Coinbase.Client.Websocket.Communicator; 14 | using Websocket.Client; 15 | 16 | namespace Crypto.Websocket.Extensions.Tests.data 17 | { 18 | public class RawFileCommunicator : IBitmexCommunicator, IBitfinexCommunicator, 19 | IBinanceCommunicator, ICoinbaseCommunicator 20 | { 21 | private readonly Subject _messageReceivedSubject = new Subject(); 22 | 23 | public IObservable MessageReceived => _messageReceivedSubject.AsObservable(); 24 | public IObservable ReconnectionHappened => Observable.Empty(); 25 | public IObservable DisconnectionHappened => Observable.Empty(); 26 | 27 | public TimeSpan? ReconnectTimeout { get; set; } = TimeSpan.FromSeconds(60); 28 | public TimeSpan? ErrorReconnectTimeout { get; set; } = TimeSpan.FromSeconds(60); 29 | public TimeSpan? LostReconnectTimeout { get; set; } = TimeSpan.FromSeconds(60); 30 | public string Name { get; set; } 31 | public bool IsStarted { get; private set; } 32 | public bool IsRunning { get; private set; } 33 | public bool IsReconnectionEnabled { get; set; } 34 | public bool IsTextMessageConversionEnabled { get; set; } 35 | public bool IsStreamDisposedAutomatically { get; set; } 36 | public ClientWebSocket NativeClient { get; } 37 | public Encoding MessageEncoding { get; set; } 38 | 39 | public string[] FileNames { get; set; } 40 | public string Delimiter { get; set; } = ";;"; 41 | public Encoding Encoding { get; set; } = Encoding.UTF8; 42 | 43 | public virtual void Dispose() 44 | { 45 | 46 | } 47 | 48 | public virtual Task Start() 49 | { 50 | StartStreaming(); 51 | 52 | return Task.CompletedTask; 53 | } 54 | 55 | public Task StartOrFail() 56 | { 57 | return Task.CompletedTask; 58 | } 59 | 60 | public Task Stop(WebSocketCloseStatus status, string statusDescription) 61 | { 62 | return Task.FromResult(true); 63 | } 64 | 65 | public Task StopOrFail(WebSocketCloseStatus status, string statusDescription) 66 | { 67 | return Task.FromResult(true); 68 | } 69 | 70 | public virtual bool Send(string message) 71 | { 72 | return true; 73 | } 74 | 75 | public bool Send(byte[] message) 76 | { 77 | return true; 78 | } 79 | 80 | public bool Send(ArraySegment message) 81 | { 82 | return true; 83 | } 84 | 85 | public bool Send(ReadOnlySequence message) 86 | { 87 | return true; 88 | } 89 | 90 | public virtual Task SendInstant(string message) 91 | { 92 | return Task.CompletedTask; 93 | } 94 | 95 | public Task SendInstant(byte[] message) 96 | { 97 | return Task.CompletedTask; 98 | } 99 | 100 | public bool SendAsText(byte[] message) 101 | { 102 | return true; 103 | } 104 | 105 | public bool SendAsText(ArraySegment message) 106 | { 107 | return true; 108 | } 109 | 110 | public bool SendAsText(ReadOnlySequence message) 111 | { 112 | return true; 113 | } 114 | 115 | public Task Reconnect() 116 | { 117 | return Task.CompletedTask; 118 | } 119 | 120 | public Task ReconnectOrFail() 121 | { 122 | return Task.CompletedTask; 123 | } 124 | 125 | public void StreamFakeMessage(ResponseMessage message) 126 | { 127 | } 128 | 129 | public Uri Url { get; set; } 130 | 131 | private void StartStreaming() 132 | { 133 | if (FileNames == null) 134 | throw new InvalidOperationException("FileNames are not set, provide at least one path to historical data"); 135 | if (string.IsNullOrEmpty(Delimiter)) 136 | throw new InvalidOperationException("Delimiter is not set (separator between messages in the file)"); 137 | 138 | foreach (var fileName in FileNames) 139 | { 140 | using var stream = GetFileStreamReader(fileName); 141 | var message = ReadByDelimeter(stream, Delimiter); 142 | while (message != null) 143 | { 144 | _messageReceivedSubject.OnNext(ResponseMessage.TextMessage(message)); 145 | message = ReadByDelimeter(stream, Delimiter); 146 | } 147 | } 148 | } 149 | 150 | 151 | private static string ReadByDelimeter(StreamReader sr, string delimiter) 152 | { 153 | var line = new StringBuilder(); 154 | int matchIndex = 0; 155 | var nextChar = (char)sr.Read(); 156 | 157 | while (nextChar > 0 && nextChar < 65535) 158 | { 159 | line.Append(nextChar); 160 | if (nextChar == delimiter[matchIndex]) 161 | { 162 | if (matchIndex == delimiter.Length - 1) 163 | { 164 | return line.ToString().Substring(0, line.Length - delimiter.Length); 165 | } 166 | matchIndex++; 167 | } 168 | else 169 | { 170 | matchIndex = 0; 171 | } 172 | nextChar = (char)sr.Read(); 173 | } 174 | 175 | return line.Length == 0 ? null : line.ToString(); 176 | } 177 | 178 | private StreamReader GetFileStreamReader(string fileName) 179 | { 180 | var fs = new FileStream(fileName, FileMode.Open); 181 | if (fileName.EndsWith("gz", StringComparison.OrdinalIgnoreCase) || 182 | fileName.EndsWith("gzip", StringComparison.OrdinalIgnoreCase)) 183 | { 184 | return new StreamReader(new GZipStream(fs, CompressionMode.Decompress), Encoding); 185 | } 186 | 187 | return new StreamReader(fs, Encoding); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/Crypto.Websocket.Extensions.Tests/data/bitmex_raw_xbtusd_2018-11-13.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/crypto-websocket-extensions/a039ba974fcfaf9887c696c4da59910d0a820868/test/Crypto.Websocket.Extensions.Tests/data/bitmex_raw_xbtusd_2018-11-13.txt.gz -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Sample/Crypto.Websocket.Extensions.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Sample/OrderBookL3Example.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using System.Threading.Tasks; 5 | using Bitfinex.Client.Websocket; 6 | using Bitfinex.Client.Websocket.Client; 7 | using Bitfinex.Client.Websocket.Requests.Configurations; 8 | using Bitfinex.Client.Websocket.Websockets; 9 | using Bitmex.Client.Websocket.Websockets; 10 | using Crypto.Websocket.Extensions.Core.OrderBooks; 11 | using Crypto.Websocket.Extensions.Core.OrderBooks.Sources; 12 | using Crypto.Websocket.Extensions.OrderBooks.SourcesL3; 13 | using Microsoft.Extensions.Logging; 14 | using Serilog; 15 | 16 | namespace Crypto.Websocket.Extensions.Sample 17 | { 18 | public static class OrderBookL3Example 19 | { 20 | public static async Task RunOnlyOne() 21 | { 22 | var optimized = true; 23 | var levelsCount = 20; 24 | 25 | //var ob = await StartBitfinex("BTCUSD", optimized); 26 | var ob = await StartBitfinex("btcf0:ustf0", optimized); 27 | ob.NotifyForLevelAndAbove = 30; 28 | 29 | Log.Information("Waiting for price change..."); 30 | 31 | Observable.CombineLatest(new[] 32 | { 33 | //ob.OrderBookUpdatedStream 34 | ob.TopNLevelsUpdatedStream 35 | }) 36 | .Subscribe(x => HandleQuoteChanged(ob, levelsCount)); 37 | } 38 | 39 | private static void HandleQuoteChanged(CryptoOrderBook ob, int levelsCount) 40 | { 41 | var bids = ob.BidLevelsPerPrice.Take(levelsCount).SelectMany(x => x.Value).ToArray(); 42 | var asks = ob.AskLevelsPerPrice.Take(levelsCount).SelectMany(x => x.Value).ToArray(); 43 | 44 | var max = Math.Max(bids.Length, asks.Length); 45 | 46 | var msg = string.Empty; 47 | 48 | for (int i = 0; i < max; i++) 49 | { 50 | var bid = bids.Length > i ? bids[i] : null; 51 | var ask = asks.Length > i ? asks[i] : null; 52 | 53 | var bidMsg = 54 | bid != null ? $"#{i + 1} {bid?.Id} " + 55 | $"{"p: " + (bid?.Price ?? 0).ToString("#.00#") + " a: " + (bid?.Amount ?? 0).ToString("0.00#")} " + 56 | $"[{bid.PriceUpdatedCount}/{bid.AmountUpdatedCount}] [{bid.AmountDifference:0.000}/{bid.AmountDifferenceAggregated:0.000}]" 57 | : " "; 58 | var askMsg = 59 | ask != null ? $"#{i + 1} {ask?.Id} " + 60 | $"{"p: " + (ask?.Price ?? 0).ToString("#.00#") + " a: " + (ask?.Amount ?? 0).ToString("0.00#")} " + 61 | $"[{ask.PriceUpdatedCount}/{ask.AmountUpdatedCount}] [{ask.AmountDifference:0.000}/{ask.AmountDifferenceAggregated:0.000}]" 62 | : " "; 63 | 64 | bidMsg = $"{bidMsg,80}"; 65 | askMsg = $"{askMsg,80}"; 66 | 67 | msg += $"{Environment.NewLine}{bidMsg} {askMsg}"; 68 | 69 | } 70 | 71 | Log.Information($"TOP LEVEL {ob.ExchangeName} {ob.TargetPairOriginal}: {msg}{Environment.NewLine}"); 72 | 73 | } 74 | 75 | 76 | 77 | 78 | private static async Task StartBitfinex(string pair, bool optimized) 79 | { 80 | var url = BitfinexValues.BitfinexPublicWebsocketUrl; 81 | var communicator = new BitfinexWebsocketCommunicator(url, Program.Logger.CreateLogger()) { Name = "Bitfinex" }; 82 | var client = new BitfinexWebsocketClient(communicator, Program.Logger.CreateLogger()); 83 | 84 | var source = new BitfinexOrderBookL3Source(client); 85 | var orderBook = new CryptoOrderBook(pair, source, CryptoOrderBookType.L3); 86 | 87 | if (optimized) 88 | { 89 | ConfigureOptimized(source, orderBook); 90 | } 91 | 92 | await communicator.Start(); 93 | 94 | // Send configuration request to enable server timestamps 95 | client.Send(new ConfigurationRequest(ConfigurationFlag.Sequencing | ConfigurationFlag.Timestamp)); 96 | 97 | // Send subscription request to raw order book data 98 | client.Send(new Bitfinex.Client.Websocket.Requests.Subscriptions.RawBookSubscribeRequest(pair, "100")); 99 | 100 | return orderBook; 101 | } 102 | 103 | private static void ConfigureOptimized(IOrderBookSource source, ICryptoOrderBook orderBook) 104 | { 105 | source.BufferEnabled = true; 106 | source.BufferInterval = TimeSpan.FromMilliseconds(0); 107 | 108 | orderBook.DebugEnabled = false; 109 | orderBook.DebugLogEnabled = false; 110 | orderBook.ValidityCheckEnabled = false; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Sample/OrdersExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Binance.Client.Websocket; 4 | using Binance.Client.Websocket.Client; 5 | using Binance.Client.Websocket.Signing; 6 | using Binance.Client.Websocket.Websockets; 7 | using Bitmex.Client.Websocket; 8 | using Bitmex.Client.Websocket.Client; 9 | using Bitmex.Client.Websocket.Websockets; 10 | using Crypto.Websocket.Extensions.Core.Orders; 11 | using Crypto.Websocket.Extensions.Core.Orders.Models; 12 | using Crypto.Websocket.Extensions.Core.Positions.Models; 13 | using Crypto.Websocket.Extensions.Core.Wallets.Models; 14 | using Crypto.Websocket.Extensions.Orders.Sources; 15 | using Crypto.Websocket.Extensions.Positions.Sources; 16 | using Crypto.Websocket.Extensions.Wallets.Sources; 17 | using Microsoft.Extensions.Logging; 18 | using Serilog; 19 | 20 | namespace Crypto.Websocket.Extensions.Sample 21 | { 22 | public static class OrdersExample 23 | { 24 | private const string ApiKey = ""; 25 | private const string ApiSecret = ""; 26 | 27 | public static async Task RunEverything() 28 | { 29 | //var ordBitmex = await StartBitmex(false, HandleOrderChanged, HandleWalletsChanged, HandlePositionsChanged); 30 | var ordBinance = await StartBinance(HandleOrderChanged); 31 | 32 | Log.Information("Waiting for orders..."); 33 | } 34 | 35 | private static void HandleOrderChanged(CryptoOrder order) 36 | { 37 | Log.Information($"Order '{order.ClientId}' [{order.Pair} {order.Side} {order.Type}] changed. " + 38 | $"Price: {order.PriceGrouped}, Amount: {order.AmountOrig:#.#####}/{order.AmountOrigQuote}, " + 39 | $"cumulative: {order.AmountFilledCumulative:#.#####}/{order.AmountFilledCumulativeQuote}, " + 40 | $"filled: {order.AmountFilled:#.#####}/{order.AmountFilledQuote}, " + 41 | $"Status: {order.OrderStatus} ({order.OrderStatusRaw})"); 42 | } 43 | 44 | private static void HandleWalletsChanged(CryptoWallet[] wallets) 45 | { 46 | foreach (var wallet in wallets) 47 | { 48 | HandleWalletChanged(wallet); 49 | } 50 | } 51 | 52 | private static void HandleWalletChanged(CryptoWallet wallet) 53 | { 54 | Log.Information($"Wallet '{wallet.Type}' " + 55 | $"Balance: {wallet.Balance} {wallet.Currency}, " + 56 | $"Available: {wallet.BalanceAvailable} {wallet.Currency}, " + 57 | $"Pnl: {wallet.RealizedPnl:#.#####}/{wallet.UnrealizedPnl:#.#####}"); 58 | } 59 | 60 | private static void HandlePositionsChanged(CryptoPosition[] positions) 61 | { 62 | foreach (var pos in positions) 63 | { 64 | HandlePositionChanged(pos); 65 | } 66 | } 67 | 68 | private static void HandlePositionChanged(CryptoPosition pos) 69 | { 70 | Log.Information($"Position '{pos.Pair}' [{pos.Side}], " + 71 | $"price: {pos.LastPrice:0.00######}, amount: {pos.Amount}/{pos.AmountQuote}, " + 72 | $"leverage: {pos.Leverage}x, " + 73 | $"pnl realized: {pos.RealizedPnl:0.00######}, unrealized: {pos.UnrealizedPnl:0.00######}, " + 74 | $"liquidation: {pos.LiquidationPrice:0.00######}"); 75 | } 76 | 77 | 78 | private static async Task StartBitmex(bool isTestnet, Action handler, 79 | Action walletHandler, Action positionHandler) 80 | { 81 | var url = isTestnet ? BitmexValues.ApiWebsocketTestnetUrl : BitmexValues.ApiWebsocketUrl; 82 | var communicator = new BitmexWebsocketCommunicator(url, Program.Logger.CreateLogger()) { Name = "Bitmex" }; 83 | var client = new BitmexWebsocketClient(communicator, Program.Logger.CreateLogger()); 84 | 85 | var source = new BitmexOrderSource(client); 86 | var orders = new CryptoOrders(source); 87 | orders.OrderChangedStream.Subscribe(handler); 88 | 89 | var walletSource = new BitmexWalletSource(client); 90 | walletSource.WalletChangedStream.Subscribe(walletHandler); 91 | 92 | var positionSource = new BitmexPositionSource(client); 93 | positionSource.PositionsStream.Subscribe(positionHandler); 94 | 95 | client.Streams.AuthenticationStream.Subscribe(x => 96 | { 97 | Log.Information($"[Bitmex] Authenticated '{x.Success}'"); 98 | client.Send(new Bitmex.Client.Websocket.Requests.WalletSubscribeRequest()); 99 | client.Send(new Bitmex.Client.Websocket.Requests.MarginSubscribeRequest()); 100 | client.Send(new Bitmex.Client.Websocket.Requests.PositionSubscribeRequest()); 101 | client.Send(new Bitmex.Client.Websocket.Requests.OrderSubscribeRequest()); 102 | }); 103 | 104 | communicator.ReconnectionHappened.Subscribe(x => 105 | { 106 | Log.Information("[Bitmex] Reconnected, type: {type}", x.Type); 107 | client.Authenticate(ApiKey, ApiSecret); 108 | }); 109 | 110 | await communicator.Start(); 111 | 112 | 113 | return orders; 114 | } 115 | 116 | private static async Task StartBinance(Action handler) 117 | { 118 | var url = BinanceValues.ApiWebsocketUrl; 119 | var communicator = new BinanceWebsocketCommunicator(url, Program.Logger.CreateLogger()) { Name = "Binance" }; 120 | var client = new BinanceWebsocketClient(communicator, Program.Logger.CreateLogger()); 121 | 122 | var source = new BinanceOrderSource(client); 123 | var orders = new CryptoOrders(source); 124 | orders.OrderChangedStream.Subscribe(handler); 125 | 126 | communicator.ReconnectionHappened.Subscribe(x => 127 | { 128 | Log.Information("[Binance] Reconnected, type: {type}", x.Type); 129 | }); 130 | 131 | await communicator.Authenticate(ApiKey, new BinanceHmac(ApiSecret)); 132 | await communicator.Start(); 133 | 134 | return orders; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime; 6 | using System.Runtime.Loader; 7 | using System.Threading; 8 | using Serilog; 9 | using Serilog.Events; 10 | using Serilog.Extensions.Logging; 11 | 12 | namespace Crypto.Websocket.Extensions.Sample 13 | { 14 | class Program 15 | { 16 | public static SerilogLoggerFactory Logger; 17 | private static readonly ManualResetEvent ExitEvent = new ManualResetEvent(false); 18 | 19 | static void Main(string[] args) 20 | { 21 | var defaultCulture = CultureInfo.InvariantCulture; 22 | CultureInfo.DefaultThreadCurrentCulture = defaultCulture; 23 | CultureInfo.DefaultThreadCurrentUICulture = defaultCulture; 24 | 25 | GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; 26 | 27 | Logger = InitLogging(); 28 | 29 | AppDomain.CurrentDomain.ProcessExit += CurrentDomainOnProcessExit; 30 | AssemblyLoadContext.Default.Unloading += DefaultOnUnloading; 31 | Console.CancelKeyPress += ConsoleOnCancelKeyPress; 32 | 33 | Console.WriteLine("|========================|"); 34 | Console.WriteLine("| WEBSOCKET EXTENSIONS |"); 35 | Console.WriteLine("|========================|"); 36 | Console.WriteLine(); 37 | 38 | Log.Debug("===================================="); 39 | Log.Debug(" STARTING "); 40 | Log.Debug("===================================="); 41 | 42 | 43 | OrderBookExample.RunEverything().Wait(); 44 | //OrderBookExample.RunOnlyOne(true).Wait(); 45 | //OrderBookL3Example.RunOnlyOne().Wait(); 46 | 47 | //TradesExample.RunEverything().Wait(); 48 | 49 | //OrdersExample.RunEverything().Wait(); 50 | 51 | 52 | ExitEvent.WaitOne(); 53 | 54 | Log.Debug("===================================="); 55 | Log.Debug(" STOPPING "); 56 | Log.Debug("===================================="); 57 | Log.CloseAndFlush(); 58 | } 59 | 60 | 61 | 62 | 63 | private static SerilogLoggerFactory InitLogging() 64 | { 65 | var executingDir = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); 66 | var logPath = Path.Combine(executingDir, "logs", "verbose.log"); 67 | var logger = new LoggerConfiguration() 68 | .MinimumLevel.Verbose() 69 | .WriteTo.File(logPath, rollingInterval: RollingInterval.Day) 70 | .WriteTo.Console(LogEventLevel.Debug, 71 | outputTemplate: "{Timestamp:HH:mm:ss.ffffff} [{Level:u3}] {Message}{NewLine}") 72 | .CreateLogger(); 73 | Log.Logger = logger; 74 | return new SerilogLoggerFactory(logger); 75 | } 76 | 77 | private static void CurrentDomainOnProcessExit(object sender, EventArgs eventArgs) 78 | { 79 | Log.Warning("Exiting process"); 80 | ExitEvent.Set(); 81 | } 82 | 83 | private static void DefaultOnUnloading(AssemblyLoadContext assemblyLoadContext) 84 | { 85 | Log.Warning("Unloading process"); 86 | ExitEvent.Set(); 87 | } 88 | 89 | private static void ConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs e) 90 | { 91 | Log.Warning("Canceling process"); 92 | e.Cancel = true; 93 | ExitEvent.Set(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Tests.Integration/BinanceOrderBookSourceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Binance.Client.Websocket; 4 | using Binance.Client.Websocket.Client; 5 | using Binance.Client.Websocket.Subscriptions; 6 | using Binance.Client.Websocket.Websockets; 7 | using Crypto.Websocket.Extensions.Core.OrderBooks; 8 | using Crypto.Websocket.Extensions.OrderBooks.Sources; 9 | using Xunit; 10 | 11 | namespace Crypto.Websocket.Extensions.Tests.Integration 12 | { 13 | public class BinanceOrderBookSourceTests 14 | { 15 | [Fact] 16 | public async Task ConnectToSource_ShouldHandleOrderBookCorrectly() 17 | { 18 | var url = BinanceValues.ApiWebsocketUrl; 19 | using (var communicator = new BinanceWebsocketCommunicator(url)) 20 | { 21 | using (var client = new BinanceWebsocketClient(communicator)) 22 | { 23 | var pair = "BTCUSDT"; 24 | 25 | client.SetSubscriptions( 26 | new OrderBookDiffSubscription(pair) 27 | ); 28 | 29 | var source = new BinanceOrderBookSource(client); 30 | var orderBook = new CryptoOrderBook(pair, source); 31 | 32 | await communicator.Start(); 33 | 34 | // Binance is special 35 | // We need to load snapshot in advance manually via REST call 36 | await source.LoadSnapshot(pair); 37 | 38 | await Task.Delay(TimeSpan.FromSeconds(5)); 39 | 40 | Assert.True(orderBook.BidPrice > 0); 41 | Assert.True(orderBook.AskPrice > 0); 42 | 43 | Assert.NotEmpty(orderBook.BidLevels); 44 | Assert.NotEmpty(orderBook.AskLevels); 45 | } 46 | } 47 | } 48 | 49 | [Fact] 50 | public async Task AutoSnapshotReloading_ShouldWorkAfterTimeout() 51 | { 52 | var url = BinanceValues.ApiWebsocketUrl; 53 | using (var communicator = new BinanceWebsocketCommunicator(url)) 54 | { 55 | using (var client = new BinanceWebsocketClient(communicator)) 56 | { 57 | var pair = "BTCUSDT"; 58 | 59 | client.SetSubscriptions( 60 | new OrderBookDiffSubscription(pair) 61 | ); 62 | 63 | var source = new BinanceOrderBookSource(client) 64 | { 65 | LoadSnapshotEnabled = true 66 | }; 67 | var orderBook = new CryptoOrderBook(pair, source) 68 | { 69 | SnapshotReloadTimeout = TimeSpan.FromSeconds(5), 70 | SnapshotReloadEnabled = true 71 | }; 72 | 73 | await Task.Delay(TimeSpan.FromSeconds(13)); 74 | 75 | Assert.True(orderBook.BidPrice > 0); 76 | Assert.True(orderBook.AskPrice > 0); 77 | 78 | Assert.NotEmpty(orderBook.BidLevels); 79 | Assert.NotEmpty(orderBook.AskLevels); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Tests.Integration/BitfinexOrderBookSourceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bitfinex.Client.Websocket; 4 | using Bitfinex.Client.Websocket.Client; 5 | using Bitfinex.Client.Websocket.Requests.Subscriptions; 6 | using Bitfinex.Client.Websocket.Utils; 7 | using Bitfinex.Client.Websocket.Websockets; 8 | using Crypto.Websocket.Extensions.Core.OrderBooks; 9 | using Crypto.Websocket.Extensions.OrderBooks.Sources; 10 | using Xunit; 11 | 12 | namespace Crypto.Websocket.Extensions.Tests.Integration 13 | { 14 | public class BitfinexOrderBookSourceTests 15 | { 16 | [Fact] 17 | public async Task ConnectToSource_ShouldHandleOrderBookCorrectly() 18 | { 19 | var url = BitfinexValues.ApiWebsocketUrl; 20 | using (var communicator = new BitfinexWebsocketCommunicator(url)) 21 | { 22 | using (var client = new BitfinexWebsocketClient(communicator)) 23 | { 24 | var pair = "BTCUSD"; 25 | 26 | var source = new BitfinexOrderBookSource(client); 27 | var orderBook = new CryptoOrderBook(pair, source); 28 | 29 | await communicator.Start(); 30 | client.Send(new BookSubscribeRequest(pair, BitfinexPrecision.P0, BitfinexFrequency.Realtime, "100")); 31 | 32 | await Task.Delay(TimeSpan.FromSeconds(5)); 33 | 34 | Assert.True(orderBook.BidPrice > 0); 35 | Assert.True(orderBook.AskPrice > 0); 36 | 37 | Assert.NotEmpty(orderBook.BidLevels); 38 | Assert.NotEmpty(orderBook.AskLevels); 39 | } 40 | } 41 | } 42 | 43 | [Fact] 44 | public async Task AutoSnapshotReloading_ShouldWorkAfterTimeout() 45 | { 46 | var url = BitfinexValues.ApiWebsocketUrl; 47 | using (var communicator = new BitfinexWebsocketCommunicator(url)) 48 | { 49 | using (var client = new BitfinexWebsocketClient(communicator)) 50 | { 51 | var pair = "LTCUSD"; 52 | 53 | var source = new BitfinexOrderBookSource(client) 54 | { 55 | LoadSnapshotEnabled = true 56 | }; 57 | var orderBook = new CryptoOrderBook(pair, source) 58 | { 59 | SnapshotReloadTimeout = TimeSpan.FromSeconds(5), 60 | SnapshotReloadEnabled = true 61 | }; 62 | 63 | await Task.Delay(TimeSpan.FromSeconds(13)); 64 | 65 | Assert.True(orderBook.BidPrice > 0); 66 | Assert.True(orderBook.AskPrice > 0); 67 | 68 | Assert.NotEmpty(orderBook.BidLevels); 69 | Assert.NotEmpty(orderBook.AskLevels); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Tests.Integration/BitmexOrderBookSourceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Bitmex.Client.Websocket; 5 | using Bitmex.Client.Websocket.Client; 6 | using Bitmex.Client.Websocket.Requests; 7 | using Bitmex.Client.Websocket.Websockets; 8 | using Crypto.Websocket.Extensions.Core.OrderBooks; 9 | using Crypto.Websocket.Extensions.OrderBooks.Sources; 10 | using Xunit; 11 | 12 | namespace Crypto.Websocket.Extensions.Tests.Integration 13 | { 14 | public class BitmexOrderBookSourceTests 15 | { 16 | [Fact] 17 | public async Task ConnectToSource_ShouldHandleOrderBookCorrectly() 18 | { 19 | var url = BitmexValues.ApiWebsocketUrl; 20 | using (var communicator = new BitmexWebsocketCommunicator(url)) 21 | { 22 | using (var client = new BitmexWebsocketClient(communicator)) 23 | { 24 | var pair = "XBTUSD"; 25 | 26 | var source = new BitmexOrderBookSource(client); 27 | var orderBook = new CryptoOrderBook(pair, source); 28 | 29 | await communicator.Start(); 30 | client.Send(new BookSubscribeRequest(pair)); 31 | 32 | await Task.Delay(TimeSpan.FromSeconds(10)); 33 | 34 | Assert.True(orderBook.BidPrice > 0); 35 | Assert.True(orderBook.AskPrice > 0); 36 | 37 | Assert.NotEmpty(orderBook.BidLevels); 38 | Assert.NotEmpty(orderBook.AskLevels); 39 | } 40 | } 41 | } 42 | 43 | [Fact(Skip = "Flaky test")] 44 | public async Task ConnectToSource_ShouldHandleOrderBookOneByOne() 45 | { 46 | var url = BitmexValues.ApiWebsocketUrl; 47 | using (var communicator = new BitmexWebsocketCommunicator(url)) 48 | { 49 | using (var client = new BitmexWebsocketClient(communicator)) 50 | { 51 | var pair = "XBTUSD"; 52 | var called = 0; 53 | 54 | var source = new BitmexOrderBookSource(client); 55 | var orderBook = new CryptoOrderBook(pair, source) 56 | { 57 | DebugEnabled = true 58 | }; 59 | 60 | orderBook.OrderBookUpdatedStream.Subscribe(x => 61 | { 62 | called++; 63 | Thread.Sleep(2000); 64 | }); 65 | 66 | await communicator.Start(); 67 | client.Send(new BookSubscribeRequest(pair)); 68 | 69 | await Task.Delay(TimeSpan.FromSeconds(5)); 70 | Assert.Equal(2, called); 71 | 72 | await Task.Delay(TimeSpan.FromSeconds(2)); 73 | Assert.Equal(3, called); 74 | } 75 | } 76 | } 77 | 78 | [Fact] 79 | public async Task AutoSnapshotReloading_ShouldWorkAfterTimeout() 80 | { 81 | var url = BitmexValues.ApiWebsocketUrl; 82 | using (var communicator = new BitmexWebsocketCommunicator(url)) 83 | { 84 | using (var client = new BitmexWebsocketClient(communicator)) 85 | { 86 | var pair = "XBTUSD"; 87 | 88 | var source = new BitmexOrderBookSource(client) 89 | { 90 | LoadSnapshotEnabled = true 91 | }; 92 | var orderBook = new CryptoOrderBook(pair, source) 93 | { 94 | SnapshotReloadTimeout = TimeSpan.FromSeconds(5), 95 | SnapshotReloadEnabled = true 96 | }; 97 | 98 | await Task.Delay(TimeSpan.FromSeconds(13)); 99 | 100 | Assert.True(orderBook.BidPrice > 0); 101 | Assert.True(orderBook.AskPrice > 0); 102 | 103 | Assert.NotEmpty(orderBook.BidLevels); 104 | Assert.NotEmpty(orderBook.AskLevels); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Tests.Integration/CoinbaseOrderBookSourceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Coinbase.Client.Websocket; 4 | using Coinbase.Client.Websocket.Channels; 5 | using Coinbase.Client.Websocket.Client; 6 | using Coinbase.Client.Websocket.Communicator; 7 | using Coinbase.Client.Websocket.Requests; 8 | using Crypto.Websocket.Extensions.Core.OrderBooks; 9 | using Crypto.Websocket.Extensions.OrderBooks.Sources; 10 | using Xunit; 11 | 12 | namespace Crypto.Websocket.Extensions.Tests.Integration 13 | { 14 | public class CoinbaseOrderBookSourceTests 15 | { 16 | [Fact(Skip = "Coinbase no longer working")] 17 | public async Task ConnectToSource_ShouldHandleOrderBookCorrectly() 18 | { 19 | var url = CoinbaseValues.ApiWebsocketUrl; 20 | using (var communicator = new CoinbaseWebsocketCommunicator(url)) 21 | { 22 | using (var client = new CoinbaseWebsocketClient(communicator)) 23 | { 24 | var pair = "BTC-USD"; 25 | 26 | var source = new CoinbaseOrderBookSource(client); 27 | var orderBook = new CryptoOrderBook(pair, source); 28 | 29 | await communicator.Start(); 30 | 31 | client.Send(new SubscribeRequest( 32 | new[] { pair }, 33 | ChannelSubscriptionType.Level2 34 | )); 35 | 36 | await Task.Delay(TimeSpan.FromSeconds(5)); 37 | 38 | Assert.True(orderBook.BidPrice > 0); 39 | Assert.True(orderBook.AskPrice > 0); 40 | 41 | Assert.NotEmpty(orderBook.BidLevels); 42 | Assert.NotEmpty(orderBook.AskLevels); 43 | } 44 | } 45 | } 46 | 47 | [Fact(Skip = "Flaky test")] 48 | public async Task AutoSnapshotReloading_ShouldWorkAfterTimeout() 49 | { 50 | var url = CoinbaseValues.ApiWebsocketUrl; 51 | using (var communicator = new CoinbaseWebsocketCommunicator(url)) 52 | { 53 | using (var client = new CoinbaseWebsocketClient(communicator)) 54 | { 55 | var pair = "BTC-USD"; 56 | 57 | var source = new CoinbaseOrderBookSource(client) 58 | { 59 | LoadSnapshotEnabled = true 60 | }; 61 | var orderBook = new CryptoOrderBook(pair, source) 62 | { 63 | SnapshotReloadTimeout = TimeSpan.FromSeconds(2), 64 | SnapshotReloadEnabled = true 65 | }; 66 | 67 | await Task.Delay(TimeSpan.FromSeconds(20)); 68 | 69 | Assert.True(orderBook.BidPrice > 0); 70 | Assert.True(orderBook.AskPrice > 0); 71 | 72 | Assert.NotEmpty(orderBook.BidLevels); 73 | Assert.NotEmpty(orderBook.AskLevels); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test_integration/Crypto.Websocket.Extensions.Tests.Integration/Crypto.Websocket.Extensions.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------