├── .github
├── dependabot.yml
└── workflows
│ └── pr_validation.yaml
├── .gitignore
├── Akka.CQRS.sln
├── LICENSE
├── README.md
├── RELEASE_NOTES.md
├── appsettings.json
├── build-system
├── README.md
├── azure-pipeline.template.yaml
├── linux-pr-validation.yaml
├── windows-pr-validation.yaml
└── windows-release.yaml
├── build.cmd
├── build.fsx
├── build.ps1
├── build.sh
├── docker-compose.yaml
├── docs
├── api
│ └── index.md
├── articles
│ ├── index.md
│ └── toc.yml
├── docfx.json
├── images
│ ├── akka-cqrs-architectural-overview.png
│ ├── akka-cqrs-inmemory-replication.png
│ ├── docker-for-windows-networking.png
│ └── icon.png
├── index.md
├── toc.yml
└── web.config
├── global.json
├── serve-docs.cmd
├── serve-docs.ps1
└── src
├── Akka.CQRS.Infrastructure.Tests
├── Akka.CQRS.Infrastructure.Tests.csproj
└── ConfigSpecs.cs
├── Akka.CQRS.Infrastructure
├── Akka.CQRS.Infrastructure.csproj
├── Ops
│ ├── OpsConfig.cs
│ └── ops.conf
├── SqlDbHoconHelper.cs
├── StockEventTagger.cs
└── StockShardMsgRouter.cs
├── Akka.CQRS.Matching.Tests
├── Akka.CQRS.Matching.Tests.csproj
└── MatchingEngineSpecs.cs
├── Akka.CQRS.Matching
├── Akka.CQRS.Matching.csproj
└── MatchingEngine.cs
├── Akka.CQRS.Pricing.Actors
├── Akka.CQRS.Pricing.Actors.csproj
├── MatchAggregator.cs
├── PriceInitiatorActor.cs
├── PriceViewActor.cs
├── PriceViewMaster.cs
└── UnexpectedEndOfStream.cs
├── Akka.CQRS.Pricing.Cli
├── Akka.CQRS.Pricing.Cli.csproj
├── PriceCmdHandler.cs
├── PriceCmdRouter.cs
├── PriceTrackingActor.cs
└── PricingCmd.cs
├── Akka.CQRS.Pricing.Service
├── Akka.CQRS.Pricing.Service.csproj
├── Dockerfile
├── Program.cs
└── app.conf
├── Akka.CQRS.Pricing
├── Akka.CQRS.Pricing.csproj
├── Commands
│ ├── FetchPriceAndVolume.cs
│ ├── GetPriceHistory.cs
│ ├── Ping.cs
│ └── PriceAndVolumeSnapshot.cs
├── Events
│ ├── IPriceUpdate.cs
│ ├── IVolumeUpdate.cs
│ ├── PriceChanged.cs
│ └── VolumeChanged.cs
├── MatchAggregatorSnapshot.cs
├── PriceTopicHelpers.cs
└── Views
│ ├── EMWA.cs
│ ├── MatchAggregate.cs
│ └── PriceHistory.cs
├── Akka.CQRS.Subscriptions.Tests
├── Akka.CQRS.Subscriptions.Tests.csproj
├── DistributedPubSub
│ ├── DistributedPubSubEnd2EndSpecs.cs
│ └── DistributedPubSubFormatterSpecs.cs
└── TradeEventExtensionsSpecs.cs
├── Akka.CQRS.Subscriptions
├── Akka.CQRS.Subscriptions.csproj
├── DistributedPubSub
│ ├── DistributedPubSubTopicFormatter.cs
│ ├── DistributedPubSubTradeEventPublisher.cs
│ └── DistributedPubSubTradeEventSubscriptionManager.cs
├── ITradeEventPublisher.cs
├── ITradeEventSubscriptionManager.cs
├── InMem
│ └── InMemoryTradeEventPublisher.cs
├── NoOp
│ └── NoOpTradeEventSubscriptionManager.cs
├── TradeEventHelpers.cs
├── TradeEventType.cs
├── TradeSubscribe.cs
├── TradeSubscribeAck.cs
├── TradeSubscribeNack.cs
├── TradeUnsubscribe.cs
├── TradeUnsubscribeAck.cs
└── TradeUnsubscribeNack.cs
├── Akka.CQRS.Tests
├── Akka.CQRS.Tests.csproj
└── OrderSpecs.cs
├── Akka.CQRS.TradePlacers.Service
├── Akka.CQRS.TradePlacers.Service.csproj
├── Dockerfile
├── Program.cs
└── app.conf
├── Akka.CQRS.TradeProcessor.Actors
├── Akka.CQRS.TradeProcessor.Actors.csproj
├── AskerActor.cs
├── BidderActor.cs
└── OrderBookActor.cs
├── Akka.CQRS.TradeProcessor.Service
├── Akka.CQRS.TradeProcessor.Service.csproj
├── Dockerfile
├── Program.cs
└── app.conf
├── Akka.CQRS
├── Akka.CQRS.csproj
├── AvailableTickerSymbols.cs
├── Commands
│ ├── GetOrderBookSnapshot.cs
│ └── GetRecentMatches.cs
├── Entities
│ ├── Order.cs
│ └── OrderExtensions.cs
├── EntityIdHelper.cs
├── Events
│ ├── Ask.cs
│ ├── Bid.cs
│ ├── Fill.cs
│ └── Match.cs
├── ITimestamper.cs
├── ITradeEvent.cs
├── ITradeOrderGenerator.cs
├── IWithOrderId.cs
├── IWithStockId.cs
├── OrderbookSnapshot.cs
├── PriceRange.cs
├── PriceRangeExtensions.cs
└── Util
│ ├── CurrentUtcTimestamper.cs
│ └── GuidTradeOrderIdGenerator.cs
├── Directory.Build.props
└── Directory.Packages.props
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "11:00"
8 | open-pull-requests-limit: 10
--------------------------------------------------------------------------------
/.github/workflows/pr_validation.yaml:
--------------------------------------------------------------------------------
1 | name: pr_validation
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - dev
8 | - main
9 | pull_request:
10 | branches:
11 | - master
12 | - dev
13 | - main
14 |
15 | jobs:
16 | test:
17 | name: Test-${{matrix.os}}
18 | runs-on: ${{matrix.os}}
19 |
20 | strategy:
21 | matrix:
22 | os: [ubuntu-latest, windows-latest]
23 |
24 | steps:
25 | - name: "Checkout"
26 | uses: actions/checkout@v3.0.2
27 | with:
28 | lfs: true
29 | fetch-depth: 0
30 |
31 | - name: "Install .NET SDK"
32 | uses: actions/setup-dotnet@v2.1.0
33 | with:
34 | dotnet-version: |
35 | 6.0.x
36 | global-json-file: "./global.json"
37 |
38 | - name: "dotnet build"
39 | run: dotnet build -c Release
40 |
41 | - name: "dotnet test"
42 | run: dotnet test -c Release
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Custom
2 | tools/
3 | build/
4 | .nuget/
5 | .dotnet/
6 | .idea/
7 | .[Dd][Ss]_[Ss]tore
8 |
9 | ## NBench output
10 | [Pp]erf[Rr]esult*/
11 |
12 | ## Ignore Visual Studio temporary files, build results, and
13 | ## files generated by popular Visual Studio add-ons.
14 | ##
15 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
16 |
17 | # User-specific files
18 | *.suo
19 | *.user
20 | *.userosscache
21 | *.sln.docstates
22 |
23 | # User-specific files (MonoDevelop/Xamarin Studio)
24 | *.userprefs
25 |
26 | # Build results
27 | [Dd]ebug/
28 | [Dd]ebugPublic/
29 | [Rr]elease/
30 | [Rr]eleases/
31 | x64/
32 | x86/
33 | bld/
34 | [Bb]in/
35 | [Oo]bj/
36 | [Ll]og/
37 |
38 | #FAKE
39 | .fake
40 | tools/
41 |
42 | #DocFx output
43 | _site/
44 |
45 | # Visual Studio 2015 cache/options directory
46 | .vs/
47 | # Uncomment if you have tasks that create the project's static files in wwwroot
48 | #wwwroot/
49 |
50 | # MSTest test Results
51 | [Tt]est[Rr]esult*/
52 | [Bb]uild[Ll]og.*
53 |
54 | # NUNIT
55 | *.VisualState.xml
56 | TestResult.xml
57 |
58 | # Build Results of an ATL Project
59 | [Dd]ebugPS/
60 | [Rr]eleasePS/
61 | dlldata.c
62 |
63 | # .NET Core
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 | **/Properties/launchSettings.json
68 |
69 | *_i.c
70 | *_p.c
71 | *_i.h
72 | *.ilk
73 | *.meta
74 | *.obj
75 | *.pch
76 | *.pdb
77 | *.pgc
78 | *.pgd
79 | *.rsp
80 | *.sbr
81 | *.tlb
82 | *.tli
83 | *.tlh
84 | *.tmp
85 | *.tmp_proj
86 | *.log
87 | *.vspscc
88 | *.vssscc
89 | .builds
90 | *.pidb
91 | *.svclog
92 | *.scc
93 |
94 | # Chutzpah Test files
95 | _Chutzpah*
96 |
97 | # Visual C++ cache files
98 | ipch/
99 | *.aps
100 | *.ncb
101 | *.opendb
102 | *.opensdf
103 | *.sdf
104 | *.cachefile
105 | *.VC.db
106 | *.VC.VC.opendb
107 |
108 | # Visual Studio profiler
109 | *.psess
110 | *.vsp
111 | *.vspx
112 | *.sap
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # Visual Studio code coverage results
135 | *.coverage
136 | *.coveragexml
137 |
138 | # NCrunch
139 | _NCrunch_*
140 | .*crunch*.local.xml
141 | nCrunchTemp_*
142 |
143 | # MightyMoose
144 | *.mm.*
145 | AutoTest.Net/
146 |
147 | # Web workbench (sass)
148 | .sass-cache/
149 |
150 | # Installshield output folder
151 | [Ee]xpress/
152 |
153 | # DocProject is a documentation generator add-in
154 | DocProject/buildhelp/
155 | DocProject/Help/*.HxT
156 | DocProject/Help/*.HxC
157 | DocProject/Help/*.hhc
158 | DocProject/Help/*.hhk
159 | DocProject/Help/*.hhp
160 | DocProject/Help/Html2
161 | DocProject/Help/html
162 |
163 | # Click-Once directory
164 | publish/
165 |
166 | # Publish Web Output
167 | *.[Pp]ublish.xml
168 | *.azurePubxml
169 | # TODO: Comment the next line if you want to checkin your web deploy settings
170 | # but database connection strings (with potential passwords) will be unencrypted
171 | *.pubxml
172 | *.publishproj
173 |
174 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
175 | # checkin your Azure Web App publish settings, but sensitive information contained
176 | # in these scripts will be unencrypted
177 | PublishScripts/
178 |
179 | # NuGet Packages
180 | *.nupkg
181 | # The packages folder can be ignored because of Package Restore
182 | **/packages/*
183 | # except build/, which is used as an MSBuild target.
184 | !**/packages/build/
185 | # Uncomment if necessary however generally it will be regenerated when needed
186 | #!**/packages/repositories.config
187 | # NuGet v3's project.json files produces more ignorable files
188 | *.nuget.props
189 | *.nuget.targets
190 |
191 | # Microsoft Azure Build Output
192 | csx/
193 | *.build.csdef
194 |
195 | # Microsoft Azure Emulator
196 | ecf/
197 | rcf/
198 |
199 | # Windows Store app package directories and files
200 | AppPackages/
201 | BundleArtifacts/
202 | Package.StoreAssociation.xml
203 | _pkginfo.txt
204 |
205 | # Visual Studio cache files
206 | # files ending in .cache can be ignored
207 | *.[Cc]ache
208 | # but keep track of directories ending in .cache
209 | !*.[Cc]ache/
210 |
211 | # Others
212 | ClientBin/
213 | ~$*
214 | *~
215 | *.dbmdl
216 | *.dbproj.schemaview
217 | *.jfm
218 | *.pfx
219 | *.publishsettings
220 | orleans.codegen.cs
221 |
222 | # Since there are multiple workflows, uncomment next line to ignore bower_components
223 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
224 | #bower_components/
225 |
226 | # RIA/Silverlight projects
227 | Generated_Code/
228 |
229 | # Backup & report files from converting an old project file
230 | # to a newer Visual Studio version. Backup files are not needed,
231 | # because we have git ;-)
232 | _UpgradeReport_Files/
233 | Backup*/
234 | UpgradeLog*.XML
235 | UpgradeLog*.htm
236 |
237 | # SQL Server files
238 | *.mdf
239 | *.ldf
240 | *.ndf
241 |
242 | # Business Intelligence projects
243 | *.rdl.data
244 | *.bim.layout
245 | *.bim_*.settings
246 |
247 | # Microsoft Fakes
248 | FakesAssemblies/
249 |
250 | # GhostDoc plugin setting file
251 | *.GhostDoc.xml
252 |
253 | # Node.js Tools for Visual Studio
254 | .ntvs_analysis.dat
255 | node_modules/
256 |
257 | # Typescript v1 declaration files
258 | typings/
259 |
260 | # Visual Studio 6 build log
261 | *.plg
262 |
263 | # Visual Studio 6 workspace options file
264 | *.opt
265 |
266 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
267 | *.vbw
268 |
269 | # Visual Studio LightSwitch build output
270 | **/*.HTMLClient/GeneratedArtifacts
271 | **/*.DesktopClient/GeneratedArtifacts
272 | **/*.DesktopClient/ModelManifest.xml
273 | **/*.Server/GeneratedArtifacts
274 | **/*.Server/ModelManifest.xml
275 | _Pvt_Extensions
276 |
277 | # Paket dependency manager
278 | .paket/paket.exe
279 | paket-files/
280 |
281 | # FAKE - F# Make
282 | .fake/
283 |
284 | # JetBrains Rider
285 | .idea/
286 | *.sln.iml
287 |
288 | # CodeRush
289 | .cr/
290 |
291 | # Python Tools for Visual Studio (PTVS)
292 | __pycache__/
293 | *.pyc
294 |
295 | # Cake - Uncomment if you are using it
296 | # tools/**
297 | # !tools/packages.config
298 |
299 | # Telerik's JustMock configuration file
300 | *.jmconfig
301 |
302 | # BizTalk build output
303 | *.btp.cs
304 | *.btm.cs
305 | *.odx.cs
306 | *.xsd.cs
307 |
--------------------------------------------------------------------------------
/Akka.CQRS.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26430.14
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS", "src\Akka.CQRS\Akka.CQRS.csproj", "{E945AABA-2779-41E8-9B43-8898FFD64F22}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Tests", "src\Akka.CQRS.Tests\Akka.CQRS.Tests.csproj", "{0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{79D71264-186B-4F62-8930-35DD9ECCAF3B}"
11 | ProjectSection(SolutionItems) = preProject
12 | build.cmd = build.cmd
13 | build.fsx = build.fsx
14 | build.ps1 = build.ps1
15 | build.sh = build.sh
16 | EndProjectSection
17 | EndProject
18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Matching", "src\Akka.CQRS.Matching\Akka.CQRS.Matching.csproj", "{F6F74FCA-8E53-4620-90C7-9A91278A3521}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Matching.Tests", "src\Akka.CQRS.Matching.Tests\Akka.CQRS.Matching.Tests.csproj", "{F1EFDCB9-FD61-4782-A12A-108C73DB368E}"
21 | EndProject
22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libraries", "libraries", "{08DA7279-F347-4F54-95B6-7335F52746A6}"
23 | EndProject
24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "trade-platform", "trade-platform", "{E1C0B656-C8EA-4F93-8855-798548B72B13}"
25 | EndProject
26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.TradeProcessor.Service", "src\Akka.CQRS.TradeProcessor.Service\Akka.CQRS.TradeProcessor.Service.csproj", "{5EB6F2C5-FEF1-402A-AB76-3A9F62B2EFDE}"
27 | EndProject
28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.TradeProcessor.Actors", "src\Akka.CQRS.TradeProcessor.Actors\Akka.CQRS.TradeProcessor.Actors.csproj", "{733BA8FC-C198-4C83-BA57-FE3672CBB307}"
29 | EndProject
30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Subscriptions", "src\Akka.CQRS.Subscriptions\Akka.CQRS.Subscriptions.csproj", "{53895A69-5516-4598-8D76-32F9F2C67F0E}"
31 | EndProject
32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Infrastructure", "src\Akka.CQRS.Infrastructure\Akka.CQRS.Infrastructure.csproj", "{290432CA-71FE-4045-9503-D294CC2586C4}"
33 | EndProject
34 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.TradePlacers.Service", "src\Akka.CQRS.TradePlacers.Service\Akka.CQRS.TradePlacers.Service.csproj", "{BD6F0E08-1A18-44CE-A121-4AD09D63C0B0}"
35 | EndProject
36 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pricing-platform", "pricing-platform", "{C60F4548-6FFD-4D0E-B8C3-6CD6BC8FBC2C}"
37 | EndProject
38 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Pricing", "src\Akka.CQRS.Pricing\Akka.CQRS.Pricing.csproj", "{A14AB205-AEF1-47ED-9836-D241D69889C4}"
39 | EndProject
40 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Pricing.Actors", "src\Akka.CQRS.Pricing.Actors\Akka.CQRS.Pricing.Actors.csproj", "{F6F32FB5-332F-48C1-8EC4-3FBD313BA411}"
41 | EndProject
42 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Pricing.Service", "src\Akka.CQRS.Pricing.Service\Akka.CQRS.Pricing.Service.csproj", "{ABB3AA71-9C0E-4455-9631-5D59031A8885}"
43 | EndProject
44 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.CQRS.Pricing.Cli", "src\Akka.CQRS.Pricing.Cli\Akka.CQRS.Pricing.Cli.csproj", "{333F7F76-8429-487D-BEC8-1AE57ABC7D49}"
45 | EndProject
46 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.CQRS.Infrastructure.Tests", "src\Akka.CQRS.Infrastructure.Tests\Akka.CQRS.Infrastructure.Tests.csproj", "{73E9BD37-D311-42FF-80E5-3E6C38835910}"
47 | EndProject
48 | Global
49 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
50 | Debug|Any CPU = Debug|Any CPU
51 | Release|Any CPU = Release|Any CPU
52 | EndGlobalSection
53 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
54 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Debug|Any CPU.Build.0 = Debug|Any CPU
56 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Release|Any CPU.ActiveCfg = Release|Any CPU
57 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Release|Any CPU.Build.0 = Release|Any CPU
58 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Debug|Any CPU.Build.0 = Debug|Any CPU
60 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Release|Any CPU.ActiveCfg = Release|Any CPU
61 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Release|Any CPU.Build.0 = Release|Any CPU
62 | {F6F74FCA-8E53-4620-90C7-9A91278A3521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
63 | {F6F74FCA-8E53-4620-90C7-9A91278A3521}.Debug|Any CPU.Build.0 = Debug|Any CPU
64 | {F6F74FCA-8E53-4620-90C7-9A91278A3521}.Release|Any CPU.ActiveCfg = Release|Any CPU
65 | {F6F74FCA-8E53-4620-90C7-9A91278A3521}.Release|Any CPU.Build.0 = Release|Any CPU
66 | {F1EFDCB9-FD61-4782-A12A-108C73DB368E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67 | {F1EFDCB9-FD61-4782-A12A-108C73DB368E}.Debug|Any CPU.Build.0 = Debug|Any CPU
68 | {F1EFDCB9-FD61-4782-A12A-108C73DB368E}.Release|Any CPU.ActiveCfg = Release|Any CPU
69 | {F1EFDCB9-FD61-4782-A12A-108C73DB368E}.Release|Any CPU.Build.0 = Release|Any CPU
70 | {5EB6F2C5-FEF1-402A-AB76-3A9F62B2EFDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71 | {5EB6F2C5-FEF1-402A-AB76-3A9F62B2EFDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
72 | {5EB6F2C5-FEF1-402A-AB76-3A9F62B2EFDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
73 | {5EB6F2C5-FEF1-402A-AB76-3A9F62B2EFDE}.Release|Any CPU.Build.0 = Release|Any CPU
74 | {733BA8FC-C198-4C83-BA57-FE3672CBB307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75 | {733BA8FC-C198-4C83-BA57-FE3672CBB307}.Debug|Any CPU.Build.0 = Debug|Any CPU
76 | {733BA8FC-C198-4C83-BA57-FE3672CBB307}.Release|Any CPU.ActiveCfg = Release|Any CPU
77 | {733BA8FC-C198-4C83-BA57-FE3672CBB307}.Release|Any CPU.Build.0 = Release|Any CPU
78 | {53895A69-5516-4598-8D76-32F9F2C67F0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79 | {53895A69-5516-4598-8D76-32F9F2C67F0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
80 | {53895A69-5516-4598-8D76-32F9F2C67F0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
81 | {53895A69-5516-4598-8D76-32F9F2C67F0E}.Release|Any CPU.Build.0 = Release|Any CPU
82 | {290432CA-71FE-4045-9503-D294CC2586C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
83 | {290432CA-71FE-4045-9503-D294CC2586C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
84 | {290432CA-71FE-4045-9503-D294CC2586C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
85 | {290432CA-71FE-4045-9503-D294CC2586C4}.Release|Any CPU.Build.0 = Release|Any CPU
86 | {BD6F0E08-1A18-44CE-A121-4AD09D63C0B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
87 | {BD6F0E08-1A18-44CE-A121-4AD09D63C0B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
88 | {BD6F0E08-1A18-44CE-A121-4AD09D63C0B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
89 | {BD6F0E08-1A18-44CE-A121-4AD09D63C0B0}.Release|Any CPU.Build.0 = Release|Any CPU
90 | {A14AB205-AEF1-47ED-9836-D241D69889C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
91 | {A14AB205-AEF1-47ED-9836-D241D69889C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
92 | {A14AB205-AEF1-47ED-9836-D241D69889C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
93 | {A14AB205-AEF1-47ED-9836-D241D69889C4}.Release|Any CPU.Build.0 = Release|Any CPU
94 | {F6F32FB5-332F-48C1-8EC4-3FBD313BA411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
95 | {F6F32FB5-332F-48C1-8EC4-3FBD313BA411}.Debug|Any CPU.Build.0 = Debug|Any CPU
96 | {F6F32FB5-332F-48C1-8EC4-3FBD313BA411}.Release|Any CPU.ActiveCfg = Release|Any CPU
97 | {F6F32FB5-332F-48C1-8EC4-3FBD313BA411}.Release|Any CPU.Build.0 = Release|Any CPU
98 | {ABB3AA71-9C0E-4455-9631-5D59031A8885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
99 | {ABB3AA71-9C0E-4455-9631-5D59031A8885}.Debug|Any CPU.Build.0 = Debug|Any CPU
100 | {ABB3AA71-9C0E-4455-9631-5D59031A8885}.Release|Any CPU.ActiveCfg = Release|Any CPU
101 | {ABB3AA71-9C0E-4455-9631-5D59031A8885}.Release|Any CPU.Build.0 = Release|Any CPU
102 | {333F7F76-8429-487D-BEC8-1AE57ABC7D49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
103 | {333F7F76-8429-487D-BEC8-1AE57ABC7D49}.Debug|Any CPU.Build.0 = Debug|Any CPU
104 | {333F7F76-8429-487D-BEC8-1AE57ABC7D49}.Release|Any CPU.ActiveCfg = Release|Any CPU
105 | {333F7F76-8429-487D-BEC8-1AE57ABC7D49}.Release|Any CPU.Build.0 = Release|Any CPU
106 | {73E9BD37-D311-42FF-80E5-3E6C38835910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
107 | {73E9BD37-D311-42FF-80E5-3E6C38835910}.Debug|Any CPU.Build.0 = Debug|Any CPU
108 | {73E9BD37-D311-42FF-80E5-3E6C38835910}.Release|Any CPU.ActiveCfg = Release|Any CPU
109 | {73E9BD37-D311-42FF-80E5-3E6C38835910}.Release|Any CPU.Build.0 = Release|Any CPU
110 | EndGlobalSection
111 | GlobalSection(SolutionProperties) = preSolution
112 | HideSolutionNode = FALSE
113 | EndGlobalSection
114 | GlobalSection(NestedProjects) = preSolution
115 | {E945AABA-2779-41E8-9B43-8898FFD64F22} = {08DA7279-F347-4F54-95B6-7335F52746A6}
116 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970} = {08DA7279-F347-4F54-95B6-7335F52746A6}
117 | {F6F74FCA-8E53-4620-90C7-9A91278A3521} = {08DA7279-F347-4F54-95B6-7335F52746A6}
118 | {F1EFDCB9-FD61-4782-A12A-108C73DB368E} = {08DA7279-F347-4F54-95B6-7335F52746A6}
119 | {5EB6F2C5-FEF1-402A-AB76-3A9F62B2EFDE} = {E1C0B656-C8EA-4F93-8855-798548B72B13}
120 | {733BA8FC-C198-4C83-BA57-FE3672CBB307} = {E1C0B656-C8EA-4F93-8855-798548B72B13}
121 | {53895A69-5516-4598-8D76-32F9F2C67F0E} = {08DA7279-F347-4F54-95B6-7335F52746A6}
122 | {290432CA-71FE-4045-9503-D294CC2586C4} = {08DA7279-F347-4F54-95B6-7335F52746A6}
123 | {BD6F0E08-1A18-44CE-A121-4AD09D63C0B0} = {E1C0B656-C8EA-4F93-8855-798548B72B13}
124 | {A14AB205-AEF1-47ED-9836-D241D69889C4} = {C60F4548-6FFD-4D0E-B8C3-6CD6BC8FBC2C}
125 | {F6F32FB5-332F-48C1-8EC4-3FBD313BA411} = {C60F4548-6FFD-4D0E-B8C3-6CD6BC8FBC2C}
126 | {ABB3AA71-9C0E-4455-9631-5D59031A8885} = {C60F4548-6FFD-4D0E-B8C3-6CD6BC8FBC2C}
127 | {333F7F76-8429-487D-BEC8-1AE57ABC7D49} = {C60F4548-6FFD-4D0E-B8C3-6CD6BC8FBC2C}
128 | {73E9BD37-D311-42FF-80E5-3E6C38835910} = {08DA7279-F347-4F54-95B6-7335F52746A6}
129 | EndGlobalSection
130 | GlobalSection(ExtensibilityGlobals) = postSolution
131 | SolutionGuid = {B99E6BB8-642A-4A68-86DF-69567CBA700A}
132 | EndGlobalSection
133 | EndGlobal
134 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2015-2017 {user}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
1 | #### 0.1.0 August 14 2017 ####
2 | First release
--------------------------------------------------------------------------------
/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "SignClient": {
3 | "AzureAd": {
4 | "AADInstance": "https://login.microsoftonline.com/",
5 | "ClientId": "",
6 | "TenantId": ""
7 | },
8 | "Service": {
9 | "Url": "",
10 | "ResourceId": ""
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/build-system/README.md:
--------------------------------------------------------------------------------
1 | # Azure Pipelines Build Files
2 | These `.yaml` files are used by Windows Azure DevOps Pipelines to help execute the following types of builds:
3 |
4 | - Pull request validation on Linux (Mono / .NET Core)
5 | - Pull request validation on Windows (.NET Framework / .NET Core)
6 | - NuGet releases with automatic release notes posted to a Github Release repository.
7 |
8 | **NOTE**: you will need to change some of the pipeline variables inside the `windows-release.yaml` for your specific project and you will also want to create variable groups with your signing and NuGet push information.
--------------------------------------------------------------------------------
/build-system/azure-pipeline.template.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | name: ''
3 | vmImage: ''
4 | scriptFileName: ''
5 | scriptArgs: 'all'
6 | timeoutInMinutes: 120
7 |
8 | jobs:
9 | - job: ${{ parameters.name }}
10 | timeoutInMinutes: ${{ parameters.timeoutInMinutes }}
11 | pool:
12 | vmImage: ${{ parameters.vmImage }}
13 | steps:
14 | - checkout: self # self represents the repo where the initial Pipelines YAML file was found
15 | clean: false # whether to fetch clean each time
16 | submodules: recursive # set to 'true' for a single level of submodules or 'recursive' to get submodules of submodules
17 | persistCredentials: true
18 | # Linux or macOS
19 | - task: Bash@3
20 | displayName: Linux / OSX Build
21 | inputs:
22 | filePath: ${{ parameters.scriptFileName }}
23 | arguments: ${{ parameters.scriptArgs }}
24 | continueOnError: true
25 | condition: in( variables['Agent.OS'], 'Linux', 'Darwin' )
26 | # Windows
27 | - task: BatchScript@1
28 | displayName: Windows Build
29 | inputs:
30 | filename: ${{ parameters.scriptFileName }}
31 | arguments: ${{ parameters.scriptArgs }}
32 | continueOnError: true
33 | condition: eq( variables['Agent.OS'], 'Windows_NT' )
34 | - task: PublishTestResults@2
35 | inputs:
36 | testRunner: VSTest
37 | testResultsFiles: '**/*.trx' #TestResults folder usually
38 | testRunTitle: ${{ parameters.name }}
39 | mergeTestResults: true
40 | - script: 'echo 1>&2'
41 | failOnStderr: true
42 | displayName: 'If above is partially succeeded, then fail'
43 | condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues')
--------------------------------------------------------------------------------
/build-system/linux-pr-validation.yaml:
--------------------------------------------------------------------------------
1 | # Pull request validation for Linux against the `dev` and `master` branches
2 | # See https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema for reference
3 | trigger:
4 | branches:
5 | include:
6 | - dev
7 | - master
8 |
9 | name: $(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r)
10 |
11 | pr:
12 | autoCancel: true # indicates whether additional pushes to a PR should cancel in-progress runs for the same PR. Defaults to true
13 | branches:
14 | include: [ dev, master ] # branch names which will trigger a build
15 |
16 | jobs:
17 | - template: azure-pipeline.template.yaml
18 | parameters:
19 | name: Ubuntu
20 | vmImage: 'ubuntu-22.04'
21 | scriptFileName: ./build.sh
22 | scriptArgs: all
--------------------------------------------------------------------------------
/build-system/windows-pr-validation.yaml:
--------------------------------------------------------------------------------
1 | # Pull request validation for Windows against the `dev` and `master` branches
2 | # See https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema for reference
3 | trigger:
4 | branches:
5 | include:
6 | - dev
7 | - master
8 |
9 | pr:
10 | autoCancel: true # indicates whether additional pushes to a PR should cancel in-progress runs for the same PR. Defaults to true
11 | branches:
12 | include: [ dev, master ] # branch names which will trigger a build
13 |
14 | name: $(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r)
15 |
16 | jobs:
17 | - template: azure-pipeline.template.yaml
18 | parameters:
19 | name: Windows
20 | vmImage: 'windows-latest'
21 | scriptFileName: build.cmd
22 | scriptArgs: all
--------------------------------------------------------------------------------
/build-system/windows-release.yaml:
--------------------------------------------------------------------------------
1 | # Release task for PbLib projects
2 | # See https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema for reference
3 |
4 | pool:
5 | vmImage: windows-latest
6 | demands: Cmd
7 |
8 | trigger:
9 | branches:
10 | include:
11 | - refs/tags/*
12 |
13 | variables:
14 | - group: signingSecrets #create this group with SECRET variables `signingUsername` and `signingPassword`
15 | - group: nugetKeys #create this group with SECRET variables `nugetKey`
16 | - name: githubConnectionName
17 | value: yourConnection #replace this
18 | - name: projectName
19 | value: yourProjectName #replace this
20 | - name: githubRepositoryName
21 | value: yourOrganization/yourRepo #replace this
22 |
23 | steps:
24 | - task: BatchScript@1
25 | displayName: 'FAKE Build'
26 | inputs:
27 | filename: build.cmd
28 | arguments: 'All SignClientUser=$(signingUsername) SignClientSecret=$(signingPassword) nugetpublishurl=https://www.nuget.org/api/v2/package nugetkey=$(nugetKey)'
29 |
30 | - task: GitHubRelease@0
31 | displayName: 'GitHub release (create)'
32 | inputs:
33 | gitHubConnection: $(githubConnectionName)
34 | repositoryName: $(githubRepositoryName)
35 | title: '$(projectName) v$(Build.SourceBranchName)'
36 | releaseNotesFile: 'RELEASE_NOTES.md'
37 | assets: |
38 | bin\nuget\*.nupkg
--------------------------------------------------------------------------------
/build.cmd:
--------------------------------------------------------------------------------
1 | PowerShell.exe -file "build.ps1" %*
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | ##########################################################################
3 | # This is the Fake bootstrapper script for Linux and OS X.
4 | ##########################################################################
5 |
6 | # Define directories.
7 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
8 | TOOLS_DIR=$SCRIPT_DIR/tools
9 | SIGNCLIENT_DIR=$TOOLS_DIR/signclient
10 | NUGET_EXE=$TOOLS_DIR/nuget.exe
11 | NUGET_URL=https://dist.nuget.org/win-x86-commandline/v4.0.0/nuget.exe
12 | FAKE_VERSION=4.61.2
13 | FAKE_EXE=$TOOLS_DIR/FAKE/tools/FAKE.exe
14 | DOTNET_VERSION=2.1.500
15 | DOTNET_INSTALLER_URL=https://raw.githubusercontent.com/dotnet/cli/v$DOTNET_VERSION/scripts/obtain/dotnet-install.sh
16 | DOTNET_CHANNEL=LTS;
17 | DOCFX_VERSION=2.40.5
18 | DOCFX_EXE=$TOOLS_DIR/docfx.console/tools/docfx.exe
19 |
20 | # Define default arguments.
21 | TARGET="Default"
22 | CONFIGURATION="Release"
23 | VERBOSITY="verbose"
24 | DRYRUN=
25 | SCRIPT_ARGUMENTS=()
26 |
27 | # Parse arguments.
28 | for i in "$@"; do
29 | case $1 in
30 | -t|--target) TARGET="$2"; shift ;;
31 | -c|--configuration) CONFIGURATION="$2"; shift ;;
32 | -v|--verbosity) VERBOSITY="$2"; shift ;;
33 | -d|--dryrun) DRYRUN="-dryrun" ;;
34 | --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;;
35 | *) SCRIPT_ARGUMENTS+=("$1") ;;
36 | esac
37 | shift
38 | done
39 |
40 | # Make sure the tools folder exist.
41 | if [ ! -d "$TOOLS_DIR" ]; then
42 | mkdir "$TOOLS_DIR"
43 | fi
44 |
45 | ###########################################################################
46 | # INSTALL .NET CORE CLI
47 | ###########################################################################
48 |
49 | echo "Installing .NET CLI..."
50 | if [ ! -d "$SCRIPT_DIR/.dotnet" ]; then
51 | mkdir "$SCRIPT_DIR/.dotnet"
52 | fi
53 | curl -Lsfo "$SCRIPT_DIR/.dotnet/dotnet-install.sh" $DOTNET_INSTALLER_URL
54 | bash "$SCRIPT_DIR/.dotnet/dotnet-install.sh" --version $DOTNET_VERSION --channel $DOTNET_CHANNEL --install-dir .dotnet --no-path
55 | export PATH="$SCRIPT_DIR/.dotnet":$PATH
56 | export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
57 | export DOTNET_CLI_TELEMETRY_OPTOUT=1
58 | chmod -R 0755 ".dotnet"
59 | "$SCRIPT_DIR/.dotnet/dotnet" --info
60 |
61 |
62 | ###########################################################################
63 | # INSTALL NUGET
64 | ###########################################################################
65 |
66 | # Download NuGet if it does not exist.
67 | if [ ! -f "$NUGET_EXE" ]; then
68 | echo "Downloading NuGet..."
69 | curl -Lsfo "$NUGET_EXE" $NUGET_URL
70 | if [ $? -ne 0 ]; then
71 | echo "An error occured while downloading nuget.exe."
72 | exit 1
73 | fi
74 | fi
75 |
76 | ###########################################################################
77 | # INSTALL FAKE
78 | ###########################################################################
79 |
80 | if [ ! -f "$FAKE_EXE" ]; then
81 | mono "$NUGET_EXE" install Fake -ExcludeVersion -Version $FAKE_VERSION -OutputDirectory "$TOOLS_DIR"
82 | if [ $? -ne 0 ]; then
83 | echo "An error occured while installing Cake."
84 | exit 1
85 | fi
86 | fi
87 |
88 | # Make sure that Fake has been installed.
89 | if [ ! -f "$FAKE_EXE" ]; then
90 | echo "Could not find Fake.exe at '$FAKE_EXE'."
91 | exit 1
92 | fi
93 |
94 | ###########################################################################
95 | # INSTALL DOCFX
96 | ###########################################################################
97 | if [ ! -f "$DOCFX_EXE" ]; then
98 | mono "$NUGET_EXE" install docfx.console -ExcludeVersion -Version $DOCFX_VERSION -OutputDirectory "$TOOLS_DIR"
99 | if [ $? -ne 0 ]; then
100 | echo "An error occured while installing DocFx."
101 | exit 1
102 | fi
103 | fi
104 |
105 | # Make sure that DocFx has been installed.
106 | if [ ! -f "$DOCFX_EXE" ]; then
107 | echo "Could not find docfx.exe at '$DOCFX_EXE'."
108 | exit 1
109 | fi
110 |
111 | ###########################################################################
112 | # INSTALL SignTool
113 | ###########################################################################
114 | if [ ! -f "$SIGNTOOL_EXE" ]; then
115 | "$SCRIPT_DIR/.dotnet/dotnet" tool install SignClient --version 1.0.82 --tool-path "$SIGNCLIENT_DIR"
116 | if [ $? -ne 0 ]; then
117 | echo "SignClient already installed."
118 | fi
119 | fi
120 |
121 |
122 | ###########################################################################
123 | # WORKAROUND FOR MONO
124 | ###########################################################################
125 | export FrameworkPathOverride=/usr/lib/mono/4.5/
126 |
127 | ###########################################################################
128 | # RUN BUILD SCRIPT
129 | ###########################################################################
130 |
131 | # Start Fake
132 | exec mono "$FAKE_EXE" build.fsx "${SCRIPT_ARGUMENTS[@]}" --verbosity=$VERBOSITY --configuration=$CONFIGURATION --target=$TARGET $DRYRUN
133 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | sql:
5 | image: mcr.microsoft.com/mssql/server:2019-latest
6 | hostname: sql
7 | restart: always
8 | ports:
9 | - '1633:1433'
10 | environment:
11 | ACCEPT_EULA: "Y"
12 | MSSQL_SA_PASSWORD: "This!IsOpenSource1"
13 |
14 | lighthouse:
15 | image: petabridge/lighthouse:latest
16 | hostname: lighthouse
17 | ports:
18 | - '9110:9110'
19 | - '4053:4053'
20 | environment:
21 | ACTORSYSTEM: "AkkaTrader"
22 | CLUSTER_PORT: 4053
23 | CLUSTER_IP: "lighthouse"
24 | CLUSTER_SEEDS: "akka.tcp://AkkaTrader@lighthouse:4053"
25 |
26 | tradeprocessor:
27 | image: akka.cqrs.tradeprocessor
28 | ports:
29 | - '0:9110'
30 | environment:
31 | CLUSTER_SEEDS: "akka.tcp://AkkaTrader@lighthouse:4053"
32 | CLUSTER_PORT: 5110
33 | SQL_CONNECTION_STR: "Server=sql,1433;User Id=sa;Password=This!IsOpenSource1;TrustServerCertificate=true"
34 | SQL_PROVIDER_NAME: "SqlServer.2019"
35 | restart: on-failure
36 | depends_on:
37 | - "sql"
38 | - "lighthouse"
39 |
40 | traders:
41 | image: akka.cqrs.traders
42 | ports:
43 | - '0:9110'
44 | environment:
45 | CLUSTER_SEEDS: "akka.tcp://AkkaTrader@lighthouse:4053"
46 | CLUSTER_PORT: 5110
47 | restart: on-failure
48 | depends_on:
49 | - "lighthouse"
50 |
51 | lighthouse_pricing:
52 | image: petabridge/lighthouse:latest
53 | hostname: lighthouse_pricing
54 | ports:
55 | - '9111:9110'
56 | - '4054:4054'
57 | environment:
58 | ACTORSYSTEM: "AkkaPricing"
59 | CLUSTER_PORT: 4054
60 | CLUSTER_IP: "lighthouse_pricing"
61 | CLUSTER_SEEDS: "akka.tcp://AkkaPricing@lighthouse_pricing:4054"
62 |
63 | pricing-engine:
64 | image: akka.cqrs.pricing
65 | deploy:
66 | replicas: 3
67 | ports:
68 | - '0:9110'
69 | environment:
70 | CLUSTER_SEEDS: "akka.tcp://AkkaPricing@lighthouse_pricing:4054"
71 | CLUSTER_PORT: 6055
72 | SQL_CONNECTION_STR: "Server=sql,1433;User Id=sa;Password=This!IsOpenSource1;TrustServerCertificate=true"
73 | SQL_PROVIDER_NAME: "SqlServer.2019"
74 | restart: on-failure
75 | depends_on:
76 | - "sql"
77 | - "lighthouse_pricing"
--------------------------------------------------------------------------------
/docs/api/index.md:
--------------------------------------------------------------------------------
1 | # API Docs
--------------------------------------------------------------------------------
/docs/articles/index.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Article text goes here.
--------------------------------------------------------------------------------
/docs/articles/toc.yml:
--------------------------------------------------------------------------------
1 | - name: Introduction
2 | href: index.md
--------------------------------------------------------------------------------
/docs/docfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": [
3 | {
4 | "src": [
5 | {
6 | "files": [ "**/*.csproj" ],
7 | "exclude": [
8 | "**/obj/**",
9 | "**/bin/**",
10 | "_site/**",
11 | "**/*Tests*.csproj",
12 | "**/*Tests.*.csproj"
13 | ],
14 | "src": "../src"
15 | }
16 | ],
17 | "dest": "api"
18 | }
19 | ],
20 | "build": {
21 | "content": [
22 | {
23 | "files": [
24 | "api/**.yml",
25 | "api/index.md"
26 | ]
27 | },
28 | {
29 | "files": [
30 | "articles/**.md",
31 | "articles/**/toc.yml",
32 | "toc.yml",
33 | "*.md"
34 | ],
35 | "exclude": [
36 | "obj/**",
37 | "_site/**"
38 | ]
39 | },
40 | ],
41 | "resource": [
42 | {
43 | "files": [
44 | "images/**",
45 | "web.config",
46 | ],
47 | "exclude": [
48 | "obj/**",
49 | "_site/**"
50 | ]
51 | }
52 | ],
53 | "sitemap": {
54 | "baseUrl": "https://yoursite.github.io/"
55 | },
56 | "dest": "_site",
57 | "globalMetadata": {
58 | "_appTitle": "Akka.CQRS",
59 | "_disableContribution": "true",
60 | "_appLogoPath": "/images/icon.png",
61 | },
62 | "globalMetadataFiles": [],
63 | "fileMetadataFiles": [],
64 | "template": [
65 | "default",
66 | "template"
67 | ],
68 | "postProcessors": ["ExtractSearchIndex"],
69 | "noLangKeyword": false
70 | }
71 | }
--------------------------------------------------------------------------------
/docs/images/akka-cqrs-architectural-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aaronontheweb/InMemoryCQRSReplication/5ffdee8533876c09b4f70ba6c5425d95f102f186/docs/images/akka-cqrs-architectural-overview.png
--------------------------------------------------------------------------------
/docs/images/akka-cqrs-inmemory-replication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aaronontheweb/InMemoryCQRSReplication/5ffdee8533876c09b4f70ba6c5425d95f102f186/docs/images/akka-cqrs-inmemory-replication.png
--------------------------------------------------------------------------------
/docs/images/docker-for-windows-networking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aaronontheweb/InMemoryCQRSReplication/5ffdee8533876c09b4f70ba6c5425d95f102f186/docs/images/docker-for-windows-networking.png
--------------------------------------------------------------------------------
/docs/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aaronontheweb/InMemoryCQRSReplication/5ffdee8533876c09b4f70ba6c5425d95f102f186/docs/images/icon.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Introduction to My Project
--------------------------------------------------------------------------------
/docs/toc.yml:
--------------------------------------------------------------------------------
1 | - name: Home
2 | href: index.md
3 | - name: Documentation
4 | href: articles/
5 | - name: API Reference
6 | href: api/
--------------------------------------------------------------------------------
/docs/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "rollForward": "latestMinor",
4 | "version": "7.0"
5 | }
6 | }
--------------------------------------------------------------------------------
/serve-docs.cmd:
--------------------------------------------------------------------------------
1 | PowerShell.exe -file "serve-docs.ps1" %* .\docs\docfx.json --serve -p 8090
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure.Tests/Akka.CQRS.Infrastructure.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(NetVersion)
4 |
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure.Tests/ConfigSpecs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Akka.CQRS.Infrastructure.Ops;
3 | using FluentAssertions;
4 | using Xunit;
5 |
6 | namespace Akka.CQRS.Infrastructure.Tests
7 | {
8 | public class ConfigSpecs
9 | {
10 | [Fact]
11 | public void ShouldLoadOpsConfig()
12 | {
13 | var config = OpsConfig.GetOpsConfig();
14 | config.GetConfig("akka.cluster").HasPath("split-brain-resolver.active-strategy").Should().BeTrue();
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure/Akka.CQRS.Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | Shared, non-domain-specific infrastructure used by various Akka.NET services.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure/Ops/OpsConfig.cs:
--------------------------------------------------------------------------------
1 | using Akka.Configuration;
2 |
3 | namespace Akka.CQRS.Infrastructure.Ops
4 | {
5 | ///
6 | /// Helper class for ensuring that certain parts of Akka.NET's
7 | /// configuration are applied consistently across all services.
8 | ///
9 | public class OpsConfig
10 | {
11 | public static Akka.Configuration.Config GetOpsConfig()
12 | {
13 | return ConfigurationFactory.FromResource("Akka.CQRS.Infrastructure.Ops.ops.conf");
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure/Ops/ops.conf:
--------------------------------------------------------------------------------
1 | # Akka.Cluster split-brain resolver configurations
2 | akka.cluster{
3 | downing-provider-class = "Akka.Cluster.SplitBrainResolver, Akka.Cluster"
4 | split-brain-resolver {
5 | active-strategy = keep-majority
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure/SqlDbHoconHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Akka.Configuration;
3 |
4 | namespace Akka.CQRS.Infrastructure
5 | {
6 | ///
7 | /// Shared utility class for formatting SQL connection strings into the required
8 | /// Akka.Persistence.Sql HOCON .
9 | ///
10 | public static class SqlDbHoconHelper
11 | {
12 | public static Configuration.Config GetSqlHocon(string connectionStr, string providerName)
13 | {
14 | var hocon = $@"
15 | akka.persistence.journal.sql {{
16 | connection-string = ""{connectionStr}""
17 | provider-name = ""{providerName}""
18 | }}
19 | akka.persistence.query.journal.sql {{
20 | connection-string = ""{connectionStr}""
21 | provider-name = ""{providerName}""
22 | }}
23 | akka.persistence.snapshot-store.sql{{
24 | connection-string = ""{connectionStr}""
25 | provider-name = ""{providerName}""
26 | }}";
27 | return hocon;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure/StockEventTagger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using System.Text;
5 | using Akka.CQRS.Events;
6 | using Akka.Persistence.Journal;
7 |
8 | namespace Akka.CQRS.Infrastructure
9 | {
10 | ///
11 | /// Used to tag trade events so they can be consumed inside Akka.Persistence.Query
12 | ///
13 | public sealed class StockEventTagger : IWriteEventAdapter
14 | {
15 | public string Manifest(object evt)
16 | {
17 | return string.Empty;
18 | }
19 |
20 | public object ToJournal(object evt)
21 | {
22 | switch (evt)
23 | {
24 | case Ask ask:
25 | return new Tagged(evt, ImmutableHashSet.Empty.Add(ask.StockId).Add("Ask"));
26 | case Bid bid:
27 | return new Tagged(evt, ImmutableHashSet.Empty.Add(bid.StockId).Add("Bid"));
28 | case Fill fill:
29 | return new Tagged(evt, ImmutableHashSet.Empty.Add(fill.StockId).Add("Fill"));
30 | case Match match:
31 | return new Tagged(evt, ImmutableHashSet.Empty.Add(match.StockId).Add("Match"));
32 | default:
33 | return evt;
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Infrastructure/StockShardMsgRouter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Akka.Cluster.Sharding;
3 | using Akka.CQRS.Events;
4 | using Akka.Persistence.Extras;
5 |
6 | namespace Akka.CQRS.Infrastructure
7 | {
8 | ///
9 | /// Used to route sharding messages to order book actors hosted via Akka.Cluster.Sharding.
10 | ///
11 | public sealed class StockShardMsgRouter : HashCodeMessageExtractor
12 | {
13 | ///
14 | /// 3 nodes hosting order books, 10 shards per node.
15 | ///
16 | public const int DefaultShardCount = 30;
17 |
18 | public StockShardMsgRouter() : this(DefaultShardCount)
19 | {
20 | }
21 |
22 | public StockShardMsgRouter(int maxNumberOfShards) : base(maxNumberOfShards)
23 | {
24 | }
25 |
26 | public override string EntityId(object message)
27 | {
28 | if (message is IWithStockId stockMsg)
29 | {
30 | return stockMsg.StockId;
31 | }
32 |
33 | switch (message)
34 | {
35 | case ConfirmableMessage a:
36 | return a.Message.StockId;
37 | case ConfirmableMessage b:
38 | return b.Message.StockId;
39 | case ConfirmableMessage f:
40 | return f.Message.StockId;
41 | case ConfirmableMessage m:
42 | return m.Message.StockId;
43 | }
44 |
45 | return null;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Matching.Tests/Akka.CQRS.Matching.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;$(NetVersion)
4 |
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Matching.Tests/MatchingEngineSpecs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using Akka.CQRS.Events;
6 | using FluentAssertions;
7 | using Xunit;
8 |
9 | namespace Akka.CQRS.Matching.Tests
10 | {
11 | public class MatchingEngineSpecs
12 | {
13 | public const string TickerSymbol = "PTB";
14 | private readonly MatchingEngine _matchingEngine = new MatchingEngine(TickerSymbol);
15 |
16 | [Fact(DisplayName = "MatchingEngine should generate correct events for ask then bid")]
17 | public void MatchingEngine_should_generate_correct_order_events_for_ask_then_bid()
18 | {
19 | var ask = new Ask(TickerSymbol, "foo", 10.0m, 4.0, DateTimeOffset.Now);
20 |
21 | // bid will be slightly higher than ask - settlement price should be bid price
22 | var bid = new Bid(TickerSymbol, "bar", 11.0m, 4.0, DateTimeOffset.Now);
23 |
24 | var askEvents = _matchingEngine.WithAsk(ask);
25 | askEvents.Should().BeEmpty();
26 | _matchingEngine.AskTrades.Count.Should().Be(1);
27 |
28 | var bidEvents = _matchingEngine.WithBid(bid).ToList();
29 |
30 | // validate the correct number of outputs and remaining orders first
31 | bidEvents.Should().NotBeEmpty();
32 | bidEvents.Count.Should().Be(3);
33 |
34 | _matchingEngine.AskTrades.Count.Should().Be(0);
35 | _matchingEngine.AsksByPrice.Count.Should().Be(0);
36 | _matchingEngine.BidTrades.Count.Should().Be(0);
37 | _matchingEngine.BidsByPrice.Count.Should().Be(0);
38 |
39 | // all orders should have been completely filled
40 | var fills = bidEvents.Where(x => x is Fill).Cast().ToList();
41 | fills[0].Partial.Should().BeFalse();
42 | fills[1].Partial.Should().BeFalse();
43 |
44 | // filled price should be the bid price
45 | fills[0].Price.Should().Be(bid.BidPrice);
46 |
47 | // match information should reflect the same
48 | var match = (Match)bidEvents.Single(x => x is Match);
49 | match.BuyOrderId.Should().Be(bid.OrderId);
50 | match.SellOrderId.Should().Be(ask.OrderId);
51 | match.StockId.Should().Be(TickerSymbol);
52 | match.SettlementPrice.Should().Be(bid.BidPrice);
53 | match.Quantity.Should().Be(bid.BidQuantity);
54 | }
55 |
56 | [Fact(DisplayName = "MatchingEngine should generate correct events for bid then ask")]
57 | public void MatchingEngine_should_generate_correct_order_events_for_bid_then_ask()
58 | {
59 | var ask = new Ask(TickerSymbol, "foo", 10.0m, 4.0, DateTimeOffset.Now);
60 |
61 | // bid will be slightly higher than ask - settlement price should be bid price
62 | var bid = new Bid(TickerSymbol, "bar", 11.0m, 4.0, DateTimeOffset.Now);
63 |
64 | var bidEvents = _matchingEngine.WithBid(bid);
65 | bidEvents.Should().BeEmpty();
66 | _matchingEngine.BidTrades.Count.Should().Be(1);
67 |
68 | var askEvents = _matchingEngine.WithAsk(ask).ToList();
69 |
70 | // validate the correct number of outputs and remaining orders first
71 | askEvents.Should().NotBeEmpty();
72 | askEvents.Count.Should().Be(3);
73 |
74 | _matchingEngine.AskTrades.Count.Should().Be(0);
75 | _matchingEngine.AsksByPrice.Count.Should().Be(0);
76 | _matchingEngine.BidTrades.Count.Should().Be(0);
77 | _matchingEngine.BidsByPrice.Count.Should().Be(0);
78 |
79 | // all orders should have been completely filled
80 | var fills = askEvents.Where(x => x is Fill).Cast().ToList();
81 | fills[0].Partial.Should().BeFalse();
82 | fills[1].Partial.Should().BeFalse();
83 |
84 | // filled price should be the bid price
85 | fills[0].Price.Should().Be(bid.BidPrice);
86 |
87 | // match information should reflect the same
88 | var match = (Match)askEvents.Single(x => x is Match);
89 | match.BuyOrderId.Should().Be(bid.OrderId);
90 | match.SellOrderId.Should().Be(ask.OrderId);
91 | match.StockId.Should().Be(TickerSymbol);
92 | match.SettlementPrice.Should().Be(bid.BidPrice);
93 | match.Quantity.Should().Be(bid.BidQuantity);
94 | }
95 |
96 | [Fact(DisplayName = "Should create valid OrderbookSnapshot and recreate matching engine from snapshot")]
97 | public void ShouldCreateAndRecreateFromOrderbookSnapshot()
98 | {
99 | var ask = new Ask(TickerSymbol, "foo", 12.0m, 5.0, DateTimeOffset.Now);
100 |
101 | // bid is lower than ask - no trades
102 | var bid = new Bid(TickerSymbol, "bar", 11.0m, 4.0, DateTimeOffset.Now);
103 |
104 | var bidEvents = _matchingEngine.WithBid(bid);
105 | var askEvents = _matchingEngine.WithAsk(ask);
106 | askEvents.Should().BeEmpty();
107 | bidEvents.Should().BeEmpty();
108 |
109 | _matchingEngine.AskTrades.Count.Should().Be(1);
110 | _matchingEngine.BidTrades.Count.Should().Be(1);
111 |
112 | // create a snapshot
113 | var snapshot = _matchingEngine.GetSnapshot();
114 |
115 | // add a second ask to matching engine (need to verify immutability)
116 | // still higher than original bid - won't trigger any trades
117 | var ask2 = new Ask(TickerSymbol, "fuber", 13.0m, 5.0, DateTimeOffset.Now);
118 | var ask2Events = _matchingEngine.WithAsk(ask2);
119 | ask2Events.Should().BeEmpty();
120 |
121 | snapshot.Asks.Count.Should().Be(1);
122 | snapshot.Bids.Count.Should().Be(1);
123 | snapshot.StockId.Should().Be(_matchingEngine.StockId);
124 | _matchingEngine.AskTrades.Count.Should().Be(2);
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Matching/Akka.CQRS.Matching.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(NetStandardVersion)
5 | Matching engine logic.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Actors/Akka.CQRS.Pricing.Actors.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(NetStandardVersion)
4 | Pricing analytics actors.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Actors/MatchAggregator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Akka.Actor;
4 | using Akka.Cluster.Tools.PublishSubscribe;
5 | using Akka.CQRS.Events;
6 | using Akka.CQRS.Pricing.Commands;
7 | using Akka.CQRS.Pricing.Events;
8 | using Akka.CQRS.Pricing.Views;
9 | using Akka.CQRS.Util;
10 | using Akka.Event;
11 | using Akka.Persistence;
12 | using Akka.Persistence.Query;
13 | using Akka.Streams;
14 | using Akka.Streams.Dsl;
15 | using Petabridge.Collections;
16 |
17 | namespace Akka.CQRS.Pricing.Actors
18 | {
19 | ///
20 | /// Used to aggregate events via Akka.Persistence.Query
21 | ///
22 | public class MatchAggregator : ReceivePersistentActor
23 | {
24 | // Take a snapshot every 10 journal entries
25 | public const int SnapshotEveryN = 10;
26 |
27 | private readonly IEventsByTagQuery _eventsByTag;
28 | private MatchAggregate _matchAggregate;
29 | private readonly IActorRef _mediator;
30 | private readonly ITimestamper _timestamper;
31 | private readonly ILoggingAdapter _log = Context.GetLogger();
32 | private CircularBuffer _priceUpdates = new CircularBuffer(MatchAggregate.DefaultSampleSize);
33 | private CircularBuffer _volumeUpdates = new CircularBuffer(MatchAggregate.DefaultSampleSize);
34 | private ICancelable _publishPricesTask;
35 |
36 | private readonly string _priceTopic;
37 | private readonly string _volumeTopic;
38 |
39 | public readonly string TickerSymbol;
40 | public override string PersistenceId { get; }
41 |
42 | public long QueryOffset { get; private set; }
43 |
44 | private class PublishEvents
45 | {
46 | public static readonly PublishEvents Instance = new PublishEvents();
47 | private PublishEvents() { }
48 | }
49 |
50 | public MatchAggregator(string tickerSymbol, IEventsByTagQuery eventsByTag)
51 | : this(tickerSymbol, eventsByTag, DistributedPubSub.Get(Context.System).Mediator, CurrentUtcTimestamper.Instance)
52 | {
53 | }
54 |
55 | public MatchAggregator(string tickerSymbol, IEventsByTagQuery eventsByTag, IActorRef mediator, ITimestamper timestamper)
56 | {
57 | TickerSymbol = tickerSymbol;
58 | _priceTopic = PriceTopicHelpers.PriceUpdateTopic(TickerSymbol);
59 | _volumeTopic = PriceTopicHelpers.VolumeUpdateTopic(TickerSymbol);
60 | _eventsByTag = eventsByTag;
61 | _mediator = mediator;
62 | _timestamper = timestamper;
63 | PersistenceId = EntityIdHelper.IdForPricing(tickerSymbol);
64 |
65 | Receives();
66 | Commands();
67 | }
68 |
69 | private void Receives()
70 | {
71 | /*
72 | * Can be saved as a snapshot or as an event
73 | */
74 | Recover(o =>
75 | {
76 | if (o.Snapshot is MatchAggregatorSnapshot s)
77 | {
78 | RecoverAggregateData(s);
79 | }
80 | });
81 |
82 | Recover(s => { RecoverAggregateData(s); });
83 | }
84 |
85 | ///
86 | /// Recovery has completed successfully.
87 | ///
88 | protected override void OnReplaySuccess()
89 | {
90 | var mat = Context.Materializer();
91 | var self = Self;
92 |
93 | // transmit all tag events to myself
94 | _eventsByTag.EventsByTag(TickerSymbol, Offset.Sequence(QueryOffset))
95 | .Where(x => x.Event is Match) // only care about Match events
96 | .RunWith(Sink.ActorRef(self, UnexpectedEndOfStream.Instance), mat);
97 |
98 | _publishPricesTask = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(TimeSpan.FromSeconds(10),
99 | TimeSpan.FromSeconds(10), Self, PublishEvents.Instance, ActorRefs.NoSender);
100 |
101 | base.OnReplaySuccess();
102 | }
103 |
104 | private void RecoverAggregateData(MatchAggregatorSnapshot s)
105 | {
106 | _matchAggregate = new MatchAggregate(TickerSymbol, s.AvgPrice, s.AvgVolume);
107 | _priceUpdates.Enqueue(s.RecentPriceUpdates.ToArray());
108 | _volumeUpdates.Enqueue(s.RecentVolumeUpdates.ToArray());
109 | QueryOffset = s.QueryOffset;
110 | }
111 |
112 | private MatchAggregatorSnapshot SaveAggregateData()
113 | {
114 | return new MatchAggregatorSnapshot(QueryOffset, _matchAggregate.AvgPrice.CurrentAvg, _matchAggregate.AvgVolume.CurrentAvg, _priceUpdates.ToList(), _volumeUpdates.ToList());
115 | }
116 |
117 | private void Commands()
118 | {
119 | Command(e =>
120 | {
121 | if (e.Event is Match m)
122 | {
123 | // update the offset
124 | if (e.Offset is Sequence s)
125 | {
126 | QueryOffset = s.Value;
127 | }
128 |
129 | if (_matchAggregate == null)
130 | {
131 | _matchAggregate = new MatchAggregate(TickerSymbol, m.SettlementPrice, m.Quantity);
132 | return;
133 | }
134 |
135 | if (!_matchAggregate.WithMatch(m))
136 | {
137 | _log.Warning("Received Match for ticker symbol [{0}] - but we only accept symbols for [{1}]", m.StockId, TickerSymbol);
138 | }
139 | }
140 | });
141 |
142 | // Command sent by a PriceViewActor to pull down a complete snapshot of active pricing data
143 | Command(f =>
144 | {
145 | // no price data yet
146 | if (_priceUpdates.Count == 0 || _volumeUpdates.Count == 0)
147 | {
148 | Sender.Tell(PriceAndVolumeSnapshot.Empty(TickerSymbol));
149 | }
150 | else
151 | {
152 | Sender.Tell(new PriceAndVolumeSnapshot(TickerSymbol, _priceUpdates.ToArray(), _volumeUpdates.ToArray()));
153 | }
154 |
155 | });
156 |
157 | Command(p =>
158 | {
159 | if (_matchAggregate == null)
160 | return;
161 |
162 | var (latestPrice, latestVolume) = _matchAggregate.FetchMetrics(_timestamper);
163 |
164 | // Need to update pricing records prior to persisting our state, since this data is included in
165 | // output of SaveAggregateData()
166 | _priceUpdates.Add(latestPrice);
167 | _volumeUpdates.Add(latestVolume);
168 |
169 | PersistAsync(SaveAggregateData(), snapshot =>
170 | {
171 | _log.Info("Saved latest price {0} and volume {1}", snapshot.AvgPrice, snapshot.AvgVolume);
172 | if (LastSequenceNr % SnapshotEveryN == 0)
173 | {
174 | SaveSnapshot(snapshot);
175 | }
176 | });
177 |
178 | // publish updates to in-memory replicas
179 | _mediator.Tell(new Publish(_priceTopic, latestPrice));
180 | _mediator.Tell(new Publish(_volumeTopic, latestVolume));
181 | });
182 |
183 | Command(p =>
184 | {
185 | if (_log.IsDebugEnabled)
186 | {
187 | _log.Debug("pinged via {0}", Sender);
188 | }
189 | });
190 |
191 | Command(s =>
192 | {
193 | // clean-up prior snapshots and journal events
194 | DeleteSnapshots(new SnapshotSelectionCriteria(s.Metadata.SequenceNr-1));
195 | DeleteMessages(s.Metadata.SequenceNr);
196 | });
197 | }
198 |
199 | protected override void PostStop()
200 | {
201 | _publishPricesTask?.Cancel();
202 | base.PostStop();
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Actors/PriceInitiatorActor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Akka.Actor;
5 | using Akka.CQRS.Pricing.Commands;
6 | using Akka.Event;
7 | using Akka.Persistence;
8 | using Akka.Persistence.Query;
9 | using Akka.Streams;
10 | using Akka.Streams.Dsl;
11 |
12 | namespace Akka.CQRS.Pricing.Actors
13 | {
14 | ///
15 | /// Intended to be a Cluster Singleton. Responsible for ensuring there's at least one instance
16 | /// of a for every single persistence id found inside the datastore.
17 | ///
18 | public sealed class PriceInitiatorActor : ReceiveActor
19 | {
20 | private readonly ILoggingAdapter _log = Context.GetLogger();
21 | private readonly IPersistenceIdsQuery _tradeIdsQuery;
22 | private readonly IActorRef _pricingQueryProxy;
23 | private readonly HashSet _tickers = new HashSet();
24 |
25 | /*
26 | * Used to periodically ping Akka.Cluster.Sharding and ensure that all pricing
27 | * entities are up and producing events for their in-memory replicas over the network.
28 | *
29 | * Technically, akka.cluster.sharding.remember-entities = on should take care of this
30 | * for us in the initial pass, but the impact of having this code is virtually zero
31 | * and in the event of a network partition or an error somewhere, will effectively prod
32 | * the non-existent entity into action. Worth having it.
33 | */
34 | private ICancelable _heartbeatInterval;
35 |
36 |
37 | private class Heartbeat
38 | {
39 | public static readonly Heartbeat Instance = new Heartbeat();
40 | private Heartbeat() { }
41 | }
42 |
43 | public PriceInitiatorActor(IPersistenceIdsQuery tradeIdsQuery, IActorRef pricingQueryProxy)
44 | {
45 | _tradeIdsQuery = tradeIdsQuery;
46 | _pricingQueryProxy = pricingQueryProxy;
47 |
48 | Receive(p =>
49 | {
50 | _tickers.Add(p.StockId);
51 | _pricingQueryProxy.Tell(p);
52 | });
53 |
54 | Receive(h =>
55 | {
56 | foreach (var p in _tickers)
57 | {
58 | _pricingQueryProxy.Tell(new Ping(p));
59 | }
60 | });
61 |
62 | Receive(end =>
63 | {
64 | _log.Warning("Received unexpected end of PersistenceIds stream. Restarting.");
65 | throw new ApplicationException("Restart me!");
66 | });
67 | }
68 |
69 | protected override void PreStart()
70 | {
71 | var mat = Context.Materializer();
72 | var self = Self;
73 | _tradeIdsQuery.PersistenceIds()
74 | .Where(x => x.EndsWith(EntityIdHelper
75 | .OrderBookSuffix)) // skip persistence ids belonging to price entities
76 | .Select(x => new Ping(EntityIdHelper.ExtractTickerFromPersistenceId(x)))
77 | .RunWith(Sink.ActorRef(self, UnexpectedEndOfStream.Instance), mat);
78 |
79 | _heartbeatInterval = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(TimeSpan.FromSeconds(30),
80 | TimeSpan.FromSeconds(30), Self, Heartbeat.Instance, ActorRefs.NoSender);
81 | }
82 |
83 | protected override void PostStop()
84 | {
85 | _heartbeatInterval?.Cancel();
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Actors/PriceViewActor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using System.Text;
5 | using Akka.Actor;
6 | using Akka.Cluster.Tools.PublishSubscribe;
7 | using Akka.CQRS.Pricing.Commands;
8 | using Akka.CQRS.Pricing.Events;
9 | using Akka.CQRS.Pricing.Views;
10 | using Akka.Event;
11 |
12 | namespace Akka.CQRS.Pricing.Actors
13 | {
14 | ///
15 | /// In-memory, replicated view of the current price and volume for a specific stock.
16 | ///
17 | public sealed class PriceVolumeViewActor : ReceiveActor, IWithUnboundedStash
18 | {
19 | private readonly string _tickerSymbol;
20 | private ICancelable _pruneTimer;
21 | private readonly ILoggingAdapter _log = Context.GetLogger();
22 |
23 | // the Cluster.Sharding proxy
24 | private readonly IActorRef _priceActorGateway;
25 |
26 | // the DistributedPubSub mediator
27 | private readonly IActorRef _mediator;
28 |
29 | private IActorRef _tickerEntity;
30 | private PriceHistory _history;
31 | private readonly string _priceTopic;
32 |
33 | private sealed class Prune
34 | {
35 | public static readonly Prune Instance = new Prune();
36 | private Prune() { }
37 | }
38 |
39 | public PriceVolumeViewActor(string tickerSymbol, IActorRef priceActorGateway, IActorRef mediator)
40 | {
41 | _tickerSymbol = tickerSymbol;
42 | _priceActorGateway = priceActorGateway;
43 | _priceTopic = PriceTopicHelpers.PriceUpdateTopic(_tickerSymbol);
44 | _mediator = mediator;
45 | _history = new PriceHistory(_tickerSymbol, ImmutableSortedSet.Empty);
46 |
47 | WaitingForPriceAndVolume();
48 | }
49 |
50 | public IStash Stash { get; set; }
51 |
52 | private void WaitingForPriceAndVolume()
53 | {
54 | Receive(s =>
55 | {
56 | if (s.PriceUpdates.Length == 0) // empty set - no price data yet
57 | {
58 | _history = new PriceHistory(_tickerSymbol, ImmutableSortedSet.Empty);
59 | _log.Info("Received empty price history for [{0}]", _history.StockId);
60 | }
61 | else
62 | {
63 | _history = new PriceHistory(_tickerSymbol, s.PriceUpdates.ToImmutableSortedSet());
64 | _log.Info("Received recent price history for [{0}] - current price is [{1}] as of [{2}]", _history.StockId, _history.CurrentPrice, _history.Until);
65 | }
66 |
67 | _tickerEntity = Sender;
68 | _mediator.Tell(new Subscribe(_priceTopic, Self));
69 | });
70 |
71 | Receive(ack =>
72 | {
73 | _log.Info("Subscribed to {0} - ready for real-time processing.", _priceTopic);
74 | Become(Processing);
75 | Context.Watch(_tickerEntity);
76 | Context.SetReceiveTimeout(null);
77 | });
78 |
79 | Receive(_ =>
80 | {
81 | _log.Warning("Received no initial price values for [{0}] from source of truth after 5s. Retrying..", _tickerSymbol);
82 | _priceActorGateway.Tell(new FetchPriceAndVolume(_tickerSymbol));
83 | });
84 | }
85 |
86 | private void Processing()
87 | {
88 | Receive(p =>
89 | {
90 | _history = _history.WithPrice(p);
91 | _log.Info("[{0}] - current price is [{1}] as of [{2}]", _history.StockId, p.CurrentAvgPrice, p.Timestamp);
92 |
93 | });
94 |
95 | Receive(h =>
96 | {
97 | Sender.Tell(_history);
98 | });
99 |
100 | Receive(_ =>
101 | {
102 | Sender.Tell(_history.CurrentPriceUpdate);
103 | });
104 |
105 | Receive(_ => { }); // ignore
106 |
107 | // purge older price update entries.
108 | Receive(_ => { _history = _history.Prune(DateTimeOffset.UtcNow.AddMinutes(-5)); });
109 |
110 | Receive(t =>
111 | {
112 | if (t.ActorRef.Equals(_tickerEntity))
113 | {
114 | _log.Info("Source of truth entity terminated. Re-acquiring...");
115 | Context.SetReceiveTimeout(TimeSpan.FromSeconds(5));
116 | _priceActorGateway.Tell(new FetchPriceAndVolume(_tickerSymbol));
117 | _mediator.Tell(new Unsubscribe(_priceTopic, Self)); // unsubscribe until we acquire new source of truth pricing
118 | Become(WaitingForPriceAndVolume);
119 | }
120 | });
121 | }
122 |
123 | protected override void PreStart()
124 | {
125 | Context.SetReceiveTimeout(TimeSpan.FromSeconds(5.0));
126 | _priceActorGateway.Tell(new FetchPriceAndVolume(_tickerSymbol));
127 | _pruneTimer = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(TimeSpan.FromMinutes(5),
128 | TimeSpan.FromMinutes(5), Self, Prune.Instance, ActorRefs.NoSender);
129 | }
130 |
131 | protected override void PostStop()
132 | {
133 | _pruneTimer.Cancel();
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Actors/PriceViewMaster.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Akka.Actor;
5 | using Akka.Cluster.Tools.PublishSubscribe;
6 | using Akka.Event;
7 |
8 | namespace Akka.CQRS.Pricing.Actors
9 | {
10 | ///
11 | /// Parent actor to all instances.
12 | ///
13 | public sealed class PriceViewMaster : ReceiveActor, IWithUnboundedStash
14 | {
15 | public class BeginTrackPrices
16 | {
17 | public BeginTrackPrices(IActorRef shardRegion)
18 | {
19 | ShardRegion = shardRegion;
20 | }
21 |
22 | public IActorRef ShardRegion { get; }
23 | }
24 |
25 | private readonly ILoggingAdapter _log = Context.GetLogger();
26 |
27 | public PriceViewMaster()
28 | {
29 | WaitingForSharding();
30 | }
31 |
32 | public IStash Stash { get; set; }
33 |
34 | private void WaitingForSharding()
35 | {
36 | Receive(b =>
37 | {
38 | _log.Info("Received access to stock price mediator... Starting pricing views...");
39 | var mediator = DistributedPubSub.Get(Context.System).Mediator;
40 |
41 | foreach (var stock in AvailableTickerSymbols.Symbols)
42 | {
43 | Context.ActorOf(Props.Create(() => new PriceVolumeViewActor(stock, b.ShardRegion, mediator)),
44 | stock);
45 | }
46 |
47 | Become(Running);
48 | Stash.UnstashAll();
49 | });
50 |
51 | ReceiveAny(_ => Stash.Stash());
52 | }
53 |
54 | private void Running()
55 | {
56 | Receive(w =>
57 | {
58 | var child = Context.Child(w.StockId);
59 | if (child.IsNobody())
60 | {
61 | _log.Warning("Message received for unknown ticker symbol [{0}] - sending to dead letters.", w.StockId);
62 | }
63 |
64 | // Goes to deadletters if stock ticker symbol does not exist.
65 | child.Forward(w);
66 | });
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Actors/UnexpectedEndOfStream.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 | namespace Akka.CQRS.Pricing.Actors
7 | {
8 | ///
9 | /// Send this to ourselves in the event that our Akka.Persistence.Query stream completes, which it shouldn't.
10 | ///
11 | public sealed class UnexpectedEndOfStream
12 | {
13 | public static readonly UnexpectedEndOfStream Instance = new UnexpectedEndOfStream();
14 | private UnexpectedEndOfStream() { }
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Cli/Akka.CQRS.Pricing.Cli.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | Petabridge.Cmd palettes for accessing the pricing system.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Cli/PriceCmdHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text;
3 | using Akka.Actor;
4 | using Petabridge.Cmd.Host;
5 | using static Akka.CQRS.Pricing.Cli.PricingCmd;
6 |
7 | namespace Akka.CQRS.Pricing.Cli
8 | {
9 | ///
10 | /// The command palette handelr for .
11 | ///
12 | public sealed class PriceCommands : CommandPaletteHandler
13 | {
14 | private IActorRef _priceViewMaster;
15 |
16 | public PriceCommands(IActorRef priceViewMaster) : base(PricingCommandPalette)
17 | {
18 | _priceViewMaster = priceViewMaster;
19 | HandlerProps = Props.Create(() => new PriceCmdRouter(_priceViewMaster));
20 | }
21 |
22 | public override Props HandlerProps { get; }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Cli/PriceCmdRouter.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Linq;
9 | using System.Threading.Tasks;
10 | using Akka.Actor;
11 | using Akka.CQRS.Pricing.Commands;
12 | using Akka.CQRS.Pricing.Views;
13 | using Petabridge.Cmd;
14 | using Petabridge.Cmd.Host;
15 |
16 | namespace Akka.CQRS.Pricing.Cli
17 | {
18 | ///
19 | /// Actor responsible for carrying out commands.
20 | ///
21 | public sealed class PriceCmdRouter : CommandHandlerActor
22 | {
23 | private IActorRef _priceViewMaster;
24 |
25 | public PriceCmdRouter(IActorRef priceViewMaster) : base(PricingCmd.PricingCommandPalette)
26 | {
27 | _priceViewMaster = priceViewMaster;
28 |
29 | Process(PricingCmd.TrackPrice.Name, (command, arguments) =>
30 | {
31 | var tickerSymbol = arguments.ArgumentValues("symbol").Single();
32 |
33 | // the tracker actor will start automatically recording price information on its own. No further action needed.
34 | var trackerActor =
35 | Context.ActorOf(Props.Create(() => new PriceTrackingActor(tickerSymbol, _priceViewMaster, Sender)));
36 | });
37 |
38 | Process(PricingCmd.PriceHistory.Name, (command, arguments) =>
39 | {
40 | var tickerSymbol = arguments.ArgumentValues("symbol").Single();
41 | var getPriceTask = _priceViewMaster.Ask(new GetPriceHistory(tickerSymbol), TimeSpan.FromSeconds(5));
42 | var sender = Sender;
43 |
44 | // pipe happy results back to the sender only on successful Ask
45 | getPriceTask.ContinueWith(tr =>
46 | {
47 | return Enumerable.Select(tr.Result.HistoricalPrices, x => new CommandResponse(x.ToString(), false))
48 | .Concat(new []{ CommandResponse.Empty });
49 | }, TaskContinuationOptions.OnlyOnRanToCompletion).PipeTo(sender);
50 |
51 | // pipe unhappy results back to sender on failure
52 | getPriceTask.ContinueWith(tr =>
53 | new ErroredCommandResponse($"Error while fetching price history for {tickerSymbol} - " +
54 | $"timed out after 5s"), TaskContinuationOptions.NotOnRanToCompletion)
55 | .PipeTo(sender); ;
56 | });
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Cli/PriceTrackingActor.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 |
7 | using System;
8 | using Akka.Actor;
9 | using Akka.CQRS.Pricing.Commands;
10 | using Akka.CQRS.Pricing.Events;
11 | using Akka.CQRS.Pricing.Views;
12 | using Petabridge.Cmd;
13 |
14 | namespace Akka.CQRS.Pricing.Cli
15 | {
16 | ///
17 | /// Actor responsible for populating the output for the command.
18 | ///
19 | public sealed class PriceTrackingActor : ReceiveActor, IWithUnboundedStash
20 | {
21 | private readonly string _tickerSymbol;
22 | private readonly IActorRef _priceViewActor;
23 | private readonly IActorRef _commandHandlerActor;
24 | private ICancelable _priceCheckInterval;
25 |
26 | private IPriceUpdate _currentPrice;
27 |
28 | public PriceTrackingActor(string tickerSymbol, IActorRef priceViewActor, IActorRef commandHandlerActor)
29 | {
30 | _priceViewActor = priceViewActor;
31 | _commandHandlerActor = commandHandlerActor;
32 | _tickerSymbol = tickerSymbol;
33 |
34 | WaitingForPriceHistory();
35 | }
36 |
37 | private void WaitingForPriceHistory()
38 | {
39 | Receive(p =>
40 | {
41 | if (p.HistoricalPrices.IsEmpty)
42 | {
43 | _commandHandlerActor.Tell(new CommandResponse($"No historical price data for [{_tickerSymbol}] - waiting for updates.", false));
44 | BecomePriceUpdates();
45 | return;
46 | }
47 |
48 | _currentPrice = p.CurrentPriceUpdate;
49 | foreach (var e in p.HistoricalPrices)
50 | {
51 | _commandHandlerActor.Tell(new CommandResponse(e.ToString(), false));
52 | }
53 |
54 | BecomePriceUpdates();
55 | });
56 |
57 | Receive(t =>
58 | {
59 | _commandHandlerActor.Tell(new CommandResponse($"No historical price data for [{_tickerSymbol}] - waiting for updates.", false));
60 | BecomePriceUpdates();
61 | });
62 |
63 | ReceiveAny(_ => Stash.Stash());
64 | }
65 |
66 | private void BecomePriceUpdates()
67 | {
68 | Context.SetReceiveTimeout(null);
69 | Become(PriceUpdates);
70 | Stash.UnstashAll();
71 | }
72 |
73 | private void PriceUpdates()
74 | {
75 | Receive(p =>
76 | {
77 | _currentPrice = p;
78 | _commandHandlerActor.Tell(new CommandResponse(p.ToString(), false));
79 | });
80 |
81 | Receive(t =>
82 | {
83 | _commandHandlerActor.Tell(new CommandResponse("Price View Actor terminated."));
84 | Context.Stop(Self);
85 | });
86 | }
87 |
88 | protected override void PreStart()
89 | {
90 | var getlatestPrice = new GetLatestPrice(_tickerSymbol);
91 |
92 | // get the historical price
93 | _priceViewActor.Tell(new GetPriceHistory(_tickerSymbol));
94 | _priceCheckInterval = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(TimeSpan.FromSeconds(3),
95 | TimeSpan.FromSeconds(3), _priceViewActor, getlatestPrice, Self);
96 |
97 | Context.SetReceiveTimeout(TimeSpan.FromSeconds(1));
98 | Context.Watch(_priceViewActor);
99 | }
100 |
101 | protected override void PostStop()
102 | {
103 | _priceCheckInterval.Cancel();
104 | }
105 |
106 | public IStash Stash { get; set; }
107 | }
108 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Cli/PricingCmd.cs:
--------------------------------------------------------------------------------
1 | using Petabridge.Cmd;
2 |
3 | namespace Akka.CQRS.Pricing.Cli
4 | {
5 | ///
6 | /// Defines all of the Petabridge.Cmd commands for tracking the price.
7 | ///
8 | public static class PricingCmd
9 | {
10 | public static readonly CommandDefinition TrackPrice = new CommandDefinitionBuilder()
11 | .WithName("track").WithDescription("Track the live changes in price for a given ticker symbol continuously. Press Control + C to exit.")
12 | .WithArgument(b => b.IsMandatory(true).WithName("symbol").WithSwitch("-s").WithSwitch("-S").WithSwitch("--symbol")
13 | .WithDefaultValues(AvailableTickerSymbols.Symbols)
14 | .WithDescription("The ticker symbol for the stock."))
15 | .Build();
16 |
17 | public static readonly CommandDefinition PriceHistory = new CommandDefinitionBuilder()
18 | .WithName("history").WithDescription("Get the historical price history for the specified ticker symbol")
19 | .WithArgument(b => b.IsMandatory(true).WithName("symbol").WithSwitch("-s").WithSwitch("-S").WithSwitch("--symbol")
20 | .WithDefaultValues(AvailableTickerSymbols.Symbols)
21 | .WithDescription("The ticker symbol for the stock."))
22 | .Build();
23 |
24 | public static readonly CommandPalette PricingCommandPalette = new CommandPalette("price", new []{ TrackPrice, PriceHistory });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Service/Akka.CQRS.Pricing.Service.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | $(NetVersion)
5 |
6 |
7 |
8 |
9 | Always
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS base
2 | WORKDIR /app
3 |
4 | # should be a comma-delimited list
5 | ENV CLUSTER_SEEDS "[]"
6 | ENV CLUSTER_IP ""
7 | ENV CLUSTER_PORT "6055"
8 | ENV SQL_CONNECTION_STR "" # SQL connection string for Akka.Persistence.Sql
9 | ENV SQL_PROVIDER_NAME "" # SQL provider for Akka.Persistence.Sql
10 |
11 | COPY ./bin/Release/net7.0/publish/ /app
12 |
13 | # 9110 - Petabridge.Cmd
14 | # 6055 - Akka.Cluster
15 | EXPOSE 9110 6055
16 |
17 | # Install Petabridge.Cmd client
18 | RUN dotnet tool install --global pbm
19 |
20 | # Needed because https://stackoverflow.com/questions/51977474/install-dotnet-core-tool-dockerfile
21 | ENV PATH="${PATH}:/root/.dotnet/tools"
22 |
23 | # RUN pbm help
24 |
25 | CMD ["dotnet", "Akka.CQRS.Pricing.Service.dll"]
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Service/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Akka.Actor;
6 | using Akka.Bootstrap.Docker;
7 | using Akka.Cluster.Hosting;
8 | using Akka.Cluster.Sharding;
9 | using Akka.Cluster.Tools.PublishSubscribe;
10 | using Akka.Cluster.Tools.Singleton;
11 | using Akka.Configuration;
12 | using Akka.CQRS.Infrastructure;
13 | using Akka.CQRS.Infrastructure.Ops;
14 | using Akka.CQRS.Pricing.Actors;
15 | using Akka.CQRS.Pricing.Cli;
16 | using Akka.Hosting;
17 | using Akka.Persistence.Hosting;
18 | using Akka.Persistence.Query;
19 | using Akka.Persistence.Sql;
20 | using Akka.Persistence.Sql.Query;
21 | using Akka.Util;
22 | using Microsoft.Extensions.Hosting;
23 | using Microsoft.Extensions.Logging;
24 | using Petabridge.Cmd.Cluster;
25 | using Petabridge.Cmd.Cluster.Sharding;
26 | using Petabridge.Cmd.Host;
27 | using Petabridge.Cmd.Remote;
28 | using static Akka.CQRS.Infrastructure.SqlDbHoconHelper;
29 |
30 | namespace Akka.CQRS.Pricing.Service
31 | {
32 | public static class Program
33 | {
34 | public static async Task Main(string[] args)
35 | {
36 | var sqlConnectionString = Environment.GetEnvironmentVariable("SQL_CONNECTION_STR")?.Trim();
37 | if (string.IsNullOrEmpty(sqlConnectionString))
38 | {
39 | Console.WriteLine("ERROR! SQL connection string not provided. Can't start.");
40 | return -1;
41 | }
42 | Console.WriteLine($"Connecting to SQL server at {sqlConnectionString}");
43 |
44 | var sqlProviderName = Environment.GetEnvironmentVariable("SQL_PROVIDER_NAME")?.Trim();
45 | if (string.IsNullOrEmpty(sqlProviderName))
46 | {
47 | Console.WriteLine("ERROR! SQL provider name not provided. Can't start.");
48 | return -1;
49 | }
50 | Console.WriteLine($"Connecting to SQL provider {sqlProviderName}");
51 |
52 | // Need to wait for the SQL server to spin up
53 | await Task.Delay(TimeSpan.FromSeconds(15));
54 |
55 | var config = await File.ReadAllTextAsync("app.conf");
56 | using var host = new HostBuilder()
57 | .ConfigureServices((hostContext, services) =>
58 | {
59 |
60 | services.AddAkka("AkkaPricing", options =>
61 | {
62 | // Add HOCON configuration from Docker
63 | var conf = ConfigurationFactory.ParseString(config)
64 | .WithFallback(GetSqlHocon(sqlConnectionString, sqlProviderName))
65 | .WithFallback(OpsConfig.GetOpsConfig())
66 | .WithFallback(ClusterSharding.DefaultConfig())
67 | .WithFallback(DistributedPubSub.DefaultConfig())
68 | .WithFallback(SqlPersistence.DefaultConfiguration);
69 | options.AddHocon(conf.BootstrapFromDocker(), HoconAddMode.Prepend)
70 | .WithActors((system, registry) =>
71 | {
72 | var priceViewMaster = system.ActorOf(Props.Create(() => new PriceViewMaster()), "prices");
73 | registry.Register(priceViewMaster);
74 | // used to seed pricing data
75 | var readJournal = system.ReadJournalFor(SqlReadJournal.Identifier);
76 | Cluster.Cluster.Get(system).RegisterOnMemberUp(() =>
77 | {
78 | var sharding = ClusterSharding.Get(system);
79 |
80 | var shardRegion = sharding.Start("priceAggregator",
81 | s => Props.Create(() => new MatchAggregator(s, readJournal)),
82 | ClusterShardingSettings.Create(system),
83 | new StockShardMsgRouter());
84 |
85 | // used to seed pricing data
86 | var singleton = ClusterSingletonManager.Props(
87 | Props.Create(() => new PriceInitiatorActor(readJournal, shardRegion)),
88 | ClusterSingletonManagerSettings.Create(
89 | system.Settings.Config.GetConfig("akka.cluster.price-singleton")));
90 |
91 | // start the creation of the pricing views
92 | priceViewMaster.Tell(new PriceViewMaster.BeginTrackPrices(shardRegion));
93 | });
94 |
95 | })
96 | .AddPetabridgeCmd(cmd =>
97 | {
98 | void RegisterPalette(CommandPaletteHandler h)
99 | {
100 | if (cmd.RegisterCommandPalette(h))
101 | {
102 | Console.WriteLine("Petabridge.Cmd - Registered {0}", h.Palette.ModuleName);
103 | }
104 | else
105 | {
106 | Console.WriteLine("Petabridge.Cmd - DID NOT REGISTER {0}", h.Palette.ModuleName);
107 | }
108 | }
109 |
110 | var actorSystem = cmd.Sys;
111 | var actorRegistry = ActorRegistry.For(actorSystem);
112 | var priceViewMaster = actorRegistry.Get();
113 |
114 | RegisterPalette(ClusterCommands.Instance);
115 | RegisterPalette(new RemoteCommands());
116 | RegisterPalette(ClusterShardingCommands.Instance);
117 | RegisterPalette(new PriceCommands(priceViewMaster));
118 | cmd.Start();
119 | });
120 |
121 | });
122 | })
123 | .ConfigureLogging((hostContext, configLogging) =>
124 | {
125 | configLogging.AddConsole();
126 | })
127 | .UseConsoleLifetime()
128 | .Build();
129 | await host.RunAsync();
130 | return 0;
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing.Service/app.conf:
--------------------------------------------------------------------------------
1 | akka {
2 | actor {
3 | provider = cluster
4 | }
5 |
6 | remote {
7 | dot-netty.tcp {
8 | hostname = "127.0.0.1"
9 | port = 6055
10 | }
11 | }
12 |
13 | cluster {
14 | #will inject this node as a self-seed node at run-time
15 | seed-nodes = ["akka.tcp://AkkaPricing@127.0.0.1:6055"]
16 | roles = ["pricing-engine" , "price-events"]
17 |
18 | pub-sub {
19 | role = "price-events"
20 | }
21 |
22 | sharding {
23 | role = "pricing-engine"
24 | }
25 |
26 | price-singleton {
27 | singleton-name = "price-initiator"
28 | role = "pricing-engine"
29 | hand-over-retry-interval = 1s
30 | min-number-of-hand-over-retries = 10
31 | }
32 | }
33 |
34 | persistence {
35 | journal {
36 | plugin = "akka.persistence.journal.sql"
37 | sql {
38 | event-adapters = {
39 | stock-tagger = "Akka.CQRS.Infrastructure.StockEventTagger, Akka.CQRS.Infrastructure"
40 | }
41 | event-adapter-bindings = {
42 | "Akka.CQRS.IWithStockId, Akka.CQRS" = stock-tagger
43 | }
44 | }
45 | }
46 |
47 | snapshot-store {
48 | plugin = "akka.persistence.snapshot-store.sql"
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Akka.CQRS.Pricing.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(NetStandardVersion)
4 | Read-side pricing aggregates and views.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Commands/FetchPriceAndVolume.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace Akka.CQRS.Pricing.Commands
6 | {
7 | ///
8 | /// Fetch the N most recent price and volume updates for a specific ticker symbol.
9 | ///
10 | public sealed class FetchPriceAndVolume : IWithStockId
11 | {
12 | public FetchPriceAndVolume(string stockId)
13 | {
14 | StockId = stockId;
15 | }
16 |
17 | public string StockId { get; }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Commands/GetPriceHistory.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 |
7 | using Akka.CQRS.Pricing.Views;
8 |
9 | namespace Akka.CQRS.Pricing.Commands
10 | {
11 | ///
12 | /// Fetch an entire for a specific stock.
13 | ///
14 | public sealed class GetPriceHistory : IWithStockId
15 | {
16 | public GetPriceHistory(string stockId)
17 | {
18 | StockId = stockId;
19 | }
20 |
21 | public string StockId { get; }
22 | }
23 |
24 | ///
25 | /// Fetch the latest price only for a specific stock.
26 | ///
27 | public sealed class GetLatestPrice : IWithStockId
28 | {
29 | public GetLatestPrice(string stockId)
30 | {
31 | StockId = stockId;
32 | }
33 |
34 | public string StockId { get; }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Commands/Ping.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Akka.CQRS.Pricing.Commands
4 | {
5 | ///
6 | /// Used to heartbeat an Akka.Cluster.Sharding entity for a specific ticker symbol.
7 | ///
8 | public sealed class Ping : IWithStockId, IEquatable
9 | {
10 | public Ping(string stockId)
11 | {
12 | StockId = stockId;
13 | }
14 |
15 | public string StockId { get; }
16 |
17 | public bool Equals(Ping other)
18 | {
19 | if (ReferenceEquals(null, other)) return false;
20 | if (ReferenceEquals(this, other)) return true;
21 | return string.Equals(StockId, other.StockId);
22 | }
23 |
24 | public override bool Equals(object obj)
25 | {
26 | if (ReferenceEquals(null, obj)) return false;
27 | if (ReferenceEquals(this, obj)) return true;
28 | return obj is Ping other && Equals(other);
29 | }
30 |
31 | public override int GetHashCode()
32 | {
33 | return StockId.GetHashCode();
34 | }
35 |
36 | public static bool operator ==(Ping left, Ping right)
37 | {
38 | return Equals(left, right);
39 | }
40 |
41 | public static bool operator !=(Ping left, Ping right)
42 | {
43 | return !Equals(left, right);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Commands/PriceAndVolumeSnapshot.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 |
7 | using Akka.CQRS.Pricing.Events;
8 |
9 | namespace Akka.CQRS.Pricing.Commands
10 | {
11 | ///
12 | /// The response to a command.
13 | ///
14 | public sealed class PriceAndVolumeSnapshot : IWithStockId
15 | {
16 | public PriceAndVolumeSnapshot(string stockId, IPriceUpdate[] priceUpdates, IVolumeUpdate[] volumeUpdates)
17 | {
18 | StockId = stockId;
19 | PriceUpdates = priceUpdates;
20 | VolumeUpdates = volumeUpdates;
21 | }
22 |
23 | public string StockId { get; }
24 |
25 | public IPriceUpdate[] PriceUpdates { get; }
26 |
27 | public IVolumeUpdate[] VolumeUpdates { get; }
28 |
29 | public static readonly IPriceUpdate[] EmptyPrices = new IPriceUpdate[0];
30 | public static readonly IVolumeUpdate[] EmptyVolumes = new IVolumeUpdate[0];
31 |
32 | public static PriceAndVolumeSnapshot Empty(string stockId)
33 | {
34 | return new PriceAndVolumeSnapshot(stockId, EmptyPrices, EmptyVolumes);
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Events/IPriceUpdate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace Akka.CQRS.Pricing.Events
6 | {
7 | ///
8 | /// Used to signal a change in price for a specific stock.
9 | ///
10 | public interface IPriceUpdate : IWithStockId, IComparable
11 | {
12 | ///
13 | /// The time of this price update.
14 | ///
15 | DateTimeOffset Timestamp { get; }
16 |
17 | ///
18 | /// The current volume-weighted average price.
19 | ///
20 | decimal CurrentAvgPrice { get; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Events/IVolumeUpdate.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 |
7 | using System;
8 |
9 | namespace Akka.CQRS.Pricing.Events
10 | {
11 | ///
12 | /// Used to signal a change in volume for a specific stock.
13 | ///
14 | public interface IVolumeUpdate : IWithStockId, IComparable
15 | {
16 | ///
17 | /// The time of this price update.
18 | ///
19 | DateTimeOffset Timestamp { get; }
20 |
21 | ///
22 | /// The current trade volume.
23 | ///
24 | double CurrentVolume { get; }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Events/PriceChanged.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 |
7 | using System;
8 | using System.Collections.Generic;
9 |
10 | namespace Akka.CQRS.Pricing.Events
11 | {
12 | ///
13 | /// Concrete implementation.
14 | ///
15 | public sealed class PriceChanged : IPriceUpdate, IComparable
16 | {
17 | public PriceChanged(string stockId, decimal currentAvgPrice, DateTimeOffset timestamp)
18 | {
19 | StockId = stockId;
20 | CurrentAvgPrice = currentAvgPrice;
21 | Timestamp = timestamp;
22 | }
23 |
24 | public DateTimeOffset Timestamp { get; }
25 |
26 | public decimal CurrentAvgPrice { get; }
27 |
28 | public string StockId { get; }
29 |
30 | public int CompareTo(PriceChanged other)
31 | {
32 | if (ReferenceEquals(this, other)) return 0;
33 | if (ReferenceEquals(null, other)) return 1;
34 | return Timestamp.CompareTo(other.Timestamp);
35 | }
36 |
37 | public int CompareTo(IPriceUpdate other)
38 | {
39 | if (other is PriceChanged c)
40 | {
41 | return CompareTo(c);
42 | }
43 | throw new ArgumentException();
44 | }
45 |
46 | public override string ToString()
47 | {
48 | return $"[{StockId}][{Timestamp}] - $[{CurrentAvgPrice}]";
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Events/VolumeChanged.cs:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------
2 | //
3 | // Copyright (C) 2015 - 2019 Petabridge, LLC
4 | //
5 | // -----------------------------------------------------------------------
6 |
7 | using System;
8 |
9 | namespace Akka.CQRS.Pricing.Events
10 | {
11 | ///
12 | /// Concrete implementation.
13 | ///
14 | public sealed class VolumeChanged : IVolumeUpdate, IComparable
15 | {
16 | public VolumeChanged(string stockId, double currentVolume, DateTimeOffset timestamp)
17 | {
18 | StockId = stockId;
19 | CurrentVolume = currentVolume;
20 | Timestamp = timestamp;
21 | }
22 |
23 | public string StockId { get; }
24 |
25 | public DateTimeOffset Timestamp { get; }
26 | public double CurrentVolume { get; }
27 |
28 | public int CompareTo(IVolumeUpdate other)
29 | {
30 | if (other is VolumeChanged c)
31 | {
32 | return CompareTo(c);
33 | }
34 | throw new ArgumentException();
35 | }
36 |
37 | public int CompareTo(VolumeChanged other)
38 | {
39 | if (ReferenceEquals(this, other)) return 0;
40 | if (ReferenceEquals(null, other)) return 1;
41 | return Timestamp.CompareTo(other.Timestamp);
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/MatchAggregatorSnapshot.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Akka.CQRS.Pricing.Events;
5 |
6 | namespace Akka.CQRS.Pricing
7 | {
8 | ///
9 | /// Represents the point-in-time state of the match aggregator at any given time.
10 | ///
11 | public sealed class MatchAggregatorSnapshot
12 | {
13 | public MatchAggregatorSnapshot(long queryOffset, decimal avgPrice, double avgVolume,
14 | IReadOnlyList recentPriceUpdates, IReadOnlyList recentVolumeUpdates)
15 | {
16 | QueryOffset = queryOffset;
17 | AvgPrice = avgPrice;
18 | AvgVolume = avgVolume;
19 | RecentPriceUpdates = recentPriceUpdates;
20 | RecentVolumeUpdates = recentVolumeUpdates;
21 | }
22 |
23 | ///
24 | /// The sequence number of the Akka.Persistence.Query object to begin reading from.
25 | ///
26 | public long QueryOffset { get; }
27 |
28 | ///
29 | /// The most recently saved average price.
30 | ///
31 | public decimal AvgPrice { get; }
32 |
33 | ///
34 | /// The most recently saved average volume.
35 | ///
36 | public double AvgVolume { get; }
37 |
38 | public IReadOnlyList RecentPriceUpdates { get; }
39 |
40 | public IReadOnlyList RecentVolumeUpdates { get; }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/PriceTopicHelpers.cs:
--------------------------------------------------------------------------------
1 | namespace Akka.CQRS.Pricing
2 | {
3 | ///
4 | /// Helper methods for working with price and volume updates.
5 | ///
6 | public static class PriceTopicHelpers
7 | {
8 | public static string PriceUpdateTopic(string tickerSymbol)
9 | {
10 | return $"{tickerSymbol}-price";
11 | }
12 |
13 | public static string VolumeUpdateTopic(string tickerSymbol)
14 | {
15 | return $"{tickerSymbol}-update";
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Views/EMWA.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Akka.CQRS.Pricing
4 | {
5 | ///
6 | /// Simple data structure for self-contained EMWA mathematics.
7 | ///
8 | public struct EMWA
9 | {
10 | public EMWA(double alpha, double currentAvg)
11 | {
12 | Alpha = alpha;
13 | CurrentAvg = currentAvg;
14 | }
15 |
16 | public double Alpha { get; }
17 |
18 | public double CurrentAvg { get; }
19 |
20 | public EMWA Next(double nextValue)
21 | {
22 | return new EMWA(Alpha, Alpha * nextValue + (1 - Alpha) * CurrentAvg);
23 | }
24 |
25 | public static EMWA Init(int sampleSize, double firstReading)
26 | {
27 | var alpha = 2.0 / (sampleSize + 1);
28 | return new EMWA(alpha, firstReading);
29 | }
30 |
31 | public static double operator %(EMWA e1, EMWA e2)
32 | {
33 | return (e1.CurrentAvg - e2.CurrentAvg) / e1.CurrentAvg;
34 | }
35 |
36 | public static EMWA operator +(EMWA e1, double next)
37 | {
38 | return e1.Next(next);
39 | }
40 | }
41 |
42 | ///
43 | /// Simple data structure for self-contained EMWA mathematics using precision.
44 | ///
45 | public struct EMWAm
46 | {
47 | public EMWAm(decimal alpha, decimal currentAvg)
48 | {
49 | Alpha = alpha;
50 | CurrentAvg = currentAvg;
51 | }
52 |
53 | public decimal Alpha { get; }
54 |
55 | public decimal CurrentAvg { get; }
56 |
57 | public EMWAm Next(decimal nextValue)
58 | {
59 | return new EMWAm(Alpha, Alpha * nextValue + (1 - Alpha) * CurrentAvg);
60 | }
61 |
62 | public static EMWAm Init(int sampleSize, decimal firstReading)
63 | {
64 | var alpha = 2.0m / (sampleSize + 1);
65 | return new EMWAm(alpha, firstReading);
66 | }
67 |
68 | public static decimal operator %(EMWAm e1, EMWAm e2)
69 | {
70 | return (e1.CurrentAvg - e2.CurrentAvg) / e1.CurrentAvg;
71 | }
72 |
73 | public static EMWAm operator +(EMWAm e1, decimal next)
74 | {
75 | return e1.Next(next);
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Views/MatchAggregate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Akka.CQRS.Events;
3 | using Akka.CQRS.Pricing.Events;
4 | using Akka.CQRS.Util;
5 |
6 | namespace Akka.CQRS.Pricing.Views
7 | {
8 | ///
9 | /// Aggregates trade events in order to produce price and volume estimates.
10 | ///
11 | public sealed class MatchAggregate
12 | {
13 | ///
14 | /// By default, average all prices and volume over the past 30 matched trades.
15 | ///
16 | public const int DefaultSampleSize = 30;
17 |
18 | public MatchAggregate(string tickerSymbol, decimal initialPrice = 0.0m,
19 | double initialVolume = 0.0d, int sampleSize = DefaultSampleSize)
20 | {
21 | TickerSymbol = tickerSymbol;
22 | AvgPrice = EMWAm.Init(sampleSize, initialPrice);
23 | AvgVolume = EMWA.Init(sampleSize, initialVolume);
24 | }
25 |
26 | public string TickerSymbol { get; }
27 |
28 | public EMWA AvgVolume { get; private set; }
29 |
30 | public EMWAm AvgPrice { get; private set; }
31 |
32 | ///
33 | /// Fetch the current price and volume metrics.
34 | ///
35 | /// We don't do this on every single match since that could become noisy quickly.
36 | /// Instead we do it on a regular clock interval.
37 | ///
38 | /// Optional - the service used for time-stamping the price and volume updates.
39 | /// The current price and volume update events.
40 | public (IPriceUpdate lastestPrice, IVolumeUpdate latestVolume) FetchMetrics(ITimestamper timestampService = null)
41 | {
42 | var currentTime = timestampService?.Now ?? CurrentUtcTimestamper.Instance.Now;
43 | return (new PriceChanged(TickerSymbol, AvgPrice.CurrentAvg, currentTime),
44 | new VolumeChanged(TickerSymbol, AvgVolume.CurrentAvg, currentTime));
45 | }
46 |
47 | ///
48 | /// Feed the most recent match for to update moving price averages.
49 | ///
50 | /// The most recent matched trade for this symbol.
51 | public bool WithMatch(Match latestTrade)
52 | {
53 | if (!latestTrade.StockId.Equals(TickerSymbol))
54 | return false; // Someone fed a match for a stock other than TickerSymbol
55 |
56 | // Update EMWA quantity and volume
57 | AvgVolume += latestTrade.Quantity;
58 | AvgPrice += latestTrade.SettlementPrice;
59 | return true;
60 | }
61 | }
62 |
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Pricing/Views/PriceHistory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Immutable;
3 | using System.Linq;
4 | using Akka.CQRS.Pricing.Events;
5 |
6 | namespace Akka.CQRS.Pricing.Views
7 | {
8 | ///
9 | /// Details the price history for a specific ticker symbol.
10 | ///
11 | /// In-memory, replicated view.
12 | ///
13 | public struct PriceHistory : IWithStockId
14 | {
15 | public PriceHistory(string stockId, ImmutableSortedSet historicalPrices)
16 | {
17 | HistoricalPrices = historicalPrices;
18 | StockId = stockId;
19 | }
20 |
21 | public string StockId { get; }
22 |
23 | public DateTimeOffset From => HistoricalPrices[0].Timestamp;
24 |
25 | public DateTimeOffset Until => CurrentPriceUpdate.Timestamp;
26 |
27 | public decimal CurrentPrice => CurrentPriceUpdate.CurrentAvgPrice;
28 |
29 | public IPriceUpdate CurrentPriceUpdate => HistoricalPrices.Last();
30 |
31 | public TimeSpan Range => Until - From;
32 |
33 | public ImmutableSortedSet HistoricalPrices { get; }
34 |
35 | public PriceHistory WithPrice(IPriceUpdate update)
36 | {
37 | if(!update.StockId.Equals(StockId))
38 | throw new ArgumentOutOfRangeException($"Expected ticker symbol {StockId} but found {update.StockId}", nameof(update));
39 |
40 | return new PriceHistory(StockId, HistoricalPrices.Add(update));
41 | }
42 |
43 | ///
44 | /// Purge older price entries - resetting the window for a new trading day.
45 | ///
46 | /// Delete any entries older than this.
47 | /// An updated .
48 | public PriceHistory Prune(DateTimeOffset earliestStart)
49 | {
50 | return new PriceHistory(StockId, HistoricalPrices.Where(x => x.Timestamp < earliestStart).ToImmutableSortedSet());
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Subscriptions.Tests/Akka.CQRS.Subscriptions.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(NetVersion)
4 |
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Subscriptions.Tests/DistributedPubSub/DistributedPubSubEnd2EndSpecs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Akka.Actor;
6 | using Akka.Cluster;
7 | using Akka.Configuration;
8 | using Akka.CQRS.Events;
9 | using Akka.CQRS.Subscriptions.DistributedPubSub;
10 | using FluentAssertions;
11 | using Xunit;
12 | using Xunit.Abstractions;
13 |
14 | namespace Akka.CQRS.Subscriptions.Tests.DistributedPubSub
15 | {
16 | public class DistributedPubSubEnd2EndSpecs : TestKit.Xunit2.TestKit
17 | {
18 | private static readonly Config ClusterConfig = @"
19 | akka.actor.provider = cluster
20 | ";
21 |
22 | public DistributedPubSubEnd2EndSpecs(ITestOutputHelper output)
23 | : base(ClusterConfig, output: output) { }
24 |
25 | public Address SelfAddress => Cluster.Cluster.Get(Sys).SelfAddress;
26 |
27 | [Fact(DisplayName = "Should be able to subscribe and publish to trade event topics.")]
28 | public async Task ShouldSubscribeAndPublishToTradeEventTopics()
29 | {
30 | // Join the cluster
31 | Within(TimeSpan.FromSeconds(5), () =>
32 | {
33 | Cluster.Cluster.Get(Sys).Join(SelfAddress);
34 | AwaitCondition(
35 | () => Cluster.Cluster.Get(Sys).State.Members.Count(x => x.Status == MemberStatus.Up) == 1);
36 | });
37 |
38 | // Start DistributedPubSub
39 | var subManager = DistributedPubSubTradeEventSubscriptionManager.For(Sys);
40 | var published = DistributedPubSubTradeEventPublisher.For(Sys);
41 |
42 | // Subscribe to all topics
43 | var subAck = await subManager.Subscribe("MSFT", TestActor);
44 | subAck.TickerSymbol.Should().Be("MSFT");
45 |
46 | var bid = new Bid("MSFT", "foo", 10.0m, 1.0d, DateTimeOffset.UtcNow);
47 | published.Publish("MSFT", bid);
48 | ExpectMsg();
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Akka.CQRS.Subscriptions.Tests/DistributedPubSub/DistributedPubSubFormatterSpecs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using FluentAssertions;
4 | using static Akka.CQRS.Subscriptions.DistributedPubSub.DistributedPubSubTopicFormatter;
5 | using Xunit;
6 |
7 | namespace Akka.CQRS.Subscriptions.Tests.DistributedPubSub
8 | {
9 | public class DistributedPubSubFormatterSpecs
10 | {
11 | public static IEnumerable