├── .github └── dependabot.yml ├── .gitignore ├── .kodiak.toml ├── Akka.CQRS.sln ├── LICENSE ├── NuGet.config ├── 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 ├── deployK8sServices.cmd ├── deployK8sServices.sh ├── docker-compose.yaml ├── docker-images.txt ├── 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 ├── k8s ├── environment │ ├── configs │ │ └── configs.yaml │ ├── grafana-configmap.yaml │ ├── grafana-service.yaml │ ├── jaeger.all-in-one.yaml │ ├── mongodb-deploy.yaml │ ├── prometheus-config.yaml │ └── prometheus-service.yaml └── services │ ├── lighthouse-deploy.yaml │ ├── pricing-deploy.yaml │ ├── pricing-web-deploy.yaml │ ├── tradeprocessor-deploy.yaml │ └── traders-deploy.yaml ├── 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 │ ├── AppBootstrap.cs │ ├── MongoDbHoconHelper.cs │ ├── Ops │ │ ├── OpsConfig.cs │ │ ├── ops.conf │ │ └── phobos.conf │ ├── StockEventTagger.cs │ ├── StockShardMsgRouter.cs │ └── TradeEventConsistentHashMapping.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 │ ├── ClientHandlerActor.cs │ ├── MatchAggregator.cs │ ├── PriceInitiatorActor.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 │ ├── AkkaService.cs │ ├── Dockerfile │ ├── Program.cs │ └── app.conf ├── Akka.CQRS.Pricing.Subscriptions │ ├── Actor │ │ └── ActorMarketEventSubscriptionManager.cs │ ├── Akka.CQRS.Pricing.Subscriptions.csproj │ ├── Client │ │ ├── SubscribeClient.cs │ │ └── UnsubscribeClient.cs │ ├── DistributedPubSub │ │ ├── DistributedPubSubMarketEventPublisher.cs │ │ ├── DistributedPubSubMarketEventSubscriptionManager.cs │ │ └── DistributedPubSubPriceTopicFormatter.cs │ ├── IMarketEventPublisher.cs │ ├── IMarketEventSubscriptionManager.cs │ ├── InMem │ │ └── InMemoryMarketEventSubscriptionManager.cs │ ├── MarketEventHelpers.cs │ ├── MarketEventSubscriptionManagerBase.cs │ ├── MarketEventType.cs │ ├── MarketSubscribe.cs │ ├── MarketSubscribeAck.cs │ ├── MarketSubscribeNack.cs │ ├── MarketUnsubscribe.cs │ ├── MarketUnsubscribeAck.cs │ ├── MarketUnsubscribeNack.cs │ └── NoOp │ │ └── NoOpMarketEventSubscriptionManager.cs ├── Akka.CQRS.Pricing.Web │ ├── Actors │ │ ├── StockEventConfiguratorActor.cs │ │ └── StockPublisherActor.cs │ ├── Akka.CQRS.Pricing.Web.csproj │ ├── Controllers │ │ └── HomeController.cs │ ├── Dockerfile │ ├── Hubs │ │ ├── StockHub.cs │ │ └── StockHubHelper.cs │ ├── Models │ │ └── ErrorViewModel.cs │ ├── Program.cs │ ├── Services │ │ └── AkkaService.cs │ ├── Startup.cs │ ├── Views │ │ ├── Home │ │ │ ├── About.cshtml │ │ │ ├── Contact.cshtml │ │ │ ├── Index.cshtml │ │ │ └── Privacy.cshtml │ │ ├── Shared │ │ │ ├── Error.cshtml │ │ │ ├── _CookieConsentPartial.cshtml │ │ │ ├── _Layout.cshtml │ │ │ └── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── app.conf │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ │ ├── css │ │ ├── site.css │ │ └── site.min.css │ │ ├── favicon.ico │ │ ├── images │ │ ├── banner1.svg │ │ ├── banner2.svg │ │ └── banner3.svg │ │ ├── js │ │ ├── site.js │ │ └── site.min.js │ │ └── lib │ │ ├── bootstrap │ │ ├── .bower.json │ │ ├── LICENSE │ │ └── dist │ │ │ ├── css │ │ │ ├── bootstrap-theme.css │ │ │ ├── bootstrap-theme.css.map │ │ │ ├── bootstrap-theme.min.css │ │ │ ├── bootstrap-theme.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ └── js │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.min.js │ │ │ └── npm.js │ │ ├── jquery-validation-unobtrusive │ │ ├── .bower.json │ │ ├── LICENSE.txt │ │ ├── jquery.validate.unobtrusive.js │ │ └── jquery.validate.unobtrusive.min.js │ │ ├── jquery-validation │ │ ├── .bower.json │ │ ├── LICENSE.md │ │ └── dist │ │ │ ├── additional-methods.js │ │ │ ├── additional-methods.min.js │ │ │ ├── jquery.validate.js │ │ │ └── jquery.validate.min.js │ │ ├── jquery │ │ ├── .bower.json │ │ ├── LICENSE.txt │ │ └── dist │ │ │ ├── jquery.js │ │ │ ├── jquery.min.js │ │ │ └── jquery.min.map │ │ ├── knockoutjs │ │ ├── knockout-3.3.0.debug.js │ │ └── knockout-3.3.0.js │ │ ├── signalr │ │ ├── signalr.js │ │ ├── signalr.js.map │ │ ├── signalr.min.js │ │ └── signalr.min.js.map │ │ └── smoothie │ │ └── smoothie.js ├── Akka.CQRS.Pricing │ ├── Akka.CQRS.Pricing.csproj │ ├── Commands │ │ ├── FetchPriceAndVolume.cs │ │ ├── Ping.cs │ │ └── PriceAndVolumeSnapshot.cs │ ├── Events │ │ ├── IPriceUpdate.cs │ │ ├── IVolumeUpdate.cs │ │ ├── PriceChanged.cs │ │ └── VolumeChanged.cs │ ├── IMarketEvent.cs │ ├── MatchAggregatorSnapshot.cs │ ├── Serialization │ │ └── Proto │ │ │ └── AkkaCqrsPricing.g.cs │ └── Views │ │ ├── EMWA.cs │ │ ├── MatchAggregate.cs │ │ └── PriceHistory.cs ├── Akka.CQRS.Subscriptions.Tests │ ├── Actor │ │ └── ActorTradeSubscriptionManagerEnd2EndSpecs.cs │ ├── Akka.CQRS.Subscriptions.Tests.csproj │ ├── DistributedPubSub │ │ ├── DistributedPubSubEnd2EndSpecs.cs │ │ └── DistributedPubSubFormatterSpecs.cs │ └── TradeEventExtensionsSpecs.cs ├── Akka.CQRS.Subscriptions │ ├── Actor │ │ └── ActorTradeSubscriptionManager.cs │ ├── Akka.CQRS.Subscriptions.csproj │ ├── DistributedPubSub │ │ ├── DistributedPubSubTradeEventPublisher.cs │ │ ├── DistributedPubSubTradeEventSubscriptionManager.cs │ │ └── DistributedPubSubTradeEventTopicFormatter.cs │ ├── ITradeEventPublisher.cs │ ├── ITradeEventSubscriptionManager.cs │ ├── InMem │ │ └── InMemoryTradeEventPublisher.cs │ ├── NoOp │ │ └── NoOpTradeEventSubscriptionManager.cs │ ├── TradeEventHelpers.cs │ ├── TradeEventSubscriptionManagerBase.cs │ ├── TradeEventType.cs │ ├── TradeSubscribe.cs │ ├── TradeSubscribeAck.cs │ ├── TradeSubscribeNack.cs │ ├── TradeUnsubscribe.cs │ ├── TradeUnsubscribeAck.cs │ └── TradeUnsubscribeNack.cs ├── Akka.CQRS.Tests │ ├── Akka.CQRS.Tests.csproj │ ├── OrderSpecs.cs │ └── Serialization │ │ ├── TradeEventSerializerSpecs.cs │ │ └── XunitMemberDataHelper.cs ├── Akka.CQRS.TradePlacers.Service │ ├── Akka.CQRS.TradePlacers.Service.csproj │ ├── AkkaService.cs │ ├── Dockerfile │ ├── Program.cs │ └── app.conf ├── Akka.CQRS.TradeProcessor.Actors │ ├── Akka.CQRS.TradeProcessor.Actors.csproj │ ├── AskerActor.cs │ ├── BidderActor.cs │ ├── OrderBookActor.cs │ └── OrderBookMasterActor.cs ├── Akka.CQRS.TradeProcessor.Service │ ├── Akka.CQRS.TradeProcessor.Service.csproj │ ├── AkkaService.cs │ ├── 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 │ ├── Serialization │ │ ├── Proto │ │ │ └── AkkaCqrs.g.cs │ │ ├── TradeEventSerializer.conf │ │ └── TradeEventSerializer.cs │ └── Util │ │ ├── CurrentUtcTimestamper.cs │ │ └── GuidTradeOrderIdGenerator.cs ├── common.props └── protobuf │ ├── AKka.Cqrs.Pricing.proto │ └── Akka.Cqrs.proto ├── stopK8sServices.cmd └── stopK8sServices.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | target-branch: "dev" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | labels: 9 | - "dependencies" 10 | 11 | - package-ecosystem: "nuget" 12 | target-branch: "start" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | labels: 17 | - "dependencies" 18 | 19 | - package-ecosystem: "nuget" 20 | target-branch: "lesson2" 21 | directory: "/" 22 | schedule: 23 | interval: "daily" 24 | labels: 25 | - "dependencies" 26 | 27 | - package-ecosystem: "nuget" 28 | target-branch: "lesson3" 29 | directory: "/" 30 | schedule: 31 | interval: "daily" 32 | labels: 33 | - "dependencies" 34 | 35 | - package-ecosystem: "nuget" 36 | target-branch: "lesson4" 37 | directory: "/" 38 | schedule: 39 | interval: "daily" 40 | labels: 41 | - "dependencies" 42 | 43 | - package-ecosystem: "nuget" 44 | target-branch: "lesson5" 45 | directory: "/" 46 | schedule: 47 | interval: "daily" 48 | labels: 49 | - "dependencies" 50 | 51 | - package-ecosystem: "nuget" 52 | target-branch: "lesson6" 53 | directory: "/" 54 | schedule: 55 | interval: "daily" 56 | labels: 57 | - "dependencies" 58 | -------------------------------------------------------------------------------- /.kodiak.toml: -------------------------------------------------------------------------------- 1 | # .kodiak.toml 2 | version = 1 3 | 4 | [merge] 5 | require_automerge_label = false # merge everything 6 | method = "squash" 7 | delete_branch_on_merge = true 8 | block_on_reviews_requested = true 9 | 10 | [merge.automerge_dependencies] 11 | # only auto merge "minor" and "patch" version upgrades. 12 | # do not automerge "major" version upgrades. 13 | versions = ["minor", "patch"] 14 | usernames = ["dependabot"] 15 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 0.2.1 June 24 2020 #### 2 | * Added footer that displays assembly version on Pricing UI 3 | -------------------------------------------------------------------------------- /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 | - task: UseDotNet@2 19 | displayName: 'Use .NET Core Runtime 3.1.407' 20 | inputs: 21 | packageType: sdk 22 | version: 3.1.407 23 | # Linux or macOS 24 | - task: Bash@3 25 | displayName: Linux / OSX Build 26 | inputs: 27 | filePath: ${{ parameters.scriptFileName }} 28 | arguments: ${{ parameters.scriptArgs }} 29 | continueOnError: true 30 | condition: in( variables['Agent.OS'], 'Linux', 'Darwin' ) 31 | # Windows 32 | - task: BatchScript@1 33 | displayName: Windows Build 34 | inputs: 35 | filename: ${{ parameters.scriptFileName }} 36 | arguments: ${{ parameters.scriptArgs }} 37 | continueOnError: true 38 | condition: eq( variables['Agent.OS'], 'Windows_NT' ) 39 | - task: PublishTestResults@2 40 | inputs: 41 | testRunner: VSTest 42 | testResultsFiles: '**/*.trx' #TestResults folder usually 43 | testRunTitle: ${{ parameters.name }} 44 | mergeTestResults: true 45 | - script: 'echo 1>&2' 46 | failOnStderr: true 47 | displayName: 'If above is partially succeeded, then fail' 48 | 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 | - lesson* 9 | - start 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, start, lesson* ] # branch names which will trigger a build 15 | 16 | name: $(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r) 17 | 18 | jobs: 19 | - template: azure-pipeline.template.yaml 20 | parameters: 21 | name: Ubuntu 22 | vmImage: 'ubuntu-latest' 23 | scriptFileName: ./build.sh 24 | scriptArgs: 'all customNuGetSource=$(phobosNuGet)' 25 | -------------------------------------------------------------------------------- /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 | - start 9 | - lesson* 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, start, lesson* ] # branch names which will trigger a build 15 | 16 | name: $(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r) 17 | 18 | variables: 19 | - group: phobosKeys 20 | 21 | jobs: 22 | - template: azure-pipeline.template.yaml 23 | parameters: 24 | name: Windows 25 | vmImage: 'vs2017-win2016' 26 | scriptFileName: build.cmd 27 | scriptArgs: 'RunTests customNuGetSource=$(phobosNuGet)' 28 | -------------------------------------------------------------------------------- /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: vs2017-win2016 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: UseDotNet@2 25 | displayName: 'Use .NET Core Runtime 3.1.407' 26 | inputs: 27 | packageType: sdk 28 | version: 3.1.407 29 | - task: BatchScript@1 30 | displayName: 'FAKE Build' 31 | inputs: 32 | filename: build.cmd 33 | arguments: 'All SignClientUser=$(signingUsername) SignClientSecret=$(signingPassword) nugetpublishurl=https://www.nuget.org/api/v2/package nugetkey=$(nugetKey)' 34 | 35 | - task: GitHubRelease@0 36 | displayName: 'GitHub release (create)' 37 | inputs: 38 | gitHubConnection: $(githubConnectionName) 39 | repositoryName: $(githubRepositoryName) 40 | title: '$(projectName) v$(Build.SourceBranchName)' 41 | releaseNotesFile: 'RELEASE_NOTES.md' 42 | assets: | 43 | bin\nuget\*.nupkg -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | PowerShell.exe -file "build.ps1" %* -------------------------------------------------------------------------------- /deployK8sServices.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM deploys all Kubernetes services to their staging environment 3 | 4 | set namespace=akka-cqrs 5 | set location=%~dp0/k8s/environment 6 | 7 | echo "Deploying K8s resources from [%location%] into namespace [%namespace%]" 8 | 9 | echo "Creating Namespaces..." 10 | kubectl create namespace %namespace% 11 | 12 | echo "Using namespace [%namespace%] going forward..." 13 | 14 | echo "Creating configurations from YAML files in [%location%/configs]" 15 | for %%f in (%location%/configs/*.yaml) do ( 16 | echo "Deploying %%~nxf" 17 | kubectl apply -f "%location%/configs/%%~nxf" -n "%namespace%" 18 | ) 19 | 20 | echo "Creating environment-specific services from YAML files in [%location%]" 21 | for %%f in (%location%/*.yaml) do ( 22 | echo "Deploying %%~nxf" 23 | kubectl apply -f "%location%/%%~nxf" -n "%namespace%" 24 | ) 25 | 26 | echo "Waiting 10 seconds for infrastructure to be ready..." 27 | TIMEOUT /T 10 28 | 29 | echo "Creating all services..." 30 | for %%f in (%~dp0/k8s/services/*.yaml) do ( 31 | echo "Deploying %%~nxf" 32 | kubectl apply -f "%~dp0/k8s/services/%%~nxf" -n "%namespace%" 33 | ) 34 | 35 | echo "All services started... Printing K8s output.." 36 | kubectl get all -n "%namespace%" -------------------------------------------------------------------------------- /deployK8sServices.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # deploys all Kubernetes services 3 | 4 | find ./k8s -name "*.yaml" | while read fname; do 5 | echo "Deploying $fname" 6 | kubectl apply -f "$fname" 7 | 8 | echo "Waiting 10s before start of next deployment." 9 | sleep 10 10 | done 11 | -------------------------------------------------------------------------------- /docker-images.txt: -------------------------------------------------------------------------------- 1 | akka.cqrs.tradeprocessor 2 | akka.cqrs.traders 3 | akka.cqrs.pricing 4 | akka.cqrs.pricing.web -------------------------------------------------------------------------------- /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/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/docs/images/akka-cqrs-architectural-overview.png -------------------------------------------------------------------------------- /docs/images/akka-cqrs-inmemory-replication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/docs/images/akka-cqrs-inmemory-replication.png -------------------------------------------------------------------------------- /docs/images/docker-for-windows-networking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/docs/images/docker-for-windows-networking.png -------------------------------------------------------------------------------- /docs/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/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 | -------------------------------------------------------------------------------- /k8s/environment/configs/configs.yaml: -------------------------------------------------------------------------------- 1 | # staging settings 2 | kind: ConfigMap 3 | apiVersion: v1 4 | metadata: 5 | name: pb-configs 6 | data: 7 | # Configuration values can be set as key-value properties 8 | environment: Development -------------------------------------------------------------------------------- /k8s/environment/grafana-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: grafana-ip-service 5 | annotations: 6 | prometheus.io/scrape: 'false' 7 | prometheus.io/path: /metrics 8 | prometheus.io/port: '3000' 9 | spec: 10 | type: LoadBalancer 11 | selector: 12 | component: grafana 13 | ports: 14 | - port: 3000 15 | targetPort: 3000 16 | --- 17 | apiVersion: apps/v1 18 | kind: Deployment 19 | metadata: 20 | name: grafana 21 | labels: 22 | component: grafana 23 | spec: 24 | replicas: 1 25 | selector: 26 | matchLabels: 27 | component: grafana 28 | template: 29 | metadata: 30 | labels: 31 | component: grafana 32 | spec: 33 | containers: 34 | - image: grafana/grafana:7.3.7 35 | name: grafana-core 36 | imagePullPolicy: IfNotPresent 37 | # env: 38 | resources: 39 | # keep request = limit to keep this container in guaranteed class 40 | limits: 41 | cpu: 100m 42 | memory: 100Mi 43 | requests: 44 | cpu: 100m 45 | memory: 100Mi 46 | env: 47 | # The following env variables set up basic auth twith the default admin user and admin password. 48 | - name: GF_AUTH_BASIC_ENABLED 49 | value: "true" 50 | - name: GF_AUTH_ANONYMOUS_ENABLED 51 | value: "false" 52 | # does not really work, because of template variables in exported dashboards: 53 | # - name: GF_DASHBOARDS_JSON_ENABLED 54 | # value: "true" 55 | readinessProbe: 56 | httpGet: 57 | path: /login 58 | port: 3000 59 | volumeMounts: 60 | - name: grafana-persistent-storage 61 | mountPath: /var/lib/grafana 62 | - name: datasources-provider 63 | mountPath: /etc/grafana/provisioning/datasources 64 | - name: dashboards-volume 65 | mountPath: /var/lib/grafana/dashboards 66 | - name: dashboard-provider 67 | mountPath: /etc/grafana/provisioning/dashboards 68 | volumes: 69 | - name: grafana-persistent-storage 70 | emptyDir: {} 71 | - name: datasources-provider 72 | configMap: 73 | name: grafana-datasources-provider 74 | items: 75 | - key: providers.yaml 76 | path: providers.yaml 77 | - name: dashboards-volume 78 | configMap: 79 | name: grafana-dashs 80 | - name: dashboard-provider 81 | configMap: 82 | name: grafana-dash-provider 83 | items: 84 | - key: providers.yaml 85 | path: providers.yaml 86 | -------------------------------------------------------------------------------- /k8s/environment/mongodb-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | namespace: akka-cqrs 5 | name: mongodb 6 | spec: 7 | ports: 8 | - port: 27017 9 | selector: 10 | app: mongodb 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | namespace: akka-cqrs 16 | name: mongodb 17 | spec: 18 | replicas: 1 19 | selector: 20 | matchLabels: 21 | app: mongodb 22 | template: 23 | metadata: 24 | labels: 25 | app: mongodb 26 | spec: 27 | containers: 28 | - name: mongo 29 | image: mongo:4.0 30 | env: 31 | - name: MONGO_INITDB_DATABASE 32 | value: "akkaTrader" 33 | ports: 34 | - containerPort: 27017 35 | name: mongodb -------------------------------------------------------------------------------- /k8s/environment/prometheus-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: prometheus-ip-service 5 | annotations: 6 | prometheus.io/scrape: 'true' 7 | prometheus.io/path: /metrics 8 | prometheus.io/port: '9090' 9 | spec: 10 | type: LoadBalancer 11 | selector: 12 | app: prometheus-server 13 | ports: 14 | - port: 9090 15 | targetPort: 9090 16 | --- 17 | apiVersion: apps/v1 18 | kind: Deployment 19 | metadata: 20 | name: prometheus-deployment 21 | spec: 22 | selector: 23 | matchLabels: 24 | app: prometheus-server 25 | replicas: 1 26 | template: 27 | metadata: 28 | labels: 29 | app: prometheus-server 30 | spec: 31 | containers: 32 | - name: prometheus 33 | image: prom/prometheus:latest 34 | args: 35 | - "--config.file=/etc/prometheus/prometheus.yml" 36 | - "--storage.tsdb.path=/prometheus/" 37 | ports: 38 | - containerPort: 9090 39 | volumeMounts: 40 | - name: prometheus-config-volume 41 | mountPath: /etc/prometheus/ 42 | - name: prometheus-storage-volume 43 | mountPath: /prometheus/ 44 | resources: 45 | requests: 46 | memory: "512Mi" 47 | cpu: "500m" 48 | limits: 49 | memory: "1Gi" 50 | cpu: "1000m" 51 | volumes: 52 | - name: prometheus-config-volume 53 | configMap: 54 | defaultMode: 420 55 | name: prometheus-server-conf 56 | 57 | - name: prometheus-storage-volume 58 | emptyDir: {} -------------------------------------------------------------------------------- /k8s/services/lighthouse-deploy.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: lighthouse 6 | labels: 7 | app: lighthouse 8 | spec: 9 | clusterIP: None 10 | ports: 11 | - port: 4053 12 | selector: 13 | app: lighthouse 14 | --- 15 | apiVersion: apps/v1 16 | kind: StatefulSet 17 | metadata: 18 | name: lighthouse 19 | labels: 20 | app: lighthouse 21 | spec: 22 | serviceName: lighthouse 23 | replicas: 2 24 | selector: 25 | matchLabels: 26 | app: lighthouse 27 | template: 28 | metadata: 29 | labels: 30 | app: lighthouse 31 | spec: 32 | terminationGracePeriodSeconds: 35 33 | containers: 34 | - name: lighthouse 35 | image: petabridge/lighthouse:latest 36 | env: 37 | - name: ACTORSYSTEM 38 | value: AkkaTrader 39 | - name: POD_NAME 40 | valueFrom: 41 | fieldRef: 42 | fieldPath: metadata.name 43 | - name: CLUSTER_IP 44 | value: "$(POD_NAME).lighthouse" 45 | - name: CLUSTER_SEEDS 46 | value: akka.tcp://$(ACTORSYSTEM)@lighthouse-0.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-1.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-2.lighthouse:4053 47 | livenessProbe: 48 | tcpSocket: 49 | port: 4053 50 | ports: 51 | - containerPort: 4053 52 | protocol: TCP -------------------------------------------------------------------------------- /k8s/services/pricing-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pricing 5 | labels: 6 | app: pricing 7 | annotations: 8 | prometheus.io/scrape: 'true' 9 | prometheus.io/path: '/metrics' 10 | prometheus.io/port: '80' 11 | spec: 12 | clusterIP: None 13 | ports: 14 | - port: 5110 15 | selector: 16 | app: pricing 17 | --- 18 | apiVersion: apps/v1 19 | kind: StatefulSet 20 | metadata: 21 | name: pricing 22 | labels: 23 | app: pricing 24 | spec: 25 | serviceName: pricing 26 | replicas: 2 27 | selector: 28 | matchLabels: 29 | app: pricing 30 | template: 31 | metadata: 32 | labels: 33 | app: pricing 34 | spec: 35 | terminationGracePeriodSeconds: 35 36 | containers: 37 | - name: pricing 38 | image: akka.cqrs.pricing:0.2.1 39 | env: 40 | - name: ACTORSYSTEM 41 | value: AkkaTrader 42 | - name: ENABLE_PHOBOS 43 | value: "true" 44 | - name: MONGO_CONNECTION_STR 45 | value: "mongodb://mongodb:27017/akkaTrader" 46 | - name: STATSD_PORT 47 | value: "8125" 48 | - name: STATSD_URL 49 | value: "statsd-agent" 50 | - name: JAEGER_AGENT_HOST 51 | value: "jaeger-agent" 52 | - name: POD_NAME 53 | valueFrom: 54 | fieldRef: 55 | fieldPath: metadata.name 56 | - name: CLUSTER_IP 57 | value: "$(POD_NAME).pricing" 58 | - name: CLUSTER_PORT 59 | value: "5110" 60 | - name: CLUSTER_SEEDS 61 | value: akka.tcp://$(ACTORSYSTEM)@lighthouse-0.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-1.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-2.lighthouse:4053 62 | livenessProbe: 63 | tcpSocket: 64 | port: 5110 65 | ports: 66 | - containerPort: 5110 67 | protocol: TCP 68 | - containerPort: 80 69 | protocol: TCP -------------------------------------------------------------------------------- /k8s/services/pricing-web-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pricing-web 5 | labels: 6 | app: pricing-web 7 | annotations: 8 | prometheus.io/scrape: 'true' 9 | prometheus.io/path: '/metrics' 10 | prometheus.io/port: '80' 11 | spec: 12 | clusterIP: None 13 | ports: 14 | - port: 16666 15 | selector: 16 | app: pricing-web 17 | --- 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: pricing-ui 22 | labels: 23 | app: pricing-web 24 | spec: 25 | ports: 26 | - name: query-http 27 | port: 80 28 | protocol: TCP 29 | targetPort: 80 30 | selector: 31 | app: pricing-web 32 | type: LoadBalancer 33 | --- 34 | apiVersion: apps/v1 35 | kind: Deployment 36 | metadata: 37 | name: pricing-web 38 | labels: 39 | app: pricing-web 40 | spec: 41 | strategy: 42 | type: Recreate 43 | replicas: 2 44 | selector: 45 | matchLabels: 46 | app: pricing-web 47 | template: 48 | metadata: 49 | labels: 50 | app: pricing-web 51 | spec: 52 | terminationGracePeriodSeconds: 35 53 | containers: 54 | - name: pricing-web 55 | image: akka.cqrs.pricing.web:0.2.1 56 | env: 57 | - name: POD_NAME 58 | valueFrom: 59 | fieldRef: 60 | fieldPath: metadata.name 61 | - name: CLUSTER_IP 62 | value: "$(POD_NAME).pricing-web" 63 | - name: CLUSTER_SEEDS 64 | value: akka.tcp://AkkaTrader@pricing-0.pricing:5110,akka.tcp://AkkaTrader@pricing-1.pricing:5110,akka.tcp://AkkaTrader@pricing-2.pricing:5110 65 | - name: CLUSTER_PORT 66 | value: "16666" 67 | - name: ENABLE_PHOBOS 68 | value: "true" 69 | - name: STATSD_PORT 70 | value: "8125" 71 | - name: STATSD_URL 72 | value: "statsd-agent" 73 | - name: JAEGER_AGENT_HOST 74 | value: "jaeger-agent" 75 | - name: ASPNETCORE_ENVIRONMENT 76 | valueFrom: 77 | configMapKeyRef: 78 | name: pb-configs 79 | key: environment 80 | livenessProbe: 81 | tcpSocket: 82 | port: 16666 83 | readinessProbe: 84 | httpGet: 85 | path: "/" 86 | port: 80 87 | initialDelaySeconds: 30 88 | ports: 89 | - containerPort: 16666 90 | protocol: TCP 91 | - containerPort: 80 92 | protocol: TCP -------------------------------------------------------------------------------- /k8s/services/tradeprocessor-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: trade-processor 5 | labels: 6 | app: trade-processor 7 | annotations: 8 | prometheus.io/scrape: 'true' 9 | prometheus.io/path: '/metrics' 10 | prometheus.io/port: '80' 11 | spec: 12 | clusterIP: None 13 | ports: 14 | - port: 5110 15 | selector: 16 | app: trade-processor 17 | --- 18 | apiVersion: apps/v1 19 | kind: StatefulSet 20 | metadata: 21 | name: trade-processor 22 | labels: 23 | app: trade-processor 24 | spec: 25 | serviceName: trade-processor 26 | replicas: 2 27 | selector: 28 | matchLabels: 29 | app: trade-processor 30 | template: 31 | metadata: 32 | labels: 33 | app: trade-processor 34 | spec: 35 | terminationGracePeriodSeconds: 35 36 | containers: 37 | - name: trade-processor 38 | image: akka.cqrs.tradeprocessor:0.2.1 39 | lifecycle: 40 | preStop: 41 | exec: 42 | command: ["/bin/sh", "-c", "pbm 127.0.0.1:9110 cluster leave"] 43 | env: 44 | - name: ACTORSYSTEM 45 | value: AkkaTrader 46 | - name: ENABLE_PHOBOS 47 | value: "true" 48 | - name: MONGO_CONNECTION_STR 49 | value: "mongodb://mongodb:27017/akkaTrader" 50 | - name: STATSD_PORT 51 | value: "8125" 52 | - name: STATSD_URL 53 | value: "statsd-agent" 54 | - name: JAEGER_AGENT_HOST 55 | value: "jaeger-agent" 56 | - name: POD_NAME 57 | valueFrom: 58 | fieldRef: 59 | fieldPath: metadata.name 60 | - name: CLUSTER_IP 61 | value: "$(POD_NAME).trade-processor" 62 | - name: CLUSTER_PORT 63 | value: "5110" 64 | - name: CLUSTER_SEEDS 65 | value: akka.tcp://$(ACTORSYSTEM)@lighthouse-0.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-1.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-2.lighthouse:4053 66 | livenessProbe: 67 | tcpSocket: 68 | port: 5110 69 | ports: 70 | - containerPort: 5110 71 | protocol: TCP 72 | - containerPort: 80 73 | protocol: TCP -------------------------------------------------------------------------------- /k8s/services/traders-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | namespace: akka-cqrs 5 | name: traders 6 | labels: 7 | app: traders 8 | annotations: 9 | prometheus.io/scrape: 'true' 10 | prometheus.io/path: '/metrics' 11 | prometheus.io/port: '80' 12 | spec: 13 | clusterIP: None 14 | ports: 15 | - port: 5110 16 | selector: 17 | app: traders 18 | --- 19 | apiVersion: apps/v1 20 | kind: StatefulSet 21 | metadata: 22 | namespace: akka-cqrs 23 | name: traders 24 | labels: 25 | app: traders 26 | spec: 27 | serviceName: traders 28 | replicas: 2 29 | selector: 30 | matchLabels: 31 | app: traders 32 | template: 33 | metadata: 34 | labels: 35 | app: traders 36 | spec: 37 | terminationGracePeriodSeconds: 35 38 | containers: 39 | - name: traders 40 | image: akka.cqrs.traders:0.2.1 41 | lifecycle: 42 | preStop: 43 | exec: 44 | command: ["/bin/sh", "-c", "pbm 127.0.0.1:9110 cluster leave"] 45 | env: 46 | - name: ACTORSYSTEM 47 | value: AkkaTrader 48 | - name: ENABLE_PHOBOS 49 | value: "true" 50 | - name: STATSD_PORT 51 | value: "8125" 52 | - name: STATSD_URL 53 | value: "statsd-agent" 54 | - name: JAEGER_AGENT_HOST 55 | value: "jaeger-agent" 56 | - name: POD_NAME 57 | valueFrom: 58 | fieldRef: 59 | fieldPath: metadata.name 60 | - name: CLUSTER_IP 61 | value: "$(POD_NAME).traders" 62 | - name: CLUSTER_PORT 63 | value: "5110" 64 | - name: CLUSTER_SEEDS 65 | value: akka.tcp://$(ACTORSYSTEM)@lighthouse-0.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-1.lighthouse:4053,akka.tcp://$(ACTORSYSTEM)@lighthouse-2.lighthouse:4053 66 | livenessProbe: 67 | tcpSocket: 68 | port: 5110 69 | ports: 70 | - containerPort: 5110 71 | protocol: TCP 72 | - containerPort: 80 73 | protocol: TCP -------------------------------------------------------------------------------- /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 | 4 | $(NetCoreVersion) 5 | 6 | false 7 | 8 | Debug;Release;Phobos 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | if (config.HasPath("akka.cluster")) // so we don't barf on lesson1 (/start) 15 | { 16 | config.GetConfig("akka.cluster").HasPath("split-brain-resolver.active-strategy").Should().BeTrue(); 17 | } 18 | 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Infrastructure/Akka.CQRS.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(NetCoreVersion) 5 | Shared, non-domain-specific infrastructure used by various Akka.CQRS services. 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Infrastructure/MongoDbHoconHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Akka.Configuration; 3 | 4 | namespace Akka.CQRS.Infrastructure 5 | { 6 | /// 7 | /// Shared utility class for formatting MongoDb connection strings into the required 8 | /// Akka.Persistence.MongoDb HOCON . 9 | /// 10 | public static class MongoDbHoconHelper 11 | { 12 | public static Configuration.Config GetMongoHocon(string connectionStr) 13 | { 14 | var mongoHocon = @"akka.persistence.journal.mongodb.connection-string = """ + connectionStr + @""" 15 | akka.persistence.snapshot-store.mongodb.connection-string = """ + connectionStr + @""""; 16 | return mongoHocon; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 | #if PHOBOS 17 | public static Akka.Configuration.Config GetPhobosConfig(){ 18 | return ConfigurationFactory.FromResource("Akka.CQRS.Infrastructure.Ops.phobos.conf"); 19 | } 20 | #endif 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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/Ops/phobos.conf: -------------------------------------------------------------------------------- 1 | # Used only instances where PHOBOS_ENABLED is set to TRUE 2 | akka.actor.provider = "Phobos.Actor.Cluster.PhobosClusterActorRefProvider,Phobos.Actor.Cluster" 3 | 4 | phobos{ 5 | monitoring{ 6 | monitor-mailbox-depth = on 7 | } 8 | 9 | tracing{ 10 | provider-type = jaeger 11 | jaeger{ 12 | agent{ # for UDP reporting 13 | host = localhost 14 | port = 6831 15 | } 16 | } 17 | 18 | filter{ 19 | mode = whitelist 20 | message-types = [ 21 | "Akka.CQRS.IWithStockId, Akka.CQRS", 22 | "Akka.Persistence.Extras.IConfirmableMessage, Akka.Persistence.Extras" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | if (message is IConfirmableMessageEnvelope envelope) 34 | { 35 | return envelope.Message.StockId; 36 | } 37 | 38 | return null; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Infrastructure/TradeEventConsistentHashMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Akka.Routing; 5 | 6 | namespace Akka.CQRS.Infrastructure 7 | { 8 | /// 9 | /// Creates a 10 | /// 11 | public static class TradeEventConsistentHashMapping 12 | { 13 | public static readonly ConsistentHashMapping TradeEventMapping = msg => 14 | { 15 | if (msg is IWithStockId s) 16 | { 17 | return s.StockId; 18 | } 19 | 20 | return msg.ToString(); 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Matching.Tests/Akka.CQRS.Matching.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(NetCoreVersion) 5 | 6 | false 7 | 8 | Debug;Release;Phobos 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Matching/Akka.CQRS.Matching.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | $(NetStandardVersion) 6 | Matching engine logic. 7 | Debug;Release;Phobos 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Actors/Akka.CQRS.Pricing.Actors.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(NetStandardVersion) 5 | Pricing analytics actors. 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Actors/ClientHandlerActor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Akka.Actor; 5 | using Akka.CQRS.Pricing.Subscriptions; 6 | using Akka.CQRS.Pricing.Subscriptions.Client; 7 | using Akka.CQRS.Subscriptions; 8 | using Akka.Event; 9 | 10 | namespace Akka.CQRS.Pricing.Actors 11 | { 12 | /// 13 | /// Responsible for handling inbound requests from the 14 | /// actors running on the Web nodes. 15 | /// 16 | public sealed class ClientHandlerActor : ReceiveActor 17 | { 18 | private readonly ILoggingAdapter _log = Context.GetLogger(); 19 | private readonly IActorRef _priceRouter; 20 | 21 | public ClientHandlerActor(IActorRef priceRouter) 22 | { 23 | _priceRouter = priceRouter; 24 | 25 | Receive(s => 26 | { 27 | _log.Info("Received {0} from {1}", s, Sender); 28 | _priceRouter.Tell(new MarketSubscribe(s.StockId, MarketEventHelpers.AllMarketEventTypes, Sender)); 29 | }); 30 | 31 | Receive(a => 32 | { 33 | _log.Info("Received {0} from {1}", a, Sender); 34 | foreach (var s in AvailableTickerSymbols.Symbols) 35 | { 36 | _priceRouter.Tell(new MarketSubscribe(s, MarketEventHelpers.AllMarketEventTypes, Sender)); 37 | } 38 | }); 39 | 40 | Receive(s => 41 | { 42 | _log.Info("Received {0} from {1}", s, Sender); 43 | _priceRouter.Tell(new MarketUnsubscribe(s.StockId, MarketEventHelpers.AllMarketEventTypes, Sender)); 44 | }); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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 | 8 | namespace Akka.CQRS.Pricing.Actors 9 | { 10 | /// 11 | /// Intended to be a Cluster Singleton. Responsible for ensuring there's at least one instance 12 | /// of a for every single persistence id found inside the datastore. 13 | /// 14 | public sealed class PriceInitiatorActor : ReceiveActor 15 | { 16 | private readonly ILoggingAdapter _log = Context.GetLogger(); 17 | private readonly IActorRef _pricingQueryProxy; 18 | private readonly HashSet _tickers = new HashSet(); 19 | 20 | /* 21 | * Used to periodically ping Akka.Cluster.Sharding and ensure that all pricing 22 | * entities are up and producing events for their in-memory replicas over the network. 23 | * 24 | * Technically, akka.cluster.sharding.remember-entities = on should take care of this 25 | * for us in the initial pass, but the impact of having this code is virtually zero 26 | * and in the event of a network partition or an error somewhere, will effectively prod 27 | * the non-existent entity into action. Worth having it. 28 | */ 29 | private ICancelable _heartbeatInterval; 30 | 31 | 32 | private class Heartbeat 33 | { 34 | public static readonly Heartbeat Instance = new Heartbeat(); 35 | private Heartbeat() { } 36 | } 37 | 38 | public PriceInitiatorActor(IActorRef pricingQueryProxy) 39 | { 40 | _pricingQueryProxy = pricingQueryProxy; 41 | 42 | Receive(p => 43 | { 44 | _tickers.Add(p.StockId); 45 | _pricingQueryProxy.Tell(p); 46 | }); 47 | 48 | Receive(h => 49 | { 50 | foreach (var p in _tickers) 51 | { 52 | _pricingQueryProxy.Tell(new Ping(p)); 53 | } 54 | }); 55 | 56 | Receive(end => 57 | { 58 | _log.Warning("Received unexpected end of PersistenceIds stream. Restarting."); 59 | throw new ApplicationException("Restart me!"); 60 | }); 61 | } 62 | 63 | protected override void PreStart() 64 | { 65 | var self = Self; 66 | foreach (var t in AvailableTickerSymbols.Symbols) 67 | { 68 | self.Tell(new Ping(t)); 69 | } 70 | 71 | _heartbeatInterval = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(TimeSpan.FromSeconds(30), 72 | TimeSpan.FromSeconds(30), Self, Heartbeat.Instance, ActorRefs.NoSender); 73 | } 74 | 75 | protected override void PostStop() 76 | { 77 | _heartbeatInterval?.Cancel(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | 4 | $(NetStandardVersion) 5 | Petabridge.Cmd palettes for accessing the pricing system. 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 _matchAggregatorRouter; 15 | 16 | public PriceCommands(IActorRef matchAggregatorRouter) : base(PricingCommandPalette) 17 | { 18 | _matchAggregatorRouter = matchAggregatorRouter; 19 | HandlerProps = Props.Create(() => new PriceCmdRouter(_matchAggregatorRouter)); 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 Akka.Event; 14 | using Petabridge.Cmd; 15 | using Petabridge.Cmd.Host; 16 | 17 | namespace Akka.CQRS.Pricing.Cli 18 | { 19 | /// 20 | /// Actor responsible for carrying out commands. 21 | /// 22 | public sealed class PriceCmdRouter : CommandHandlerActor 23 | { 24 | private readonly ILoggingAdapter _log = Context.GetLogger(); 25 | private IActorRef _priceViewMaster; 26 | 27 | public PriceCmdRouter(IActorRef priceViewMaster) : base(PricingCmd.PricingCommandPalette) 28 | { 29 | _priceViewMaster = priceViewMaster; 30 | 31 | Process(PricingCmd.TrackPrice.Name, (command, arguments) => 32 | { 33 | var tickerSymbol = arguments.ArgumentValues("symbol").Single(); 34 | 35 | // the tracker actor will start automatically recording price information on its own. No further action needed. 36 | var trackerActor = 37 | Context.ActorOf(Props.Create(() => new PriceTrackingActor(tickerSymbol, _priceViewMaster, Sender))); 38 | }); 39 | 40 | Process(PricingCmd.PriceHistory.Name, (command, arguments) => 41 | { 42 | var tickerSymbol = arguments.ArgumentValues("symbol").Single(); 43 | var getPriceTask = _priceViewMaster.Ask(new FetchPriceAndVolume(tickerSymbol), TimeSpan.FromSeconds(5)); 44 | var sender = Sender; 45 | 46 | // pipe happy results back to the sender only on successful Ask 47 | getPriceTask.ContinueWith(tr => 48 | { 49 | try 50 | { 51 | if (tr.Result.PriceUpdates.Length == 0) 52 | return new[] 53 | {new CommandResponse($"No historical price data available for [{tr.Result.StockId}]")}; 54 | 55 | return Enumerable.Select(tr.Result.PriceUpdates, x => new CommandResponse(x.ToString(), false)) 56 | .Concat(new[] {CommandResponse.Empty}); 57 | } 58 | catch (Exception ex) 59 | { 60 | _log.Error(ex, "Exception while returning price history for {0}", tickerSymbol); 61 | return new[] {CommandResponse.Empty}; 62 | } 63 | 64 | }).ContinueWith(tr => 65 | { 66 | foreach(var r in tr.Result) 67 | sender.Tell(r, ActorRefs.NoSender); 68 | }); 69 | }); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /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 | 4 | Exe 5 | $(NetCoreVersion) 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | bin\Release\ 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 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 MONGO_CONNECTION_STR "" #MongoDb connection string for Akka.Persistence 9 | 10 | # 9110 - Petabridge.Cmd 11 | # 6055 - Akka.Cluster 12 | EXPOSE 9110 6055 13 | 14 | # Install Petabridge.Cmd client 15 | RUN dotnet tool install --global pbm 16 | 17 | COPY ./bin/Release/netcoreapp3.1/publish/ /app 18 | 19 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS app 20 | WORKDIR /app 21 | 22 | COPY --from=base /app /app 23 | 24 | # copy .NET Core global tool 25 | COPY --from=base /root/.dotnet /root/.dotnet/ 26 | 27 | # Needed because https://stackoverflow.com/questions/51977474/install-dotnet-core-tool-dockerfile 28 | ENV PATH="${PATH}:/root/.dotnet/tools" 29 | 30 | CMD ["dotnet", "Akka.CQRS.Pricing.Service.dll"] -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Service/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Akka.CQRS.Infrastructure; 4 | using Microsoft.AspNetCore; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Akka.CQRS.Pricing.Service 12 | { 13 | class Program 14 | { 15 | static async Task Main(string[] args) 16 | { 17 | var host = WebHost.CreateDefaultBuilder(args) 18 | .ConfigureServices((hostContext, services) => 19 | { 20 | services.AddLogging(); 21 | services.AddPhobosApm(); 22 | services.AddHostedService(); 23 | }) 24 | .ConfigureLogging((hostContext, configLogging) => 25 | { 26 | configLogging.AddConsole(); 27 | }) 28 | .Configure(app => 29 | { 30 | app.UseRouting(); 31 | 32 | // enable App.Metrics routes 33 | app.UseMetricsAllMiddleware(); 34 | app.UseMetricsAllEndpoints(); 35 | }) 36 | .Build(); 37 | 38 | await host.RunAsync(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Service/app.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | extensions = ["Akka.Cluster.Tools.Client.ClusterClientReceptionistExtensionProvider, Akka.Cluster.Tools"] 3 | actor { 4 | provider = cluster 5 | } 6 | 7 | remote { 8 | dot-netty.tcp { 9 | hostname = "127.0.0.1" 10 | port = 6055 11 | } 12 | } 13 | 14 | cluster { 15 | #will inject this node as a self-seed node at run-time 16 | seed-nodes = ["akka.tcp://AkkaPricing@127.0.0.1:6055"] 17 | roles = ["pricing-engine" , "trade-events"] 18 | 19 | pub-sub{ 20 | role = "trade-events" 21 | } 22 | 23 | sharding{ 24 | role = "pricing-engine" 25 | state-store-mode = ddata 26 | } 27 | 28 | client.receptionist.role = pricing-engine # stops ClusterClient gossip from going to the worker nodes 29 | 30 | price-singleton{ 31 | singleton-name = "price-initiator" 32 | role = "pricing-engine" 33 | hand-over-retry-interval = 1s 34 | min-number-of-hand-over-retries = 10 35 | } 36 | } 37 | 38 | persistence{ 39 | journal { 40 | plugin = "akka.persistence.journal.mongodb" 41 | mongodb.class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 42 | mongodb.collection = "EventJournal" 43 | mongodb.event-adapters = { 44 | stock-tagger = "Akka.CQRS.Infrastructure.StockEventTagger, Akka.CQRS.Infrastructure" 45 | } 46 | mongodb.event-adapter-bindings = { 47 | "Akka.CQRS.IWithStockId, Akka.CQRS" = stock-tagger 48 | } 49 | } 50 | 51 | snapshot-store { 52 | plugin = "akka.persistence.snapshot-store.mongodb" 53 | mongodb.class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 54 | mongodb.collection = "SnapshotStore" 55 | } 56 | 57 | query { 58 | mongodb { 59 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 60 | refresh-interval = 1s 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/Actor/ActorMarketEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Akka.Actor; 6 | 7 | namespace Akka.CQRS.Pricing.Subscriptions.Actor 8 | { 9 | /// 10 | /// Implements subscriptions without any of the DistributedPubSub built-in messaging types. 11 | /// 12 | public sealed class ActorMarketEventSubscriptionManager : MarketEventSubscriptionManagerBase 13 | { 14 | private readonly IActorRef _targetActor; 15 | 16 | public ActorMarketEventSubscriptionManager(IActorRef targetActor) 17 | { 18 | _targetActor = targetActor; 19 | } 20 | 21 | public override async Task Subscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 22 | { 23 | return await _targetActor.Ask(new MarketSubscribe(tickerSymbol, events, subscriber), 24 | TimeSpan.FromSeconds(3)); 25 | } 26 | 27 | public override async Task Unsubscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 28 | { 29 | return await _targetActor.Ask(new MarketUnsubscribe(tickerSymbol, events, 30 | subscriber), TimeSpan.FromSeconds(3)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/Akka.CQRS.Pricing.Subscriptions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | $(NetStandardVersion) 6 | Pub-sub abstractions for pricing events. 7 | Debug;Release;Phobos 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/Client/SubscribeClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS.Pricing.Subscriptions.Client 6 | { 7 | /// 8 | /// Sent via the , so it can't have any s 9 | /// contained inside it. Otherwise that'll result in additional Akka.Remote connections to the 10 | /// client being opened by the other members of the cluster. 11 | /// 12 | public sealed class SubscribeClient : IWithStockId 13 | { 14 | public SubscribeClient(string stockId) 15 | { 16 | StockId = stockId; 17 | } 18 | 19 | public string StockId { get; } 20 | } 21 | 22 | /// 23 | /// Subscribe client to ALL ticker symbols. 24 | /// 25 | public sealed class SubscribeClientAll 26 | { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/Client/UnsubscribeClient.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Pricing.Subscriptions.Client 2 | { 3 | /// 4 | /// Sent via the , so it can't have any s 5 | /// contained inside it. Otherwise that'll result in additional Akka.Remote connections to the 6 | /// client being opened by the other members of the cluster. 7 | /// 8 | public sealed class UnsubscribeClient : IWithStockId 9 | { 10 | public UnsubscribeClient(string stockId) 11 | { 12 | StockId = stockId; 13 | } 14 | 15 | public string StockId { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/DistributedPubSub/DistributedPubSubMarketEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Cluster.Tools.PublishSubscribe; 3 | using static Akka.CQRS.Pricing.Subscriptions.DistributedPubSub.DistributedPubSubPriceTopicFormatter; 4 | 5 | namespace Akka.CQRS.Pricing.Subscriptions.DistributedPubSub 6 | { 7 | /// 8 | /// used for distributing events over the . 9 | /// 10 | public sealed class DistributedPubSubMarketEventPublisher : IMarketEventPublisher 11 | { 12 | private readonly IActorRef _mediator; 13 | 14 | public DistributedPubSubMarketEventPublisher(IActorRef mediator) 15 | { 16 | _mediator = mediator; 17 | } 18 | 19 | public void Publish(string tickerSymbol, IMarketEvent @event) 20 | { 21 | var eventType = @event.ToMarketEventType(); 22 | var topic = ToTopic(tickerSymbol, eventType); 23 | _mediator.Tell(new Publish(topic, @event)); 24 | } 25 | 26 | public static DistributedPubSubMarketEventPublisher For(ActorSystem sys) 27 | { 28 | var mediator = Cluster.Tools.PublishSubscribe.DistributedPubSub.Get(sys).Mediator; 29 | return new DistributedPubSubMarketEventPublisher(mediator); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/DistributedPubSub/DistributedPubSubMarketEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Akka.Actor; 5 | using Akka.Cluster.Tools.PublishSubscribe; 6 | 7 | namespace Akka.CQRS.Pricing.Subscriptions.DistributedPubSub 8 | { 9 | /// 10 | /// that uses the under the hood. 11 | /// 12 | public sealed class DistributedPubSubMarketEventSubscriptionManager : MarketEventSubscriptionManagerBase 13 | { 14 | private readonly IActorRef _mediator; 15 | 16 | public DistributedPubSubMarketEventSubscriptionManager(IActorRef mediator) 17 | { 18 | _mediator = mediator; 19 | } 20 | 21 | public override async Task Subscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 22 | { 23 | var tasks = ToTopics(tickerSymbol, events).Select(x => 24 | _mediator.Ask(new Subscribe(x, subscriber), TimeSpan.FromSeconds(3))); 25 | 26 | await Task.WhenAll(tasks).ConfigureAwait(false); 27 | 28 | return new MarketSubscribeAck(tickerSymbol, events); 29 | } 30 | 31 | public override async Task Unsubscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 32 | { 33 | var tasks = ToTopics(tickerSymbol, events).Select(x => 34 | _mediator.Ask(new Unsubscribe(x, subscriber), TimeSpan.FromSeconds(3))); 35 | 36 | await Task.WhenAll(tasks).ConfigureAwait(false); 37 | 38 | return new MarketUnsubscribeAck(tickerSymbol, events); 39 | } 40 | 41 | internal static string[] ToTopics(string tickerSymbol, MarketEventType[] events) 42 | { 43 | return events.Select(x => DistributedPubSubPriceTopicFormatter.ToTopic(tickerSymbol, x)).ToArray(); 44 | } 45 | 46 | public static DistributedPubSubMarketEventSubscriptionManager For(ActorSystem system) 47 | { 48 | return new DistributedPubSubMarketEventSubscriptionManager(Cluster.Tools.PublishSubscribe.DistributedPubSub 49 | .Get(system).Mediator); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/DistributedPubSub/DistributedPubSubPriceTopicFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Akka.CQRS.Pricing.Subscriptions.DistributedPubSub 4 | { 5 | /// 6 | /// Helper methods for working with price and volume updates. 7 | /// 8 | public static class DistributedPubSubPriceTopicFormatter 9 | { 10 | public static string PriceUpdateTopic(string tickerSymbol) 11 | { 12 | return $"{tickerSymbol}-price"; 13 | } 14 | 15 | public static string VolumeUpdateTopic(string tickerSymbol) 16 | { 17 | return $"{tickerSymbol}-update"; 18 | } 19 | 20 | public static string ToTopic(string tickerSymbol, MarketEventType marketEventType) 21 | { 22 | string ToStr(MarketEventType e) 23 | { 24 | switch (e) 25 | { 26 | case MarketEventType.PriceChange: 27 | return "price"; 28 | case MarketEventType.VolumeChange: 29 | return "volume"; 30 | default: 31 | throw new ArgumentOutOfRangeException(nameof(e)); 32 | } 33 | } 34 | return $"{tickerSymbol}-{ToStr(marketEventType)}"; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/IMarketEventPublisher.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Pricing.Subscriptions 2 | { 3 | /// 4 | /// Abstraction for publishing data about instances. 5 | /// 6 | public interface IMarketEventPublisher 7 | { 8 | void Publish(string tickerSymbol, IMarketEvent @event); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/IMarketEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Threading.Tasks; 8 | using Akka.Actor; 9 | 10 | namespace Akka.CQRS.Pricing.Subscriptions 11 | { 12 | /// 13 | /// Abstraction used to manage subscriptions for s. 14 | /// 15 | public interface IMarketEventSubscriptionManager 16 | { 17 | Task Subscribe(string tickerSymbol, IActorRef subscriber); 18 | 19 | Task Subscribe(string tickerSymbol, MarketEventType @event, IActorRef subscriber); 20 | 21 | Task Subscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber); 22 | 23 | Task Unsubscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber); 24 | 25 | Task Unsubscribe(string tickerSymbol, MarketEventType @event, IActorRef subscriber); 26 | 27 | Task Unsubscribe(string tickerSymbol, IActorRef subscriber); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/InMem/InMemoryMarketEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Akka.Actor; 4 | 5 | namespace Akka.CQRS.Pricing.Subscriptions.InMem 6 | { 7 | /// 8 | /// In-memory subscription manager + publisher. 9 | /// 10 | public sealed class InMemoryMarketEventSubscriptionManager : MarketEventSubscriptionManagerBase, IMarketEventPublisher 11 | { 12 | private readonly Dictionary> _subscribers; 13 | 14 | public InMemoryMarketEventSubscriptionManager() 15 | : this(new Dictionary>()) { } 16 | 17 | public InMemoryMarketEventSubscriptionManager(Dictionary> subscribers) 18 | { 19 | _subscribers = subscribers; 20 | } 21 | 22 | public override Task Subscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 23 | { 24 | foreach (var e in events) 25 | { 26 | EnsureSub(e); 27 | _subscribers[e].Add(subscriber); 28 | } 29 | 30 | return Task.FromResult(new MarketSubscribeAck(tickerSymbol, events)); 31 | } 32 | 33 | public override Task Unsubscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 34 | { 35 | foreach (var e in events) 36 | { 37 | EnsureSub(e); 38 | _subscribers[e].Remove(subscriber); 39 | } 40 | 41 | return Task.FromResult(new MarketUnsubscribeAck(tickerSymbol, events)); 42 | } 43 | 44 | public void Publish(string tickerSymbol, IMarketEvent @event) 45 | { 46 | var eventType = @event.ToMarketEventType(); 47 | EnsureSub(eventType); 48 | 49 | foreach (var sub in _subscribers[eventType]) 50 | sub.Tell(@event); 51 | } 52 | 53 | private void EnsureSub(MarketEventType e) 54 | { 55 | if (!_subscribers.ContainsKey(e)) 56 | { 57 | _subscribers[e] = new HashSet(); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketEventHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Akka.CQRS.Pricing.Events; 4 | 5 | namespace Akka.CQRS.Pricing.Subscriptions 6 | { 7 | /// 8 | /// Extension methods for working with 9 | /// 10 | public static class MarketEventHelpers 11 | { 12 | public static readonly MarketEventType[] AllMarketEventTypes = 13 | Enum.GetValues(typeof(MarketEventType)).Cast().ToArray(); 14 | 15 | public static MarketEventType ToMarketEventType(this IMarketEvent @event) 16 | { 17 | switch (@event) 18 | { 19 | case IPriceUpdate p: 20 | return MarketEventType.PriceChange; 21 | case IVolumeUpdate v: 22 | return MarketEventType.VolumeChange; 23 | default: 24 | throw new ArgumentOutOfRangeException($"[{@event}] is not a supported market event type.", nameof(@event)); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketEventSubscriptionManagerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Akka.Actor; 3 | 4 | namespace Akka.CQRS.Pricing.Subscriptions 5 | { 6 | /// 7 | /// Abstract base class for implementations. 8 | /// 9 | public abstract class MarketEventSubscriptionManagerBase : IMarketEventSubscriptionManager 10 | { 11 | public async Task Subscribe(string tickerSymbol, IActorRef subscriber) 12 | { 13 | return await Subscribe(tickerSymbol, MarketEventHelpers.AllMarketEventTypes, subscriber); 14 | } 15 | 16 | public async Task Subscribe(string tickerSymbol, MarketEventType @event, IActorRef subscriber) 17 | { 18 | return await Subscribe(tickerSymbol, new[] { @event }, subscriber); 19 | } 20 | 21 | public abstract Task Subscribe(string tickerSymbol, MarketEventType[] events, 22 | IActorRef subscriber); 23 | 24 | public abstract Task Unsubscribe(string tickerSymbol, MarketEventType[] events, 25 | IActorRef subscriber); 26 | 27 | public async Task Unsubscribe(string tickerSymbol, MarketEventType @event, IActorRef subscriber) 28 | { 29 | return await Unsubscribe(tickerSymbol, new[] { @event }, subscriber); 30 | } 31 | 32 | public async Task Unsubscribe(string tickerSymbol, IActorRef subscriber) 33 | { 34 | return await Unsubscribe(tickerSymbol, MarketEventHelpers.AllMarketEventTypes, subscriber); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketEventType.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Pricing.Subscriptions 2 | { 3 | /// 4 | /// The type of market event we're interested in. 5 | /// 6 | public enum MarketEventType 7 | { 8 | VolumeChange, 9 | PriceChange, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketSubscribe.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | 3 | namespace Akka.CQRS.Pricing.Subscriptions 4 | { 5 | /// 6 | /// Subscribe to trade events for the specified ticker symbol. 7 | /// 8 | public sealed class MarketSubscribe : IWithStockId 9 | { 10 | public MarketSubscribe(string stockId, MarketEventType[] events, IActorRef subscriber) 11 | { 12 | StockId = stockId; 13 | Events = events; 14 | Subscriber = subscriber; 15 | } 16 | 17 | public string StockId { get; } 18 | 19 | public MarketEventType[] Events { get; } 20 | 21 | public IActorRef Subscriber { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketSubscribeAck.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Pricing.Subscriptions 2 | { 3 | /// 4 | /// Subscription to a specific ticker has been successful. 5 | /// 6 | public sealed class MarketSubscribeAck : IWithStockId 7 | { 8 | public MarketSubscribeAck(string stockId, MarketEventType[] events) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | } 13 | 14 | public string StockId { get; } 15 | 16 | public MarketEventType[] Events { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketSubscribeNack.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Pricing.Subscriptions 2 | { 3 | /// 4 | /// Subscription to a specific ticker was not successful. 5 | /// 6 | public sealed class MarketSubscribeNack : IWithStockId 7 | { 8 | public MarketSubscribeNack(string stockId, MarketEventType[] events, string reason) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | Reason = reason; 13 | } 14 | 15 | public string StockId { get; } 16 | 17 | public MarketEventType[] Events { get; } 18 | 19 | public string Reason { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketUnsubscribe.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | 3 | namespace Akka.CQRS.Pricing.Subscriptions 4 | { 5 | /// 6 | /// Unsubscribe to trade events for the specified ticker symbol. 7 | /// 8 | public sealed class MarketUnsubscribe : IWithStockId 9 | { 10 | public MarketUnsubscribe(string stockId, MarketEventType[] events, IActorRef subscriber) 11 | { 12 | StockId = stockId; 13 | Events = events; 14 | Subscriber = subscriber; 15 | } 16 | 17 | public string StockId { get; } 18 | 19 | public MarketEventType[] Events { get; } 20 | 21 | public IActorRef Subscriber { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketUnsubscribeAck.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Pricing.Subscriptions 2 | { 3 | /// 4 | /// Unsubscription to a specific ticker has been successful. 5 | /// 6 | public sealed class MarketUnsubscribeAck : IWithStockId 7 | { 8 | public MarketUnsubscribeAck(string stockId, MarketEventType[] events) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | } 13 | 14 | public string StockId { get; } 15 | 16 | public MarketEventType[] Events { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/MarketUnsubscribeNack.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Pricing.Subscriptions 2 | { 3 | /// 4 | /// Unsubscribe from a specific ticker was not successful. 5 | /// 6 | public sealed class MarketUnsubscribeNack : IWithStockId 7 | { 8 | public MarketUnsubscribeNack(string stockId, MarketEventType[] events, string reason) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | Reason = reason; 13 | } 14 | 15 | public string StockId { get; } 16 | 17 | public MarketEventType[] Events { get; } 18 | 19 | public string Reason { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Subscriptions/NoOp/NoOpMarketEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Akka.Actor; 3 | 4 | namespace Akka.CQRS.Pricing.Subscriptions.NoOp 5 | { 6 | /// 7 | /// No-op market event subscription manager. Does nothing. 8 | /// 9 | public sealed class NoOpMarketEventSubscriptionManager : MarketEventSubscriptionManagerBase 10 | { 11 | public static readonly NoOpMarketEventSubscriptionManager Instance = new NoOpMarketEventSubscriptionManager(); 12 | private NoOpMarketEventSubscriptionManager() { } 13 | 14 | public override Task Subscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 15 | { 16 | return Task.FromResult(new MarketSubscribeAck(tickerSymbol, events)); 17 | } 18 | 19 | public override Task Unsubscribe(string tickerSymbol, MarketEventType[] events, IActorRef subscriber) 20 | { 21 | return Task.FromResult(new MarketUnsubscribeAck(tickerSymbol, events)); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Actors/StockEventConfiguratorActor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Akka.Actor; 7 | using Akka.Cluster.Tools.Client; 8 | using Akka.CQRS.Pricing.Subscriptions.Client; 9 | using Akka.Event; 10 | 11 | namespace Akka.CQRS.Pricing.Web.Actors 12 | { 13 | /// 14 | /// Uses the to begin publishing events to . 15 | /// 16 | public class StockEventConfiguratorActor : ReceiveActor 17 | { 18 | private readonly ILoggingAdapter _log = Context.GetLogger(); 19 | private IActorRef _clusterClient; 20 | private readonly IActorRef _stockPublisher; 21 | private ImmutableHashSet _initialContacts; 22 | 23 | private sealed class Start 24 | { 25 | public static readonly Start Instance = new Start(); 26 | private Start() { } 27 | } 28 | 29 | public StockEventConfiguratorActor(IActorRef stockPublisher, IReadOnlyList
contactAddresses) 30 | { 31 | _initialContacts = contactAddresses.Select(x => new RootActorPath(x) / "system" / "receptionist").ToImmutableHashSet(); 32 | _stockPublisher = stockPublisher; 33 | 34 | Initializing(); 35 | } 36 | 37 | private void Initializing() 38 | { 39 | Receive(s => 40 | { 41 | _log.Info("Contacting cluster client on addresses [{0}]", string.Join(",", _initialContacts)); 42 | _clusterClient.Tell(new ClusterClient.Send("/user/subscriptions", new SubscribeClientAll())); 43 | }); 44 | 45 | Receive(t => { Self.Tell(Start.Instance); }); 46 | 47 | ReceiveAny(_ => 48 | { 49 | // connected via ClusterClient now 50 | _stockPublisher.Forward(_); 51 | }); 52 | } 53 | 54 | protected override void PreStart() 55 | { 56 | Context.SetReceiveTimeout(TimeSpan.FromSeconds(15)); 57 | Context.System.Scheduler.ScheduleTellOnce(TimeSpan.FromSeconds(2), Self, Start.Instance, ActorRefs.NoSender); 58 | _clusterClient = Context.ActorOf(Akka.Cluster.Tools.Client.ClusterClient.Props(ClusterClientSettings 59 | .Create(Context.System) 60 | .WithInitialContacts(_initialContacts))); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Actors/StockPublisherActor.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.CQRS.Pricing.Events; 7 | using Akka.CQRS.Pricing.Web.Hubs; 8 | using Akka.Event; 9 | 10 | namespace Akka.CQRS.Pricing.Web.Actors 11 | { 12 | /// 13 | /// Publishes events directly to the 14 | /// 15 | public class StockPublisherActor : ReceiveActor 16 | { 17 | private readonly ILoggingAdapter _log = Context.GetLogger(); 18 | private readonly StockHubHelper _hub; 19 | 20 | public StockPublisherActor(StockHubHelper hub) 21 | { 22 | _hub = hub; 23 | 24 | ReceiveAsync(async p => 25 | { 26 | try 27 | { 28 | _log.Info("Received event {0}", p); 29 | await hub.WritePriceChanged(p); 30 | } 31 | catch (Exception ex) 32 | { 33 | _log.Error(ex, "Error while writing price update [{0}] to StockHub", p); 34 | } 35 | }); 36 | 37 | ReceiveAsync(async p => 38 | { 39 | try 40 | { 41 | _log.Info("Received event {0}", p); 42 | await hub.WriteVolumeChanged(p); 43 | } 44 | catch (Exception ex) 45 | { 46 | _log.Error(ex, "Error while writing volume update [{0}] to StockHub", p); 47 | } 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Akka.CQRS.Pricing.Web.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(NetCoreVersion) 5 | Debug;Release;Phobos 6 | 7 | 8 | 9 | bin\Release\ 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Akka.CQRS.Pricing.Web.Models; 8 | 9 | namespace Akka.CQRS.Pricing.Web.Controllers 10 | { 11 | public class HomeController : Controller 12 | { 13 | public IActionResult Index() 14 | { 15 | ViewData["AppVersion"] = typeof(HomeController).Assembly.ImageRuntimeVersion; 16 | return View(); 17 | } 18 | 19 | public IActionResult About() 20 | { 21 | ViewData["Message"] = "Your application description page."; 22 | 23 | return View(); 24 | } 25 | 26 | public IActionResult Contact() 27 | { 28 | ViewData["Message"] = "Your contact page."; 29 | 30 | return View(); 31 | } 32 | 33 | public IActionResult Privacy() 34 | { 35 | return View(); 36 | } 37 | 38 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 39 | public IActionResult Error() 40 | { 41 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 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 "16666" 8 | 9 | #Akka.Remote inbound listening endpoint 10 | EXPOSE 80 11 | EXPOSE 16666 12 | 13 | # Install Petabridge.Cmd client 14 | RUN dotnet tool install --global pbm 15 | 16 | COPY ./bin/Release/netcoreapp3.1/publish/ /app 17 | 18 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS app 19 | WORKDIR /app 20 | 21 | COPY --from=base /app /app 22 | 23 | # copy .NET Core global tool 24 | COPY --from=base /root/.dotnet /root/.dotnet/ 25 | 26 | # Needed because https://stackoverflow.com/questions/51977474/install-dotnet-core-tool-dockerfile 27 | ENV PATH="${PATH}:/root/.dotnet/tools" 28 | 29 | # RUN pbm help 30 | 31 | CMD ["dotnet", "Akka.CQRS.Pricing.Web.dll"] 32 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Hubs/StockHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | 3 | namespace Akka.CQRS.Pricing.Web.Hubs 4 | { 5 | /// 6 | /// The SignalR hub used to stream real-time market data. 7 | /// 8 | public class StockHub : Hub 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Hubs/StockHubHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Akka.Actor; 7 | using Akka.CQRS.Pricing.Events; 8 | using Microsoft.AspNetCore.SignalR; 9 | 10 | namespace Akka.CQRS.Pricing.Web.Hubs 11 | { 12 | /// 13 | /// Used by actors to publish data directly to SignalR. 14 | /// 15 | public class StockHubHelper 16 | { 17 | private readonly IHubContext _hub; 18 | 19 | public StockHubHelper(IHubContext hub) 20 | { 21 | _hub = hub; 22 | } 23 | 24 | public async Task WriteVolumeChanged(IVolumeUpdate e) 25 | { 26 | await WriteMessage(e.ToString()); 27 | } 28 | 29 | public async Task WritePriceChanged(IPriceUpdate e) 30 | { 31 | await WriteMessage(e.ToString()); 32 | } 33 | 34 | internal async Task WriteMessage(string message) 35 | { 36 | var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); 37 | await _hub.Clients.All.SendAsync("writeEvent", message, cts.Token); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Akka.CQRS.Pricing.Web.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Akka.CQRS.Pricing.Web 12 | { 13 | public class Program 14 | { 15 | public static async Task Main(string[] args) 16 | { 17 | await CreateWebHostBuilder(args).Build().RunAsync(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Services/AkkaService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Akka.Actor; 7 | using Akka.Configuration; 8 | using Akka.CQRS.Infrastructure; 9 | using Akka.CQRS.Pricing.Web.Actors; 10 | using Akka.CQRS.Pricing.Web.Hubs; 11 | using Akka.DependencyInjection; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | using OpenTracing; 15 | using ServiceProvider = Akka.DependencyInjection.ServiceProvider; 16 | 17 | namespace Akka.CQRS.Pricing.Web.Services 18 | { 19 | /// 20 | /// Used to launch the and actors needed 21 | /// to communicate with the rest of the cluster. 22 | /// 23 | public sealed class AkkaService : IHostedService 24 | { 25 | private readonly IServiceProvider _provider; 26 | private readonly IHostApplicationLifetime _lifetime; 27 | private ActorSystem _actorSystem; 28 | 29 | public AkkaService(IServiceProvider provider, IHostApplicationLifetime lifetime) 30 | { 31 | _provider = provider; 32 | _lifetime = lifetime; 33 | } 34 | 35 | public Task StartAsync(CancellationToken cancellationToken) 36 | { 37 | var conf = ConfigurationFactory.ParseString(File.ReadAllText("app.conf")); 38 | 39 | _actorSystem = ActorSystem.Create("AkkaTrader", AppBootstrap.BootstrapAkka(_provider, 40 | new AppBootstrapConfig(false, false), conf)); 41 | 42 | var tracing = _provider.GetRequiredService(); 43 | 44 | var sp = ServiceProvider.For(_actorSystem); 45 | using(var createActorsSpan = tracing.BuildSpan("SpawnActors").StartActive()){ 46 | var stockPublisherActor = 47 | _actorSystem.ActorOf(sp.Props(), "stockPublisher"); 48 | 49 | var initialContactAddress = Environment.GetEnvironmentVariable("CLUSTER_SEEDS")?.Trim().Split(",") 50 | .Select(x => Address.Parse(x)).ToList(); 51 | 52 | if (initialContactAddress == null) 53 | { 54 | _actorSystem.Log.Error("No initial cluster contacts found. Please be sure that the CLUSTER_SEEDS environment variable is populated with at least one address."); 55 | return Task.FromException(new ConfigurationException( 56 | "No initial cluster contacts found. Please be sure that the CLUSTER_SEEDS environment variable is populated with at least one address.")); 57 | } 58 | 59 | var configurator = _actorSystem.ActorOf( 60 | Props.Create(() => new StockEventConfiguratorActor(stockPublisherActor, initialContactAddress)), 61 | "configurator"); 62 | } 63 | 64 | 65 | // need to guarantee that host shuts down if ActorSystem shuts down 66 | _actorSystem.WhenTerminated.ContinueWith(tr => 67 | { 68 | _lifetime.StopApplication(); 69 | }); 70 | 71 | return Task.CompletedTask; 72 | } 73 | 74 | public async Task StopAsync(CancellationToken cancellationToken) 75 | { 76 | await _actorSystem.Terminate(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Akka.CQRS.Infrastructure; 7 | using Akka.CQRS.Pricing.Web.Hubs; 8 | using Akka.CQRS.Pricing.Web.Services; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Hosting; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.Hosting; 16 | 17 | namespace Akka.CQRS.Pricing.Web 18 | { 19 | public class Startup 20 | { 21 | public Startup(IConfiguration configuration) 22 | { 23 | Configuration = configuration; 24 | } 25 | 26 | public IConfiguration Configuration { get; } 27 | 28 | // This method gets called by the runtime. Use this method to add services to the container. 29 | public void ConfigureServices(IServiceCollection services) 30 | { 31 | services.Configure(options => 32 | { 33 | options.MinimumSameSitePolicy = SameSiteMode.None; 34 | }); 35 | 36 | 37 | services.AddMvc(); 38 | services.AddSignalR(); 39 | services.AddPhobosApm(); 40 | services.AddTransient(); 41 | services.AddHostedService(); 42 | } 43 | 44 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 45 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 46 | { 47 | if (env.IsDevelopment()) 48 | { 49 | app.UseDeveloperExceptionPage(); 50 | } 51 | else 52 | { 53 | app.UseExceptionHandler("/Home/Error"); 54 | } 55 | 56 | app.UseRouting(); 57 | 58 | // enable App.Metrics routes 59 | app.UseMetricsAllMiddleware(); 60 | app.UseMetricsAllEndpoints(); 61 | 62 | app.UseStaticFiles(); 63 | app.UseCookiePolicy(); 64 | 65 | app.UseEndpoints(ep => 66 | { 67 | ep.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); 68 | ep.MapHub("/hubs/stockHub"); 69 | }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "About"; 3 | } 4 |

@ViewData["Title"]

5 |

@ViewData["Message"]

6 | 7 |

Use this area to provide additional information.

8 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Contact"; 3 | } 4 |

@ViewData["Title"]

5 |

@ViewData["Message"]

6 | 7 |
8 | One Microsoft Way
9 | Redmond, WA 98052-6399
10 | P: 11 | 425.555.0100 12 |
13 | 14 |
15 | Support: Support@example.com
16 | Marketing: Marketing@example.com 17 |
18 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/Home/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Privacy Policy"; 3 | } 4 |

@ViewData["Title"]

5 | 6 |

Use this page to detail your site's privacy policy.

7 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 22 |

23 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/Shared/_CookieConsentPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http.Features 2 | 3 | @{ 4 | var consentFeature = Context.Features.Get(); 5 | var showBanner = !consentFeature?.CanTrack ?? false; 6 | var cookieString = consentFeature?.CreateConsentCookie(); 7 | } 8 | 9 | @if (showBanner) 10 | { 11 | 33 | 41 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Akka.CQRS.Pricing.Web 2 | @using Akka.CQRS.Pricing.Web.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/app.conf: -------------------------------------------------------------------------------- 1 | akka.cluster.client { 2 | # Interval at which the client retries to establish contact with one of 3 | # ClusterReceptionist on the servers (cluster nodes) 4 | establishing-get-contacts-interval = 3s 5 | 6 | # Interval at which the client will ask the ClusterReceptionist for 7 | # new contact points to be used for next reconnect. 8 | refresh-contacts-interval = 60s 9 | 10 | # How often failure detection heartbeat messages should be sent 11 | heartbeat-interval = 2s 12 | 13 | acceptable-heartbeat-pause = 13s 14 | 15 | buffer-size = 1000 16 | 17 | reconnect-timeout = off 18 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification\ 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | body { 4 | padding-top: 50px; 5 | padding-bottom: 20px; 6 | } 7 | 8 | /* Wrapping element */ 9 | /* Set some basic padding to keep content from hitting the edges */ 10 | .body-content { 11 | padding-left: 15px; 12 | padding-right: 15px; 13 | } 14 | 15 | /* Carousel */ 16 | .carousel-caption p { 17 | font-size: 20px; 18 | line-height: 1.4; 19 | } 20 | 21 | /* Make .svg files in the carousel display properly in older browsers */ 22 | .carousel-inner .item img[src$=".svg"] { 23 | width: 100%; 24 | } 25 | 26 | /* QR code generator */ 27 | #qrCode { 28 | margin: 15px; 29 | } 30 | 31 | /* Hide/rearrange for smaller screens */ 32 | @media screen and (max-width: 767px) { 33 | /* Hide captions */ 34 | .carousel-caption { 35 | display: none; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/src/Akka.CQRS.Pricing.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/src/Akka.CQRS.Pricing.Web/wwwroot/js/site.min.js -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web" 13 | ], 14 | "homepage": "http://getbootstrap.com", 15 | "license": "MIT", 16 | "moduleType": "globals", 17 | "main": [ 18 | "less/bootstrap.less", 19 | "dist/js/bootstrap.js" 20 | ], 21 | "ignore": [ 22 | "/.*", 23 | "_config.yml", 24 | "CNAME", 25 | "composer.json", 26 | "CONTRIBUTING.md", 27 | "docs", 28 | "js/tests", 29 | "test-infra" 30 | ], 31 | "dependencies": { 32 | "jquery": "1.9.1 - 3" 33 | }, 34 | "version": "3.3.7", 35 | "_release": "3.3.7", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.3.7", 39 | "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" 40 | }, 41 | "_source": "https://github.com/twbs/bootstrap.git", 42 | "_target": "v3.3.7", 43 | "_originalSource": "bootstrap", 44 | "_direct": true 45 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/akkadotnet-cluster-workshop/8191b2d985f6a63dbadc12b070430d3804ca5d25/src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/bootstrap/dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/jquery-validation-unobtrusive/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation-unobtrusive", 3 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive", 4 | "version": "3.2.9", 5 | "_release": "3.2.9", 6 | "_resolution": { 7 | "type": "version", 8 | "tag": "v3.2.9", 9 | "commit": "a91f5401898e125f10771c5f5f0909d8c4c82396" 10 | }, 11 | "_source": "https://github.com/aspnet/jquery-validation-unobtrusive.git", 12 | "_target": "^3.2.9", 13 | "_originalSource": "jquery-validation-unobtrusive", 14 | "_direct": true 15 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/jquery-validation/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation", 3 | "homepage": "https://jqueryvalidation.org/", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/jquery-validation/jquery-validation.git" 7 | }, 8 | "authors": [ 9 | "Jörn Zaefferer " 10 | ], 11 | "description": "Form validation made easy", 12 | "main": "dist/jquery.validate.js", 13 | "keywords": [ 14 | "forms", 15 | "validation", 16 | "validate" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "demo", 25 | "lib" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.7.2" 29 | }, 30 | "version": "1.17.0", 31 | "_release": "1.17.0", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "1.17.0", 35 | "commit": "fc9b12d3bfaa2d0c04605855b896edb2934c0772" 36 | }, 37 | "_source": "https://github.com/jzaefferer/jquery-validation.git", 38 | "_target": "^1.17.0", 39 | "_originalSource": "jquery-validation", 40 | "_direct": true 41 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/jquery/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "main": "dist/jquery.js", 4 | "license": "MIT", 5 | "ignore": [ 6 | "package.json" 7 | ], 8 | "keywords": [ 9 | "jquery", 10 | "javascript", 11 | "browser", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/jquery/jquery-dist", 15 | "version": "3.3.1", 16 | "_release": "3.3.1", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "3.3.1", 20 | "commit": "9e8ec3d10fad04748176144f108d7355662ae75e" 21 | }, 22 | "_source": "https://github.com/jquery/jquery-dist.git", 23 | "_target": "^3.3.1", 24 | "_originalSource": "jquery", 25 | "_direct": true 26 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing.Web/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing/Akka.CQRS.Pricing.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(NetStandardVersion) 5 | Read-side pricing aggregates and views. 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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/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 : IComparable, IMarketEvent 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 : IComparable, IMarketEvent 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 | public override string ToString() 45 | { 46 | return $"[{StockId}][{Timestamp}] - [{CurrentVolume}] avg shares / trade"; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Pricing/IMarketEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS.Pricing 6 | { 7 | /// 8 | /// Marker interface for "market" events - i.e. changes in the market's view 9 | /// of price, volume, or other "aggregated" events not specific to any individual 10 | /// trade or order. 11 | /// 12 | public interface IMarketEvent : IWithStockId 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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(decimal avgPrice, double avgVolume, 14 | IReadOnlyList recentPriceUpdates, IReadOnlyList recentVolumeUpdates) 15 | { 16 | RecentAvgPrice = avgPrice; 17 | RecentAvgVolume = avgVolume; 18 | RecentPriceUpdates = recentPriceUpdates; 19 | RecentVolumeUpdates = recentVolumeUpdates; 20 | } 21 | 22 | /// 23 | /// The most recently saved average price. 24 | /// 25 | public decimal RecentAvgPrice { get; } 26 | 27 | /// 28 | /// The most recently saved average volume. 29 | /// 30 | public double RecentAvgVolume { get; } 31 | 32 | public IReadOnlyList RecentPriceUpdates { get; } 33 | 34 | public IReadOnlyList RecentVolumeUpdates { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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/Actor/ActorTradeSubscriptionManagerEnd2EndSpecs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Akka.Actor; 7 | using Akka.CQRS.Events; 8 | using Akka.CQRS.Subscriptions.Actor; 9 | using Akka.CQRS.TradeProcessor.Actors; 10 | using Akka.Persistence.Extras; 11 | using FluentAssertions; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | 15 | namespace Akka.CQRS.Subscriptions.Tests.Actor 16 | { 17 | public class ActorTradeSubscriptionManagerEnd2EndSpecs : TestKit.Xunit2.TestKit 18 | { 19 | public ActorTradeSubscriptionManagerEnd2EndSpecs(ITestOutputHelper output) 20 | : base(output: output) 21 | { 22 | _orderBookMaster = Sys.ActorOf(Props.Create(() => new OrderBookMasterActor()), "orders"); 23 | } 24 | 25 | private IActorRef _orderBookMaster; 26 | 27 | [Fact(DisplayName = 28 | "[ActorTradeSubscriptionManager] Should be able to subscribe and publish to trade event topics.")] 29 | public async Task ShouldSubscribeAndPublishToTradeEventTopics() 30 | { 31 | var subManager = new ActorTradeSubscriptionManager(_orderBookMaster); 32 | 33 | 34 | // Subscribe to all topics 35 | var subAck = await subManager.Subscribe("MSFT", TestActor); 36 | subAck.StockId.Should().Be("MSFT"); 37 | ExpectMsg(); // message should be sent back to us as well 38 | 39 | // create a matching trade, which should result in a Fill + Match being published. 40 | var time = DateTimeOffset.UtcNow; 41 | var bid = new ConfirmableMessage(new Bid("MSFT", "foo1", 10.0m, 1.0d, time), 100L, "fuber"); 42 | var ask = new ConfirmableMessage(new Ask("MSFT", "foo2", 10.0m, 1.0d, time), 101L, "fuber"); 43 | 44 | var confirmationProbe = CreateTestProbe(); 45 | 46 | _orderBookMaster.Tell(bid, confirmationProbe); 47 | _orderBookMaster.Tell(ask, confirmationProbe); 48 | 49 | confirmationProbe.ReceiveN(2).All(x => x is Confirmation).Should().BeTrue(); 50 | 51 | ExpectMsgAllOf(new Fill("foo1", "MSFT", 1.0d, 10.0m, "foo2", time), 52 | new Fill("foo2", "MSFT", 1.0d, 10.0m, "foo1", time), 53 | new Match("MSFT", "foo1", "foo2", 10.0m, 1.0d, time), bid.Message, ask.Message); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions.Tests/Akka.CQRS.Subscriptions.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(NetCoreVersion) 5 | Debug;Release;Phobos 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 = "[DistributedPubSubTradeEventSubscriptionManager] 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.StockId.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.DistributedPubSubTradeEventTopicFormatter; 5 | using Xunit; 6 | 7 | namespace Akka.CQRS.Subscriptions.Tests.DistributedPubSub 8 | { 9 | public class DistributedPubSubFormatterSpecs 10 | { 11 | public static IEnumerable GetTradeTopics() 12 | { 13 | yield return new object[] { "msft", TradeEventType.Ask, "msft-Ask" }; 14 | yield return new object[] { "msft", TradeEventType.Bid, "msft-Bid" }; 15 | yield return new object[] { "msft", TradeEventType.Match, "msft-Match" }; 16 | yield return new object[] { "msft", TradeEventType.Fill, "msft-Fill" }; 17 | } 18 | 19 | [Theory(DisplayName = "Should format name of ticker symbol + event in the format expected by DistributedPubSub")] 20 | [MemberData(nameof(GetTradeTopics))] 21 | public void ShouldFormatDistributedPubSubTopic(string ticker, TradeEventType tradeEvent, string expectedTopic) 22 | { 23 | ToTopic(ticker, tradeEvent).Should().Be(expectedTopic); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions.Tests/TradeEventExtensionsSpecs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Akka.CQRS.Events; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | namespace Akka.CQRS.Subscriptions.Tests 9 | { 10 | public class TradeEventExtensionsSpecs 11 | { 12 | public static IEnumerable GetTradeEvents() 13 | { 14 | yield return new object[] {new Ask("foo", "foo", 10.0m, 1.0d, DateTimeOffset.UtcNow), TradeEventType.Ask}; 15 | yield return new object[] { new Bid("foo", "foo", 10.0m, 1.0d, DateTimeOffset.UtcNow), TradeEventType.Bid }; 16 | yield return new object[] { new Fill("foo", "foo", 1.0d, 10.0m, "fuber", DateTimeOffset.UtcNow), TradeEventType.Fill }; 17 | yield return new object[] { new Match("foo", "bar", "fuber", 10.0m, 1.0d, DateTimeOffset.UtcNow), TradeEventType.Match }; 18 | 19 | } 20 | 21 | [Theory(DisplayName = "Should detect correct TradeEventType for ITradeEvent")] 22 | [MemberData(nameof(GetTradeEvents))] 23 | public void ShouldMatchEventWithTradeType(ITradeEvent tradeEvent, TradeEventType expectedType) 24 | { 25 | tradeEvent.ToTradeEventType().Should().Be(expectedType); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/Actor/ActorTradeSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Akka.Actor; 4 | 5 | namespace Akka.CQRS.Subscriptions.Actor 6 | { 7 | /// 8 | /// Implements subscriptions without any of the DistributedPubSub built-in messaging types. 9 | /// 10 | public sealed class ActorTradeSubscriptionManager : TradeEventSubscriptionManagerBase 11 | { 12 | private readonly IActorRef _targetActor; 13 | 14 | public ActorTradeSubscriptionManager(IActorRef targetActor) 15 | { 16 | _targetActor = targetActor; 17 | } 18 | 19 | public override async Task Subscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 20 | { 21 | return await _targetActor.Ask(new TradeSubscribe(tickerSymbol, events, subscriber), TimeSpan.FromSeconds(3)); 22 | } 23 | 24 | public override async Task Unsubscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 25 | { 26 | return await _targetActor.Ask(new TradeUnsubscribe(tickerSymbol, events, subscriber), TimeSpan.FromSeconds(3)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/Akka.CQRS.Subscriptions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | $(NetStandardVersion) 6 | Pub-sub abstractions for trade events. 7 | Debug;Release;Phobos 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/DistributedPubSub/DistributedPubSubTradeEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using System.Xml; 4 | using Akka.Actor; 5 | using Akka.Cluster.Tools.PublishSubscribe; 6 | using Akka.Util; 7 | using static Akka.CQRS.Subscriptions.DistributedPubSub.DistributedPubSubTradeEventTopicFormatter; 8 | 9 | namespace Akka.CQRS.Subscriptions.DistributedPubSub 10 | { 11 | /// 12 | /// used for distributing events over the . 13 | /// 14 | public sealed class DistributedPubSubTradeEventPublisher : ITradeEventPublisher 15 | { 16 | private readonly IActorRef _mediator; 17 | 18 | public DistributedPubSubTradeEventPublisher(IActorRef mediator) 19 | { 20 | _mediator = mediator; 21 | } 22 | 23 | public void Publish(string tickerSymbol, ITradeEvent @event) 24 | { 25 | var eventType = @event.ToTradeEventType(); 26 | var topic = ToTopic(tickerSymbol, eventType); 27 | _mediator.Tell(new Publish(topic, @event)); 28 | } 29 | 30 | public static DistributedPubSubTradeEventPublisher For(ActorSystem sys) 31 | { 32 | var mediator = Cluster.Tools.PublishSubscribe.DistributedPubSub.Get(sys).Mediator; 33 | return new DistributedPubSubTradeEventPublisher(mediator); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/DistributedPubSub/DistributedPubSubTradeEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Akka.Actor; 5 | using Akka.Cluster.Tools.PublishSubscribe; 6 | 7 | namespace Akka.CQRS.Subscriptions.DistributedPubSub 8 | { 9 | /// 10 | /// that uses the under the hood. 11 | /// 12 | public sealed class DistributedPubSubTradeEventSubscriptionManager : TradeEventSubscriptionManagerBase 13 | { 14 | private readonly IActorRef _mediator; 15 | 16 | public DistributedPubSubTradeEventSubscriptionManager(IActorRef mediator) 17 | { 18 | _mediator = mediator; 19 | } 20 | 21 | public override async Task Subscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 22 | { 23 | var tasks = ToTopics(tickerSymbol, events).Select(x => 24 | _mediator.Ask(new Subscribe(x, subscriber), TimeSpan.FromSeconds(3))); 25 | 26 | await Task.WhenAll(tasks).ConfigureAwait(false); 27 | 28 | return new TradeSubscribeAck(tickerSymbol, events); 29 | } 30 | 31 | public override async Task Unsubscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 32 | { 33 | var tasks = ToTopics(tickerSymbol, events).Select(x => 34 | _mediator.Ask(new Unsubscribe(x, subscriber), TimeSpan.FromSeconds(3))); 35 | 36 | await Task.WhenAll(tasks).ConfigureAwait(false); 37 | 38 | return new TradeUnsubscribeAck(tickerSymbol, events); 39 | } 40 | 41 | 42 | public static DistributedPubSubTradeEventSubscriptionManager For(ActorSystem sys) 43 | { 44 | var mediator = Cluster.Tools.PublishSubscribe.DistributedPubSub.Get(sys).Mediator; 45 | return new DistributedPubSubTradeEventSubscriptionManager(mediator); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/DistributedPubSub/DistributedPubSubTradeEventTopicFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Akka.Cluster.Tools.PublishSubscribe; 3 | 4 | namespace Akka.CQRS.Subscriptions.DistributedPubSub 5 | { 6 | /// 7 | /// Formats messages into -friendly topic names. 8 | /// 9 | public static class DistributedPubSubTradeEventTopicFormatter 10 | { 11 | public static string ToTopic(string tickerSymbol, TradeEventType tradeEventType) 12 | { 13 | string ToStr(TradeEventType e) 14 | { 15 | switch (e) 16 | { 17 | case TradeEventType.Ask: 18 | return "Ask"; 19 | case TradeEventType.Bid: 20 | return "Bid"; 21 | case TradeEventType.Fill: 22 | return "Fill"; 23 | case TradeEventType.Match: 24 | return "Match"; 25 | default: 26 | throw new ArgumentOutOfRangeException(nameof(e)); 27 | } 28 | } 29 | return $"{tickerSymbol}-{ToStr(tradeEventType)}"; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/ITradeEventPublisher.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace Akka.CQRS.Subscriptions 8 | { 9 | /// 10 | /// Abstraction used for publishing data about instances. 11 | /// 12 | public interface ITradeEventPublisher 13 | { 14 | void Publish(string tickerSymbol, ITradeEvent @event); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/ITradeEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Threading.Tasks; 8 | using Akka.Actor; 9 | 10 | namespace Akka.CQRS.Subscriptions 11 | { 12 | /// 13 | /// Abstraction used to manage subscriptions for s. 14 | /// 15 | public interface ITradeEventSubscriptionManager 16 | { 17 | Task Subscribe(string tickerSymbol, IActorRef subscriber); 18 | 19 | Task Subscribe(string tickerSymbol, TradeEventType @event, IActorRef subscriber); 20 | 21 | Task Subscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber); 22 | 23 | Task Unsubscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber); 24 | 25 | Task Unsubscribe(string tickerSymbol, TradeEventType @event, IActorRef subscriber); 26 | 27 | Task Unsubscribe(string tickerSymbol, IActorRef subscriber); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/InMem/InMemoryTradeEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Akka.Actor; 4 | 5 | namespace Akka.CQRS.Subscriptions.InMem 6 | { 7 | /// 8 | /// Used locally, in-memory by a single order book actor. Belongs to a single ticker symbol. 9 | /// 10 | public sealed class InMemoryTradeEventPublisher : TradeEventSubscriptionManagerBase, ITradeEventPublisher 11 | { 12 | private readonly Dictionary> _subscribers; 13 | 14 | public InMemoryTradeEventPublisher() : this(new Dictionary>()) { } 15 | 16 | public InMemoryTradeEventPublisher(Dictionary> subscribers) 17 | { 18 | _subscribers = subscribers; 19 | } 20 | 21 | public void Publish(string tickerSymbol, ITradeEvent @event) 22 | { 23 | var eventType = @event.ToTradeEventType(); 24 | EnsureSub(eventType); 25 | 26 | foreach(var sub in _subscribers[eventType]) 27 | sub.Tell(@event); 28 | } 29 | 30 | public override Task Subscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 31 | { 32 | foreach (var e in events) 33 | { 34 | EnsureSub(e); 35 | _subscribers[e].Add(subscriber); 36 | } 37 | 38 | return Task.FromResult(new TradeSubscribeAck(tickerSymbol, events)); 39 | } 40 | 41 | private void EnsureSub(TradeEventType e) 42 | { 43 | if (!_subscribers.ContainsKey(e)) 44 | { 45 | _subscribers[e] = new HashSet(); 46 | } 47 | } 48 | 49 | public override Task Unsubscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 50 | { 51 | foreach (var e in events) 52 | { 53 | EnsureSub(e); 54 | _subscribers[e].Remove(subscriber); 55 | } 56 | 57 | return Task.FromResult(new TradeUnsubscribeAck(tickerSymbol, events)); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/NoOp/NoOpTradeEventSubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Akka.Actor; 6 | 7 | namespace Akka.CQRS.Subscriptions.NoOp 8 | { 9 | /// 10 | /// Used to ignore subscription management events. 11 | /// 12 | public sealed class NoOpTradeEventSubscriptionManager : TradeEventSubscriptionManagerBase 13 | { 14 | public static readonly NoOpTradeEventSubscriptionManager Instance = new NoOpTradeEventSubscriptionManager(); 15 | private NoOpTradeEventSubscriptionManager() { } 16 | 17 | public override Task Subscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 18 | { 19 | return Task.FromResult(new TradeSubscribeAck(tickerSymbol, TradeEventHelpers.AllTradeEventTypes)); 20 | } 21 | 22 | public override Task Unsubscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber) 23 | { 24 | return Task.FromResult(new TradeUnsubscribeAck(tickerSymbol, events)); 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeEventHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Akka.CQRS.Events; 4 | 5 | namespace Akka.CQRS.Subscriptions 6 | { 7 | /// 8 | /// Extension methods for working with 9 | /// 10 | public static class TradeEventHelpers 11 | { 12 | public static readonly TradeEventType[] AllTradeEventTypes = 13 | Enum.GetValues(typeof(TradeEventType)).Cast().ToArray(); 14 | 15 | public static TradeEventType ToTradeEventType(this ITradeEvent @event) 16 | { 17 | switch (@event) 18 | { 19 | case Bid b: 20 | return TradeEventType.Bid; 21 | case Ask a: 22 | return TradeEventType.Ask; 23 | case Fill f: 24 | return TradeEventType.Fill; 25 | case Match m: 26 | return TradeEventType.Match; 27 | default: 28 | throw new ArgumentOutOfRangeException($"[{@event}] is not a supported trade event type.", nameof(@event)); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeEventSubscriptionManagerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Akka.Actor; 4 | using Akka.CQRS.Subscriptions.DistributedPubSub; 5 | 6 | namespace Akka.CQRS.Subscriptions 7 | { 8 | /// 9 | /// 10 | /// Abstract base class for working with . 11 | /// 12 | public abstract class TradeEventSubscriptionManagerBase : ITradeEventSubscriptionManager 13 | { 14 | public Task Subscribe(string tickerSymbol, IActorRef subscriber) 15 | { 16 | return Subscribe(tickerSymbol, TradeEventHelpers.AllTradeEventTypes, subscriber); 17 | } 18 | 19 | public Task Subscribe(string tickerSymbol, TradeEventType @event, IActorRef subscriber) 20 | { 21 | return Subscribe(tickerSymbol, new[] { @event }, subscriber); 22 | } 23 | 24 | public abstract Task Subscribe(string tickerSymbol, TradeEventType[] events, IActorRef subscriber); 25 | 26 | public abstract Task Unsubscribe(string tickerSymbol, TradeEventType[] events, 27 | IActorRef subscriber); 28 | 29 | public Task Unsubscribe(string tickerSymbol, TradeEventType @event, IActorRef subscriber) 30 | { 31 | return Unsubscribe(tickerSymbol, new[] { @event }, subscriber); 32 | } 33 | 34 | public Task Unsubscribe(string tickerSymbol, IActorRef subscriber) 35 | { 36 | return Unsubscribe(tickerSymbol, TradeEventHelpers.AllTradeEventTypes, subscriber); 37 | } 38 | 39 | internal static string[] ToTopics(string tickerSymbol, TradeEventType[] events) 40 | { 41 | return events.Select(x => DistributedPubSubTradeEventTopicFormatter.ToTopic(tickerSymbol, x)).ToArray(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeEventType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Akka.CQRS.Subscriptions 4 | { 5 | /// 6 | /// The type of trade event we're interested in. 7 | /// 8 | public enum TradeEventType 9 | { 10 | Bid, 11 | Ask, 12 | Fill, 13 | Match 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeSubscribe.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | 3 | namespace Akka.CQRS.Subscriptions 4 | { 5 | /// 6 | /// Subscribe to trade events for the specified ticker symbol. 7 | /// 8 | public sealed class TradeSubscribe : IWithStockId 9 | { 10 | public TradeSubscribe(string stockId, TradeEventType[] events, IActorRef subscriber) 11 | { 12 | StockId = stockId; 13 | Events = events; 14 | Subscriber = subscriber; 15 | } 16 | 17 | public string StockId { get; } 18 | 19 | public TradeEventType[] Events { get; } 20 | 21 | public IActorRef Subscriber { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeSubscribeAck.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Subscriptions 2 | { 3 | /// 4 | /// Subscription to a specific ticker has been successful. 5 | /// 6 | public sealed class TradeSubscribeAck : IWithStockId 7 | { 8 | public TradeSubscribeAck(string stockId, TradeEventType[] events) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | } 13 | 14 | public string StockId { get; } 15 | 16 | public TradeEventType[] Events { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeSubscribeNack.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Subscriptions 2 | { 3 | /// 4 | /// Subscription to a specific ticker was not successful. 5 | /// 6 | public sealed class TradeSubscribeNack : IWithStockId 7 | { 8 | public TradeSubscribeNack(string stockId, TradeEventType[] events, string reason) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | Reason = reason; 13 | } 14 | 15 | public string StockId { get; } 16 | 17 | public TradeEventType[] Events { get; } 18 | 19 | public string Reason { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeUnsubscribe.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | 3 | namespace Akka.CQRS.Subscriptions 4 | { 5 | /// 6 | /// Unsubscribe to trade events for the specified ticker symbol. 7 | /// 8 | public sealed class TradeUnsubscribe : IWithStockId 9 | { 10 | public TradeUnsubscribe(string stockId, TradeEventType[] events, IActorRef subscriber) 11 | { 12 | StockId = stockId; 13 | Events = events; 14 | Subscriber = subscriber; 15 | } 16 | 17 | public string StockId { get; } 18 | 19 | public TradeEventType[] Events { get; } 20 | 21 | public IActorRef Subscriber { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeUnsubscribeAck.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Subscriptions 2 | { 3 | /// 4 | /// Unsubscription to a specific ticker has been successful. 5 | /// 6 | public sealed class TradeUnsubscribeAck : IWithStockId 7 | { 8 | public TradeUnsubscribeAck(string stockId, TradeEventType[] events) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | } 13 | 14 | public string StockId { get; } 15 | 16 | public TradeEventType[] Events { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Subscriptions/TradeUnsubscribeNack.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS.Subscriptions 2 | { 3 | /// 4 | /// Unsubscribe from a specific ticker was not successful. 5 | /// 6 | public sealed class TradeUnsubscribeNack : IWithStockId 7 | { 8 | public TradeUnsubscribeNack(string stockId, TradeEventType[] events, string reason) 9 | { 10 | StockId = stockId; 11 | Events = events; 12 | Reason = reason; 13 | } 14 | 15 | public string StockId { get; } 16 | 17 | public TradeEventType[] Events { get; } 18 | 19 | public string Reason { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.Tests/Akka.CQRS.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | $(NetCoreVersion) 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Tests/Serialization/TradeEventSerializerSpecs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Text; 5 | using Akka.CQRS.Commands; 6 | using Akka.CQRS.Events; 7 | using Akka.CQRS.Serialization; 8 | using Akka.CQRS.Util; 9 | using FluentAssertions; 10 | using Xunit; 11 | using Xunit.Abstractions; 12 | 13 | namespace Akka.CQRS.Tests.Serialization 14 | { 15 | public class TradeEventSerializerSpecs : TestKit.Xunit2.TestKit 16 | { 17 | public TradeEventSerializerSpecs(ITestOutputHelper output) 18 | : base(TradeEventSerializer.Config, output: output) { } 19 | 20 | 21 | 22 | public static IEnumerable GetSerializableObjects() 23 | { 24 | yield return new Match("fuber", "id", "id2", 10.0m, 10.0d, CurrentUtcTimestamper.Instance.Now).ToObjectArray(); 25 | yield return new Fill("fuber2", "fub", 10.0d, 11.0001m, "fuber3", CurrentUtcTimestamper.Instance.Now).ToObjectArray(); 26 | yield return new Fill("fuber2", "fub", 10.0d, 11.0001m, "fuber3", CurrentUtcTimestamper.Instance.Now, true).ToObjectArray(); 27 | yield return new Ask("fuber1", "fub", 10.11m, 5.0d, CurrentUtcTimestamper.Instance.Now).ToObjectArray(); 28 | yield return new Bid("fuber1", "fub", 10.11m, 5.0d, CurrentUtcTimestamper.Instance.Now).ToObjectArray(); 29 | yield return new Order("fub", "stock1", TradeSide.Buy, 10.0d, 11.11m, CurrentUtcTimestamper.Instance.Now).ToObjectArray(); 30 | yield return new Order("fub", "stock1", TradeSide.Sell, 10.0d, 11.11m, CurrentUtcTimestamper.Instance.Now).ToObjectArray(); 31 | yield return new Order("fub", "stock1", TradeSide.Sell, 10.0d, 11.11m, CurrentUtcTimestamper.Instance.Now, ImmutableArray.Empty.Add(new Fill("fuber2", "fub", 10.0d, 11.0001m, "fuber3", CurrentUtcTimestamper.Instance.Now))).ToObjectArray(); 32 | } 33 | 34 | [Theory(DisplayName = "All ITradeEvents should be serializable via the TradeEventSerializer")] 35 | [InlineData(typeof(Match), 517)] 36 | [InlineData(typeof(Order), 517)] 37 | [InlineData(typeof(Fill), 517)] 38 | [InlineData(typeof(OrderbookSnapshot), 517)] 39 | [InlineData(typeof(Bid), 517)] 40 | [InlineData(typeof(Ask), 517)] 41 | [InlineData(typeof(GetRecentMatches), 517)] 42 | [InlineData(typeof(GetOrderBookSnapshot), 517)] 43 | public void ShouldUseTradeEventSerializer(Type type, int expectedSerializerId) 44 | { 45 | Sys.Serialization.FindSerializerForType(type).Identifier.Should().Be(expectedSerializerId); 46 | } 47 | 48 | [Theory] 49 | [MemberData(nameof(GetSerializableObjects))] 50 | public void ShouldSerializeTradeEvent(ITradeEvent trade) 51 | { 52 | VerifySerialization(trade); 53 | } 54 | 55 | private void VerifySerialization(ITradeEvent trade) 56 | { 57 | var serializer = Sys.Serialization.FindSerializerFor(trade).As(); 58 | var serialized = serializer.ToBinary(trade); 59 | var manifest = serializer.Manifest(trade); 60 | var deserialized = serializer.FromBinary(serialized, manifest); 61 | 62 | deserialized.Should().Be(trade); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Akka.CQRS.Tests/Serialization/XunitMemberDataHelper.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Akka.CQRS.Tests.Serialization 4 | { 5 | /// 6 | /// Helper class for writing test cases. 7 | /// 8 | public static class XunitMemberDataHelper 9 | { 10 | public static object[] ToObjectArray(this object obj) 11 | { 12 | return new object[] { obj }; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.TradePlacers.Service/Akka.CQRS.TradePlacers.Service.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | $(NetCoreVersion) 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | bin\Release\ 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Akka.CQRS.TradePlacers.Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 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 "5054" 8 | 9 | # 9110 - Petabridge.Cmd 10 | # 5055 - Akka.Cluster 11 | EXPOSE 9110 5054 12 | 13 | # Install Petabridge.Cmd client 14 | RUN dotnet tool install --global pbm 15 | 16 | COPY ./bin/Release/netcoreapp3.1/publish/ /app 17 | 18 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS app 19 | WORKDIR /app 20 | 21 | COPY --from=base /app /app 22 | 23 | # copy .NET Core global tool 24 | COPY --from=base /root/.dotnet /root/.dotnet/ 25 | 26 | # Needed because https://stackoverflow.com/questions/51977474/install-dotnet-core-tool-dockerfile 27 | ENV PATH="${PATH}:/root/.dotnet/tools" 28 | 29 | # RUN pbm help 30 | 31 | CMD ["dotnet", "Akka.CQRS.TradePlacers.Service.dll"] -------------------------------------------------------------------------------- /src/Akka.CQRS.TradePlacers.Service/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Akka.Bootstrap.Docker; 3 | using Akka.Cluster.Tools.PublishSubscribe; 4 | using Akka.CQRS.Infrastructure; 5 | using Akka.CQRS.Infrastructure.Ops; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Akka.CQRS.TradePlacers.Service 14 | { 15 | class Program 16 | { 17 | static async Task Main(string[] args) 18 | { 19 | var host = WebHost.CreateDefaultBuilder(args) 20 | .ConfigureServices((hostContext, services) => 21 | { 22 | services.AddLogging(); 23 | services.AddPhobosApm(); 24 | services.AddHostedService(); 25 | }) 26 | .ConfigureLogging((hostContext, configLogging) => 27 | { 28 | configLogging.AddConsole(); 29 | }) 30 | .Configure(app => 31 | { 32 | app.UseRouting(); 33 | 34 | // enable App.Metrics routes 35 | app.UseMetricsAllMiddleware(); 36 | app.UseMetricsAllEndpoints(); 37 | }) 38 | .Build(); 39 | 40 | await host.RunAsync(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Akka.CQRS.TradePlacers.Service/app.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | provider = cluster 4 | 5 | deployment{ 6 | /tradesRouter { 7 | router = consistent-hashing-group 8 | routees.paths = ["/user/orderbooks"] 9 | cluster { 10 | enabled = on 11 | use-role = trade-processor 12 | } 13 | } 14 | } 15 | } 16 | 17 | remote { 18 | dot-netty.tcp { 19 | hostname = "127.0.0.1" 20 | port = 5054 21 | } 22 | } 23 | 24 | cluster { 25 | #will inject this node as a self-seed node at run-time 26 | seed-nodes = ["akka.tcp://AkkaTrader@127.0.0.1:5055"] 27 | roles = ["trader", "trade-events"] 28 | 29 | pub-sub{ 30 | role = "trade-events" 31 | } 32 | 33 | sharding{ 34 | role = "trade-processor" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.TradeProcessor.Actors/Akka.CQRS.TradeProcessor.Actors.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | $(NetStandardVersion) 6 | Matching engine actors. 7 | Debug;Release;Phobos 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Akka.CQRS.TradeProcessor.Actors/OrderBookMasterActor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.CQRS.Subscriptions.InMem; 3 | using Akka.Persistence.Extras; 4 | 5 | namespace Akka.CQRS.TradeProcessor.Actors 6 | { 7 | /// 8 | /// Child-per entity parent for order books. 9 | /// 10 | public sealed class OrderBookMasterActor : ReceiveActor 11 | { 12 | public OrderBookMasterActor() 13 | { 14 | Receive(s => 15 | { 16 | var orderbookActor = Context.Child(s.StockId).GetOrElse(() => StartChild(s.StockId)); 17 | orderbookActor.Forward(s); 18 | }); 19 | 20 | Receive>(s => 21 | { 22 | var orderbookActor = Context.Child(s.Message.StockId).GetOrElse(() => StartChild(s.Message.StockId)); 23 | orderbookActor.Forward(s); 24 | }); 25 | } 26 | 27 | private IActorRef StartChild(string stockTickerSymbol) 28 | { 29 | var pub = new InMemoryTradeEventPublisher(); 30 | return Context.ActorOf(Props.Create(() => new OrderBookActor(stockTickerSymbol, pub)), stockTickerSymbol); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Akka.CQRS.TradeProcessor.Service/Akka.CQRS.TradeProcessor.Service.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | $(NetCoreVersion) 6 | Debug;Release;Phobos 7 | 8 | 9 | 10 | bin\Release\ 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Akka.CQRS.TradeProcessor.Service/AkkaService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Akka.Actor; 6 | using Akka.Cluster.Sharding; 7 | using Akka.Configuration; 8 | using Akka.CQRS.Infrastructure; 9 | using Akka.CQRS.TradeProcessor.Actors; 10 | using Microsoft.Extensions.Hosting; 11 | using Petabridge.Cmd.Cluster; 12 | using Petabridge.Cmd.Cluster.Sharding; 13 | using Petabridge.Cmd.Host; 14 | using Petabridge.Cmd.Remote; 15 | 16 | namespace Akka.CQRS.TradeProcessor.Service 17 | { 18 | public sealed class AkkaService : IHostedService 19 | { 20 | private readonly IServiceProvider _provider; 21 | private readonly IHostApplicationLifetime _lifetime; 22 | private ActorSystem _actorSystem; 23 | 24 | public AkkaService(IServiceProvider provider, IHostApplicationLifetime lifetime) 25 | { 26 | _provider = provider; 27 | _lifetime = lifetime; 28 | } 29 | 30 | public Task StartAsync(CancellationToken cancellationToken) 31 | { 32 | var config = File.ReadAllText("app.conf"); 33 | var conf = ConfigurationFactory.ParseString(config); 34 | 35 | _actorSystem = ActorSystem.Create("AkkaTrader", AppBootstrap.BootstrapAkka(_provider, 36 | new AppBootstrapConfig(true, true), conf)); 37 | 38 | Cluster.Cluster.Get(_actorSystem).RegisterOnMemberUp(() => 39 | { 40 | var sharding = ClusterSharding.Get(_actorSystem); 41 | 42 | var shardRegion = sharding.Start("orderBook", s => OrderBookActor.PropsFor(s), ClusterShardingSettings.Create(_actorSystem), 43 | new StockShardMsgRouter()); 44 | }); 45 | 46 | // need to guarantee that host shuts down if ActorSystem shuts down 47 | _actorSystem.WhenTerminated.ContinueWith(tr => 48 | { 49 | _lifetime.StopApplication(); 50 | }); 51 | 52 | // start Petabridge.Cmd (for external monitoring / supervision) 53 | var pbm = PetabridgeCmd.Get(_actorSystem); 54 | pbm.RegisterCommandPalette(ClusterCommands.Instance); 55 | pbm.RegisterCommandPalette(ClusterShardingCommands.Instance); 56 | pbm.RegisterCommandPalette(RemoteCommands.Instance); 57 | pbm.Start(); 58 | 59 | return Task.CompletedTask; 60 | } 61 | 62 | public async Task StopAsync(CancellationToken cancellationToken) 63 | { 64 | await _actorSystem.Terminate(); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Akka.CQRS.TradeProcessor.Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 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 "5055" 8 | ENV MONGO_CONNECTION_STR "" #MongoDb connection string for Akka.Persistence 9 | 10 | # 9110 - Petabridge.Cmd 11 | # 5055 - Akka.Cluster 12 | EXPOSE 9110 5055 13 | 14 | # Install Petabridge.Cmd client 15 | RUN dotnet tool install --global pbm 16 | 17 | COPY ./bin/Release/netcoreapp3.1/publish/ /app 18 | 19 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS app 20 | WORKDIR /app 21 | 22 | COPY --from=base /app /app 23 | 24 | # copy .NET Core global tool 25 | COPY --from=base /root/.dotnet /root/.dotnet/ 26 | 27 | # Needed because https://stackoverflow.com/questions/51977474/install-dotnet-core-tool-dockerfile 28 | ENV PATH="${PATH}:/root/.dotnet/tools" 29 | 30 | CMD ["dotnet", "Akka.CQRS.TradeProcessor.Service.dll"] -------------------------------------------------------------------------------- /src/Akka.CQRS.TradeProcessor.Service/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Akka.CQRS.Infrastructure; 3 | using Microsoft.AspNetCore; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Akka.CQRS.TradeProcessor.Service 11 | { 12 | class Program 13 | { 14 | static async Task Main(string[] args) 15 | { 16 | var host = WebHost.CreateDefaultBuilder(args) 17 | .ConfigureServices((hostContext, services) => 18 | { 19 | services.AddLogging(); 20 | services.AddPhobosApm(); 21 | services.AddHostedService(); 22 | }) 23 | .ConfigureLogging((hostContext, configLogging) => 24 | { 25 | configLogging.AddConsole(); 26 | }) 27 | .Configure(app => 28 | { 29 | app.UseRouting(); 30 | 31 | // enable App.Metrics routes 32 | app.UseMetricsAllMiddleware(); 33 | app.UseMetricsAllEndpoints(); 34 | }) 35 | .Build(); 36 | 37 | await host.RunAsync(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Akka.CQRS.TradeProcessor.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 = 5055 10 | } 11 | } 12 | 13 | cluster { 14 | #will inject this node as a self-seed node at run-time 15 | seed-nodes = ["akka.tcp://AkkaTrader@127.0.0.1:5055"] 16 | roles = ["trade-processor" , "trade-events"] 17 | 18 | pub-sub{ 19 | role = "trade-events" 20 | } 21 | 22 | sharding{ 23 | role = "trade-processor" 24 | state-store-mode = ddata 25 | } 26 | } 27 | 28 | persistence{ 29 | journal { 30 | plugin = "akka.persistence.journal.mongodb" 31 | mongodb.class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 32 | mongodb.collection = "EventJournal" 33 | #mongodb.stored-as = binary 34 | } 35 | 36 | snapshot-store { 37 | plugin = "akka.persistence.snapshot-store.mongodb" 38 | mongodb.class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 39 | mongodb.collection = "SnapshotStore" 40 | #mongodb.stored-as = binary 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/Akka.CQRS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | $(NetStandardVersion) 6 | Core messages, domain events, and infrastructure. 7 | Debug;Release;Phobos 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Akka.CQRS/AvailableTickerSymbols.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS 6 | { 7 | /// 8 | /// The set of available ticker symbols for this demo. 9 | /// 10 | public static class AvailableTickerSymbols 11 | { 12 | public static readonly string[] Symbols = 13 | { "MSFT", "AMZN", "GOOG", "TSLA", "TEAM", "AMD", "WDC", "STX", "UBER", "SNAP", "FB", "NET", "DT", "ESTC", 14 | "FSLY", "UPWK", "INTC", "HPE", "BB", "QCOM", "APPL", "DDOG", "NEWR", "RACE", "SAVE", "AAL", "UAL", "DAL"}; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Akka.CQRS/Commands/GetOrderBookSnapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS.Commands 6 | { 7 | /// 8 | /// Specifics the level of detail for an OrderBook snapshot. 9 | /// 10 | public enum DetailLevel 11 | { 12 | /// 13 | /// Lists all of the details 14 | /// 15 | Full, 16 | 17 | /// 18 | /// Lists only the aggregates 19 | /// 20 | Summary 21 | } 22 | 23 | /// 24 | /// 25 | /// Query the current order book snapshot 26 | /// 27 | public class GetOrderBookSnapshot : IWithStockId, ITradeEvent 28 | { 29 | public GetOrderBookSnapshot(string stockId) 30 | { 31 | StockId = stockId; 32 | } 33 | 34 | public string StockId { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Akka.CQRS/Commands/GetRecentMatches.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS.Commands 6 | { 7 | /// 8 | /// Query an order book for the set of recent matches 9 | /// 10 | public sealed class GetRecentMatches : IWithStockId, ITradeEvent 11 | { 12 | public GetRecentMatches(string stockId) 13 | { 14 | StockId = stockId; 15 | } 16 | 17 | public string StockId { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Akka.CQRS/Entities/OrderExtensions.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Collections.Generic; 8 | using Akka.CQRS.Events; 9 | 10 | namespace Akka.CQRS 11 | { 12 | /// 13 | /// Sorts open orders by their price. 14 | /// 15 | public sealed class OrderPriceComparer : IComparer 16 | { 17 | public static readonly OrderPriceComparer Instance = new OrderPriceComparer(); 18 | 19 | private OrderPriceComparer() { } 20 | 21 | public int Compare(Order x, Order y) 22 | { 23 | if (x.Price.Equals(y.Price)) 24 | return 0; 25 | if (x.Price < y.Price) 26 | return -1; 27 | return 1; 28 | } 29 | } 30 | 31 | /// 32 | /// Extension methods for working with , , and . 33 | /// 34 | public static class OrderExtensions 35 | { 36 | public static Order ToOrder(this Bid bid) 37 | { 38 | return new Order(bid.OrderId, bid.StockId, TradeSide.Buy, bid.BidQuantity, bid.BidPrice, bid.TimeIssued); 39 | } 40 | 41 | public static Order ToOrder(this Ask ask) 42 | { 43 | return new Order(ask.OrderId, ask.StockId, TradeSide.Sell, ask.AskQuantity, ask.AskPrice, ask.TimeIssued); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/EntityIdHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS 6 | { 7 | /// 8 | /// Utility class for naming some of our persistent entities 9 | /// 10 | public static class EntityIdHelper 11 | { 12 | public const string OrderBookSuffix = "-orderBook"; 13 | public const string PriceSuffix = "-prices"; 14 | 15 | public static string IdForOrderBook(string tickerSymbol) 16 | { 17 | return tickerSymbol + OrderBookSuffix; 18 | } 19 | 20 | public static string ExtractTickerFromPersistenceId(string persistenceId) 21 | { 22 | return persistenceId.Split('-')[0]; 23 | } 24 | 25 | public static string IdForPricing(string tickerSymbol) 26 | { 27 | return tickerSymbol + PriceSuffix; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Akka.CQRS/Events/Ask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Akka.CQRS.Events 4 | { 5 | /// 6 | /// 7 | /// Represents a "sell"-side event 8 | /// 9 | public sealed class Ask : IWithStockId, IWithOrderId 10 | { 11 | public Ask(string stockId, string orderId, decimal askPrice, 12 | double askQuantity, DateTimeOffset timeIssued) 13 | { 14 | StockId = stockId; 15 | AskPrice = askPrice; 16 | AskQuantity = askQuantity; 17 | TimeIssued = timeIssued; 18 | OrderId = orderId; 19 | } 20 | 21 | public string StockId { get; } 22 | 23 | public decimal AskPrice { get; } 24 | 25 | public double AskQuantity { get; } 26 | 27 | public DateTimeOffset TimeIssued { get; } 28 | public string OrderId { get; } 29 | 30 | private bool Equals(Ask other) 31 | { 32 | return string.Equals(StockId, other.StockId) && AskPrice == other.AskPrice && AskQuantity.Equals(other.AskQuantity) && string.Equals(OrderId, other.OrderId); 33 | } 34 | 35 | public override bool Equals(object obj) 36 | { 37 | if (ReferenceEquals(null, obj)) return false; 38 | if (ReferenceEquals(this, obj)) return true; 39 | return obj is Ask other && Equals(other); 40 | } 41 | 42 | public override int GetHashCode() 43 | { 44 | unchecked 45 | { 46 | var hashCode = StockId.GetHashCode(); 47 | hashCode = (hashCode * 397) ^ AskPrice.GetHashCode(); 48 | hashCode = (hashCode * 397) ^ AskQuantity.GetHashCode(); 49 | hashCode = (hashCode * 397) ^ OrderId.GetHashCode(); 50 | return hashCode; 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/Events/Bid.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS.Events 6 | { 7 | /// 8 | /// 9 | /// Represents a "buy"-side event 10 | /// 11 | public sealed class Bid : IWithStockId, IWithOrderId 12 | { 13 | public Bid(string stockId, string orderId, decimal bidPrice, 14 | double bidQuantity, DateTimeOffset timeIssued) 15 | { 16 | StockId = stockId; 17 | BidPrice = bidPrice; 18 | BidQuantity = bidQuantity; 19 | TimeIssued = timeIssued; 20 | OrderId = orderId; 21 | } 22 | 23 | public string StockId { get; } 24 | 25 | public decimal BidPrice { get; } 26 | 27 | public double BidQuantity { get; } 28 | 29 | public DateTimeOffset TimeIssued { get; } 30 | public string OrderId { get; } 31 | 32 | private bool Equals(Bid other) 33 | { 34 | return string.Equals(StockId, other.StockId) && BidPrice == other.BidPrice && BidQuantity.Equals(other.BidQuantity) && string.Equals(OrderId, other.OrderId); 35 | } 36 | 37 | public override bool Equals(object obj) 38 | { 39 | if (ReferenceEquals(null, obj)) return false; 40 | if (ReferenceEquals(this, obj)) return true; 41 | return obj is Bid other && Equals(other); 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | unchecked 47 | { 48 | var hashCode = StockId.GetHashCode(); 49 | hashCode = (hashCode * 397) ^ BidPrice.GetHashCode(); 50 | hashCode = (hashCode * 397) ^ BidQuantity.GetHashCode(); 51 | hashCode = (hashCode * 397) ^ OrderId.GetHashCode(); 52 | return hashCode; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Akka.CQRS/Events/Fill.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS.Events 6 | { 7 | /// 8 | /// Fill an open order 9 | /// 10 | public sealed class Fill : IWithOrderId, IWithStockId, IEquatable 11 | { 12 | public Fill(string orderId, string stockId, double quantity, decimal price, 13 | string filledById, DateTimeOffset timestamp, bool partialFill = false) 14 | { 15 | OrderId = orderId; 16 | Quantity = quantity; 17 | Price = price; 18 | FilledById = filledById; 19 | Timestamp = timestamp; 20 | StockId = stockId; 21 | Partial = partialFill; 22 | } 23 | 24 | public string OrderId { get; } 25 | 26 | public double Quantity { get; } 27 | 28 | public decimal Price { get; } 29 | 30 | public string FilledById { get; } 31 | 32 | public DateTimeOffset Timestamp { get; } 33 | 34 | /// 35 | /// When true, indicates that the order was only partially filled. 36 | /// 37 | public bool Partial { get; } 38 | 39 | public string StockId { get; } 40 | 41 | public bool Equals(Fill other) 42 | { 43 | if (ReferenceEquals(null, other)) return false; 44 | if (ReferenceEquals(this, other)) return true; 45 | return string.Equals(OrderId, other.OrderId) && Quantity.Equals(other.Quantity) 46 | && Price == other.Price && string.Equals(FilledById, other.FilledById) && Partial == other.Partial && string.Equals(StockId, other.StockId); 47 | } 48 | 49 | public override bool Equals(object obj) 50 | { 51 | if (ReferenceEquals(null, obj)) return false; 52 | if (ReferenceEquals(this, obj)) return true; 53 | return obj is Fill other && Equals(other); 54 | } 55 | 56 | public override int GetHashCode() 57 | { 58 | unchecked 59 | { 60 | var hashCode = OrderId.GetHashCode(); 61 | hashCode = (hashCode * 397) ^ Quantity.GetHashCode(); 62 | hashCode = (hashCode * 397) ^ Price.GetHashCode(); 63 | hashCode = (hashCode * 397) ^ FilledById.GetHashCode(); 64 | hashCode = (hashCode * 397) ^ Partial.GetHashCode(); 65 | hashCode = (hashCode * 397) ^ StockId.GetHashCode(); 66 | return hashCode; 67 | } 68 | } 69 | 70 | public static bool operator ==(Fill left, Fill right) 71 | { 72 | return Equals(left, right); 73 | } 74 | 75 | public static bool operator !=(Fill left, Fill right) 76 | { 77 | return !Equals(left, right); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Akka.CQRS/Events/Match.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS.Events 6 | { 7 | /// 8 | /// Matches a buy / sell-side order 9 | /// 10 | public sealed class Match : IWithStockId, ITradeEvent, IEquatable 11 | { 12 | public Match(string stockId, string buyOrderId, string sellOrderId, decimal settlementPrice, double quantity, DateTimeOffset timeStamp) 13 | { 14 | StockId = stockId; 15 | SettlementPrice = settlementPrice; 16 | Quantity = quantity; 17 | TimeStamp = timeStamp; 18 | BuyOrderId = buyOrderId; 19 | SellOrderId = sellOrderId; 20 | } 21 | 22 | public string StockId { get; } 23 | 24 | public string BuyOrderId { get; } 25 | 26 | public string SellOrderId { get; } 27 | 28 | public decimal SettlementPrice { get; } 29 | 30 | public double Quantity { get; } 31 | 32 | public DateTimeOffset TimeStamp { get; } 33 | 34 | public bool Equals(Match other) 35 | { 36 | if (ReferenceEquals(null, other)) return false; 37 | if (ReferenceEquals(this, other)) return true; 38 | return string.Equals(StockId, other.StockId) 39 | && string.Equals(BuyOrderId, other.BuyOrderId) 40 | && string.Equals(SellOrderId, other.SellOrderId); 41 | } 42 | 43 | public override bool Equals(object obj) 44 | { 45 | if (ReferenceEquals(null, obj)) return false; 46 | if (ReferenceEquals(this, obj)) return true; 47 | return obj is Match other && Equals(other); 48 | } 49 | 50 | public override int GetHashCode() 51 | { 52 | unchecked 53 | { 54 | var hashCode = StockId.GetHashCode(); 55 | hashCode = (hashCode * 397) ^ BuyOrderId.GetHashCode(); 56 | hashCode = (hashCode * 397) ^ SellOrderId.GetHashCode(); 57 | return hashCode; 58 | } 59 | } 60 | 61 | public static bool operator ==(Match left, Match right) 62 | { 63 | return Equals(left, right); 64 | } 65 | 66 | public static bool operator !=(Match left, Match right) 67 | { 68 | return !Equals(left, right); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Akka.CQRS/ITimestamper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS 6 | { 7 | /// 8 | /// Produces records for trades and orders. 9 | /// 10 | public interface ITimestamper 11 | { 12 | DateTimeOffset Now { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Akka.CQRS/ITradeEvent.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | namespace Akka.CQRS 7 | { 8 | /// 9 | /// Marker interfaces for trade activity 10 | /// 11 | public interface ITradeEvent 12 | { 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/ITradeOrderGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace Akka.CQRS 5 | { 6 | /// 7 | /// Generates unique trade order Ids. 8 | /// 9 | public interface ITradeOrderIdGenerator 10 | { 11 | string NextId(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Akka.CQRS/IWithOrderId.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | namespace Akka.CQRS 7 | { 8 | /// 9 | /// Marker interface for routing messages with specific trade IDs 10 | /// 11 | public interface IWithOrderId : ITradeEvent 12 | { 13 | /// 14 | /// Unique identifier for a specific order 15 | /// 16 | string OrderId { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/IWithStockId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS 6 | { 7 | /// 8 | /// Marker interface used for routing messages for specific stock IDs 9 | /// 10 | public interface IWithStockId 11 | { 12 | /// 13 | /// The ticker symbol for a specific stock. 14 | /// 15 | string StockId { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Akka.CQRS/OrderbookSnapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Akka.CQRS.Events; 5 | 6 | namespace Akka.CQRS 7 | { 8 | /// 9 | /// Indicates which side of the trade this transaction occurred on. 10 | /// 11 | public enum TradeSide 12 | { 13 | Buy, 14 | Sell 15 | } 16 | 17 | /// 18 | /// The full state of the current order book for a given . 19 | /// 20 | public sealed class OrderbookSnapshot : IWithStockId, ITradeEvent 21 | { 22 | public OrderbookSnapshot(string stockId, DateTimeOffset timestamp, double askQuantity, double bidQuantity, IReadOnlyCollection asks, IReadOnlyCollection bids) 23 | { 24 | StockId = stockId; 25 | Timestamp = timestamp; 26 | AskQuantity = askQuantity; 27 | BidQuantity = bidQuantity; 28 | Asks = asks; 29 | Bids = bids; 30 | } 31 | 32 | public string StockId { get; } 33 | 34 | public DateTimeOffset Timestamp { get; } 35 | 36 | public double AskQuantity { get; } 37 | 38 | public double BidQuantity { get; } 39 | 40 | public IReadOnlyCollection Asks { get; } 41 | 42 | public IReadOnlyCollection Bids { get; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Akka.CQRS/PriceRange.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.CQRS 2 | { 3 | /// 4 | /// Represents a price band, typically weighted by buy/sell volume. 5 | /// 6 | public struct PriceRange 7 | { 8 | public PriceRange(decimal min, decimal mean, decimal max) 9 | { 10 | Min = min; 11 | Mean = mean; 12 | Max = max; 13 | } 14 | 15 | public decimal Min { get; } 16 | 17 | public decimal Mean { get; } 18 | 19 | public decimal Max { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/PriceRangeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Akka.CQRS 6 | { 7 | /// 8 | /// Utility class for helping generate values 9 | /// within a . 10 | /// 11 | public static class PriceRangeExtensions 12 | { 13 | public static decimal WithinRange(this Random r, PriceRange range) 14 | { 15 | var sample = NextDecimalSample(r); 16 | return range.Max * sample + range.Min * (1 - sample); 17 | } 18 | 19 | /* 20 | * Random RNG algorithms provided by Jon Skeet's answer: https://stackoverflow.com/a/609529/377476 21 | * And by Bryan Loeper's answer: https://stackoverflow.com/a/28860710/377476 22 | * 23 | */ 24 | public static int NextInt32(this Random rng) 25 | { 26 | var firstBits = rng.Next(0, 1 << 4) << 28; 27 | var lastBits = rng.Next(0, 1 << 28); 28 | return firstBits | lastBits; 29 | } 30 | 31 | public static decimal NextDecimalSample(this Random random) 32 | { 33 | var sample = 1m; 34 | //After ~200 million tries this never took more than one attempt but it is possible to generate combinations of a, b, and c with the approach below resulting in a sample >= 1. 35 | while (sample >= 1) 36 | { 37 | var a = random.NextInt32(); 38 | var b = random.NextInt32(); 39 | //The high bits of 0.9999999999999999999999999999m are 542101086. 40 | var c = random.Next(542101087); 41 | sample = new decimal(a, b, c, false, 28); 42 | } 43 | return sample; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Akka.CQRS/Serialization/TradeEventSerializer.conf: -------------------------------------------------------------------------------- 1 | # Protobuf serializer for IWithTrace messages 2 | akka.actor { 3 | serializers { 4 | trade-events = "Akka.CQRS.Serialization.TradeEventSerializer, Akka.CQRS" 5 | } 6 | serialization-bindings { 7 | "Akka.CQRS.ITradeEvent, Akka.CQRS" = trade-events 8 | } 9 | 10 | serialization-identifiers { 11 | "Akka.CQRS.Serialization.TradeEventSerializer, Akka.CQRS" = 517 12 | } 13 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/Util/CurrentUtcTimestamper.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System; 8 | 9 | namespace Akka.CQRS.Util 10 | { 11 | /// 12 | /// 13 | /// Uses to provide timestamp signatures. 14 | /// 15 | public sealed class CurrentUtcTimestamper : ITimestamper 16 | { 17 | public static readonly CurrentUtcTimestamper Instance = new CurrentUtcTimestamper(); 18 | private CurrentUtcTimestamper() { } 19 | public DateTimeOffset Now => DateTimeOffset.UtcNow; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Akka.CQRS/Util/GuidTradeOrderIdGenerator.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2015 - 2019 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System; 8 | 9 | namespace Akka.CQRS.Util 10 | { 11 | /// 12 | /// Creates trade order ids using s. 13 | /// 14 | public sealed class GuidTradeOrderIdGenerator : ITradeOrderIdGenerator 15 | { 16 | public static readonly GuidTradeOrderIdGenerator Instance = new GuidTradeOrderIdGenerator(); 17 | private GuidTradeOrderIdGenerator() { } 18 | 19 | public string NextId() 20 | { 21 | return Guid.NewGuid().ToString(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright © 2015-2019 Petabridge 4 | Petabridge 5 | 0.2.2 6 | Added footer that displays assembly version on Pricing UI 7 | 8 | 9 | 10 | 11 | 12 | 13 | $(NoWarn);CS1591 14 | 15 | 16 | 2.4.1 17 | 17.2.0 18 | 1.4.40 19 | 1.4.40 20 | 0.5.3 21 | 0.5.4 22 | 6.7.0 23 | 3.21.4 24 | 0.8.5 25 | 3.1.0 26 | 1.1.4 27 | 4.3.0 28 | 1.0.3 29 | 30 | 31 | netcoreapp3.1 32 | netstandard2.0 33 | 34 | 35 | $(DefineConstants);RELEASE; 36 | 37 | 38 | netcoreapp3.1 39 | netstandard2.0 40 | 41 | -------------------------------------------------------------------------------- /src/protobuf/AKka.Cqrs.Pricing.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package Akka.CQRS.Pricing.Serialization.Msgs; 3 | 4 | message PriceChanged{ 5 | string stockId = 1; 6 | double currentAvgPrice = 2; /* normally a decimal in C# - might have loss of precision here */ 7 | int64 timeIssued = 3; 8 | } 9 | 10 | message VolumeChanged{ 11 | string stockId = 1; 12 | double currentAvgVolume = 2; 13 | int64 timeIssued = 3; 14 | } 15 | 16 | message FetchPriceAndVolume{ 17 | string stockId = 1; 18 | } 19 | 20 | message Ping{ 21 | string stockId = 1; 22 | } 23 | 24 | message PriceAndVolumeSnapshot{ 25 | string stockId = 1; 26 | repeated PriceChanged priceUpdates = 2; 27 | repeated VolumeChanged volumeUpdates = 3; 28 | } 29 | 30 | message MatchAggregatorSnapshot{ 31 | double avgPrice = 1; 32 | double avgVolume = 2; 33 | repeated PriceChanged priceUpdates = 3; 34 | repeated VolumeChanged volumeUpdates = 4; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/protobuf/Akka.Cqrs.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package Akka.CQRS.Serialization.Msgs; 3 | 4 | enum TradeEvent{ 5 | BID = 0; 6 | ASK = 1; 7 | FILL = 2; 8 | MATCH = 3; 9 | } 10 | 11 | /* Trading events */ 12 | message Bid{ 13 | string orderId = 1; 14 | string stockId = 2; 15 | double quantity = 3; 16 | double price = 4; /* normally a decimal in C# - might have loss of precision here */ 17 | int64 timeIssued = 5; 18 | } 19 | 20 | message Ask{ 21 | string orderId = 1; 22 | string stockId = 2; 23 | double quantity = 3; 24 | double price = 4; /* normally a decimal in C# - might have loss of precision here */ 25 | int64 timeIssued = 5; 26 | } 27 | 28 | message Fill{ 29 | string orderId = 1; 30 | string stockId = 2; 31 | double quantity = 3; 32 | double price = 4; /* normally a decimal in C# - might have loss of precision here */ 33 | int64 timeIssued = 5; 34 | string filledById = 6; 35 | bool partialFill = 7; 36 | } 37 | 38 | message Match{ 39 | string stockId = 1; 40 | string buyOrderId = 2; 41 | string sellOrderId = 3; 42 | double quantity = 4; 43 | double price = 5; /* normally a decimal in C# - might have loss of precision here */ 44 | int64 timeIssued = 6; 45 | } 46 | 47 | /* used in Order structs */ 48 | enum TradeSide{ 49 | BUY = 0; 50 | SELL = 1; 51 | } 52 | 53 | message Order{ 54 | string orderId = 1; 55 | string stockId = 2; 56 | TradeSide side = 3; 57 | double quantity = 4; 58 | double price = 5; /* normally a decimal in C# - might have loss of precision here */ 59 | int64 timeIssued = 6; 60 | repeated Fill fills = 16; 61 | } 62 | 63 | message OrderbookSnapshot{ 64 | string stockId = 1; 65 | int64 timeIssued = 2; 66 | double askQuantity = 3; 67 | double bidQuantity = 4; 68 | repeated Order asks = 5; 69 | repeated Order bids = 6; 70 | } 71 | 72 | message GetOrderbookSnapshot{ 73 | string stockId = 1; 74 | } 75 | 76 | message GetRecentMatches{ 77 | string stockId = 1; 78 | } -------------------------------------------------------------------------------- /stopK8sServices.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM destroys all K8s services 3 | 4 | kubectl -n akka-cqrs delete statefulsets,deployments,po,svc --all -------------------------------------------------------------------------------- /stopK8sServices.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # destroys all K8s services 3 | 4 | kubectl -n akka-cqrs delete statefulsets,deployments,po,svc --all --------------------------------------------------------------------------------