├── .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 |
--------------------------------------------------------------------------------