Compare commits

...
168 Commits
Author SHA1 Message Date
John Colvin 8206132a37 quantity conversion in pnl for dj 2020-06-09 17:27:29 -05:00
John Colvin f8b3fd2e14 QuantityConversionData.to/from_display extensions use 'safe_divide' 2020-06-09 17:10:21 -05:00
John Colvin 8b17ee72ff quantity conversion in ladder when last size is transposed over volume column 2020-06-09 10:50:06 -05:00
John Colvin 31ed473999 quantity conversion in time and sales for dj 2020-06-08 17:25:13 -05:00
John Colvin 673805a7f4 quantity conversion in fills for dj 2020-06-08 17:10:39 -05:00
John Colvin 91422449e9 quantity conversion in manual fills for dj 2020-06-08 17:02:55 -05:00
John Colvin 438089a1cd quantity conversion in mview for dj 2020-06-08 16:36:57 -05:00
John Colvin 6a186d1d7e MarketViewRow - remove unreferenced fields 2020-06-08 16:33:55 -05:00
John Colvin 9d831a2569 fix line endings in MarketViewVM.cs 2020-06-08 16:33:55 -05:00
John Colvin d47cf1a54e (Dynamic)DoubleDivisorConverter uses safe_divide extension 2020-06-08 16:33:55 -05:00
John Colvin 8c9f669e72 refactor IsRealNonZero to is_real_positive (orig name was a typo), add a few more similar extensions to MathUtils 2020-06-08 16:33:55 -05:00
John Colvin f5bda11797 quantity conversion in orderbook for dj 2020-06-08 12:19:43 -05:00
John Colvin 1841a75fec quantity conversion in ladder/ticket for dj 2020-06-08 12:19:43 -05:00
John Colvin 34cdaeec93 don't use Disposable.replace bc it creates the next disposable before disposing the previous one 2020-06-07 16:01:27 -05:00
Taha Afzal 3230fb9e48 Regular TAS contract's formatting did not have the word TAS in it 2020-06-05 16:42:05 -04:00
Taha Afzal 888b4e22c0 Ignoring strips with > 512 components 2020-06-05 14:11:34 -04:00
Taha Afzal 534ce5a8ec strips are XL too 2020-06-05 13:12:57 -04:00
Taha Afzal c2b9ac57dc Fixing build 2020-06-05 11:55:54 -04:00
Taha Afzal 2783361325 Merge branch 'master' into DJ_ICE 2020-06-05 10:50:42 -04:00
Taha Afzal 46d35131d7 Overridable cpps 2020-06-05 10:01:18 -04:00
Pedro Pinto 3b95154c04 Updated ICE Onixs 2020-06-05 09:11:03 -04:00
Pedro Pinto 1a6b918853 Updated docker 2020-06-04 13:43:25 -04:00
Pedro Pinto bc22605a25 Added +x 2020-06-04 13:43:00 -04:00
Pedro Pinto f3d28add1c Added publishing of new routing info when orders are recorevered from disk 2020-06-04 13:41:34 -04:00
Taha Afzal 63d260a6f8 Security formatting for ICE 2020-06-03 09:42:44 -04:00
Taha Afzal 9b4a63b4a8 Changes to make HNG/HIS/HEN/HHD to work 2020-06-02 11:15:47 -04:00
Pedro Pinto a502c365e5 Build tweaks 2020-05-28 12:15:16 -04:00
Pedro Pinto fdacd8b543 Added -f when removing simlinks in build setup. 2020-05-28 12:02:46 -04:00
John Colvin 3d1de9a0c7 nuget_restore.cmd uses externals package source (builds offline) 2020-05-28 12:02:31 -04:00
John Colvin b2b3fa5949 update jenkinsbuild.cmd to use externals nuget source (for windows) 2020-05-28 12:02:17 -04:00
Pedro Pinto 86ecf69820 Updated external subrepos 2020-05-28 11:53:51 -04:00
Pedro Pinto 8f537a8fda Merged in build_improvements (pull request #414)
Build improvements
2020-05-26 16:32:43 +00:00
Pedro Pinto 01b9ff6b47 Dropped celoxica 2020-05-26 12:28:35 -04:00
Pedro Pinto 2609663d3e Updated setup_builds to work even if its run outside the tree. 2020-05-26 12:06:08 -04:00
Pedro Pinto fd89241611 Updated externals pointer 2020-05-26 11:49:29 -04:00
John Colvin a69118c5d3 fix line endings in release_notes 2020-05-22 23:42:27 -05:00
John Colvin 5accd0e2b4 Merge branch 'master' into prod 2020-05-22 19:14:16 -05:00
John Colvin df5ad61cd2 release notes for synthetic gui integer tick sizes 2020-05-22 19:12:29 -05:00
John Colvin 4a7674eb29 synthetic gui supports integer tick sizes 2020-05-22 18:15:53 -05:00
Eric Harding 5a458287f4 update license search info in disable_onixs.py 2020-05-22 15:18:46 -04:00
John Colvin 5921f9f6b5 Merge branch 'master' into prod 2020-05-22 12:34:18 -05:00
John Colvin 4c73a63ae8 release notes for bug fix (unsorted custom state grid params in orderbook) 2020-05-22 12:30:53 -05:00
John Colvin 42b1c921a8 OrderBook custom state grid params are sorted alphabetically (again) 2020-05-22 12:24:42 -05:00
Pedro Pinto fab3334b1e Updated release notes 2020-05-22 11:23:03 -04:00
Pedro Pinto 7239dd3ade Merge branch 'master' into prod 2020-05-22 11:20:17 -04:00
John Colvin dd4c8eb649 Merged csharp-api-examples-sln into master 2020-05-22 10:19:13 -05:00
John Colvin 76a29bdc8a client ui supports all integer tick sizes (not just 1) 2020-05-21 15:26:50 -05:00
John Colvin 06d708f3ea new csharp api examples 2020-05-21 15:17:32 -05:00
John Colvin e2f70cbc19 minor config tweaks needed after upgrading VisualStudio 2020-05-21 13:04:00 -05:00
Pedro Pinto 9d54e47a03 Update CMakeDeps 2020-05-19 17:00:24 -04:00
John Colvin faccdf581e Merged in limit-algo-clean-history (pull request #413)
Limit algo clean history
2020-05-19 13:53:07 +00:00
John Colvin 1a64714a54 always log errors observed in LadderGridHost._volume_from_row (not just when it fails after max_num_tries) 2020-05-18 17:03:39 -05:00
John Colvin 3ba6792a85 wrap LadderGridHost._volume_from_row in try/catch (addresses Pure client crash) 2020-05-18 16:46:23 -05:00
Pedro Pinto c32a7f1d62 Made native orders calculate route when recovered from disk.
This is in response to the Pure debacle of May 17th. CME changed the
MSGW used for certain products. As we only route orders once, when
they are created, any orders that already existed never transitioned
to the new MSGW. This made it so that users could no longer control
these orders. With this change we recalculate routes when the orders
are first loaded from disk. Assuming we have up to date securities,
something that sometimes does not happen unfortunately, this should
result in the order being automatically relocated to its new MSGW. As
a bonus we also get the hability to change MSGW configuration even if
there are working for that MSGW.
2020-05-18 08:59:42 -04:00
John Colvin 10e75f30e6 fix algo params grid enum template for GenericOrderVMs 2020-05-15 13:16:25 -05:00
John Colvin 20eac19c4f GenericOrderVM - normalize white space 2020-05-15 13:16:25 -05:00
John Colvin 8c0765a2c7 fix build 2020-05-15 10:34:59 -05:00
Taha Afzal 7a1f88e813 Merge branch 'limit-algo-clean-history' of bitbucket.org:bluetradesys/spark into limit-algo-clean-history 2020-05-14 22:01:32 -04:00
Taha Afzal 3420f14c8a DeleteAndCover detached hedges by default: 2020-05-14 22:01:22 -04:00
John Colvin 86277dfbfe config toggle for new 'specific' CustomOrderDefinition params dialog (disabled by default) 2020-05-14 20:14:51 -05:00
John Colvin 4839e464c6 added dialog to edit CustomOrderDefinition params of a specific algo node 2020-05-14 20:02:14 -05:00
John Colvin dc2ad071a9 GenericOrderHelper should use 'FirstOrDefault' (not 'First') 2020-05-14 19:59:06 -05:00
John Colvin 65e49992f9 LimitCoverAlgo.do_update - remove redundant conversion from DynamicOrderDefinition to LimitCoverDefinition 2020-05-14 17:08:01 -05:00
John Colvin afbb9ae92d LimitCover algo - 'Hedge Ticks' and 'Top Ticks Offset' params actually assigned to DeleteAndCover child 2020-05-14 12:29:29 -05:00
John Colvin ff088142d9 fix custom state output grid 2020-05-14 12:29:29 -05:00
Pedro Pinto 93b7cf346b Added an option for the Cover algo to detach hedges. 2020-05-14 10:20:46 -04:00
Eric Harding f0edeadd50 Make shared mem size configurable 2020-05-13 20:29:38 -05:00
Taha Afzal 8ee9b84488 Merge branch 'limit-algo-clean-history' of bitbucket.org:bluetradesys/spark into limit-algo-clean-history 2020-05-13 20:39:13 -04:00
Taha Afzal e3648f5373 Better Logging 2020-05-13 20:39:07 -04:00
John Colvin facd3a9d81 LimitCoverAlgo - remove unnecessary else block in 'do_before_launch' 2020-05-13 17:26:13 -05:00
John Colvin 84969cb481 LimitCoverAlgo - dont update me->child_qty in the child_changed callback, since its async 2020-05-13 17:21:32 -05:00
John Colvin 6d521e77db LimitCoverAlgo - don't override do_launch_attached (it's not supported) 2020-05-13 17:15:30 -05:00
John Colvin 82c49f8f49 remove redundant methods: tick_up/less, payup/less 2020-05-13 17:15:30 -05:00
John Colvin e074309da1 typo broke build (missed constructor in 'TickOffset' -> 'PriceOffset' refactor) 2020-05-13 17:15:30 -05:00
John Colvin 2d622a5f39 LimitCover algo - convert 'ticks_offset' param be in price terms 2020-05-13 16:55:01 -05:00
John Colvin d51d20e84c LimitCover algo - remove cancel_on_blah params, add hedge_ticks 2020-05-13 16:18:46 -05:00
John Colvin 2ccdef9dc5 fix clang build errors 2020-05-13 15:59:45 -05:00
John Colvin 42e62a30bf to_string(OrderError) uses generated protobuf enum Name method 2020-05-13 15:32:27 -05:00
John Colvin c233c607fa Formatter.h - pass strings/templated args by const ref instead of value 2020-05-13 13:40:07 -05:00
John Colvin 462cb930f7 dont redefine ChildTemplate 2020-05-13 13:20:55 -05:00
Pedro Pinto 2c0f920422 Merge branch 'master' of bitbucket.org:bluetradesys/spark 2020-05-13 08:54:14 -04:00
Pedro Pinto 5ee26065ca Updated .gitignore 2020-05-13 08:53:52 -04:00
John Colvin 550a4e8d50 add config toggle to enable/disable LimitCover algo 2020-05-12 19:11:16 -05:00
John Colvin 99b8221efc LimitCoverAlgo unit tests 2020-05-12 19:11:16 -05:00
John Colvin 19e74c30f8 AlgoTestDriverBase - AlgoState << operator 2020-05-12 19:03:53 -05:00
John Colvin 26693a4c98 AlgoTestDriverBase - after_child_changed callback, and find_child method 2020-05-12 19:03:53 -05:00
John Colvin d8bdb69a7c AlgoTestDriverBase - ChildTemplate impl, added OrderDefinition param to child_launching callback 2020-05-12 19:03:53 -05:00
John Colvin 6d357a74e1 AlgoTestDriverBase - expose cancel/cancel_child methods 2020-05-12 19:03:53 -05:00
John Colvin 73f4e4814c LimitCover algo for pure 2020-05-12 19:03:53 -05:00
John Colvin 312bc4be15 add Formatter.h (defines commonly needed << operators for algos, basic string formatting utils) 2020-05-12 18:55:37 -05:00
John Colvin a0c3ba449a add DynamicOrderAdapter (util to keep param proto conversion code out of the algo) 2020-05-12 18:55:37 -05:00
John Colvin d5a2a7f3f0 orderman_utils.h pragma once 2020-05-12 18:55:37 -05:00
John Colvin eda8c94def replicate_to_linux_vm.py - ignore some dirs generated by visual studio 2020-05-12 18:55:37 -05:00
John Colvin 9a8e80e149 add payup/less price calc utils (Security.h)' 2020-05-12 18:55:37 -05:00
John Colvin 6a7e3d739d fix typo 'epislon' -> 'epsilon' 2020-05-12 15:58:00 -05:00
Taha Afzal e6b8274813 DeletAndCoverOrderDefinition does not contain the nested enum anymore 2020-05-12 15:58:00 -05:00
Taha Afzal 9783520c62 DeleteAndCover completed 2020-05-12 15:58:00 -05:00
Taha Afzal 7e7f0bc37f Adding DeleteAndCover proto 2020-05-12 15:58:00 -05:00
John Colvin b9e2ecdffa Merged in transitive-security-compare (pull request #411)
Transitive security compare

Approved-by: Eric Harding
2020-05-11 19:42:50 +00:00
Eric Harding f1c021a474 Merged in security_preload (pull request #412)
Security preload
2020-05-11 19:41:00 +00:00
Eric Harding 6fda54ffa1 Merged master into security_preload 2020-05-11 15:44:50 +00:00
Taha Afzal bdb0adc16c TASCounter test fixed 2020-05-07 13:07:44 -04:00
Eric Harding 4b19b0ea14 update launch.json 2020-05-06 18:26:03 -05:00
Eric Harding fab1b4dd0f Add SecurityPreload key to pre-load securities from the security DB for sunday startup. 2020-05-06 18:25:29 -05:00
John Colvin f3ed3075cf fix Security comparison impl (its transitive now) 2020-05-06 17:35:46 -05:00
John Colvin 742af36b93 Security comparison unit test (addresses bug that caused Krandall OrderBook crash) 2020-05-06 17:35:46 -05:00
John Colvin c573b5fbb2 add ProtoBufCapture extension (writes TableUpdate state to file when subscription is disposed) 2020-05-06 17:30:00 -05:00
Eric Harding 2d6d4af594 add vscode spark local linux debugger settings 2020-05-06 12:27:11 -05:00
John Colvin 2dcfad979c move FileStreamHelper.cs to spark_core 2020-05-06 12:11:03 -05:00
Taha Afzal ba7e35649e Ignore Spread Pos Limits are defaulted to true 2020-05-06 10:36:40 -04:00
Pedro Pinto cda4a870b7 Merge branch 'master' of bitbucket.org:bluetradesys/spark 2020-05-05 06:14:30 -04:00
Pedro Pinto b3a8564365 Fixed int32 params in DynamicOrders to be int32 rathern than uint32. 2020-05-05 06:13:40 -04:00
Pedro Pinto 1e0114a4cf Treated disconnected orders as working. 2020-05-05 06:13:06 -04:00
Eric Harding 3a6e95ef05 Downgrade security scanner message to info 2020-05-04 16:16:55 -04:00
Eric Harding 6875263743 Update 'update' instructions for spark_sdk 2020-05-01 14:17:07 -04:00
Taha Afzal eac828f323 Merge branch 'master' into TASCounter 2020-05-01 13:49:27 -04:00
Taha Afzal 1635d6f649 Added test for TASCounter 2020-05-01 13:48:50 -04:00
Eric Harding 2da535e9ff Update onixs license 2020-05-01 12:51:31 -04:00
Taha Afzal a98be44a40 Merge branch 'master' into TASCounter 2020-04-30 13:59:49 -04:00
John Colvin 6210ac2ee6 OrderBook custom state grid only shows data if the selected rows are of the same root/branch 2020-04-29 16:21:17 -05:00
Pedro Pinto cfebab0488 Merged in api_improvements (pull request #405)
Api improvements

Approved-by: John Colvin <john.colvin@bluetradesys.com>
2020-04-28 20:06:53 +00:00
Pedro Pinto eacd766359 Updated release notes. 2020-04-28 16:05:21 -04:00
Pedro Pinto 8fb8f8ff58 Merge branch 'master' into api_improvements 2020-04-28 15:58:43 -04:00
Taha Afzal f4b22816a2 Putting config to hide TASCounter 2020-04-27 23:39:41 -04:00
Taha Afzal 1029a464d7 Fixing the bts::timestamp tp protobuf timestamp function (unused, but useful) 2020-04-27 09:57:21 -04:00
Taha Afzal fd8b8e03ea cleanup, comments 2020-04-26 23:10:16 -04:00
Pedro Pinto 41cf66a943 Added the hability to retrieve all securities in the API. 2020-04-24 08:00:13 -04:00
Pedro Pinto 1c57c9a012 Added position updates in the API. 2020-04-24 07:03:40 -04:00
Taha Afzal 8ccab66120 Merge branch 'add_custom_algo_state_reporting_to_api' into TASCounter 2020-04-23 18:14:28 -04:00
John Colvin e2b2f7ec9c better default widths for custom state grid columns 2020-04-23 16:11:35 -05:00
John Colvin b3c897bf0f custom state grid params are sorted alphabetically 2020-04-23 16:02:38 -05:00
John Colvin 87c9f54c8b custom state grid populates with first child row that has params (if selected row doesn't have any itself) 2020-04-23 15:59:49 -05:00
John Colvin df56b76ab6 OrderBookRowVM - remove dead property 'is_visible' 2020-04-23 15:54:30 -05:00
Taha Afzal 28ff13db11 Merge branch 'add_custom_algo_state_reporting_to_api' into TASCounter 2020-04-23 13:23:30 -04:00
Taha Afzal 2957075769 comments 2020-04-23 13:22:33 -04:00
John Colvin a6477824a8 OrderBookVM - clear the previous 'custom state' params before adding the new ones 2020-04-23 11:16:47 -05:00
Taha Afzal 17431f5ac0 Code complete, output is not showing in the UI which is making testing harder 2020-04-23 00:09:23 -04:00
Taha Afzal 8708f02fe0 Merge branch 'add_custom_algo_state_reporting_to_api' into TASCounter 2020-04-21 17:44:58 -04:00
Taha Afzal 752fb2bc4a Merge branch 'master' into TASCounter 2020-04-21 17:10:32 -04:00
Taha Afzal 6efb78ecf9 Code complete 2020-04-21 14:26:32 -04:00
John Colvin 75c1970aef remove useless IObservable 'Select' op in 'SubscribeEach' extension 2020-04-20 10:39:31 -05:00
John Colvin c2583518d7 OrderBook was only updating custom state grid when selection changed 2020-04-20 10:37:12 -05:00
John Colvin 8792871387 add algo custom state grid to orderbook (right panel) 2020-04-17 16:36:05 -05:00
Eric Harding 68a593af7b Add more info to system test 2020-04-17 16:48:10 -04:00
John Colvin 17c2321a82 Merged in SP-289-naudio-crash (pull request #407)
SP-289 naudio crash

Approved-by: Eric Harding <eric.harding@bluetradesys.com>
2020-04-16 18:11:18 +00:00
John Colvin 0affc8a942 SP-289 naudio crash 2020-04-16 12:07:44 -05:00
John Colvin ce36ea4109 move Delegates.cs to spark_core 2020-04-16 10:47:41 -05:00
Pedro Pinto 7c29a3d1d8 Added custom state reporting to API. 2020-04-13 11:30:32 -04:00
Taha Afzal 90bc20575a TimeStamps fixed 2020-04-13 08:56:49 -04:00
Pedro Pinto cb268f505a Added settlements to the new algo API. 2020-04-13 08:19:55 -04:00
Pedro Pinto dfd93b836b Updated externals 2020-04-13 07:44:52 -04:00
Taha Afzal 28e9c66675 Added all the state, order execution remains 2020-04-10 12:54:57 -04:00
Taha Afzal c0520d6a31 Initial Check-in, skeleton code 2020-04-09 09:30:00 -04:00
Pedro Pinto 1ba6e3280c Updated CMakeDeps 2020-04-06 09:38:19 -04:00
Eric Harding 3a6f50dfea Merge branch 'prod' of bitbucket.org:bluetradesys/spark 2020-04-03 15:40:30 -04:00
Eric Harding d711f99548 fix externals pointer 2020-04-03 15:40:15 -04:00
pfisher 07ace80871 fixed keycode.conf 2020-04-03 13:40:56 -05:00
John Colvin 93c6277bfc Merged prod into master 2020-04-03 13:20:33 -05:00
Jake Norbut 7ac9d05ae2 Merged in manual_fills_permissions (pull request #403)
add permission filter to manual fills

Approved-by: John Colvin <john.colvin@bluetradesys.com>
2020-04-03 18:18:16 +00:00
Eric Harding 5a4b31ccc0 add cpp sample to service.md api page 2020-03-20 12:10:40 -04:00
John Colvin fb568aab39 SP-288 ladder crash (IndexOutOfRange ex accessing price array) 2020-03-19 15:28:27 -05:00
Eric Harding 64416de10d Fix the braces in my sample. 2020-03-16 20:41:23 -04:00
Eric Harding 46953cb639 add c# market data sample 2020-03-16 20:39:48 -04:00
Eric Harding 7e68a235ee Add securities sub to client_sdk_example 2020-03-16 15:18:39 -04:00
Jake Norbut 85f2285ac0 add permission filter to manual fills 2020-03-09 09:28:23 -05:00
Eric Harding 70e0979836 override sim port for sdk image 2020-03-06 12:45:09 -05:00
Kevin Darby 1f80c3366e Merged in master (pull request #401)
Master
2020-03-03 19:16:43 +00:00
Kevin Darby 19e66a1c32 add a new stunnel path for Pete 2020-02-25 11:16:40 -05:00
219 changed files with 7807 additions and 2052 deletions

No files matched your search

+1
View File
@@ -35,3 +35,4 @@ trace_data/
.ionide/*
/.ignore
/build
.clangd/
+24
View File
@@ -1,6 +1,30 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Spark Local",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceRoot}/build/ninja_debug/server/sparkd",
"args": [
"run",
// "test",
// "-a",
// "[ilink3]"
],
"stopAtEntry": false,
"cwd": "${workspaceRoot}/build/ninja_debug/server/",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
},
{
"name": "SparkVM Launch",
"type": "cppdbg",
+1 -1
View File
@@ -55,7 +55,7 @@ else ()
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/web ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/data_files ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/src/lib/libspark.so ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.13.0.0 ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.16.0.0 ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libACE-5.7.5.so ${CMAKE_BINARY_DIR}/server
COMMAND mkdir ARGS -p ${CMAKE_BINARY_DIR}/server/plugins
COMMENT "Creating server runtime layout"
@@ -124,4 +124,17 @@ service OrderService {
```
### C++ Example
```cpp
#include "lib/api/public/ServiceContext.h"
#include "lib/api/services/OrderService.client.pb.h"
using namespace bts::api;
ServiceContext ctx;
auto orders = ctx.locate<orderman::OrderService>();
auto unsub = orders.subscribe_fills().subscribe([](const auto& update) {
std::cout
<< "Added " << update.added().size() << " items, "
<< "Removed " << update.removed().size() << " items, "
<< "Updated " << update.updated().size() << " items, "
<< "Total items " << update.all().size() << std::endl;
});
std::this_thread::sleep_for(std::chrono::minutes(5));
```
+49 -1
View File
@@ -1,3 +1,51 @@
+++
title = "Market Data"
+++
+++
Use the MarketDataService to subscribe enumerate all available securities.
```cs
using System;
using System.Threading;
using bts.api;
using bts.api.core;
namespace sample {
class Program {
static void Main(string[] args) {
using var context = new ServiceContext();
var md = context.locate<MarketDataService>();
using var sub = md.subscribe_securities().Subscribe(update => {
foreach (var security in update.added) {
Console.WriteLine($"Added security {security.Id}: {security.Symbol}");
}
});
Thread.Sleep(TimeSpan.FromMinutes(5));
}
}
}
```
```cpp
#include "lib/api/public/ServiceContext.h"
#include "spark/lib/domain_model/Security.h"
#include "lib/api/services/MarketDataService.client.pb.h"
#include <thread>
using namespace bts::api;
int main(int argc, char** argv) {
ServiceContext ctx;
auto md_service = ctx.locate<bts::api::MarketDataService>();
auto sub = md_service.subscribe_securities().subscribe([](const auto& update) {
for (const auto& s : update.added()) {
std::cout << "Added security: " << s->id() << ": " << s->symbol() << std::endl;
}
});
std::this_thread::sleep_for(std::chrono::minutes(5));
return 0;
}
```
@@ -68,12 +68,21 @@ To update your Spark SDK simply download an unzip a new [spark_sdk.zip](https://
To preserve the old image so you can go back to it later run `docker tag spark_sdk spark_sdk:your_custom_tag` before loading the new SDK image.
<!--
Then stop, remove and restart spark_sdk.
### Re-build the Dev Container
If you are using Visual Studio Code you also need to rebuild the dev-container so it picks up the new image.
#### Inside VSCode
To re-build from inside Visual Studio Code. Click the 'Remote Explorer' icon on the left hand side.
You should see your spark_sdk project listed under the Containers section. Right click on it and select **rebuild-container**.
#### Command Line
To re-build from the command line run `docker ps -a` and find the row with the spark_sdk image.
```
c:\bts> docker stop spark_sdk
c:\bts> docker rm spark_sdk
c:\bts> docker run -d -v %cd%:/sdk --name spark_sdk bluetradingsytems/spark_sdk
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
54d96cfa3888 spark_sdk "/bin/sh -c 'echo Co…" About a minute ago Up About a minute 127.0.0.1:9000-9001->9000-9001/tcp quizzical_lichterman
```
Then run `docker rm 54d96cfa3888` (use your container id instead of 54d96cfa3888) or `docker rm quizzical_lichterman` (use your container name).
``` -->
+22 -1
View File
@@ -1,9 +1,29 @@
+++
+++
title = "Release notes"
weight = 3
sort_by = "weight"
insert_anchor_links = "right"
+++
## 2.16.2
### User Visible Change
- Synthetic creation dialog has a "fractional" checkbox next to the tick settings. Users may uncheck this box to enter integer tick sizes, so the synthetic security's prices will be displayed without decimals, or fractional units.
## 2.16.1
### Bug Fixes
- OrderBook's algo state parameters (in the panel on the right side) are sorted alphabetically
## 2.16.0
### Bug Fixes
- Solved an issue that could result in incorrect ladder levels
- Solved an issue that could lead to incorrect routing of orders in the event of certain MSGW changes
### API Changes
- Add C# client side API samples for order entry, position monitoring and market data observation
## 2.15.10
### Bug Fixes
- Solved an issue that could cause the user interface to fail when playing some alert sounds
### API Changes
- Added settlement and position information to the API
- Added the ability for user algos to report custom state to the user interface
- Added the ability for algos to discover all active securities
## 2.15.3
### Bug Fixes
- Fixed a bug where some CME currency spreads exhibited incorrect tick values
@@ -708,3 +728,4 @@ insert_anchor_links = "right"
- Start, Day and Total PnL columns are painted with a red or green background color based on if the pnl value is negative or positive respectively
- Corrected cash per point values, used in pnl calculations, for a number of products
- PnL report will now remember any customizations you make to filtering and/or sorting criteria
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "2.1.508",
"rollForward": "latestMajor"
}
}
+4
View File
@@ -22,6 +22,10 @@
<GenDir>$(BaseOutputPath)\gen\$(MSBuildProjectName)</GenDir>
</PropertyGroup>
<PropertyGroup Condition="'$(VCTargetsPath)'==''">
<VCTargetsPath>C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\VC\VCTargets</VCTargetsPath>
</PropertyGroup>
<Import Project="$(ComputerSpecificConfig)" Condition="Exists($(ComputerSpecificConfig))" />
<ItemGroup>
+10 -1
View File
@@ -115,7 +115,7 @@ namespace mbts.ui.shell
{
try
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\BTS\\stunnel\\";
string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\BTS\\stunnel\\";
if (File.Exists(path + "config\\stunnel.conf"))
{
uint job_error = BTSUINativeMethods.setup_process_job_object();
@@ -124,6 +124,15 @@ namespace mbts.ui.shell
kill_stunnel();
start_stunnel(path, job_error);
}
path = AppDomain.CurrentDomain.BaseDirectory + "\\..\\stunnel\\";
if (File.Exists(path + "config\\stunnel.conf"))
{
uint job_error = BTSUINativeMethods.setup_process_job_object();
//if we can't put this process and stunnel into a job, try to stop stunnel then restart
if (job_error != 0)
kill_stunnel();
start_stunnel(path, job_error);
}
}
catch(Exception ex)
{
@@ -17,7 +17,7 @@ namespace bts.shell {
class MarketShellModule : IShellModule {
[ImportingConstructor]
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model) {
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub);
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub, orders_model.security_auxiliary_model);
_mvf = new MarketViewWindowFactory(this, bus, settings_hub, orders_model);
}
@@ -1,10 +1,12 @@
using bts.api;
using bts.api.orderman;
using bts.collections;
using bts.utils;
using mbts.co2;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
@@ -24,7 +26,6 @@ namespace mbts.spark {
readonly AnimationManager _animation = new AnimationManager();
readonly ObservableCollection<OrderRecordImmutable> _orders = new ObservableCollection<OrderRecordImmutable>();
readonly ObservableFilterCollection<OrderRecordImmutable> _working_orders, _working_bids, _working_asks;
readonly string _security_text, _security_key_text;
readonly Property<double> _bid_price, _ask_price, _last_price, _day_change, _settlement, _session_high, _session_low;
readonly Property<bool> _last_price_from_spread;
readonly CellProperty<int> _position;
@@ -33,48 +34,51 @@ namespace mbts.spark {
readonly CellProperty<bool> _inside_bids, _inside_asks;
readonly CellProperty<int> _working_bid_volume, _working_ask_volume;
readonly CellProperty<double> _working_best_bid, _working_best_ask;
readonly CellProperty<string> _working_bids_text, _working_asks_text;
readonly MyOrderSummarizer _order_summarizer = new MyOrderSummarizer();
readonly MDDisplayHelper.SizeFormatter _size_formatter = MDDisplayHelper.get_formatter(MarketSizeDisplay.get_default());
Property<uint> _bid_direct_size, _bid_merged_size, _ask_direct_size, _ask_merged_size;
PropertyAutoWrapper<uint, uint>
_bid_direct_size,
_bid_implied_size,
_bid_merged_size,
_ask_direct_size,
_ask_implied_size,
_ask_merged_size;
double _raw_day_position;
string _bid_merged_size_text, _ask_merged_size_text;
ulong _bbo_ref, _last_ref;
uint? _display_qty_divisor_backing;
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) : base(smd) {
_bus = bus;
_orders_model = orders_model;
_security_text = smd.security.ToString();
_security_key_text = security_key.ToString();
string format_price(double price) => security.ContractSpec.format(price);
_position = new CellProperty<int>(this, nameof(position), 0);
_position = new CellProperty<int>(this, nameof(position), fmt_display_qty_int);
_bid_price = backing(nameof(bid_price), double.NaN);
_ask_price = backing(nameof(ask_price), double.NaN);
_last_price = backing(nameof(last_price), double.NaN);
_last_size = new CellProperty<uint>(this, nameof(last_size), 0);
_last_size = new CellProperty<uint>(this, nameof(last_size), fmt_display_qty_uint);
_day_change = backing(nameof(day_change), double.NaN);
_session_low = backing(nameof(session_low), double.NaN);
_session_high = backing(nameof(session_high), double.NaN);
_last_price_from_spread = backing(nameof(last_price_from_spread), false);
_settlement = backing(nameof(settlement), double.NaN);
_volume = new CellProperty<ulong>(this, nameof(volume), 0);
_volume = new CellProperty<ulong>(this, nameof(volume), fmt_display_qty_ulong, 0);
_inside_bids = new CellProperty<bool>(this, nameof(inside_bids));
_inside_asks = new CellProperty<bool>(this, nameof(inside_asks));
_working_bid_volume = new CellProperty<int>(this, nameof(working_bid_volume));
_working_ask_volume = new CellProperty<int>(this, nameof(working_ask_volume));
//
_bid_direct_size = backing(nameof(bid_direct_size), default(uint));
_bid_merged_size = backing(nameof(bid_merged_size), default(uint));
_ask_direct_size = backing(nameof(ask_direct_size), default(uint));
_ask_merged_size = backing(nameof(ask_merged_size), default(uint));
_bid_direct_size = backing(nameof(bid_direct_size), 0u, convert_display_qty_uint);
_bid_implied_size = backing(nameof(bid_implied_size), 0u, convert_display_qty_uint);
_bid_merged_size = backing(nameof(bid_merged_size), 0u, convert_display_qty_uint);
_ask_direct_size = backing(nameof(ask_direct_size), 0u, convert_display_qty_uint);
_ask_implied_size = backing(nameof(ask_implied_size), 0u, convert_display_qty_uint);
_ask_merged_size = backing(nameof(ask_merged_size), 0u, convert_display_qty_uint);
//
_working_orders = new ObservableFilterCollection<OrderRecordImmutable>(_orders, o => o.is_active());
_working_bids = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Buy);
_working_asks = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Sell);
_working_best_bid = new CellProperty<double>(this, nameof(working_best_bid), format_price, double.NaN);
_working_best_ask = new CellProperty<double>(this, nameof(working_best_ask), format_price, double.NaN);
_working_bids_text = new CellProperty<string>(this, nameof(working_bids_text), string.Empty);
_working_asks_text = new CellProperty<string>(this, nameof(working_asks_text), string.Empty);
//
default_command = new DelegateCommand(_show_ladder);
bid_price_click = new DelegateCommand(() => _show_order_ticket(MarketViewView.Columns.Bid));
@@ -90,17 +94,45 @@ namespace mbts.spark {
_animation.AnimationsChanged += on_animations_changed;
void on_animations_changed() => RaisePropertyChanged(nameof(animations));
_dispose = Disposable.Create(
attach_prop_changed_cb(_bid_direct_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_direct_size_text))),
attach_prop_changed_cb(_bid_implied_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_implied_size_text))),
attach_prop_changed_cb(_bid_merged_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_merged_size_text))),
attach_prop_changed_cb(_ask_direct_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_direct_size_text))),
attach_prop_changed_cb(_ask_implied_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_implied_size_text))),
attach_prop_changed_cb(_ask_merged_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_merged_size_text))),
_orders_model.on_security_root_orders(security.Id).OnDispatcher().Subscribe(_on_orders),
_position.OnDispatcher().Subscribe(v => _animation.animate_position(v.newval > v.oldval)),
_orders_model.on_security_positions(security.Id).OnDispatcher().Subscribe(t => {
if (Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position)))
if (Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position))) {
RaisePropertyChanged(nameof(raw_day_position_text));
raise_invalidated();
}
}),
_inside_bids.OnDispatcher().Subscribe(v => _animation.animate_working_bids_inside(v.newval)),
_inside_asks.OnDispatcher().Subscribe(v => _animation.animate_working_asks_inside(v.newval)),
_day_change.OnDispatcher().Subscribe(v => _animation.animate_day_change(v.newval > v.oldval)),
Disposable.Create(() => _animation.AnimationsChanged -= on_animations_changed)
);
uint convert_display_qty_uint(uint value) =>
value.safe_divide(_display_qty_divisor);
string fmt_display_qty_int(int value) =>
value.safe_divide(_display_qty_divisor).ToString();
string fmt_display_qty_uint(uint value) =>
value.safe_divide(_display_qty_divisor).ToString();
string fmt_display_qty_ulong(ulong value) =>
value.safe_divide(_display_qty_divisor).ToString();
PropertyChange<T> mk_echo_prop_changed_cb<T>(params string[] props) =>
(oldval, newval) => RaisePropertyChanged(props);
IDisposable attach_prop_changed_cb<TView, TModel>(PropertyAutoWrapper<TModel, TView> prop, PropertyChange<(TModel, TView)> callback) {
prop.changed += callback;
return Disposable.Create(() => prop.changed -= callback);
}
}
public event Action invalidated;
@@ -109,32 +141,63 @@ namespace mbts.spark {
public bool inside_bids => _inside_bids.value;
public bool inside_asks => _inside_asks.value;
public int position => _position.value;
public string position_text => _position.text;
public double raw_day_position => _raw_day_position;
public string raw_day_position_text => SparkUtils.fmt_position(_raw_day_position.safe_divide(_display_qty_divisor));
public double last_price => _last_price.Value;
public bool last_price_from_spread => _last_price_from_spread.Value;
public uint last_size => _last_size.value;
public string last_size_text => _last_size.text;
// bid price/size
public double bid_price => _bid_price.Value;
public uint bid_direct_size => _bid_direct_size.Value;
public uint bid_implied_size => _bid_merged_size.Value - _bid_direct_size.Value;
public uint bid_merged_size => _bid_merged_size.Value;
public string bid_merged_size_text => _bid_merged_size_text ?? (_bid_merged_size_text = _size_formatter(_bid_merged_size.Value, bid_implied_size));
public uint bid_direct_size => _bid_direct_size.model;
public uint bid_implied_size => _bid_merged_size.model - _bid_direct_size.model;
public uint bid_merged_size => _bid_merged_size.model;
// bid size text
public string bid_direct_size_text => _bid_direct_size.view.ToString();
public string bid_implied_size_text => _bid_implied_size.view.ToString();
public string bid_merged_size_text => _bid_merged_size.view.ToString();
// ask price/size
public double ask_price => _ask_price.Value;
public uint ask_direct_size => _ask_direct_size.Value;
public uint ask_implied_size => _ask_merged_size.Value - _ask_direct_size.Value;
public uint ask_merged_size => _ask_merged_size.Value;
public string ask_merged_size_text => _ask_merged_size_text ?? (_ask_merged_size_text = _size_formatter(_ask_merged_size.Value, ask_implied_size));
public uint ask_direct_size => _ask_direct_size.model;
public uint ask_implied_size => _ask_implied_size.model;
public uint ask_merged_size => _ask_merged_size.model;
// ask size text
public string ask_direct_size_text => _ask_direct_size.view.ToString();
public string ask_implied_size_text => _ask_implied_size.view.ToString();
public string ask_merged_size_text => _ask_merged_size.view.ToString();
//
public ulong volume => _volume.value;
public string volume_text => _volume.text;
public double settlement => _settlement.Value;
public double day_change => _day_change.Value;
public double session_low => _session_low.Value;
public double session_high => _session_high.Value;
public int working_bid_volume => _working_bid_volume.value;
public int working_ask_volume => _working_ask_volume.value;
public string working_bids_text => OrderUtils.order_collection_summary(_order_summarizer.best_working_bid, _order_summarizer.best_working_bid_qty, _order_summarizer.working_bid_qty, security.ContractSpec);
public string working_asks_text => OrderUtils.order_collection_summary(_order_summarizer.best_working_ask, _order_summarizer.best_working_ask_qty, _order_summarizer.working_ask_qty, security.ContractSpec);
public string working_bids_text => OrderUtils.order_collection_summary(
_order_summarizer.best_working_bid,
_order_summarizer.best_working_bid_qty.safe_divide(_display_qty_divisor),
_order_summarizer.working_bid_qty.safe_divide(_display_qty_divisor),
security.ContractSpec);
public string working_asks_text => OrderUtils.order_collection_summary(
_order_summarizer.best_working_ask,
_order_summarizer.best_working_ask_qty.safe_divide(_display_qty_divisor),
_order_summarizer.working_ask_qty.safe_divide(_display_qty_divisor),
security.ContractSpec);
public double working_best_bid => _working_best_bid.value;
public double working_best_ask => _working_best_ask.value;
// ice props
public string market_description => security.market_description();
public string cleared_alias => security.cleared_alias();
public string product_description => security.product_name();
public string hub_alias => security.hub_alias();
public string strip_name => security.strip_name();
public string mic_code => security.mic_code();
// animations
public IList<ui.spark.CellAnimation> animations => _animation.get_animations().ToList();
public ObservableCollection<RowAnimation> row_animations { get; } = new ObservableCollection<RowAnimation>();
// commands
public ICommand bid_price_click { get; }
public ICommand bid_merged_size_click { get; }
public ICommand bid_direct_size_click { get; }
@@ -146,27 +209,30 @@ namespace mbts.spark {
public ICommand ask_implied_size_click { get; }
public ICommand working_asks_click { get; }
public ICommand default_command { get; }
public string market_description {get;}
public string cleared_alias { get; }
public string product_description { get; }
public string hub_alias { get; }
public string strip_name{ get; }
public string mic_code { get; }
public ObservableCollection<RowAnimation> row_animations { get; } = new ObservableCollection<RowAnimation>();
public IReadOnlyList<DIYGrid.IGridCellStore> cell_stores => new DIYGrid.IGridCellStore[0];
#endregion
uint _display_qty_divisor => _display_qty_divisor_backing ??
(_display_qty_divisor_backing = _orders_model.security_auxiliary_model.get_display_quantity_divisor(security)).Value;
public void raise_invalidated() => invalidated?.Invoke();
public void refresh() {
bool dirty = false;
if (md.md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
_bid_price.set(b.price);
refresh_size(ref b, _bid_direct_size, _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
// don't use the '||' operator - every 'set_new' call must be made
// (the operation mustn't terminate the first time a set_new call returns true).
dirty |= (
_bid_direct_size.set_new(b.direct_size) |
_bid_implied_size.set_new(b.implied_size) |
_bid_merged_size.set_new(b.size) |
_ask_direct_size.set_new(a.direct_size) |
_ask_implied_size.set_new(a.implied_size) |
_ask_merged_size.set_new(a.size));
_ask_price.set(a.price);
refresh_size(ref a, _ask_direct_size, _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
_inside_bids.on_next_distinct(_bid_price.Value >= _working_best_bid.value, ref dirty);
_inside_asks.on_next_distinct(_ask_price.Value <= _working_best_ask.value, ref dirty);
}
@@ -183,16 +249,6 @@ namespace mbts.spark {
_volume.on_next_distinct(md.md.total_volume, ref dirty);
if (dirty)
invalidated?.Invoke();
void refresh_size(ref NativePriceSize nps, Property<uint> direct, Property<uint> merged, ref string text, string text_prop) {
if (nps.size != merged.Value || nps.direct_size != direct.Value) {
merged.set(nps.size);
direct.set(nps.direct_size);
text = null;
dirty = true;
RaisePropertyChanged(text_prop);
}
}
}
public bool Equals(Row other) =>
@@ -9,6 +9,7 @@ using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Data;
using System.Windows.Media;
@@ -18,6 +19,9 @@ using static mbts.ui.spark.MarketViewView;
namespace mbts.spark {
partial class MarketViewVM : SecurityMDGrid<MarketViewVM.Row, SecurityMD> {
static IReadOnlyList<Func<MarketViewVM, IDataGridColumnFilter<Row>>> _filter_getters =
ObjectSnapshot.property_delegates_of_type<MarketViewVM, IDataGridColumnFilter<Row>>();
readonly IBus _bus;
readonly OrdersModel _orders_model;
IWindowSettingsManager<GridSettings> _settings_manager;
@@ -37,21 +41,21 @@ namespace mbts.spark {
const double working_orders_col_width = 105;
IMultiValueConverter price_converter = new ui.spark.converters.TickDisplayConverter();
add_col(Columns.Security, "Security", r => r.security.ToString(), nameof(Row.security), DefaultColumnWidths.Security, security_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Volume, "Volume", r => r.volume.ToString(), nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
add_col(Columns.Volume, "Volume", r => r.volume_text, nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
add_col(Columns.LastSpd, "Spd", r => r.last_price_from_spread ? "S" : "", nameof(Row.last_price_from_spread), DefaultColumnWidths.Side, last_price_from_spread_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Last, "Last", r => r.format_price(r.last_price), nameof(Row.last_price), DefaultColumnWidths.Price, last_price_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.RawDayPosition, "Raw Day Pos", r => SparkUtils.fmt_position(r.raw_day_position), nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size_text, nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.Position, "Pos", r => r.position_text, nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.RawDayPosition, "Raw Day Pos", r => r.raw_day_position_text, nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingBids, working_col_header, nameof(Row.working_bids_text), nameof(Row.working_best_bid), nameof(Row.working_bids_click), working_bid_volume_filter, BTSColors.MarketData.bid, System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size_text, nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size_text, nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.BidMergedSize, merged_size_col_header, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), nameof(Row.bid_merged_size_click), bid_merged_size_filter, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), nameof(Row.bid_price_click), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), nameof(Row.ask_price_click), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.AskMergedSize, merged_size_col_header, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), nameof(Row.ask_merged_size_click), ask_merged_size_filter, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size_text, nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size_text, nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingAsks, working_col_header, nameof(Row.working_asks_text), nameof(Row.working_best_ask), nameof(Row.working_asks_click), working_ask_volume_filter, BTSColors.MarketData.ask, System.Windows.HorizontalAlignment.Right);
add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
@@ -151,14 +155,16 @@ namespace mbts.spark {
working_ask_volume_filter.load_state(filters.working_ask_volume);
volume_filter.load_state(filters.volume);
security_key_filter.load_state(filters.security_key);
market_description_filter.load_state(filters.market_description);
cleared_alias_filter.load_state(filters.cleared_alias);
product_description_filter.load_state(filters.product_description);
hub_alias_filter.load_state(filters.hub_alias);
strip_name_filter.load_state(filters.strip_name);
mic_code_filter.load_state(filters.mic_code);
}
rows_view.set_sort_descriptions(s.sorting_criterion, columns);
}
}), DataGridFilterHelper.hook_filters(rows_view, security_filter,
bid_price_filter, bid_merged_size_filter, bid_direct_size_filter, bid_implied_size_filter,
ask_price_filter, ask_merged_size_filter, ask_direct_size_filter, ask_implied_size_filter,
last_price_filter, last_price_from_spread_filter, last_size_filter, day_change_filter, settlement_filter,
session_low_filter, session_high_filter, position_filter, raw_day_position_filter, working_bid_volume_filter, working_ask_volume_filter, volume_filter, security_key_filter),
}), DataGridFilterHelper.hook_filters(rows_view, _filters),
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager),
WorkLoop.Start("MarketView", _refresh)
);
@@ -233,13 +239,15 @@ namespace mbts.spark {
public IDataGridColumnPersistentFilter<Row, ULongMinMaxState> volume_filter { get; } = new ULongMinMax<Row>(r => r.volume, nameof(Row.volume));
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MarketDescription : "", nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ClearedAlias : "", nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ProductName : "", nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.market_description, nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.cleared_alias, nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.product_description, nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.hub_alias, nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.strip_name, nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.mic_code, nameof(Row.mic_code));
IEnumerable<IDataGridColumnFilter<Row>> _filters =>
_filter_getters.Select(fn => fn(this));
public FilterStates get_filter_states() {
return new FilterStates {
@@ -22,12 +22,15 @@ namespace bts.shell.time_and_sales {
.to_safe();
readonly TradeMessageImmutable _trade;
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
uint? _display_size;
public TimeAndSalesRowVM(int row_key, SecurityImmutable security, TradeMessageImmutable trade) {
public TimeAndSalesRowVM(int row_key, SecurityImmutable security, TradeMessageImmutable trade, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
this.row_key = row_key;
this.security = security;
_trade = trade;
trade_key = trade.Key;
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
}
public event Action invalidated;
@@ -39,6 +42,8 @@ namespace bts.shell.time_and_sales {
public DateTime time => ((ulong)_trade.Time).lldatetime().ToLocalTime();
public double price => _trade.price();
public uint size => _trade.Size;
public uint display_size => _display_size ??
(_display_size = size.safe_divide(_get_display_qty_divisor(security))).Value;
public AggressorSide aggressor_side => _trade.AggressorSide;
public void raise_invalidated() =>
@@ -40,6 +40,7 @@ namespace bts.shell.time_and_sales {
_refresh_filter,
_refresh_sort,
_refresh_selected_rows;
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
IComparer<TimeAndSalesRowVM> _comparer_backing;
Func<TimeAndSalesRowVM, bool> _filter_backing;
ImmutableHashSet<TimeAndSalesRowVM> _selected_rows = ImmutableHashSet<TimeAndSalesRowVM>.Empty;
@@ -58,8 +59,9 @@ namespace bts.shell.time_and_sales {
_filter_change_count,
_sort_change_count;
public TimeAndSalesVM(IWindowSettingsManager<GridSettings> settings_manager) {
public TimeAndSalesVM(IWindowSettingsManager<GridSettings> settings_manager, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
_sort_manager = new ColumnSortManager(columns.WrapSelector(c => c as ISortableDIYGridColumn, null), TimeAndSalesView.Columns.RowKey.ToString());
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
_comparer_backing = _sort_manager.BuildComparer(_get_column_comparer);
_log = new LoggerContext(Log.Ui, $"Time And Sales VM ({Interlocked.Increment(ref _id_counter)})", null);
_settings_manager = settings_manager;
@@ -95,7 +97,7 @@ namespace bts.shell.time_and_sales {
System.Windows.HorizontalAlignment.Right);
add_column(
TimeAndSalesView.Columns.Size,
r => r.size.ToString(),
r => r.display_size.ToString(),
DefaultColumnHeaders.Size,
DefaultColumnWidths.Size,
size_filter,
@@ -304,7 +306,7 @@ namespace bts.shell.time_and_sales {
}
var all_rows_bd = _all_rows.ToBuilder();
foreach (var t in trades) {
var r = _row_factory.create(t.security, t.trade);
var r = _row_factory.create(t.security, t.trade, _get_display_qty_divisor);
all_rows_bd.Add(r);
if (_filter_backing == null || _filter_backing(r))
rows.InsertSorted(r, _comparer);
@@ -400,11 +402,11 @@ namespace bts.shell.time_and_sales {
// the row key must be unique to each row in the collection
int _row_id_counter;
public TimeAndSalesRowVM create(TradeInfo trade_info) =>
new TimeAndSalesRowVM(_row_id_counter++, trade_info.security, trade_info.trade);
public TimeAndSalesRowVM create(TradeInfo trade_info, Func<SecurityImmutable?, uint> get_display_qty_divisor) =>
new TimeAndSalesRowVM(_row_id_counter++, trade_info.security, trade_info.trade, get_display_qty_divisor);
public TimeAndSalesRowVM create(SecurityImmutable security, TradeMessageImmutable trade) =>
new TimeAndSalesRowVM(_row_id_counter++, security, trade);
public TimeAndSalesRowVM create(SecurityImmutable security, TradeMessageImmutable trade, Func<SecurityImmutable?, uint> get_display_qty_divisor) =>
new TimeAndSalesRowVM(_row_id_counter++, security, trade, get_display_qty_divisor);
}
}
}
@@ -26,6 +26,7 @@ namespace bts.shell.time_and_sales {
readonly TimeAndSalesWindowFactory _factory;
readonly IWindowSettingsManager<GridSettings> _settings_manager;
readonly bool _can_pause_updates;
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
TimeAndSalesVM _vm;
TimeAndSalesModel _model;
SecurityCriteria _security_criteria_backing;
@@ -35,12 +36,21 @@ namespace bts.shell.time_and_sales {
SparkMenuDisplayVM _menu_display_vm;
IDisposable _subscriptions;
public TimeAndSalesWindow(TimeAndSalesWindowFactory factory, IBus bus, MDConsumer consumer, TimeAndSalesSettings state, ImageSource icon, IWindowSettingsManager<GridSettings> settings_manager, bool can_pause_updates)
public TimeAndSalesWindow(
TimeAndSalesWindowFactory factory,
IBus bus,
MDConsumer consumer,
TimeAndSalesSettings state,
ImageSource icon,
IWindowSettingsManager<GridSettings> settings_manager,
bool can_pause_updates,
Func<SecurityImmutable?, uint> get_display_qty_divisor)
: base("Time And Sales", bus, factory, SparkCommands.ShowTimeAndSales.icon) {
Log.assert(state != null);
_consumer = consumer;
_factory = factory;
_state = state ?? TimeAndSalesSettings.Default(new SecurityCriteria());
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
if (_state.security_criteria == null)
_state = _state.with(new SecurityCriteria());
_security_criteria_backing = _state.security_criteria;
@@ -86,7 +96,7 @@ namespace bts.shell.time_and_sales {
_settings_manager.instance_settings = _state.grid;
_menu_display_vm = new SparkMenuDisplayVM();
_menu_display_vm.load_from(_state.menu_display);
_vm = new TimeAndSalesVM(_settings_manager);
_vm = new TimeAndSalesVM(_settings_manager, _get_display_qty_divisor);
var selected_rows_src = Observable.Create<ImmutableHashSet<TimeAndSalesRowVM>>(o => {
_vm.selected_rows_changed += o.OnNext;
return Disposable.Create(() => _vm.selected_rows_changed -= o.OnNext);
@@ -20,12 +20,15 @@ namespace bts.shell {
readonly MDConsumer _consumer;
readonly UserSettingsHub _settings_hub;
readonly TimeAndSalesConfig _time_and_sales_conf;
readonly ISecurityAuxiliaryDataModel _security_auxiliary_model;
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
public TimeAndSalesWindowFactory(IShellModule module, IBus bus, MDConsumer consumer, UserSettingsHub settings_hub) : base(module, "TimeAndSales") {
public TimeAndSalesWindowFactory(IShellModule module, IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, ISecurityAuxiliaryDataModel security_auxiliary_model)
: base(module, "TimeAndSales") {
_bus = bus;
_consumer = consumer;
_settings_hub = settings_hub;
_security_auxiliary_model = security_auxiliary_model;
_time_and_sales_conf = new TimeAndSalesConfig(Configuration.get<bts.config.TimeAndSalesOptions>());
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
@@ -38,7 +41,8 @@ namespace bts.shell {
public override Task<IShellWindow> load(TimeAndSalesSettings state) =>
Task.FromResult<IShellWindow>(new TimeAndSalesWindow(this, _bus, _consumer, state, SparkCommands.PickTimeAndSales.icon,
new WindowSettingsManager<GridSettings>(_settings_hub, state.grid, s => s.time_and_sales_grid, (s, g) => s.with(time_and_sales:g)),
_time_and_sales_conf.can_pause_updates));
_time_and_sales_conf.can_pause_updates,
_security_auxiliary_model.get_display_quantity_divisor));
public async void show(SecurityCriteria criteria) {
var w = await load(new TimeAndSalesSettings(criteria));
@@ -549,24 +549,48 @@
</u:BindingUpdateGroupCollection>
</u:Behavior.BindingUpdateGroups>
</TextBox>
<TextBlock
<StackPanel
x:Name="PART_TickFactors_Label"
Grid.Column="2"
VerticalAlignment="Center"
Margin="{StaticResource Margin.LHeaderPanelItem}"
Text="Ticks"
IsEnabled="True"
Orientation="Horizontal"
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
/>
<c:TickFactorsPicker
>
<TextBlock
VerticalAlignment="Center"
Margin="{StaticResource Margin.LHeaderPanelItem}"
Text="Ticks ("
/>
<CheckBox
Margin="1,0,0,0"
IsChecked="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}}"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Padding="0"
>
fractional
</CheckBox>
<TextBlock VerticalAlignment="Center" Margin="1,0,0,0">)</TextBlock>
</StackPanel>
<Grid
x:Name="PART_TickFactors_Picker"
Grid.Column="3"
Margin="{StaticResource Margin.LHeaderPanelItem}"
HorizontalAlignment="Stretch"
IsEnabled="True"
SelectedTickFactors="{Binding TickFactors, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Margin="{StaticResource Margin.LHeaderPanelItem}"
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
/>
>
<c:TickFactorsPicker
HorizontalAlignment="Stretch"
SelectedTickFactors="{Binding TickFactors, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Visibility="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static cv:BooleanToVisibilityConverter.HiddenInstance}}"
/>
<c:TickUpDownInt
HorizontalAlignment="Stretch"
Minimum="1"
Value="{Binding IntegerTickSize, RelativeSource={RelativeSource TemplatedParent}}"
Visibility="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource FalseVisibilityConverter}}"
/>
</Grid>
<TextBlock
x:Name="PART_Pricing_Label"
Grid.Column="4"
@@ -52,6 +52,26 @@ namespace mbts.ui.spark {
public TickFactors TickFactors { get => GetValue(TickFactorsProperty) as TickFactors; set => SetValue(TickFactorsProperty, value); }
#endregion
#region UseFractionalTicks
public static readonly DependencyProperty UseFractionalTicksProperty = WPFHelper.create_dp<bool, ExecutionStrategyEditor>(
nameof(UseFractionalTicks), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, true);
public bool UseFractionalTicks {
get => (bool)GetValue(UseFractionalTicksProperty);
set => SetValue(UseFractionalTicksProperty, value);
}
#endregion
#region IntegerTickSize
public static readonly DependencyProperty IntegerTickSizeProperty = WPFHelper.create_dp<int, ExecutionStrategyEditor>(
nameof(IntegerTickSize), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 1);
public int IntegerTickSize {
get => (int)GetValue(IntegerTickSizeProperty);
set => SetValue(IntegerTickSizeProperty, value);
}
#endregion
#region IsNew
public static readonly DependencyProperty IsNewProperty =
DependencyProperty.Register(
@@ -36,6 +36,8 @@
ContractPickerModel="{Binding contract_picker_model}"
HedgePricing="{Binding hedge_pricing}"
HedgePricingItemsSource="{Binding hedge_pricing_options}"
UseFractionalTicks="{Binding use_fractional_ticks}"
IntegerTickSize="{Binding integer_tick_size}"
/>
<c:DialogSkirtOKCancel Grid.Row="1" OKCommand="{Binding commit}" CloseOnOK="False"/>
</Grid>
@@ -2,7 +2,6 @@
using bts.collections;
using bts.shell;
using bts.spark;
using bts.utils;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
@@ -30,8 +29,16 @@ namespace mbts.spark {
readonly Property<bool> _has_working_orders, _can_add_leg;
Security _synthetic;
bool _set_name_automatically, _set_ticks_automatically, _set_quantities_automatically, _set_price_mults_automatically, _is_setting_quantities, _is_setting_price_mults;
bool
_set_name_automatically,
_set_ticks_automatically,
_set_quantities_automatically,
_set_price_mults_automatically,
_is_setting_quantities,
_is_setting_price_mults,
_use_fractional_ticks = true;
TickFactors _tick_factors;
int _integer_tick_size = 1;
ExecutionStrategyImmutable _strategy;
HedgePricing _hedge_pricing;
@@ -44,6 +51,10 @@ namespace mbts.spark {
: this(synthetic, strategy, false) {
ExceptionAssert.InvalidOperation.That(synthetic.Id != 0, "This constructor may only be used for already-existing synthetic securities (id must be non-zero).");
_tick_factors = TickUtils.get_regional_ticks(synthetic.ContractSpec).low_tick;
if (TickUtils.is_integer_tick(synthetic.ContractSpec)) {
_use_fractional_ticks = false;
_integer_tick_size = DoubleHelper.to_optional_int32(synthetic.ContractSpec.LowTick) ?? 0;
}
title = $"Edit '{synthetic.Synthetic.Name}'";
icon = Icons.ModifySynthetic;
_has_working_orders = backing(nameof(has_working_orders), security_has_working_orders.OnDispatcher());
@@ -131,14 +142,14 @@ namespace mbts.spark {
flash_error_msg("Name already taken", SparkUtils.UI.MessageFlashDuration, nameof(name)).ignore();
valid = false;
}
if (_tick_factors == null || !_tick_factors.is_valid()) {
flash_error_msg("Please select ticks", SparkUtils.UI.MessageFlashDuration, nameof(tick_factors)).ignore();
if (use_fractional_ticks ? (_tick_factors == null || !_tick_factors.is_valid()) : integer_tick_size == 0) {
flash_error_msg("Please select ticks", SparkUtils.UI.MessageFlashDuration, use_fractional_ticks ? nameof(tick_factors) : nameof(integer_tick_size)).ignore();
valid = false;
}
if (valid) {
_synthetic.Synthetic.Legs.SetItems(leg_tuples.Select(l => l.leg));
_synthetic.Symbol = _deduce_symbol(_synthetic);
_synthetic.ContractSpec = _tick_factors.to_spec();
_synthetic.ContractSpec = use_fractional_ticks ? _tick_factors.to_spec() : TickUtils.create_integer_tick_spec(integer_tick_size);
uint? id = await model.create_synthetic(new SecurityImmutable(_synthetic));
if (id.HasValue && id != 0) {
if (await model.save_strategy(id.Value, get_strategy())) {
@@ -295,6 +306,20 @@ namespace mbts.spark {
public bool has_working_orders => _has_working_orders.Value;
public bool is_new { get; }
public bool is_new_leg_popup_open { get; set; }
public bool use_fractional_ticks {
get => _use_fractional_ticks;
set {
if (Set(ref _use_fractional_ticks, value))
_set_ticks_automatically = false;
}
}
public int integer_tick_size {
get => _integer_tick_size;
set {
if (Set(ref _integer_tick_size, value))
_set_ticks_automatically = false;
}
}
public bool can_add_leg => _can_add_leg.Value;
public SyntheticContract.Types.PriceCalc price_calc { get => _synthetic.Synthetic.PriceCalc; set => _synthetic.Synthetic.PriceCalc = value; }
public ObservableCollection<LegSpreaderParamsTuple> leg_tuples { get; } = new ObservableCollection<LegSpreaderParamsTuple>();
@@ -323,6 +348,16 @@ namespace mbts.spark {
_strategy = strategy;
hedge_pricing = strategy.HedgePricing;
leg_tuples.SetItems(_get_legs(_synthetic, strategy));
if (synthetic.HasContractSpec) {
/// assign the fields <see cref="_use_fractional_ticks"/> and <see cref="_integer_tick_size"/>,
/// rather than the properties <see cref="use_fractional_ticks"/> and <see cref="integer_tick_size"/>
/// because the setters of the former flip <see cref="_set_ticks_automatically"/> to false if the value changes.
/// This behavior causes the ticks to be automatically deduced as legs are added/removed,
/// until the user explicitly enters a different value (as the properties are bound to tick input controls in the view).
Set(ref _use_fractional_ticks, !TickUtils.is_integer_tick(synthetic.ContractSpec), nameof(use_fractional_ticks));
if (!use_fractional_ticks)
Set(ref _integer_tick_size, DoubleHelper.to_optional_int32(synthetic.ContractSpec.LowTick) ?? 0, nameof(integer_tick_size));
}
RaisePropertyChanged(nameof(name), nameof(tick_factors));
}
@@ -236,12 +236,20 @@ namespace mbts.spark {
get => algo_root;
set => algo_root = value;
}
IAlgoVM IAlgoParamVM<IAlgoVM>.display_value {
get => algo_root;
set => algo_root = value;
}
bool IAlgoParamVM.has_value => _algo_root != null;
bool IAlgoParamVM.is_enabled { get; set; } = true;
object IAlgoParamVM.value {
get => algo_root;
set => algo_root = (IAlgoVM)value;
}
object IAlgoParamVM.display_value {
get => algo_root;
set => algo_root = (IAlgoVM)value;
}
AlgoParamPermissions _permissions = AlgoParamPermissions.ReadWrite;
AlgoParamPermissions IAlgoParamVM.permissions { get => _permissions; set => _permissions = value; }
@@ -199,18 +199,30 @@
Grid.Column="1"
HorizontalAlignment="Center"
Foreground="{u:BTSColor MarketData.bid}"
Text="{Binding DataContext.bbo.bid_size, RelativeSource={RelativeSource AncestorType=usp:AlgoEditorGridView}}"
ToolTip="Best Bid Size"
/>
>
<TextBlock.Text>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="DataContext.bbo.bid_size" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock
x:Name="ask_text"
Grid.Column="2"
HorizontalAlignment="Center"
Margin="4,0"
Foreground="{u:BTSColor MarketData.ask}"
Text="{Binding DataContext.bbo.ask_size, RelativeSource={RelativeSource AncestorType=usp:AlgoEditorGridView}}"
ToolTip="Best Ask Size"
/>
>
<TextBlock.Text>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="DataContext.bbo.ask_size" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding path_disambiguation}" Value="{x:Static sys:String.Empty}">
@@ -272,6 +284,14 @@
</Grid>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<!--TODO: replace all the param.value bindings with param.display_value.
The reason this has not already been done is that the display_value property is new,
and only necessary for the size param as of 6/8/2020 (slightly more uniform code doesn't warrant rocking the boat).-->
<u:TemplateSelectorKeyValue Key="{x:Static usp:AlgoParamTemplateKeys.Size}">
<DataTemplate>
<TextBlock Text="{Binding param.display_value}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</usp:AlgoParamTemplateSelector.KeyedTemplates>
<!--Bool-->
<usp:AlgoParamTemplateSelector.BoolTemplate>
@@ -360,6 +380,22 @@
/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<!--TODO: replace all the param.value bindings with param.display_value.
The reason this has not already been done is that the display_value property is new,
and only necessary for the size param as of 6/8/2020 (slightly more uniform code doesn't warrant rocking the boat).-->
<u:TemplateSelectorKeyValue Key="{x:Static usp:AlgoParamTemplateKeys.Size}">
<DataTemplate>
<c:TickUpDownNullableInt Focusable="False" HorizontalAlignment="Stretch" Value="{Binding param.display_value, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl">
<c:TickUpDownNullableInt.CommandBindings>
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
</c:TickUpDownNullableInt.CommandBindings>
<c:TickUpDownNullableInt.InputBindings>
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
</c:TickUpDownNullableInt.InputBindings>
</c:TickUpDownNullableInt>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</usp:AlgoParamTemplateSelector.KeyedTemplates>
<!--Bool-->
<usp:AlgoParamTemplateSelector.BoolTemplate>
@@ -122,7 +122,7 @@ namespace mbts.spark {
add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, dv_filter_rx(_model.factory.subscribe_accounts().Select(d => d.Values.Ordered()), r => r.account), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account);
add_col(Columns.Exchange, DefaultColumnHeaders.Exchange, r => r.exchange.ToString(), dv_filter(EnumHelper.get_values<Venue>().ToArray(), r => r.security.exchange_or_default()), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Exchange);
add_col(Columns.TimeInForce, DefaultColumnHeaders.TimeInForce, r => r.time_in_force.ToString(), dv_filter(EnumHelper.get_values<TimeInForce>().ToArray(), r => r.order?.time_in_force() ?? TimeInForce.Undefined), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.TimeInForce);
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.display_size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
add_col(Columns.Price, DefaultColumnHeaders.Price, r => r.format_price(r.price), new DoubleMinMax<FillTuple>(r => r.fill.LastPrice), DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.Price);
add_col(Columns.FillKind, DefaultColumnHeaders.FillKind, r => r.fill_kind.ToString(), dv_filter(EnumHelper.get_values<FillKind>().ToArray(), r => r.fill.FillKind), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
add_col(Columns.FillStatus, DefaultColumnHeaders.FillStatus, r => r.fill_status.ToString(), dv_filter(EnumHelper.get_values<FillStatus>().ToArray(), r => r.fill.FillStatus), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
@@ -286,6 +286,7 @@ namespace mbts.spark {
Color? _root_algo_badge_color;
OrderMetadata _order_metadata;
bool _is_selected;
double? _display_size;
public Row(FillTuple fill_tuple, OrdersModel model) :
base(fill_tuple.security ?? SecurityDataHelper.Defaults.SecurityImmutable) {
@@ -306,6 +307,8 @@ namespace mbts.spark {
public Side side => _fill.Side;
public double price => _fill.LastPrice;
public double size => _fill.LastShares;
public double display_size => _display_size ??
(_display_size = size.safe_divide(_model.security_auxiliary_model.get_display_quantity_divisor(security))).Value;
public FillKind fill_kind => _fill.FillKind;
public FillStatus fill_status => _fill.FillStatus;
public string fill_key => _fill.FillKey;
@@ -4,6 +4,7 @@ using bts.shell.orderbook;
using mbts.persist;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -169,7 +170,8 @@ namespace mbts.spark {
}
private void _set_badge_text() {
text_lines = BadgeRender.format(side, quantity, filled, _current_badge_style);
var fmt = MDDisplayHelper.get_value_formatter(_owner.display_quantity_divisor);
text_lines = BadgeRender.format(side, quantity, filled, _current_badge_style, fmt);
RaisePropertyChanged(nameof(text_lines));
}
@@ -1,6 +1,5 @@
using bts.api;
using bts.api.orderman;
using bts.collections;
using bts.shell;
using bts.spark;
using bts.utils;
@@ -8,6 +7,7 @@ using mbts.co2;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.utils;
using System;
using System.Collections.Generic;
@@ -66,6 +66,8 @@ namespace mbts.spark {
current_settings = init;
this.settings = settings;
_equality = TickUtils.tick_equality_comparer(_model.security.ContractSpec);
display_quantity_divisor = model.order_gateway.security_auxiliary_model.get_display_quantity_divisor(model.security);
size_calculator.display_divisor = display_quantity_divisor;
_slot_price_vs_orders = new Dictionary<double, LadderOrderVM>(_equality);
this.order_properties_vm = order_properties_vm;
_high = backing(nameof(high), double.NaN);
@@ -292,6 +294,7 @@ namespace mbts.spark {
remove => _on_disposed_impl -= value;
}
public SecurityImmutable security => _model.security;
public uint display_quantity_divisor { get; }
public LadderSettings current_settings { get; private set; }
public double high => _high.Value;
public double low => _low.Value;
@@ -369,14 +372,14 @@ namespace mbts.spark {
get {
string rv = null;
if (show_day_position)
rv = SparkUtils.fmt_position(day_position);
rv = SparkUtils.fmt_position(day_position / display_quantity_divisor);
if (show_raw_day_position) {
var raw_day_text = SparkUtils.fmt_position(raw_day_position);
var raw_day_text = SparkUtils.fmt_position(raw_day_position / display_quantity_divisor);
rv = rv == null ? raw_day_text : $"{rv} / {raw_day_text}";
}
if (show_account_position) {
var acc_text = SparkUtils.fmt_position(position);
rv = rv == null ? SparkUtils.fmt_position(position) : $"{rv} ({acc_text})";
var acc_text = SparkUtils.fmt_position(position / display_quantity_divisor);
rv = rv == null ? acc_text : $"{rv} ({acc_text})";
}
return rv ?? "";
}
@@ -46,12 +46,12 @@ namespace mbts.spark {
void _reset_ladder_vm() {
if (_value == null && !_security_md.HasValue)
return;
Disposable.replace(ref _value,
_security_md.HasValue ?
LadderVM.create_readonly(_bus,
new LadderModel(security_md.Value, _model, _model.mbo_service,
new Func<OrderSecurity, bool>(osec => _selected_order_tree.Contains(osec.order.Key))),
Observable.Return(LadderSettings.sanitize(LadderSettings.Default))) : null);
_value?.Dispose();
_value = _security_md.HasValue ?
LadderVM.create_readonly(_bus,
new LadderModel(security_md.Value, _model, _model.mbo_service,
new Func<OrderSecurity, bool>(osec => _selected_order_tree.Contains(osec.order.Key))),
Observable.Return(LadderSettings.sanitize(LadderSettings.Default))) : null;
RaisePropertyChanged(nameof(value));
}
}
@@ -66,7 +66,8 @@ namespace mbts.spark {
double _last_origin_price;
MarketBreakLines _mkt_break_lines;
NativePriceSize _last_trade;
Func<NativePriceSize, string> _fmt_size = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default());
Func<NativePriceSize, string> _fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default());
Func<uint, string> _fmt_mkt_quantity_impl = v => v.ToString();
int _old_tooltip_row = int.MaxValue;
NativePriceSize? _old_last_trade;
string _direction_arrow = string.Empty;
@@ -468,7 +469,7 @@ namespace mbts.spark {
security_id: _vm.model.security.Id,
price_level: price_for_point(point)
);
var model = new MBOPopupModel(_vm.model.mbo_service, _vm.model.security, query);
var model = new MBOPopupModel(_vm.model.mbo_service, _vm.model.security, query, fmt_mbo_qty);
// Load the first set of orders so we know if there are any before showing the popup
var result = await model.load_orders();
if (result.success && result.value.orders.Count > 0) {
@@ -489,6 +490,8 @@ namespace mbts.spark {
};
view.RequestClose += (o, _) => popup.IsOpen = false;
}
string fmt_mbo_qty(int value) =>
value < 0 ? $"-{_fmt_mkt_quantity((uint)value)}" : _fmt_mkt_quantity((uint)value);
}
protected override void OnMouseDoubleClick(MouseButtonEventArgs e) {
@@ -534,14 +537,44 @@ namespace mbts.spark {
_size_from_row(row, _price_list, _asks, _vm.security.ContractSpec);
uint _volume_from_row(int row) {
if (_price_list != null) {
var price = _price_list[row];
var vapls = _volume_container.read_new_value();
if (vapls != null) {
return vapls.SelectFirstOrDefault(v => MathUtils.epsilon_equals(v.Price, price), v => v.Volume);
}
const int max_tries = 3;
var errors = new List<string>(0);
int num_tries = 0;
for (; num_tries < max_tries; num_tries++) {
try {
var rv = impl();
if (num_tries > 0) {
log_errors(true);
}
return rv;
} catch (Exception ex) {
errors.Add(ex.ToString());
}
}
log_errors(false);
return 0;
void log_errors(bool succeeded) {
// Only log distinct errors to avoid useless noise.
// It's likely that the exception will always be the same (IndexOutOfRange),
// however, this bug is not fully understood, so assume nothing.
var error_msg = string.Join("; ", errors.Distinct());
if (succeeded) {
Log.Ui.error($"Ladder '{security}' got volume for row at index '{row}' after {num_tries + 1} attempts (price_list array length is {_price_list.Length}). The following exceptions were observed: {error_msg}");
} else {
Log.Ui.error($"Ladder '{security}' failed to get volume for row at index '{row}' after {max_tries} attempts (price_list array length is {_price_list.Length}): {error_msg}");
}
}
uint impl() {
if (_price_list.SafeTryGetAt(row, out var price)) {
var vapls = _volume_container.read_new_value();
if (vapls != null) {
return vapls.SelectFirstOrDefault(v => MathUtils.epsilon_equals(v.Price, price), v => v.Volume);
}
}
return 0u;
}
return 0u;
}
@@ -626,7 +659,7 @@ namespace mbts.spark {
}
void render_market_col(NativePriceSize[] depth) {
var mid = LadderRender.index_of(_price_list, _last_origin_price, _price_comparer);
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings, _fmt_size);
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings, _fmt_mkt_depth_item);
}
}
_apply_column_visual_offset();
@@ -704,7 +737,7 @@ namespace mbts.spark {
_hide_tool_tip();
}
tooltip_view.Content = $"{volume}";
tooltip_view.Content = _fmt_mkt_quantity(volume);
tooltip_view.IsOpen = true;
this.ToolTip = tooltip_view;
} else {
@@ -820,7 +853,7 @@ namespace mbts.spark {
if (last_trade_changed) {
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
if (last_index > 0)
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column);
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
}
if (depth_changed)
_refresh_sizes();
@@ -862,21 +895,21 @@ namespace mbts.spark {
void _refresh_last_trade() {
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
if (last_index > 0)
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column);
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
}
void _refresh_piq() {
if (!_settings.piq_column.show)
return;
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, origin_index, _price_list, _vm.price_vs_piq, _settings.row_height, security.ContractSpec, _settings.piq_column);
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, origin_index, _price_list, _vm.price_vs_piq, _settings.row_height, security.ContractSpec, _settings.piq_column, _fmt_mkt_quantity);
}
void _refresh_sizes() {
Log.assert_d(_price_list != null);
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column, _fmt_size);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column, _fmt_size);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column, _fmt_mkt_depth_item);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column, _fmt_mkt_depth_item);
}
/// <summary>
@@ -898,7 +931,17 @@ namespace mbts.spark {
if (vapls != null) {
Log.assert_d(_price_list != null);
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
LadderRender.render_volumes(_volume_vis_tup.visual, _volume_vis_tup.glyph_context, _price_list, vapls, _settings.row_height, security.ContractSpec, _settings.volume_column, last_index, _get_last_arrow_string());
LadderRender.render_volumes(
_volume_vis_tup.visual,
_volume_vis_tup.glyph_context,
_price_list,
vapls,
_settings.row_height,
security.ContractSpec,
_settings.volume_column,
last_index,
_get_last_arrow_string(),
_fmt_mkt_quantity);
_old_last_trade = _last_trade;
}
@@ -921,7 +964,7 @@ namespace mbts.spark {
}
// regardless of if we've arrived here via a new Last (and new direction arrow)
// or we're reusing the old one, this format bit is the same
return $"{_direction_arrow} {_last_trade.size} ";
return $"{_direction_arrow} {_fmt_mkt_quantity(_last_trade.size)} ";
}
}
@@ -1079,7 +1122,9 @@ namespace mbts.spark {
GlyphContext glyph_context(LadderColumnSettings cs) =>
GlyphContext.create(cs.font_family, cs.font_weight);
_fmt_size = MDDisplayHelper.get_formatter_nps(_settings.size_display);
var mkt_qty_display_divisor = _vm?.display_quantity_divisor ?? 1;
_fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(_settings.size_display, mkt_qty_display_divisor);
_fmt_mkt_quantity_impl = MDDisplayHelper.get_value_formatter(mkt_qty_display_divisor);
_set_columns();
_apply_column_visual_offset();
if (prev_row_height != _settings.row_height)
@@ -1128,6 +1173,22 @@ namespace mbts.spark {
}
visible_columns_changed?.Invoke();
}
// This is designed to be a redundant null check for _fmt_mkt_depth_item_impl.
string _fmt_mkt_depth_item(NativePriceSize value) {
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
// This shouldn't happen, but better to avoid the potential null ref just in case it does. Expect the unexpected.
var impl = _fmt_mkt_depth_item_impl;
return impl is null ? MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default())?.ToString() ?? "Error" : impl(value);
}
// This is designed to be a redundant null check for _fmt_mkt_quantity_impl.
string _fmt_mkt_quantity(uint value) {
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
// This shouldn't happen, but better to avoid the potential null ref just in case it does. Expect the unexpected.
var impl = _fmt_mkt_quantity_impl;
return impl is null ? value.ToString() : impl(value);
}
void _init_tactile_adorner() {
Disposable.dispose(ref _tactile_adorner);
@@ -12,20 +12,24 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using bts.utils;
namespace mbts.spark {
static class BadgeRender {
// IMPORTANT: the return values for this method MUST BE IN SYNC with the "format" method, else "measure_height" and "LadderRender.measure_row_height" will be broken.
static int _line_count_for_style(LadderSettings.BadgeStyle badge_style) => badge_style == LadderSettings.BadgeStyle.Alternate ? 2 : 1;
// IMPORTANT: at the risk of being redundant, see above comment.
public static IReadOnlyList<string> format(Side side, uint quantity, uint filled, LadderSettings.BadgeStyle style) {
public static IReadOnlyList<string> format(Side side, uint quantity, uint filled, LadderSettings.BadgeStyle style, Func<uint, string> fmt_value = null) {
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
switch (style) {
case LadderSettings.BadgeStyle.Alternate:
var working_qty = quantity > filled ? quantity - filled : 0;
return new[] {
$"{side.abbreviate()}:{filled}",
$"W:{quantity - filled}"
$"{side.abbreviate()}:{fmt_value(filled)}",
$"W:{fmt_value(working_qty)}"
};
default: return new[] { $"{filled}/{quantity}" };
default: return new[] { $"{fmt_value(filled)}/{fmt_value(quantity)}" };
}
}
@@ -193,16 +197,17 @@ namespace mbts.spark {
}
public static void render_volumes(
DrawingVisual dv,
GlyphContext glyph_context,
double[] prices,
IEnumerable<VolumeAtPriceLevelImmutable> data,
double row_height,
ContractSpecificationImmutable spec,
VolumeColumnSettings settings,
int last_index,
string last_size_and_direction
) {
DrawingVisual dv,
GlyphContext glyph_context,
double[] prices,
IEnumerable<VolumeAtPriceLevelImmutable> data,
double row_height,
ContractSpecificationImmutable spec,
VolumeColumnSettings settings,
int last_index,
string last_size_and_direction,
Func<uint, string> fmt_value) {
//
if (!data.Any()) {
_clear_visual(dv);
return;
@@ -224,6 +229,8 @@ namespace mbts.spark {
_clear_visual(dv);
return;
}
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
// inflate the max so that the graph never fills the available space
double max = data.Max(v => v.Volume) * 1.1;
_render_cells(dv, row_height, data.Select(d => (d.Price, d)), settings.general, prices, spec, render_cell);
@@ -255,11 +262,11 @@ namespace mbts.spark {
}
x_bar += width;
if (show_numbers)
numbers_text.Add(v.ToString());
numbers_text.Add(fmt_value(v));
}
if (settings.total.show_numbers) {
numbers_text.Add(vap.Volume.ToString());
numbers_text.Add(fmt_value(vap.Volume));
}
if (settings.show_ltq && last_trade_here) {
numbers_text.Clear();
@@ -309,19 +316,33 @@ namespace mbts.spark {
}
}
internal static void render_last(DrawingVisual dv, GlyphContext glyph_context, NativePriceSize last, int index, double row_height, LadderColumnSettings settings) {
internal static void render_last(DrawingVisual dv, GlyphContext glyph_context, NativePriceSize last, int index, double row_height, LadderColumnSettings settings, Func<uint, string> fmt_value) {
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
using (var dc = dv.RenderOpen()) {
var gr = format_text(last.size.ToString(), index, row_height, settings, glyph_context);
var gr = format_text(fmt_value(last.size), index, row_height, settings, glyph_context);
dc.DrawGlyphRun(settings.foreground.rgb_brush(), gr);
}
}
// this is basically a copy of redner_sizes up above
internal static void render_piqs(DrawingVisual dv, GlyphContext glyph_context, int origin_index, double[] prices, IEnumerable<(double price, uint piq)> pvp, double row_height, ContractSpecificationImmutable spec, LadderColumnSettings settings) {
internal static void render_piqs(
DrawingVisual dv,
GlyphContext glyph_context,
int origin_index,
double[] prices,
IEnumerable<(double price, uint piq)> pvp,
double row_height,
ContractSpecificationImmutable spec,
LadderColumnSettings settings,
Func<uint, string> fmt_value) {
//
using (var dc = dv.RenderOpen()) {
using (_draw_with_clip(dc, settings, row_height, prices.Length)) {
if (!prices.within_range(origin_index) || pvp.is_null_or_empty())
return;
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
var foreground = settings.foreground.rgb_brush();
int curr_index = origin_index;
double curr_price = prices[curr_index];
@@ -332,7 +353,7 @@ namespace mbts.spark {
curr_index += tick_diff;
curr_price = price;
if (curr_index >= 0 && curr_index < prices.Length) {
var gr = format_text(entry.piq.ToString(), curr_index, row_height, settings, glyph_context);
var gr = format_text(fmt_value(entry.piq), curr_index, row_height, settings, glyph_context);
dc.DrawGlyphRun(foreground, gr);
} else break;
}
@@ -371,4 +392,4 @@ namespace mbts.spark {
using (dv.RenderOpen()) { }
}
}
}
}
@@ -426,9 +426,26 @@
Text="{Binding left_panel_width, Converter={StaticResource VolumeTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Grid.Column="2" Grid.Row="5" Text="{Binding volume, Mode=OneWay}" FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock
Grid.Column="2"
Grid.Row="5"
FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"
>
<TextBlock.Text>
<MultiBinding>
<MultiBinding.Converter>
<converters:MultiValueConverterPipe>
<x:Static MemberType="{x:Type converters:DynamicDoubleDivisorConverter}" Member="Instance"/>
<x:Static MemberType="{x:Type converters:ValueConverters}" Member="SimpleString"/>
</converters:MultiValueConverterPipe>
</MultiBinding.Converter>
<Binding Path="volume" Mode="OneWay"/>
<Binding Path="display_quantity_divisor" Mode="OneTime"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
<!-- Account combobox -->
<ComboBox DataContext="{Binding order_properties_vm}" Margin="2,10,2,2"
@@ -507,7 +524,7 @@
mbts:TextBoxBehavior.IncrementDecrementOnMouseClick="True"
HorizontalContentAlignment="Right"
Margin="2"
Text="{Binding size_calculator.value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding size_calculator.display_value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ContextMenu="{x:Null}"
/>
<!-- Qty quick buttons -->
@@ -522,7 +539,14 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding}" Content="{Binding}" Focusable="False"/>
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Content="{Binding}" Focusable="False">
<Button.CommandParameter>
<MultiBinding Converter="{x:Static converters:DynamicDoubleMultiplierConverter.Instance}">
<Binding/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=spark:LadderView}"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -3,6 +3,7 @@ using bts.api.position;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
@@ -25,24 +26,27 @@ namespace bts.shell.manual_fills {
readonly Property<bool> _is_dirty;
readonly Property<EditableItemState> _editable_item_state;
readonly DelegateProperty<bool> _is_valid;
readonly DisplayQuantityMultiplier _size = new DisplayQuantityMultiplier();
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
SecurityImmutable? _security;
AccountTuple? _account;
UserTuple? _user;
Side _side;
uint _size;
double _price;
bool _is_selected;
ManualFillData? _data;
IDisposable _committed_snapshot;
public ManualFillRow(ManualFillData data) {
public ManualFillRow(ManualFillData data, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
_data = data;
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
key = data.key;
_security = data.value.security;
_account = data.value.account;
_user = data.value.user;
_side = data.value.manual_fill.Side;
_size = data.value.manual_fill.Quantity;
_size.actual_value = data.value.manual_fill.Quantity;
_size.conversion_data = new QuantityConversionData(_get_display_qty_divisor(data.value.security), QuantityConversionData.OpType.DivideActual);
_price = data.value.manual_fill.Price;
if (placeholder_key.HasValue) {
_is_dirty = backing(nameof(is_dirty), false);
@@ -53,6 +57,7 @@ namespace bts.shell.manual_fills {
_editable_item_state = backing(nameof(editable_item_state), _is_dirty, ch => ch.newval ? EditableItemState.Modified : EditableItemState.None);
}
_is_valid = backing(nameof(is_valid), () => _validate(this));
_dispose = _size.connect_owner(this, nameof(size), nameof(display_size));
}
public event Action<bool> is_selected_changed;
@@ -65,8 +70,10 @@ namespace bts.shell.manual_fills {
public SecurityImmutable? security {
get => _security;
set {
if (_set(ref _security, value))
if (_set(ref _security, value)) {
_size.conversion_data = new QuantityConversionData(_get_display_qty_divisor(value), QuantityConversionData.OpType.DivideActual);
RaisePropertyChanged(nameof(spec));
}
}
}
public ContractSpecificationImmutable? spec => security.spec_or_default();
@@ -75,7 +82,8 @@ namespace bts.shell.manual_fills {
public UserTuple? user { get => _user; set => _set(ref _user, value); }
// side/size/price
public Side side { get => _side; set => _set(ref _side, value); }
public uint size { get => _size; set => _set(ref _size, value); }
public uint size { get => _size.actual_value; set => _size.actual_value = value; }
public uint display_size { get => _size.display_value; set => _size.display_value = value; }
public double price { get => _price; set => _set(ref _price, value); }
// is selected
public bool is_selected {
@@ -71,7 +71,7 @@
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Accounts}}" SelectedItem="{Binding account}"/>
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Users}}" SelectedItem="{Binding user}"/>
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Sides}}" SelectedItem="{Binding side}" Width="48"/>
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding size}"/>
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding display_size}"/>
<usp:PriceEntryControl Grid.Row="2" Grid.Column="10" MinWidth="55" Value="{Binding price}" ContractSpec="{Binding spec}"/>
</Grid>
<c:DialogSkirt Grid.Row="1" Margin="0,6,0,0">
@@ -1,4 +1,5 @@
using bts.api.core;
using bts.api;
using bts.api.core;
using bts.api.position;
using bts.collections;
using mbts.spark;
@@ -103,5 +104,8 @@ namespace bts.shell.manual_fills {
public async Task<bool> update_fills(IEnumerable<ManualFill> fills) =>
await _model.update_manual_fills(fills);
public uint get_display_qty_divisor(SecurityImmutable? security) =>
_model.security_auxiliary_model.get_display_quantity_divisor(security);
}
}
@@ -2,6 +2,7 @@ using bts.api;
using bts.api.core;
using bts.api.position;
using bts.collections;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
@@ -67,7 +68,7 @@ namespace bts.shell.manual_fills {
RXCollection.connect(_persisted_rows,
_model.manual_fills.OnDispatcher(),
Observable.Return(Comparer<ManualFillData>.Create((a, b) => a.key.CompareTo(b.key))),
mfd => new ManualFillRow(mfd),
mfd => new ManualFillRow(mfd, model.get_display_qty_divisor),
mfr => mfr.data),
_bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(m => {
security_map = new FilteredDictionary<uint, SecurityMD>(m, smd => smd.security.ContractCase == Security.ContractOneofCase.Future);
@@ -125,7 +126,8 @@ namespace bts.shell.manual_fills {
new ManualFillKey(_placeholder_id_counter++),
new ManualFillTuple(new ManualFillImmutable(new ManualFill {
Side = Side.Buy
}), null, accounts.FirstOrDefault(), users.FirstOrDefault())));
}), null, accounts.FirstOrDefault(), users.FirstOrDefault())),
_model.get_display_qty_divisor);
void _add_fill(ManualFillRow new_row) =>
_placeholder_rows.InsertSorted((new_row ?? (new_row = row_factory())).placeholder_key.HasValue ?
@@ -209,11 +211,20 @@ namespace bts.shell.manual_fills {
} else {
add_imported_fills();
}
void add_imported_fills() =>
_placeholder_rows.InsertSorted(parsed_fills.Select(pmf => new ManualFillRow(new ManualFillData(new ManualFillKey(_placeholder_id_counter++), new ManualFillTuple(new ManualFillImmutable(pmf.result),
pmf.result.SecurityKey != 0 && security_map.TryGetValue(pmf.result.SecurityKey, out var smd) ? smd.security : (SecurityImmutable?)null,
accounts.FirstOrDefault(a => a?.id == pmf.result.AccountId),
users.FirstOrDefault(a => a?.id == pmf.result.UserId))))), _row_key_comparer);
void add_imported_fills() {
_placeholder_rows.InsertSorted(parsed_fills.Select(mk_row), _row_key_comparer);
ManualFillRow mk_row(ParsedManualFill pmf) {
var data = new ManualFillData(
new ManualFillKey(_placeholder_id_counter++),
new ManualFillTuple(
new ManualFillImmutable(pmf.result),
pmf.result.SecurityKey != 0 && security_map.TryGetValue(pmf.result.SecurityKey, out var smd) ? smd.security : (SecurityImmutable?)null,
accounts.FirstOrDefault(a => a?.id == pmf.result.AccountId),
users.FirstOrDefault(a => a?.id == pmf.result.UserId)));
return new ManualFillRow(data, _model.get_display_qty_divisor);
}
}
} catch (Exception ex) {
BTSMessageBox.Error($"Error importing fills: {ex}.", ManualFillsWindowFactory.window_name);
}
@@ -212,12 +212,12 @@
<DataGridTemplateColumn c:DataGridEx.CellKey="{x:Static Size, MemberType={u:Nested usp:ManualFillsView.Columns}}" Header="Qty" SortMemberPath="side">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding size}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding display_size}" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<c:TickUpDownUInt MinWidth="40" Value="{Binding size, UpdateSourceTrigger=PropertyChanged}"/>
<c:TickUpDownUInt MinWidth="40" Value="{Binding display_size, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
@@ -597,19 +597,19 @@ namespace mbts.spark {
public class SizeColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
public SizeColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
: base(Columns.Size, "Qty", r => r.quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.quantity)) { }
: base(Columns.Size, "Qty", r => r.display_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.quantity)) { }
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.quantity.ToString(), position, row_height,
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.display_quantity.ToString(), position, row_height,
RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding);
}
public class FilledColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
public FilledColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
: base(Columns.Filled, "Filled Qty", r => r.filled_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.filled_quantity)) { }
: base(Columns.Filled, "Filled Qty", r => r.display_filled_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.filled_quantity)) { }
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.filled_quantity.ToString(), position, row_height,
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.display_filled_quantity.ToString(), position, row_height,
RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding);
}
@@ -1163,7 +1163,7 @@ namespace mbts.spark {
public override FrameworkElement view_factory() {
var tick_up_down = new TickUpDownNullableUIntCommit();
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.SourceValueProperty, nameof(OrderBookRowVM.prospective_quantity));
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.SourceValueProperty, nameof(OrderBookRowVM.prospective_display_quantity));
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.OnCommitProperty, nameof(OrderBookRowVM.modify_order_quantity_andor_price));
tick_up_down.InputBindings.Add(_create_keyinput_binding(Key.Enter, nameof(OrderBookRowVM.modify_order_quantity_andor_price)));
tick_up_down.InputBindings.Add(_create_keyinput_binding(Key.Escape, nameof(OrderBookRowVM.revert_prospective_order_quantity_andor_price)));
@@ -29,16 +29,16 @@ namespace mbts.spark {
int _depth_level;
SecurityImmutable? _security => order_tuple.order.security;
// for mini-ob
uint _prospective_quantity;
DisplayQuantityMultiplier _prospective_qty_translator_backing;
uint _display_qty_divisor_backing;
bool _display_qty_divisor_dirty = true;
double _prospective_price;
Action<bool> _on_selection_changed;
public OrderBookRowVM(OrderBookVM owner, OrderSecurityRootPair order_tuple, bool is_expanded, Action<bool> on_selection_changed) {
_owner = owner;
this.order_tuple = order_tuple;
is_visible = !is_child;
_is_expanded = is_expanded;
prospective_quantity = this.quantity;
prospective_price = this.price;
_on_selection_changed = on_selection_changed;
}
@@ -95,14 +95,18 @@ namespace mbts.spark {
public string last_price_text => _fmt_price(last_price);
public string avg_price_text => _fmt_price(avg_price);
public uint quantity => order.quantity();
public uint display_quantity => quantity.safe_divide(_display_qty_divisor);
public uint filled_quantity => order.filled();
public uint display_filled_quantity => filled_quantity.safe_divide(_display_qty_divisor);
public string last_msg => order.last_msg();
// piq
public uint piq => order_tuple.order.piq ?? uint.MaxValue;
public string piq_text => piq == uint.MaxValue ? string.Empty : piq.ToString();
public string piq_text => piq == uint.MaxValue ? string.Empty : piq.safe_divide(_display_qty_divisor).ToString();
public bool is_non_terminal => order.is_non_terminal();
public bool is_attachable { get => _is_attachable; set => SetVisual(ref _is_attachable, value); }
public bool is_detachable { get => _is_detachable; set => SetVisual(ref _is_detachable, value); }
public string filled_status_desc => order.filled_status_description(security.spec_or_default());
public string filled_status_desc => order.filled_status_description(security.spec_or_default(), _display_qty_divisor);
// Root Algo
public CustomOrderDefinition.CustomOneofCase root_algo_order_type => order_tuple.root.algo.value.CustomCase;
@@ -113,7 +117,6 @@ namespace mbts.spark {
// Tree
public bool is_parent { get => _is_parent; set => Set(ref _is_parent, value); }
public bool is_child => order.has_parent();
public bool is_visible { get; set; }
public bool is_expanded {
get => _is_expanded;
set {
@@ -131,12 +134,12 @@ namespace mbts.spark {
}
}
public uint prospective_quantity {
get => _prospective_quantity;
set {
if (Set(ref _prospective_quantity, value)) {
RaisePropertyChanged(nameof(is_qty_dirty));
}
}
get => _prospective_qty_translator.actual_value;
set => _prospective_qty_translator.actual_value = value;
}
public uint prospective_display_quantity {
get => _prospective_qty_translator.display_value;
set => _prospective_qty_translator.display_value = value;
}
public double prospective_price {
get => _prospective_price;
@@ -226,6 +229,29 @@ namespace mbts.spark {
}
}
uint _display_qty_divisor {
get {
_sync_display_qty_if_dirty();
return _display_qty_divisor_backing;
}
}
IDisplayTranslator<uint> _prospective_qty_translator {
get {
if (_prospective_qty_translator_backing == null) {
_prospective_qty_translator_backing = new DisplayQuantityMultiplier {
conversion_data = new QuantityConversionData(_display_qty_divisor, QuantityConversionData.OpType.DivideActual),
actual_value = quantity
};
_prospective_qty_translator.value_changed += () =>
RaisePropertyChanged(nameof(is_qty_dirty));
} else {
_sync_display_qty_if_dirty();
}
return _prospective_qty_translator_backing;
}
}
#region IHierarchicalRow
bool IHierarchicalRow.is_parent => is_parent;
bool IHierarchicalRow.is_expanded => is_expanded;
@@ -269,22 +295,35 @@ namespace mbts.spark {
if ((Columns.OrderType & dirty) != 0)
RaisePropertyChanged(nameof(order_type), nameof(order_metadata));
// quantity
if ((Columns.Size & dirty) != 0) {
RaisePropertyChanged(nameof(quantity), nameof(is_qty_dirty));
// we need to update the prospective_quantity here b/c an update could have come in while the row was in edit mode
// via the Mini OB update column mechanism (vs the EditableQty column)
// prospecive price does not need to be updated in the same manner b/c if the price is changed, the whole
// row will no longer match the filter
prospective_quantity = quantity;
}
// price
if ((Columns.Price & dirty) != 0) {
RaisePropertyChanged(nameof(price));
RaisePropertyChanged(nameof(is_price_dirty));
}
// security
if ((Columns.Security & dirty) != 0)
RaisePropertyChanged(nameof(security), nameof(security_text));
if ((Columns.Security & dirty) != 0) {
_display_qty_divisor_dirty = true;
RaisePropertyChanged(
// security props
nameof(security),
nameof(security_text),
// qty props
nameof(display_quantity),
nameof(prospective_quantity),
nameof(prospective_display_quantity),
nameof(is_qty_dirty),
//
nameof(piq_text),
nameof(display_filled_quantity));
}
if ((Columns.Size & dirty) != 0) {
RaisePropertyChanged(nameof(quantity), nameof(display_quantity));
if (_prospective_qty_translator_backing != null)
_prospective_qty_translator_backing.actual_value = quantity;
}
if ((Columns.Filled & dirty) != 0) {
RaisePropertyChanged(nameof(filled_quantity), nameof(display_filled_quantity));
}
// badge fill
if (old_order_tuple.order.meta?.badge_color != order_tuple.order.meta?.badge_color) {
// invalidate by setting to null (will be re-instantiated in the public getter)
@@ -361,6 +400,15 @@ namespace mbts.spark {
security.Value.ContractSpec.format(v) :
Math.Round(v, _unknown_security_price_num_fractional_digits, MidpointRounding.AwayFromZero).ToString(_unknown_security_price_format_string);
void _sync_display_qty_if_dirty() {
if (_display_qty_divisor_dirty) {
_display_qty_divisor_dirty = false;
_display_qty_divisor_backing = _owner.get_display_qty_divisor(security);
if (_prospective_qty_translator_backing != null)
_prospective_qty_translator_backing.conversion_data = new QuantityConversionData(_display_qty_divisor_backing, QuantityConversionData.OpType.DivideActual);
}
}
void RaisePropertyChanged(params string[] props) {
var pc = PropertyChanged;
if (pc == null)
@@ -399,6 +447,7 @@ namespace mbts.spark {
Columns.LastMessage |
Columns.Piq,
filled_status_desc_mask =
Columns.Security |
Columns.Filled |
Columns.AvgFilled |
Columns.Status;
@@ -51,6 +51,7 @@ namespace mbts.spark {
readonly OrderParamsAggregator _selected_params_mon;
readonly CommonValueMonitor<SecurityImmutable?> _selected_security_mon = new CommonValueMonitor<SecurityImmutable?>();
readonly RightPanelModel _right_panel = new RightPanelModel();
readonly CustomStateSynchronizer _custom_state_sync;
OrderBookFillsGridSettings _cached_fills_settings = OrderBookStartupData.DefaultFillsGrid;
double
_raw_right_panel_width = OrderBookStartupData.Default.right_panel_width,
@@ -124,6 +125,8 @@ namespace mbts.spark {
_row_collection.CollectionChanged += _row_collection_CollectionChanged;
_current_row = backing<OrderBookRowVM>(nameof(current_row));
_columns.filter_changed += _on_filter_changed;
_custom_state_sync = new CustomStateSynchronizer(this);
// commands
collapse_all_rows_command = new DelegateCommand(_row_collection.collapse_all);
expand_all_rows_command = new DelegateCommand(_row_collection.expand_all);
var modify_selected_orders_async_cmd = new AsyncCommand(_modify_selected_orders_async, () => _selected_params_mon.all_active && order_mod_vm.is_dirty);
@@ -188,6 +191,7 @@ namespace mbts.spark {
security_plurality = p;
RaisePropertyChanged(nameof(security_plurality));
}),
_current_row.Subscribe(_ => _custom_state_sync.sync()),
orders_source.OnDispatcher().Subscribe(t => {
if (_selected_rows.Count == 0) {
_cached_orders = t.state;
@@ -207,6 +211,8 @@ namespace mbts.spark {
}
}
}
_custom_state_sync.sync(true);
orders_changed?.Invoke();
}),
model.on_orders_can_attach.OnDispatcher().Subscribe(table => {
_cached_attachable = new MappedDictionary<uint, KeyValuePair<uint, bool>, bool>(table.state, kv => kv.Value);
@@ -375,7 +381,7 @@ namespace mbts.spark {
}
public event Action<OrderRecordImmutable?> selected_order_changed;
public event Action begin_attaching, end_attaching;
public event Action begin_attaching, end_attaching, orders_changed;
public event Action<(IEnumerable, bool)> request_selection;
public ObservableAppendCollection<DIYGrid.ColumnBase> columns { get; }
@@ -395,6 +401,7 @@ namespace mbts.spark {
}
}
public OrderRecordImmutable? current_order => current_row?.order;
public ObservableCollection<NamedValueVM> current_algo_custom_state { get; } = new ObservableCollection<NamedValueVM>();
public AlgoDefinitionVM algo_vm => order_mod_vm.algo_vm;
public OrderParamsVM order_mod_vm { get; }
public FillsVMContainer fills_vm { get; } = new FillsVMContainer();
@@ -474,6 +481,28 @@ namespace mbts.spark {
public IObservable<bool> subscribe_show_fills() => _show_fills.observe_current(true);
public IObservable<bool> subscribe_show_ladder() => _show_ladder.observe_current(true);
public IObservable<bool> subscribe_show_order_params() => _show_order_params.observe_current(true);
public IObservable<OrderSecurityAlgo?> subscribe_single_selected_order() =>
Observable.Create<OrderSecurityAlgo?>(o => {
OrderSecurityAlgo? last_fired = null;
orders_changed += notify;
_current_row.Changed += on_current_row_changed;
notify();
return Disposable.Create(dispose);
// local methods
void notify() {
var next = current_row == null ? (OrderSecurityAlgo?)null : current_row.order_tuple.order;
if (next != last_fired)
o.OnNext(last_fired = next);
}
void on_current_row_changed(OrderBookRowVM oldval, OrderBookRowVM newval) =>
notify();
void dispose() {
orders_changed -= notify;
_current_row.Changed -= on_current_row_changed;
}
});
public void row_selection_changed(SelectionChangedEventArgs e) {
try {
@@ -503,6 +532,7 @@ namespace mbts.spark {
_can_attach_selected_orders.refresh();
_can_detach_selected_orders.refresh();
selected_order_changed?.Invoke(current_order);
_custom_state_sync.sync();
} catch (Exception ex) {
// [John]I observed a strange null-reference exception in this callback when the launcher was shutting down (see https://bluetradesys.atlassian.net/browse/SPARK-2089)
Log.Ui.error($"[{nameof(OrderBookVM)}] Error handling selection change: {ex}");
@@ -609,6 +639,9 @@ namespace mbts.spark {
public OrderMetadata get_metadata(OrderBookRowVM row) => _model.factory.get_metadata(row.order.actual_order_type());
internal uint get_display_qty_divisor(SecurityImmutable? security) =>
_model.security_auxiliary_model.get_display_quantity_divisor(security);
internal void show_order_modify(OrderBookRowVM row) =>
_bus.post(SparkTags.ShowModifyOrderTag, row.order_key);
@@ -723,13 +756,52 @@ namespace mbts.spark {
}
}
class CustomStateSynchronizer {
readonly OrderBookVM _vm;
OrderBookRowVM _synced_row;
public CustomStateSynchronizer(OrderBookVM vm) =>
_vm = vm;
public void sync(bool force_refresh = false) {
var row = _vm.current_row != null &&
_vm.selected_rows.Select(r => r.order_tuple.root.order.Key).Distinct().TryGetSingle(out uint droot) &&
_vm.current_row.order_tuple.root.order.Key == droot ?
_vm.current_row : null;
if (force_refresh || row != _synced_row) {
_synced_row = row;
_refresh();
}
}
void _refresh() {
_vm.current_algo_custom_state.Clear();
if (_synced_row == null)
return;
var dcs = _synced_row.order.dynamic_custom_state();
// while dcs is not expected to be null, best to play it safe and double check.
if (dcs.is_null_or_empty()) {
var descendent_rows = _vm._row_collection.get_descendent_rows(_synced_row.order_key);
if (_vm._row_collection.filter != null)
descendent_rows = descendent_rows.Where(child => _vm._row_collection.filter(child.order_tuple));
foreach (var dr in descendent_rows) {
dcs = dr.order.dynamic_custom_state();
if (!dcs.is_null_or_empty())
break;
}
}
foreach (var p in dcs.empty_if_null().OrderBy(kv => kv.Key))
_vm.current_algo_custom_state.Add(new NamedValueVM(p.Key, DynamicOrderHelper.fmt_param(p.Value)));
}
}
public class OrderParamsVM : DisposableViewModelBase {
readonly SelectorProperty<AccountTuple, AccountTuple?> _accounts;
readonly OrderBookVM _owner;
readonly Property<bool> _is_dirty;
IRaiseCanExecuteChangedCommand _payup_bid_cmd, _payup_ask_cmd;
IAlgoParamVM<double> _price_param;
IAlgoParamVM<uint?> _size_param;
AlgoParamVM<uint?> _size_param;
AlgoParamPermissions
_price_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write,
_size_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write;
@@ -766,7 +838,9 @@ namespace mbts.spark {
_price_param = null;
_size_param = null;
} else {
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size) {
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size, EqualityComparer<uint?>.Default,
to_display_value: v => v.HasValue ? v.Value / display_quantity_divisor : v,
from_display_value: v => v.HasValue ? v.Value * display_quantity_divisor : v) {
permissions = _size_permissions & _param_permissions
};
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_eq_comparer) {
@@ -809,10 +883,11 @@ namespace mbts.spark {
get => _security_md;
set {
if (Set(ref _security_md, value)) {
RaisePropertyChanged(nameof(security), nameof(spec));
RaisePropertyChanged(nameof(security), nameof(spec), nameof(display_quantity_divisor));
bbo.security_md = value;
_payup_bid_cmd.RaiseCanExecuteChanged();
_payup_ask_cmd.RaiseCanExecuteChanged();
_size_param?.refresh_display_value();
}
}
}
@@ -834,6 +909,7 @@ namespace mbts.spark {
public AlgoDefinitionVM algo_vm { get; }
public IReadOnlyObservableCollection<AlgoParamRowVM> param_rows { get; }
public BBOVM bbo { get; }
public uint display_quantity_divisor => _owner.get_display_qty_divisor(security);
public ICommand send_order { get; }
public ICommand payup_bid_cmd => _payup_bid_cmd;
public ICommand payup_ask_cmd => _payup_ask_cmd;
@@ -1,4 +1,5 @@
using bts.api;
using bts;
using bts.api;
using bts.api.orderman;
using bts.shell;
using bts.shell.fills;
@@ -6,9 +7,11 @@ using bts.shell.orderbook;
using bts.spark;
using bts.view;
using mbts.persist;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.extensions;
using mbts.ui.spark;
using mbts.ui.utilities;
using mbts.ui.utility;
using mbts.utils;
using System;
@@ -24,7 +27,10 @@ namespace mbts.spark {
const string _window_type = "OrderBook", _default_title = "Order Book";
readonly INotificationService _notification_service;
readonly IWindowSettingsManager<OrderBookGridSettings> _settings_manager;
readonly bool _can_edit_specific_algo_params;
IDisposable _attachment_exit_sub;
// initialized in thread_init
List<IDisposable> _thread_init_resources;
OrderBookStartupData _state;
OrdersModel _model;
OrderBookVM _vm;
@@ -34,13 +40,14 @@ namespace mbts.spark {
SparkCommand[] _commands;
public OrderBookWindow(OrdersModel model, OrderBookWindowFactory factory, IBus bus, INotificationService notification_service,
OrderBookStartupData state, ImageSource icon, IWindowSettingsManager<OrderBookGridSettings> settings_manager)
OrderBookStartupData state, ImageSource icon, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool can_edit_specific_algo_params)
: base(_default_title, bus, factory, SparkCommands.ShowOrders.icon) {
_model = model;
_factory = factory;
_notification_service = notification_service;
_state = state;
_settings_manager = settings_manager;
_can_edit_specific_algo_params = can_edit_specific_algo_params;
thread_id = config.Configuration.get<bts.config.MultiThreadedWindows>().order_book ? "OrderBook" : null;
}
@@ -64,12 +71,14 @@ namespace mbts.spark {
public string thread_id { get; }
public override void Dispose() {
_commands.OfType<IDisposable>().DisposeAll();
Disposable.dispose(_thread_init_resources);
Disposable.dispose(ref _attachment_exit_sub);
Disposable.dispose(ref _vm);
}
public void thread_init() {
Log.assert(_thread_init_resources == null, $"{nameof(_thread_init_resources)} must be initialized in thread_init.");
_thread_init_resources = new List<IDisposable>();
_menu_display_vm = new SparkMenuDisplayVM();
if (_state != null)
_menu_display_vm.load_from(_state.menu_display);
@@ -147,6 +156,15 @@ namespace mbts.spark {
can_execute: () => _vm.selected_orders.Any(x => passes_recreate_search_criteria(x))
);
recreate_selected_orders_command.group = CommandGroups.Modify;
ICommand edit_algo_params_cmd;
if (_can_edit_specific_algo_params) {
_thread_init_resources.Add(CommandHelper.bind_param_source(
new DialogCommand<OrderSecurityAlgo?, Window>(mk_algo_params_dialog, has_algo_params),
_vm.subscribe_single_selected_order(),
out edit_algo_params_cmd, true));
} else {
edit_algo_params_cmd = DelegateCommand.empty;
}
var attach_selected_orders_command = new SparkCommandCommand(
"Attach To Selected",
@@ -194,14 +212,22 @@ namespace mbts.spark {
show_grid_settings_command.group = CommandGroups.Auxiliary;
_commands = new SparkCommand[] {
// modify commands
toggle_show_right_panel_command,
attach_selected_orders_command,
detach_selected_orders_command,
recreate_selected_orders_command,
new SparkCommandCommand("Algo Params...", Icons.Algo, edit_algo_params_cmd) {
group = CommandGroups.Modify,
show_in_toolbar = false,
show_in_context_menu = _can_edit_specific_algo_params
},
// cancel commands
cancel_selected_orders_command,
cancel_buy_orders_command,
cancel_sell_orders_command,
cancel_all_orders_command,
toggle_collapse_all_command,
toggle_expand_all_command,
show_grid_settings_command,
@@ -210,9 +236,12 @@ namespace mbts.spark {
hidden_cmd(new SparkCommandCommand(OrderBookView.CommandDescriptions.RevertOrders, null, _vm.revert_selected_orders_cmd, OrderBookView.KeyboardShortcuts.RevertOrders)),
_menu_display_vm.spark_cmd
};
_vm.as_demuxer().subscribe<bool>(nameof(OrderBookVM.show_right_panel))
.Subscribe(_ => toggle_show_right_panel_command.title = get_toggle_show_right_panel_cmd_title());
_thread_init_resources.AddRange(_commands.OfType<IDisposable>());
_thread_init_resources.Add(_vm.as_demuxer()
.subscribe<bool>(nameof(OrderBookVM.show_right_panel))
.Subscribe(_ => toggle_show_right_panel_command.title = get_toggle_show_right_panel_cmd_title()));
_vm.selected_order_changed += vm_selected_order_changed;
_thread_init_resources.Add(Disposable.Create(() => _vm.selected_order_changed -= vm_selected_order_changed));
// local methods
string get_toggle_show_right_panel_cmd_title() =>
_vm.show_right_panel ? OrderBookView.CommandDescriptions.HideRightPanel : OrderBookView.CommandDescriptions.ShowRightPanel;
@@ -234,6 +263,36 @@ namespace mbts.spark {
attach_selected_orders_command.raiseCanExecuteChanged();
detach_selected_orders_command.raiseCanExecuteChanged();
}
bool has_algo_params(OrderSecurityAlgo? order) =>
order.HasValue && !OrderDataHelper.get_metadata(order.Value.algo.value.CustomCase).NoProps;
Window mk_algo_params_dialog(OrderSecurityAlgo? order) {
if (!has_algo_params(order))
return null;
var algo_def = AlgoDefinitionVM.create(_model, order.Value.algo, editing_permissions: AlgoEditingPermissions.AllowEditValues);
var algo_params_vm = new OrderBookAlgoParamsVM(algo_def, order.Value.order.is_non_terminal());
var dialog = new OrderBookAlgoParamsDialog {
Content = algo_params_vm
};
ui.WindowStyle.SetTitleContent(dialog, algo_def);
dialog.Closed += (s, e) => {
Disposable.dispose(ref algo_params_vm);
dialog = null;
};
algo_params_vm.RequestCommit += commit;
algo_params_vm.RequestClose += (s, e) => close_dialog();
return dialog;
void close_dialog() {
dialog?.Close();
dialog = null;
}
void commit(CustomOrderDefinitionImmutable value) {
_model.update_order(order.Value.order, new_algo: value).ignore();
close_dialog();
}
}
}
protected override void OnParentViewCreated(SparkWindowView parent_view) {
@@ -1,6 +1,7 @@
using bts.api;
using bts.shell.orderbook;
using bts.spark;
using mbts.config;
using mbts.persist;
using mbts.spark;
using mbts.ui;
@@ -15,8 +16,11 @@ namespace bts.shell {
readonly INotificationServiceDataCache _notification_service_data_cache;
readonly OrdersModel _model;
readonly UserSettingsHub _settings_hub;
readonly OrderBookConfig _config;
public OrderBookWindowFactory(IShellModule owner, IBus bus, INotificationService notification_service, INotificationServiceDataCache notification_service_data_cache, OrdersModel model, UserSettingsHub settings_hub) : base(owner, "SparkOrderBookWindow") {
var raw_config = Configuration.get<bts.config.OrderBookOptions>();
_config = new OrderBookConfig(raw_config.can_edit_specific_algo_params);
_bus = bus;
_notification_service = notification_service;
_notification_service_data_cache = notification_service_data_cache;
@@ -29,7 +33,8 @@ namespace bts.shell {
var settings_manager = new WindowSettingsManager<OrderBookGridSettings>(_settings_hub, state.grid_settings_wrapper(),
us => us.order_book_grid ?? OrderBookStartupData.DefaultGrid,
(us, s) => us.with(order_book_grid: s));
var w = new OrderBookWindow(_model, this, _bus, _notification_service, state, SparkCommands.ShowOrders.icon, settings_manager.as_non_shared());
var w = new OrderBookWindow(_model, this, _bus, _notification_service, state,
SparkCommands.ShowOrders.icon, settings_manager.as_non_shared(), _config.can_edit_specific_algo_params);
return Task.FromResult<IShellWindow>(w);
}
@@ -37,5 +42,12 @@ namespace bts.shell {
var w = await load(startup_data);
_bus.post(SparkTags.ShowWindow, w);
}
readonly struct OrderBookConfig {
public readonly bool can_edit_specific_algo_params;
public OrderBookConfig(bool can_edit_specific_algo_params) =>
this.can_edit_specific_algo_params = can_edit_specific_algo_params;
}
}
}
@@ -0,0 +1,40 @@
<Window
x:Class="bts.shell.orderbook.OrderBookAlgoParamsDialog"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sp="clr-namespace:mbts.spark"
xmlns:ob="clr-namespace:bts.shell.orderbook"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
u:WindowStyle.SizeToContent="WidthAndHeight"
Margin="0"
Padding="0"
Style="{StaticResource BTSWindowTitleTemplate}"
>
<u:WindowStyle.TitleContentTemplate>
<DataTemplate DataType="{x:Type sp:AlgoDefinitionVM}">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<usp:OrderTypeItem Margin="4,0" DisplayName="{Binding name, StringFormat='{}{0} Params'}" BadgeFill="{Binding badge_fill}"/>
</StackPanel>
</DataTemplate>
</u:WindowStyle.TitleContentTemplate>
<Window.ContentTemplate>
<DataTemplate DataType="{x:Type ob:OrderBookAlgoParamsVM}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<usp:AlgoEditorGridView
Width="390"
Margin="5"
AlgoParamRows="{Binding param_rows}"
CanUserChangeAlgoRoot="False"
/>
<c:DialogSkirt Grid.Row="1" RightItems="{Binding dialog_skirt}"/>
</Grid>
</DataTemplate>
</Window.ContentTemplate>
</Window>
@@ -0,0 +1,49 @@
using bts.api.orderman;
using bts.collections;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Windows;
namespace bts.shell.orderbook {
/// <summary>
/// Interaction logic for OrderBookAlgoParamsDialog.xaml
/// </summary>
partial class OrderBookAlgoParamsDialog : Window {
public OrderBookAlgoParamsDialog() =>
InitializeComponent();
}
class OrderBookAlgoParamsVM : DisposableViewModelBase, IRequestViewClose {
readonly ObservableAggregateCollection<AlgoParamRowVM> _param_rows = new ObservableAggregateCollection<AlgoParamRowVM>();
readonly DialogSkirtItem _commit, _cancel;
public OrderBookAlgoParamsVM(AlgoDefinitionVM algo_definition, bool can_user_edit) {
this.algo_definition = algo_definition;
_dispose = AlgoParamRowsHelper.connect(algo_definition, _param_rows, false);
_commit = new DialogSkirtItem("OK", new DelegateCommand(commit, () => can_user_edit)) { is_default = true };
_cancel = new DialogSkirtItem("Cancel", new DelegateCommand(close)) { is_cancel = true };
// local methods
void commit() {
RequestCommit?.Invoke(new CustomOrderDefinitionImmutable(algo_definition.algo_root.created_assigned()));
close();
}
void close() => RequestClose?.Invoke(this, EventArgs.Empty);
}
public event EventHandler RequestClose;
public event Action<CustomOrderDefinitionImmutable> RequestCommit;
public AlgoDefinitionVM algo_definition { get; }
public IReadOnlyObservableCollection<AlgoParamRowVM> param_rows => _param_rows;
public IEnumerable<DialogSkirtItem> dialog_skirt {
get {
yield return _commit;
yield return _cancel;
}
}
}
}
@@ -62,7 +62,7 @@
</Setter>
</Style>
<Style x:Key="{x:Static usp:OrderParamsPanel.BestBidSizeButtonStyleKey}" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_actual_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="CommandParameter" Value="{Binding BestBidSize, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="ToolTip">
<Setter.Value>
@@ -82,7 +82,7 @@
</Setter>
</Style>
<Style x:Key="{x:Static usp:OrderParamsPanel.BestAskSizeButtonStyleKey}" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_actual_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="CommandParameter" Value="{Binding BestAskSize, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="ToolTip">
<Setter.Value>
@@ -101,7 +101,7 @@
<TextBlock.Inlines>
<Run Text="{Binding security, Mode=OneWay}" FontWeight="Bold"/>
<Run Text="{Binding side, Mode=OneWay}" Foreground="{Binding side, Converter={StaticResource SideToBrushConverter}}"/>
<Run Text="{Binding quantity, Mode=OneWay}"/>
<Run Text="{Binding display_quantity, Mode=OneWay}"/>
<Run>@</Run>
<Run>
<Run.Text>
@@ -484,6 +484,7 @@
<Border Padding="12,12,12,6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
@@ -495,7 +496,45 @@
AlgoParamRows="{Binding param_rows}"
IsReadOnly="{Binding DataContext.can_user_modify_selected_orders, ElementName=Root, Converter={StaticResource NotConverter}}"
/>
<StackPanel Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,8,0,0" Orientation="Horizontal">
<c:ExpanderToggleButton
x:Name="algo_state_toggle"
Grid.Row="1"
BorderThickness="0"
Content="Algo State"
HorizontalContentAlignment="Left"
Margin="0,4,0,0"
VerticalAlignment="Top"
Visibility="{Binding current_algo_custom_state.Count, Converter={StaticResource IntNotZeroVisibilityConverter}}"
/>
<c:DIYGrid Grid.Row="1" Rows="{Binding current_algo_custom_state}" Margin="0,28,0,4">
<c:DIYGrid.Visibility>
<MultiBinding>
<MultiBinding.Converter>
<cv:MultiValueConverterPipe>
<cv:BooleanAndConverter/>
<cv:BooleanToVisibilityConverter/>
</cv:MultiValueConverterPipe>
</MultiBinding.Converter>
<Binding Path="current_algo_custom_state.Count" Mode="OneWay" Converter="{StaticResource IntNotZeroConverter}"/>
<Binding Path="IsChecked" ElementName="algo_state_toggle"/>
</MultiBinding>
</c:DIYGrid.Visibility>
<c:DIYGrid.Columns>
<c:DIYGridColumnCollection>
<c:DIYGridColumn id="Name" Header="Param" Width="176"/>
<c:DIYGridColumn id="Value" Header="Value" Width="176"/>
</c:DIYGridColumnCollection>
</c:DIYGrid.Columns>
<c:DIYGrid.RowTemplate>
<DataTemplate>
<Grid c:DIYGrid.SyncWithColumnGeometry="True">
<c:TextCell c:DIYGrid.ColumnKey="Name" Text="{Binding name}"/>
<c:TextCell c:DIYGrid.ColumnKey="Value" Text="{Binding value}"/>
</Grid>
</DataTemplate>
</c:DIYGrid.RowTemplate>
</c:DIYGrid>
<StackPanel Grid.Row="2" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,8,0,0" Orientation="Horizontal">
<Button x:Name="modify_orders_button" Command="{Binding modify_selected_orders_cmd}">
<Button.Margin>
<Thickness Right="{StaticResource DialogSkirtButtonPadding.Right}"/>
@@ -5,6 +5,7 @@ using bts.utils;
using mbts;
using mbts.spark;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.controls;
using mbts.utils;
using System;
@@ -140,15 +141,18 @@ namespace bts.shell.pnl {
public PositionPriceSize summary =>
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders) =>
orders.Aggregate(Empty, (seed, order) => seed.Add(order));
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders, QuantityConversionData qty_conversion) =>
Create(orders.Select(o => (o, qty_conversion)));
public WorkingOrderData Add(OrderRecordImmutable order) {
public static WorkingOrderData Create(IEnumerable<(OrderRecordImmutable order, QuantityConversionData qty_conversion)> orders) =>
orders.Aggregate(Empty, (seed, tup) => seed.Add(tup.order, tup.qty_conversion));
public WorkingOrderData Add(OrderRecordImmutable order, QuantityConversionData qty_conversion) {
if (order.working_quantity() <= 0)
return new WorkingOrderData(long_data, short_data);
switch (order.side()) {
case Side.Buy: return new WorkingOrderData(long_data.Add(order), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order));
case Side.Buy: return new WorkingOrderData(long_data.Add(order, qty_conversion), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order, qty_conversion));
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
}
}
@@ -165,18 +169,21 @@ namespace bts.shell.pnl {
}
readonly struct WorkingOrderSideData {
public readonly double avg_price;
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data;
public readonly double avg_price, display_avg_price;
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable order, QuantityConversionData qty_conversion)>> data;
public readonly int working_total;
public readonly int display_working_total;
WorkingOrderSideData(double avg_price, int working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data) {
WorkingOrderSideData(double avg_price, int working_total, double display_avg_price, int display_working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>> data) {
this.avg_price = avg_price;
this.working_total = working_total;
this.display_avg_price = display_avg_price;
this.data = data;
this.display_working_total = display_working_total;
}
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(OrderDataHelper.PriceComparer));
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(OrderDataHelper.PriceComparer));
public double top_price =>
data.Count > 0 ?
@@ -185,23 +192,35 @@ namespace bts.shell.pnl {
public int top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.working_quantity()) :
data.Values.First().Values.Sum(o => o.order.working_quantity()) :
0;
public WorkingOrderSideData Add(OrderRecordImmutable order_record) {
uint order = order_record.Key;
public int display_top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.qty_conversion.to_display(o.order.working_quantity())) :
0;
public WorkingOrderSideData Add(OrderRecordImmutable order_record) =>
Add(order_record, new QuantityConversionData());
public WorkingOrderSideData Add(OrderRecordImmutable order_record, QuantityConversionData qty_conversion) {
int working_qty = order_record.working_quantity();
double price = order_record.price();
if (working_qty <= 0)
throw new ArgumentException(nameof(working_qty));
uint order = order_record.Key;
int dworking_qty = qty_conversion.to_display(working_qty);
double price = order_record.price();
if (working_total == 0) {
return new WorkingOrderSideData(price, working_qty, data.Add(price, ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
Log.assert(display_working_total == 0);
return new WorkingOrderSideData(price, working_qty, display_avg_price, dworking_qty, data.Add(price, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>.Empty.Add(order, (order_record, qty_conversion))));
} else return new WorkingOrderSideData(
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
working_total + working_qty,
display_avg_price + ((price - display_avg_price) * ((double)dworking_qty / (display_working_total + dworking_qty))),
display_working_total + dworking_qty,
data.SetItem(price, data.TryGetValue(price, out var curr) ?
curr.Add(order, order_record) :
ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
curr.Add(order, (order_record, qty_conversion)) :
ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>.Empty.Add(order, (order_record, qty_conversion))));
}
public WorkingOrderSideData Remove(uint order, double price) {
@@ -210,22 +229,30 @@ namespace bts.shell.pnl {
throw new KeyNotFoundException(nameof(price));
if (!wdata.TryGetValue(order, out var worder))
throw new KeyNotFoundException(nameof(order));
var wqty = worder.working_quantity();
var wqty = worder.order.working_quantity();
var dwqty = worder.qty_conversion.to_display(wqty);
wdata = wdata.Remove(order);
if (wdata.Count == 0)
tmp_data = tmp_data.Remove(price);
if (tmp_data.Count == 0)
return new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(data.KeyComparer));
return new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(data.KeyComparer));
return new WorkingOrderSideData(
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
working_total - wqty,
display_avg_price - ((price - display_avg_price) * (dwqty / (display_working_total - dwqty))),
display_working_total - dwqty,
tmp_data);
}
public override string ToString() {
if (data.Count > 0) {
var best_order = data.Values.First().Values.First();
return OrderUtils.order_collection_summary(best_order.price(), avg_price, best_order.working_quantity(), working_total, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
return OrderUtils.order_collection_summary(
best_order.order.price(),
display_avg_price,
best_order.qty_conversion.to_display(best_order.order.working_quantity()),
display_working_total,
SecurityDataHelper.Defaults.ContractSpecificationImmutable);
}
return string.Empty;
}
@@ -248,6 +275,7 @@ namespace bts.shell.pnl {
public readonly PNLKey key;
public readonly SecurityImmutable? security;
public readonly QuantityConversionData qty_conversion;
public readonly string account_name;
public readonly PNLCalculationType calc_type;
public readonly PositionPriceSize day, raw_day, sod;
@@ -273,7 +301,8 @@ namespace bts.shell.pnl {
WorkingOrderData? working = null,
SecurityImmutable? security = null,
string account_name = null,
PNLCalculationType calc_type = PNLCalculationType.Mid) {
PNLCalculationType calc_type = PNLCalculationType.Mid,
QuantityConversionData? qty_conversion = null) {
//
this.key = key;
this.cpp = cpp;
@@ -288,6 +317,7 @@ namespace bts.shell.pnl {
this.security = security;
this.account_name = account_name;
this.calc_type = calc_type;
this.qty_conversion = qty_conversion ?? new QuantityConversionData();
}
[Flags]
@@ -368,6 +398,10 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
sod_long_avg_price_delta_raw, sod.long_pos,
sod_short_avg_price_delta_raw, sod.short_pos);
// display
public double display_sod_net_pos => qty_conversion.to_display(sod_net_pos);
public double display_sod_long_pos => qty_conversion.to_display(sod_long_pos);
public double display_sod_short_pos => qty_conversion.to_display(sod_short_pos);
/// Day
public double day_pnl => cpp *
@@ -395,11 +429,19 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
day_long_avg_price_delta_raw, day.long_pos,
day_short_avg_price_delta_raw, day.short_pos);
// display
public double display_day_net_pos => qty_conversion.to_display(day_net_pos);
public double display_day_long_pos => qty_conversion.to_display(day_long_pos);
public double display_day_short_pos => qty_conversion.to_display(day_short_pos);
/// Raw Day
public double raw_day_long_pos => raw_day.long_pos;
public double raw_day_short_pos => raw_day.short_pos;
public double raw_day_net_pos => raw_day.net_pos;
// display
public double display_raw_day_net_pos => qty_conversion.to_display(raw_day_net_pos);
public double display_raw_day_long_pos => qty_conversion.to_display(raw_day_long_pos);
public double display_raw_day_short_pos => qty_conversion.to_display(raw_day_short_pos);
/// Total
public double total_pnl => DoubleUtil.nan_to_zero_sum(sod_pnl, day_pnl);
@@ -428,6 +470,10 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
total_long_avg_price_delta_raw, total_long_pos,
total_short_avg_price_delta_raw, total_short_pos);
// display
public double display_total_net_pos => qty_conversion.to_display(total_net_pos);
public double display_total_long_pos => qty_conversion.to_display(total_long_pos);
public double display_total_short_pos => qty_conversion.to_display(total_short_pos);
#region Working
// long
@@ -453,6 +499,10 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
working_long_avg_price_delta_raw, working_long_pos,
working_short_avg_price_delta_raw, working_short_pos);
// display
public double display_working_net_pos => qty_conversion.to_display(working_net_pos);
public double display_working_long_pos => qty_conversion.to_display(working_long_pos);
public double display_working_short_pos => qty_conversion.to_display(working_short_pos);
#endregion
WaterfallPriceFlags _waterfall_flags() {
@@ -508,7 +558,8 @@ namespace bts.shell.pnl {
settlement_price.Equals(other.settlement_price) &&
calc_type.Equals(other.calc_type) &&
raw_day == other.raw_day &&
working.summary == other.working.summary;
working.summary == other.working.summary &&
qty_conversion == other.qty_conversion;
public override bool Equals(object obj) =>
obj is PNLRowData other && Equals(other);
@@ -523,7 +574,7 @@ namespace bts.shell.pnl {
}
static class PNLRowDataFactory {
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value) t) =>
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value, QuantityConversionData qty_conversion) t) =>
new PNLRowData(new PNLKey(t.value.Pos.AccountId, t.value.Pos.SecurityId),
t.value.CashValue,
t.value.BidPrice,
@@ -535,7 +586,8 @@ namespace bts.shell.pnl {
t.value.Pos.raw_day_pps(),
WorkingOrderData.Empty,
t.sec,
t.value.Pos.Account);
t.value.Pos.Account,
qty_conversion: t.qty_conversion);
}
static class PNLDataExtensions {
@@ -554,6 +606,7 @@ namespace bts.shell.pnl {
working ?? pnl_data.working,
security ?? pnl_data.security,
account_name ?? pnl_data.account_name,
calc_type ?? pnl_data.calc_type);
calc_type ?? pnl_data.calc_type,
qty_conversion: pnl_data.qty_conversion);
}
}
}
@@ -16,7 +16,15 @@ namespace bts.shell.pnl {
decimal sod_long_pos,
decimal sod_short_pos,
decimal working_long_pos = 0,
decimal working_short_pos = 0) {
decimal working_short_pos = 0,
decimal? display_day_long_pos = null,
decimal? display_day_short_pos = null,
decimal? display_raw_day_long_pos = null,
decimal? display_raw_day_short_pos = null,
decimal? display_sod_long_pos = null,
decimal? display_sod_short_pos = null,
decimal? display_working_long_pos = null,
decimal? display_working_short_pos = null) {
//
this.day_pnl = day_pnl;
this.day_long_pos = day_long_pos;
@@ -28,6 +36,15 @@ namespace bts.shell.pnl {
this.sod_short_pos = sod_short_pos;
this.working_long_pos = working_long_pos;
this.working_short_pos = working_short_pos;
// display
this.display_day_long_pos = display_day_long_pos ?? day_long_pos;
this.display_day_short_pos = display_day_short_pos ?? day_short_pos;
this.display_raw_day_long_pos = display_raw_day_long_pos ?? raw_day_long_pos;
this.display_raw_day_short_pos = display_raw_day_short_pos ?? raw_day_short_pos;
this.display_sod_long_pos = display_sod_long_pos ?? sod_long_pos;
this.display_sod_short_pos = display_sod_short_pos ?? sod_short_pos;
this.display_working_long_pos = display_working_long_pos ?? working_long_pos;
this.display_working_short_pos = display_working_short_pos ?? working_short_pos;
}
// sod
@@ -35,24 +52,48 @@ namespace bts.shell.pnl {
public decimal sod_long_pos { get; }
public decimal sod_short_pos { get; }
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
// sod display
public decimal display_sod_long_pos { get; }
public decimal display_sod_short_pos { get; }
public decimal display_sod_net_pos => display_sod_long_pos - display_sod_short_pos;
// day
public decimal day_pnl { get; }
public decimal day_long_pos { get; }
public decimal day_short_pos { get; }
public decimal day_net_pos => day_long_pos - day_short_pos;
// display
public decimal display_day_long_pos { get; }
public decimal display_day_short_pos { get; }
public decimal display_day_net_pos => display_day_long_pos - display_day_short_pos;
// raw day
public decimal raw_day_long_pos { get; }
public decimal raw_day_short_pos { get; }
public decimal raw_day_net_pos => raw_day_long_pos - raw_day_short_pos;
// display
public decimal display_raw_day_long_pos { get; }
public decimal display_raw_day_short_pos { get; }
public decimal display_raw_day_net_pos => display_raw_day_long_pos - display_raw_day_short_pos;
// total
public decimal total_pnl => sod_pnl + day_pnl;
public decimal total_net_pos => total_long_pos - total_short_pos;
public decimal total_long_pos => sod_long_pos + day_long_pos;
public decimal total_short_pos => sod_short_pos + day_short_pos;
// display
public decimal display_total_long_pos => display_sod_long_pos + display_day_long_pos;
public decimal display_total_short_pos => display_sod_short_pos + display_day_short_pos;
public decimal display_total_net_pos => display_total_long_pos - display_total_short_pos;
// working
public decimal working_long_pos { get; }
public decimal working_net_pos => working_long_pos - working_short_pos;
public decimal working_short_pos { get; }
public decimal working_net_pos => working_long_pos - working_short_pos;
// display
public decimal display_working_long_pos { get; }
public decimal display_working_short_pos { get; }
public decimal display_working_net_pos => display_working_long_pos - display_working_short_pos;
public bool Equals(PNLNormalizedData other) =>
day_pnl == other.day_pnl &&
@@ -64,6 +105,17 @@ namespace bts.shell.pnl {
sod_long_pos == other.sod_long_pos &&
sod_short_pos == other.sod_short_pos &&
working_long_pos == other.working_long_pos &&
working_short_pos == other.working_short_pos &&
// display
sod_long_pos == other.sod_long_pos &&
sod_short_pos == other.sod_short_pos &&
day_long_pos == other.day_long_pos &&
day_short_pos == other.day_short_pos &&
raw_day_long_pos == other.raw_day_long_pos &&
raw_day_short_pos == other.raw_day_short_pos &&
total_long_pos == other.total_long_pos &&
total_short_pos == other.total_short_pos &&
working_long_pos == other.working_long_pos &&
working_short_pos == other.working_short_pos;
public override bool Equals(object obj) =>
@@ -102,7 +154,16 @@ namespace bts.shell.pnl {
a.sod_long_pos + b.sod_long_pos,
a.sod_short_pos + b.sod_short_pos,
a.working_long_pos + b.working_long_pos,
a.working_short_pos + b.working_short_pos);
a.working_short_pos + b.working_short_pos,
// display
a.display_day_long_pos + b.display_day_long_pos,
a.display_day_short_pos + b.display_day_short_pos,
a.display_raw_day_long_pos + b.display_raw_day_long_pos,
a.display_raw_day_short_pos + b.display_raw_day_short_pos,
a.display_sod_long_pos + b.display_sod_long_pos,
a.display_sod_short_pos + b.display_sod_short_pos,
a.display_working_long_pos + b.display_working_long_pos,
a.display_working_short_pos + b.display_working_short_pos);
public static PNLNormalizedData operator -(PNLNormalizedData a, PNLNormalizedData b) =>
new PNLNormalizedData(
@@ -115,6 +176,15 @@ namespace bts.shell.pnl {
a.sod_long_pos - b.sod_long_pos,
a.sod_short_pos - b.sod_short_pos,
a.working_long_pos - b.working_long_pos,
a.working_short_pos - b.working_short_pos);
a.working_short_pos - b.working_short_pos,
// display
a.display_day_long_pos - b.display_day_long_pos,
a.display_day_short_pos - b.display_day_short_pos,
a.display_raw_day_long_pos - b.display_raw_day_long_pos,
a.display_raw_day_short_pos - b.display_raw_day_short_pos,
a.display_sod_long_pos - b.display_sod_long_pos,
a.display_sod_short_pos - b.display_sod_short_pos,
a.display_working_long_pos - b.display_working_long_pos,
a.display_working_short_pos - b.display_working_short_pos);
}
}
@@ -14,7 +14,7 @@ namespace bts.shell.pnl {
public readonly DIYGridAnimationModel<PNLView.Columns, TRow> animation_model = new DIYGridAnimationModel<PNLView.Columns, TRow>(TimeSpan.FromSeconds(0.3));
static readonly Func<WorkingOrderSideData, string> _fmt_working_order_data =
d => OrderUtils.order_collection_summary(d.top_price, d.top_volume, d.working_total);
d => OrderUtils.order_collection_summary(d.top_price, d.display_top_volume, d.display_working_total);
static readonly IComparer<WorkingOrderSideData> _working_total_comparer =
Comparer<int>.Default.wrap<int, WorkingOrderSideData>(d => d.working_total);
@@ -31,40 +31,40 @@ namespace bts.shell.pnl {
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.security?.Symbol ?? string.Empty);
security_cell = _cell_map.add(ColumnKeys.Security, d => d.security, null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
security_cell = _cell_map.add(ColumnKeys.Security, d => (d.security), null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
security_key = _cell_map.add(ColumnKeys.SecurityKey, d => d.key.security_id);
cpp = _cell_map.add(ColumnKeys.CPP, d => d.cpp);
// sod
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.display_sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.display_sod_long_pos);
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price_raw);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.display_sod_short_pos);
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price_raw);
// day
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.display_day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.display_day_long_pos);
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price_raw);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.display_day_short_pos);
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price_raw);
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
// raw day
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.display_raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.display_raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.display_raw_day_short_pos);
// working
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.display_working_net_pos);
working_long_pos = _cell_map.add_working_long(ColumnKeys.WorkingLongPos, d => d.working.long_data, d => d.bid_price);
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price_raw);
working_short_pos = _cell_map.add_working_short(ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price);
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price_raw);
// total
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.display_total_net_pos);
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.display_total_long_pos);
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price_raw);
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.display_total_short_pos);
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price_raw);
// market
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
@@ -112,7 +112,16 @@ namespace bts.shell.pnl {
sod_long,
sod_short,
DoubleUtil.round_to_decimal(data.working_long_pos),
DoubleUtil.round_to_decimal(data.working_short_pos));
DoubleUtil.round_to_decimal(data.working_short_pos),
// display
DoubleUtil.round_to_decimal(data.display_day_long_pos),
DoubleUtil.round_to_decimal(data.display_day_short_pos),
DoubleUtil.round_to_decimal(data.display_raw_day_long_pos),
DoubleUtil.round_to_decimal(data.display_raw_day_short_pos),
DoubleUtil.round_to_decimal(data.display_sod_long_pos),
DoubleUtil.round_to_decimal(data.display_sod_short_pos),
DoubleUtil.round_to_decimal(data.display_working_long_pos),
DoubleUtil.round_to_decimal(data.display_working_short_pos));
if (_normalized_data != nd) {
_normalized_data = nd;
updated?.Invoke(nd);
@@ -206,4 +215,4 @@ namespace bts.shell.pnl {
public override string ToString() =>
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
}
}
}
@@ -112,27 +112,27 @@ namespace bts.shell.pnl {
});
// sod
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.display_sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.display_sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.display_sod_short_pos);
// day
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.display_day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.display_day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.display_day_short_pos);
// raw day
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.display_raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.display_raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.display_raw_day_short_pos);
// total
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.total_short_pos);
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.display_total_net_pos);
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.display_total_long_pos);
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.display_total_short_pos);
// working
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.working_short_pos);
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.display_working_net_pos);
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.display_working_long_pos);
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.display_working_short_pos);
_cell_map.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
+18 -5
View File
@@ -2,6 +2,7 @@
using bts.api;
using bts.shell;
using bts.spark;
using bts.utils;
using mbts.config;
using mbts.persist;
using mbts.spark;
@@ -114,6 +115,7 @@ namespace mbts.ui {
void dispose_core() {
Disposable.dispose(ref _settings_sub);
Disposable.dispose(ref _toast_controller);
Disposable.dispose(ref _sound_player);
if (_dispatcher != null && !_dispatcher.HasShutdownStarted) {
_dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
}
@@ -158,15 +160,25 @@ namespace mbts.ui {
}
}
class SoundNotificationPlayer {
readonly IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> _players;
public static SoundNotificationPlayer create() => new SoundNotificationPlayer(_all_players());
class SoundNotificationPlayer : IDisposable {
IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> _players;
SoundNotificationPlayer(IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> players) =>
_players = players;
public static SoundNotificationPlayer create() =>
new SoundNotificationPlayer(_all_players());
public void Dispose() {
if (_players != null) {
_players.Values.DisposeAll();
_players = null;
}
}
public void play_sounds(IEnumerable<SoundNotification> sounds) {
if (_players == null)
return;
foreach (var s in sounds) {
switch (s.sound_type_case) {
case SoundNotification.SoundTypeOneofCase.System: _players.GetValueOrDefault(s.system_sound.Value)?.play(); break;
@@ -177,7 +189,8 @@ namespace mbts.ui {
}
static IReadOnlyDictionary<SoundNotification.SystemSound, NAudioPlayer> _all_players() =>
EnumHelper.get_values<SoundNotification.SystemSound>().ToReadOnlyDictionary(s => s, NAudioPlayer.create);
EnumHelper.get_values<SoundNotification.SystemSound>()
.ToReadOnlyDictionaryWhere<SoundNotification.SystemSound, NAudioPlayer>(NAudioPlayer.try_create);
}
// This class adopts to role of view-controller in the mvc paradigm, rather than view-model in mvvm.
+75 -22
View File
@@ -1,4 +1,5 @@
using bts.shell;
using bts;
using bts.shell;
using mbts.ui.utility;
using NAudio.Wave;
using System;
@@ -6,29 +7,71 @@ using System.IO;
using System.Threading.Tasks;
namespace mbts.ui {
class NAudioPlayer {
class NAudioPlayer : IDisposable {
// number of milliseconds to delay playing sound,
// during which time all subsequent requests to play the same sound will be ignored.
const int _delay_ms = 75;
readonly int _start_position;
IDisposable _stream_in;
WaveOutEvent _wave_out = new WaveOutEvent();
WaveFileReader _wave_reader;
bool _is_playing, _is_play_requested;
public NAudioPlayer(Stream uwave_stream, int start_position = 0) {
var ms = new MemoryStream();
uwave_stream.CopyTo(ms);
ms.Position = 0;
_wave_reader = new WaveFileReader(ms);
/// <paramref name="stream_in"/> should not be disposed by the caller,
/// as NAudioPlayer handles that in its own Dispose method;
/// this constructor is private is to prevent such misuse.
NAudioPlayer(Stream stream_in, int start_position = 0) {
_stream_in = stream_in;
_wave_reader = new WaveFileReader(stream_in);
_wave_reader.Position = _start_position = (start_position < _wave_reader.Length ? start_position : 0);
_wave_out.Init(_wave_reader);
_wave_out.PlaybackStopped += _on_playback_stopped;
}
public static NAudioPlayer create(SoundNotification.SystemSound system_sound) {
var stream = _get_stream(system_sound);
var sb = EnumHelper.get_attribute<SoundNotification.SystemSound, SilenceBufferAttribute>(system_sound);
return sb != null ? new NAudioPlayer(stream, sb.start_position) : new NAudioPlayer(stream);
/// <see cref="_playback_stopped"/> is a proxy/buffer for the _wave_out.PlaybackStopped event.
/// It serves a couple purposes:
/// 1) The main callback (<see cref="_on_playback_stopped(object, StoppedEventArgs)"/>)
/// is guarenteed to be executed first (though it would be anyways, since that
/// callback is hooked up in the constructor, the proxy event makes it obvious).
/// 2) <see cref="_playback_stopped"/> is only raised after playback is truly complete,
/// meaning there are no pending requests to repeat the sound.
event Action _playback_stopped;
public static bool try_create(SoundNotification.SystemSound system_sound, out NAudioPlayer result) {
if (_try_get_stream(system_sound, out var stream)) {
var sb = EnumHelper.get_attribute<SoundNotification.SystemSound, SilenceBufferAttribute>(system_sound);
result = sb != null ? new NAudioPlayer(stream, sb.start_position) : new NAudioPlayer(stream);
return true;
}
result = null;
return false;
}
public static void play(SoundNotification.SystemSound system_sound) {
if (try_create(system_sound, out var player)) {
try {
var temp = player;
temp._playback_stopped += temp.Dispose;
player.play();
player = null;
} catch (Exception ex) {
Log.Ui.warn($"Encountered exception playing the system sound '{system_sound}': {ex}.");
} finally {
player?.Dispose();
}
} else {
Log.Ui.warn($"Unable to play the system sound '{system_sound}': unexpected sound type (did not find an audio stream that corresponds to this enum value).");
}
}
public void Dispose() {
if (_wave_out != null) {
_wave_out.PlaybackStopped -= _on_playback_stopped;
_wave_out.Dispose();
_wave_out = null;
}
Disposable.dispose(ref _wave_reader);
Disposable.dispose(ref _stream_in);
}
public void play() {
@@ -46,6 +89,8 @@ namespace mbts.ui {
if (_is_play_requested) {
_is_play_requested = false;
_play_core();
} else {
_playback_stopped?.Invoke();
}
}
@@ -56,18 +101,26 @@ namespace mbts.ui {
_is_playing = false;
}
static Stream _get_stream(SoundNotification.SystemSound system_sound) {
static bool _try_get_stream(SoundNotification.SystemSound system_sound, out Stream stream) {
UnmanagedMemoryStream um_stream;
switch (system_sound) {
case SoundNotification.SystemSound.GameCorrect: return bts.Properties.Resources.game_sound_correct;
case SoundNotification.SystemSound.GameIncorrect: return bts.Properties.Resources.game_sound_wrong;
case SoundNotification.SystemSound.GameOverArcade: return bts.Properties.Resources.game_over_arcade;
case SoundNotification.SystemSound.Whistle: return bts.Properties.Resources.whistle;
case SoundNotification.SystemSound.BellCounter: return bts.Properties.Resources.bell_counter;
case SoundNotification.SystemSound.Powerup: return bts.Properties.Resources.powerup;
case SoundNotification.SystemSound.CashRegister: return bts.Properties.Resources.cash_register_purchase;
case SoundNotification.SystemSound.Reload: return bts.Properties.Resources.reload_1911;
default: throw new NotImplementedException($"Unknown {nameof(SoundNotification.SystemSound)}: {system_sound}.");
case SoundNotification.SystemSound.GameCorrect: um_stream = bts.Properties.Resources.game_sound_correct; break;
case SoundNotification.SystemSound.GameIncorrect: um_stream = bts.Properties.Resources.game_sound_wrong; break;
case SoundNotification.SystemSound.GameOverArcade: um_stream = bts.Properties.Resources.game_over_arcade; break;
case SoundNotification.SystemSound.Whistle: um_stream = bts.Properties.Resources.whistle; break;
case SoundNotification.SystemSound.BellCounter: um_stream = bts.Properties.Resources.bell_counter; break;
case SoundNotification.SystemSound.Powerup: um_stream = bts.Properties.Resources.powerup; break;
case SoundNotification.SystemSound.CashRegister: um_stream = bts.Properties.Resources.cash_register_purchase; break;
case SoundNotification.SystemSound.Reload: um_stream = bts.Properties.Resources.reload_1911; break;
default:
stream = null;
return false;
}
stream = new MemoryStream();
um_stream.CopyTo(stream);
um_stream.Dispose();
stream.Position = 0;
return true;
}
}
}
}
+13 -1
View File
@@ -10,6 +10,7 @@ namespace mbts.spark {
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged, ITemplateSelectorKey {
readonly IEqualityComparer<T> _comparer;
readonly string _template_key;
readonly Func<T, T> _to_display_value, _from_display_value;
AlgoParamPermissions _permissions;
bool _is_enabled, _has_value;
T _committed_value, _value;
@@ -20,13 +21,15 @@ namespace mbts.spark {
public AlgoParamVM(string id, string display_name, T value, string template_key = null)
: this(id, display_name, value, EqualityComparer<T>.Default, template_key) { }
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null) {
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null, Func<T, T> to_display_value = null, Func<T, T> from_display_value = null) {
this.id = id;
this.display_name = display_name;
_value = value;
_committed_value = value;
_comparer = comparer;
_template_key = template_key;
_to_display_value = to_display_value;
_from_display_value = from_display_value;
}
public event PropertyChangedEventHandler PropertyChanged;
@@ -43,6 +46,10 @@ namespace mbts.spark {
_toggle_is_dirty();
}
}
public T display_value {
get => _to_display_value == null ? value : _to_display_value(value);
set => this.value = _from_display_value == null ? value : _from_display_value(value);
}
public AlgoParamPermissions permissions {
get => _permissions;
set {
@@ -54,6 +61,7 @@ namespace mbts.spark {
public bool has_value { get => _has_value; set => _set(ref _has_value, value); }
public bool is_dirty { get; private set; }
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
object IAlgoParamVM.display_value { get => display_value; set => display_value = (T)value; }
string ITemplateSelectorKey.key => _template_key;
@@ -64,6 +72,9 @@ namespace mbts.spark {
}
}
public void refresh_display_value() =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(display_value)));
public IObservable<T> subscribe_value() {
return Observable.Create<T>(o => {
o.OnNext(_value);
@@ -113,6 +124,7 @@ namespace mbts.spark {
value_changed(_value, _value = new_value);
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(value)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(display_value)));
}
void _toggle_is_dirty() {
@@ -18,6 +18,7 @@ namespace mbts.spark {
readonly Property<TimeInForce> _current_time_in_force;
readonly Property<CustomOrderDefinition.CustomOneofCase> _current_order_type;
readonly Property<bool> _price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty;
uint _display_quantity_divisor = 1;
public ExistingOrderTicketVM(OrdersModel model, IObservable<(SecurityMD? security_md, OrderRecordImmutable order)> order_source, IObservable<int[]> calculator_buttons) {
uint? key = null;
@@ -40,7 +41,7 @@ namespace mbts.spark {
(val, curr) => !double.IsNaN(val) && val != curr));
_size_dirty = backing(
nameof(size_dirty),
size_calculator.observe_current(true)
size_calculator.observe_actual_value()
.CombineLatest(_current_size.observe_current(true),
(val, curr) => val.HasValue && val.Value != curr));
_account_dirty = backing(
@@ -83,14 +84,17 @@ namespace mbts.spark {
_dispose = Disposable.Create(
size_calculator,
properties_vm,
security_md_source.OnDispatcher().Subscribe(smd => bbo.security_md = smd),
security_md_source.OnDispatcher().Subscribe(smd => {
bbo.security_md = smd;
display_quantity_divisor = model.security_auxiliary_model.get_display_quantity_divisor(smd.security);
}),
bbo,
order_source.OnDispatcher().Subscribe(ort => {
key = ort.order.Key;
if (DoubleHelper.IsNaNOrInfinity(price))
price = ort.order.State.Price;
if (!size_calculator.value.HasValue || !size_calculator.value_setter_called)
size_calculator.bypass_value_setter(ort.order.State.Quantity);
size_calculator.bypass_value_setter(ort.order.State.Quantity, true);
}),
Observable.Merge(_price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty).OnDispatcher().Subscribe(_ => RaisePropertyChanged(nameof(dirty_items_text))),
_price_dirty.observe_current(true).CombineLatest(_size_dirty.observe_current(true), (pd, sd) => pd || sd).DistinctUntilChanged().OnDispatcher().Subscribe(modify_order));
@@ -104,6 +108,13 @@ namespace mbts.spark {
public OrderPropertiesEditorVM properties_vm { get; }
public double price { get => _price.Value; set => _price.Value = value; }
public CalculatorLogic size_calculator { get; }
public uint display_quantity_divisor {
get => _display_quantity_divisor;
private set {
Set(ref _display_quantity_divisor, value);
size_calculator.display_divisor = value;
}
}
public double current_price => _current_price.Value;
public uint current_size => _current_size.Value;
public AccountTuple current_account => _current_account.Value;
@@ -137,4 +148,4 @@ namespace mbts.spark {
if (_order_type_dirty.Value) yield return "type";
}
}
}
}
+12 -1
View File
@@ -31,6 +31,7 @@ namespace mbts.spark {
class NewOrderTicketVM : DisposableViewModelBase, IRequestViewClose {
readonly Property<SecurityImmutable> _security;
uint _display_quantity_divisor = 1;
public NewOrderTicketVM(OrdersModel model, SecurityMD security_md, OrderDefinitionImmutable def, double price, uint size, IObservable<int[]> calculator_buttons)
: this(model, Observable.Return(security_md), Observable.Return(new InlineOrderTicketProperties(model, def, price, size)), calculator_buttons) { }
@@ -61,7 +62,10 @@ namespace mbts.spark {
set_price_command = new DelegateCommand<double>(set_price);
_dispose = Disposable.Create(
bbo,
security_md_source.OnDispatcher().Subscribe(smd => bbo.security_md = smd),
security_md_source.OnDispatcher().Subscribe(smd => {
bbo.security_md = smd;
display_quantity_divisor = model.security_auxiliary_model.get_display_quantity_divisor(smd.security);
}),
size_calculator = new CalculatorLogic(0, calculator_buttons),
props_source.OnDispatcher().Subscribe(p => on_props(p.price, p.size))
);
@@ -83,6 +87,13 @@ namespace mbts.spark {
public SecurityImmutable security => _security.Value;
public double price { get; set; }
public CalculatorLogic size_calculator { get; }
public uint display_quantity_divisor {
get => _display_quantity_divisor;
private set {
Set(ref _display_quantity_divisor, value);
size_calculator.display_divisor = value;
}
}
public OrderPropertiesEditorVM properties_vm { get; }
public BBOVM bbo { get; }
public bool close_on_send { get; set; }
@@ -85,7 +85,7 @@
usp:OrderModificationControl.IsDirty="{Binding size_dirty, FallbackValue={x:Null}}"
CommitOnEnter="True"
Focusable="False"
Value="{Binding size_calculator.value}"
Value="{Binding size_calculator.display_value}"
/>
</Grid>
<Grid Grid.Column="2">
@@ -114,15 +114,26 @@
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button
Content="{Binding bbo.bid_size}" Command="{Binding size_calculator.set_value_command}"
CommandParameter="{Binding bbo.bid_size}" Foreground="{u:BTSColor MarketData.bid}"
/>
Command="{Binding size_calculator.set_actual_value_command}"
CommandParameter="{Binding bbo.bid_size}"
Foreground="{u:BTSColor MarketData.bid}"
>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="bbo.bid_size"/>
<Binding Path="display_quantity_divisor"/>
</MultiBinding>
</Button>
<Button
Grid.Column="2"
Content="{Binding bbo.ask_size}" Command="{Binding size_calculator.set_value_command}"
Command="{Binding size_calculator.set_actual_value_command}"
CommandParameter="{Binding bbo.ask_size}"
Foreground="{u:BTSColor MarketData.ask}"
/>
>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="bbo.ask_size"/>
<Binding Path="display_quantity_divisor"/>
</MultiBinding>
</Button>
</Grid>
<Grid Grid.Row="4" Grid.RowSpan="3">
<Grid.RowDefinitions>
@@ -139,9 +150,18 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Margin="2,4,2,0" Content="{Binding}" Focusable="False" CommandParameter="{Binding}"
Margin="2,4,2,0"
Content="{Binding}"
Focusable="False"
Command="{Binding Path=DataContext.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
/>
>
<Button.CommandParameter>
<MultiBinding Converter="{x:Static cv:DynamicDoubleMultiplierConverter.Instance}">
<Binding/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationControl}"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
+25 -12
View File
@@ -1,13 +1,16 @@
<Window x:Class="mbts.ui.spark.OrderTicket"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:msys="clr-namespace:bts.api;assembly=spark_api"
xmlns:u="clr-namespace:mbts.ui"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
u:WindowStyle.SizeToContent="WidthAndHeight"
u:WindowStyle.TitleContent="{Binding bbo, Mode=OneWay}"
Style="{StaticResource BTSWindowTitleTemplate}">
<Window
x:Class="mbts.ui.spark.OrderTicket"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:msys="clr-namespace:bts.api;assembly=spark_api"
xmlns:u="clr-namespace:mbts.ui"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
u:WindowStyle.SizeToContent="WidthAndHeight"
u:WindowStyle.TitleContent="{Binding bbo, Mode=OneWay}"
Style="{StaticResource BTSWindowTitleTemplate}"
>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -123,7 +126,12 @@
<TextBlock>
<TextBlock.Inlines>
<Run Text="{Binding security, Mode=OneWay}" FontWeight="Bold"/>
<Run Foreground="{u:BTSColor MarketData.bid}" FontSize="10" Text="{Binding bid_size, Mode=OneWay, StringFormat='{}({0})'}"/>
<Run Foreground="{u:BTSColor MarketData.bid}" FontSize="10">
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}" Mode="OneWay" StringFormat="({0})">
<Binding Path="bid_size" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderTicket}"/>
</MultiBinding>
</Run>
<Run Foreground="{u:BTSColor MarketData.bid}" FontWeight="Bold">
<Run.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
@@ -140,7 +148,12 @@
</MultiBinding>
</Run.Text>
</Run>
<Run Foreground="{u:BTSColor MarketData.ask}" FontSize="10" Text="{Binding ask_size, Mode=OneWay, StringFormat='{}({0})'}"/>
<Run Foreground="{u:BTSColor MarketData.ask}" FontSize="10">
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}" Mode="OneWay" StringFormat="({0})">
<Binding Path="ask_size" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderTicket}"/>
</MultiBinding>
</Run>
</TextBlock.Inlines>
</TextBlock>
</DataTemplate>
@@ -23,6 +23,32 @@ namespace mbts.spark {
public IEnumerable<string> dynamic_types => _dynamic_types.DynamicOrderDesc.Select(d => d.DynamicOrderType);
public static string fmt_param(DynamicOrderParam dyn_param) {
if (dyn_param == null)
return null;
switch (dyn_param.ValueCase) {
case DynamicOrderParam.ValueOneofCase.AsArray:
return string.Join(", ", dyn_param.AsArray.Element.Select(fmt_param));
case DynamicOrderParam.ValueOneofCase.AsBool:
return dyn_param.AsBool.ToString();
case DynamicOrderParam.ValueOneofCase.AsDouble:
return dyn_param.AsDouble.ToString();
case DynamicOrderParam.ValueOneofCase.AsEnumerated:
return dyn_param.AsEnumerated.EnumValue;
case DynamicOrderParam.ValueOneofCase.AsInt32:
return dyn_param.AsInt32.ToString();
case DynamicOrderParam.ValueOneofCase.AsQuantity:
return dyn_param.AsQuantity.ToString();
case DynamicOrderParam.ValueOneofCase.AsString:
return dyn_param.AsString;
default:
// implement additional cases as needed
// (as of 4/17/20, this method is only used to format
// dynamic orders' CustomState params in the orderbook)
return $"({dyn_param.ValueCase})";
}
}
public void add_dynamic_order_defs(List<CustomOrderDefinitionImmutable> defs, Func<CustomOrderDefinition.CustomOneofCase, CustomOrderDefinition> factory) {
foreach (var d in _dynamic_types.DynamicOrderDesc) {
var dorder = factory(CustomOrderDefinition.CustomOneofCase.Dynamic);
@@ -33,7 +33,7 @@ namespace mbts.spark
{
return;
}
var ev = fd.EnumType.Values.First(x => x.Name == value)?.Number;
var ev = fd.EnumType.Values.FirstOrDefault(x => x.Name == value)?.Number;
if(ev.HasValue)
fd.Accessor.SetValue(message, ev.Value);
}
@@ -2,7 +2,6 @@
using bts.api;
using bts.api.orderman;
using bts.collections;
using bts.spark;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using mbts.collections;
@@ -15,21 +14,19 @@ using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using AlgoType = bts.api.orderman.CustomOrderDefinition.CustomOneofCase;
namespace mbts.spark {
static partial class OrderTypeVMFactory
{
internal class GenericOrderFactory
{
static public IAlgoVM create_vm(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions)
{
static partial class OrderTypeVMFactory {
internal class GenericOrderFactory {
static public IAlgoVM create_vm(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions) {
var prop = typeof(CustomOrderDefinition).GetProperty(algo_type.ToString(),
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
BindingFlags.Instance | BindingFlags.Public);
var rt = prop.PropertyType;
var propi = typeof(CustomOrderDefinitionImmutable).GetProperty(algo_type.ToString(),
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
BindingFlags.Instance | BindingFlags.Public);
var rti = propi.PropertyType;
var vmt = typeof(GenericOrderVM<,>);
@@ -38,38 +35,33 @@ namespace mbts.spark {
object vm = Activator.CreateInstance(makevm, key, algo_type, display_name, editing_permissions);
return (IAlgoVM)vm;
}
static public void setup_generic_custom_order(CustomOrderDefinition custom, CustomOrderDefinition.CustomOneofCase type)
{
static public void setup_generic_custom_order(CustomOrderDefinition custom, CustomOrderDefinition.CustomOneofCase type) {
var prop = typeof(CustomOrderDefinition).GetProperty(type.ToString(),
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
BindingFlags.Instance | BindingFlags.Public);
var rt = prop.PropertyType;
var rtt = Activator.CreateInstance(rt);
var ci = rt.GetProperties().Where(x => x.PropertyType == typeof(CustomOrderDefinition));
foreach(var cid in ci)
foreach (var cid in ci)
cid.SetValue(rtt, new CustomOrderDefinition { Limit = new LimitOrderDefinition() });
prop.SetValue(custom, rtt);
}
}
}
class GenericOrderVM<TMutable, TImmutable> : OrderTypeVMBase<TMutable, TImmutable>
where TMutable : class, IMessage, new() where TImmutable : struct, IImmutableMessage
{
where TMutable : class, IMessage, new() where TImmutable : struct, IImmutableMessage {
readonly ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string> _parameters =
new ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string>(vm => vm.descriptor.Name);
readonly AlgoType _algo_type;
public GenericOrderVM(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions)
: base(key, display_name, editing_permissions)
{
: base(key, display_name, editing_permissions) {
_algo_type = algo_type;
_parameters = new ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string>(vm => vm.descriptor.Name);
_resources.AddRange(
ObservableAutoWrapper.connect_item_hooks(_parameters, p =>
{
ObservableAutoWrapper.connect_item_hooks(_parameters, p => {
p.PropertyChanged += on_param_prop_changed;
return Disposable.Create(() =>
{
return Disposable.Create(() => {
p.PropertyChanged -= on_param_prop_changed;
remove_property(p.descriptor.Name);
p.Dispose();
@@ -80,84 +72,74 @@ namespace mbts.spark {
}
public string algo_type => _algo_type.ToString();
public override string abbreviation =>
public override string abbreviation =>
model == null ? display_name : model.factory.abbreviate_algo(new OneOf<CustomOrderDefinition.CustomOneofCase, string>(_algo_type));
public override AlgoDisplayGroup group => AlgoDisplayGroup.API;
public ObservableCollection<GenericOrderParamVM<TMutable>> parameters => _parameters;
protected override void on_model_internal()
{
protected override void on_model_internal() =>
_sync_generic_params();
}
void _sync_generic_params()
{
if (model != null)
{
Google.Protobuf.Reflection.MessageDescriptor desc = new TMutable().Descriptor;
void _sync_generic_params() {
if (model != null) {
MessageDescriptor desc = new TMutable().Descriptor;
_parameters.SyncWithDictionary(
desc.Fields.InDeclarationOrder().Where(_is_value_param_desc).ToDictionary(p => p.Name), _mk_param_vm);
}
}
static bool _is_value_param_desc(FieldDescriptor desc)
{
return (desc.FieldType != Google.Protobuf.Reflection.FieldType.Message ||
static bool _is_value_param_desc(FieldDescriptor desc) =>
(desc.FieldType != FieldType.Message ||
_is_quantity_measure_desc(desc));
}
static bool _is_quantity_measure_desc(FieldDescriptor desc)
{
return desc.FieldType == Google.Protobuf.Reflection.FieldType.Message &&
static bool _is_quantity_measure_desc(FieldDescriptor desc) =>
desc.FieldType == FieldType.Message &&
desc.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName;
}
protected override IEnumerable<ChildAlgoData> get_selector_data()
{
var v = OrderDataHelper.get_child_algo_data(_algo_type);
return v;
}
protected override IEnumerable<ChildAlgoData> get_selector_data() =>
OrderDataHelper.get_child_algo_data(_algo_type);
GenericOrderParamVM<TMutable> _mk_param_vm(FieldDescriptor descriptor)
{
GenericOrderParamVM<TMutable> _mk_param_vm(FieldDescriptor descriptor) {
ExceptionAssert.Argument.NotNull(descriptor, nameof(descriptor));
if (descriptor.FieldType == FieldType.Message &&
descriptor.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName)
{
if (descriptor.FieldType == FieldType.Message &&
descriptor.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName) {
var qm = new GenericOrderQuantityVM<TMutable>(descriptor);
return attached(qm, GenericOrderHelper.get_quantity_value, GenericOrderHelper.set_quantity_value);
}
else if(descriptor.FieldType == FieldType.Enum)
{
var pvm = new GenericOrderEnumeratedVM<TMutable>(descriptor);
attached(pvm, GenericOrderHelper.get_enumerated_value, GenericOrderHelper.set_enumerated_value);
return pvm;
}
else if (descriptor.FieldType == FieldType.Bool)
} else if (descriptor.FieldType == FieldType.Enum) {
return EnumParamFactory.create(this, descriptor);
} else if (descriptor.FieldType == FieldType.Bool) {
return mk<bool?, bool>(descriptor, v => v, v => v.GetValueOrDefault());
else if (descriptor.FieldType == FieldType.Double || descriptor.FieldType == FieldType.Float)
} else if (descriptor.FieldType == FieldType.Double || descriptor.FieldType == FieldType.Float) {
return mk<double, double>(descriptor, v => v, v => v, double.NaN);
else if (descriptor.FieldType == FieldType.Int32)
} else if (descriptor.FieldType == FieldType.Int32) {
return mk<int?, int>(descriptor, v => v, v => v.GetValueOrDefault());
else if (descriptor.FieldType == FieldType.UInt32)
} else if (descriptor.FieldType == FieldType.UInt32) {
return mk<uint?, uint>(descriptor, v => v, v => v.GetValueOrDefault());
else if (descriptor.FieldType == FieldType.String)
} else if (descriptor.FieldType == FieldType.String) {
return mk<string, string>(descriptor, v => v, v => v);
else
return null;
} else return null;
}
GenericOrderParamVM<TMutable, TView, TModel> mk<TView, TModel>(FieldDescriptor fdesc, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default)
{
var pvm = new GenericOrderParamVM<TMutable, TView, TModel>(fdesc, convert, convert_back);
return attached<TView, GenericOrderParamVM<TMutable, TView, TModel>>(pvm, get_from_param, assign_to_param, default_value);
GenericOrderParamVM<TMutable, TView, TModel> mk<TView, TModel>(
FieldDescriptor fdesc,
Func<TModel, TView> convert,
Func<TView, TModel> convert_back,
TView default_value = default) {
//
var pvm = new GenericOrderParamVM<TMutable, TView, TModel>(fdesc, convert, convert_back);
return attached(pvm, get_from_param, assign_to_param, default_value);
TView get_from_param(IMessage param, FieldDescriptor fd) => convert(ConvertUtils.try_convert<TModel>(fdesc.Accessor.GetValue(param)));
void assign_to_param(IMessage param, FieldDescriptor fd, TView value) => fdesc.Accessor.SetValue(param, convert_back(value));
}
TParam attached<T, TParam>(TParam pvm, Func<IMessage, FieldDescriptor, T> selector, Action<IMessage, FieldDescriptor, T> assign_to_param, T default_value = default) where TParam : GenericOrderParamVM<TMutable>, IGenericOrderParamVM<T>
{
add_property<T>(
TParam attached<T, TParam>(
TParam pvm,
Func<IMessage, FieldDescriptor, T>
selector, Action<IMessage, FieldDescriptor, T> assign_to_param,
T default_value = default)
where TParam : GenericOrderParamVM<TMutable>, IGenericOrderParamVM<T> {
//
add_property(
pvm.descriptor.Name,
mdef => mdef != null ? selector(mdef, pvm.descriptor) : default_value,
(mdef, v) => assign_to_param(mdef, pvm.descriptor, v),
@@ -166,7 +148,37 @@ namespace mbts.spark {
pvm.subscribe_value(),
default_value);
return pvm;
}
}
static class EnumParamFactory {
[ThreadStatic]
static Dictionary<Type, IFactory> _factories_backing;
static Dictionary<Type, IFactory> _factories =>
_factories_backing ?? (_factories_backing = new Dictionary<Type, IFactory>());
public static GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor) {
if (descriptor.FieldType != FieldType.Enum)
throw ExceptionBuilder.Argument.unexpected_case(descriptor.FieldType, $"{nameof(descriptor)}.{nameof(FieldDescriptor.FieldType)}");
if (!_factories.TryGetValue(descriptor.EnumType.ClrType, out var f)) {
// Impl<T> is a nested type, so parent class's generic type args must be included.
_factories[descriptor.EnumType.ClrType] = f = (IFactory)Activator.CreateInstance(typeof(Impl<>)
.MakeGenericType(typeof(TMutable), typeof(TImmutable), descriptor.EnumType.ClrType));
}
return f.create(owner, descriptor);
}
interface IFactory {
GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor);
}
class Impl<T> : IFactory where T : struct {
static readonly Func<T, T?> _convert = v => v;
static readonly Func<T?, T> _convert_back = v => v ?? default;
public GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor) =>
owner.mk(descriptor, _convert, _convert_back, default);
}
}
}
}
}
}
@@ -70,6 +70,7 @@ namespace mbts.spark {
string id { get; }
string display_name { get; }
object value { get; set; }
object display_value { get; set; }
bool is_enabled { get; set; }
bool is_dirty { get; }
bool has_value { get; }
@@ -81,6 +82,7 @@ namespace mbts.spark {
interface IAlgoParamVM<T> : IAlgoParamVM {
event PropertyChange<T> value_changed;
new T value { get; set; }
new T display_value { get; set; }
}
interface IAlgoSelector : IAlgoParamVM<IAlgoVM>, IObserver<(IReadOnlyDictionary<AlgoKey, AlgoData> algo_data, AlgoKey? default_algo)> {
@@ -985,8 +985,10 @@ namespace mbts.spark {
#region IAlgoParamVM
IAlgoVM IAlgoParamVM<IAlgoVM>.value { get => selected_algo; set => selected_algo = value; }
IAlgoVM IAlgoParamVM<IAlgoVM>.display_value { get => selected_algo; set => selected_algo = value; }
event PropertyChange<IAlgoVM> IAlgoParamVM<IAlgoVM>.value_changed { add => selected_algo_changed += value; remove => selected_algo_changed -= value; }
object IAlgoParamVM.value { get => selected_algo; set => selected_algo = (IAlgoVM)value; }
object IAlgoParamVM.display_value { get => selected_algo; set => selected_algo = (IAlgoVM)value; }
bool IAlgoParamVM.has_value => _algo != null;
// changing child algos is not allowed, so an algo selector is never dirty
bool IAlgoParamVM.is_dirty => false;
+7 -2
View File
@@ -63,6 +63,11 @@ namespace mbts.spark {
public static string last_msg(this OrderRecordImmutable order) =>
order.HasState ? order.State.LastMsg : string.Empty;
public static IReadOnlyDictionary<string, DynamicOrderParam> dynamic_custom_state(this OrderRecordImmutable order) =>
order.HasState && order.State.HasDynamic ?
(order.State.Dynamic.CustomState ?? DictionaryHelper.empty<string, DynamicOrderParam>()) :
DictionaryHelper.empty<string, DynamicOrderParam>();
public static DateTime created(this OrderRecordImmutable order) =>
order.HasState && order.State.HasCreated ? order.State.Created.ToDateTime().ToLocalTime() : DateTime.MinValue;
@@ -235,8 +240,8 @@ namespace mbts.spark {
public static string price_quantity_description(double price, uint quantity, ContractSpecificationImmutable spec) =>
new StringBuilderEx(quantity.ToString(), " @ ", spec.format(price) ?? price.ToString("#0.00##")).ToString();
public static string filled_status_description(this OrderRecordImmutable order, ContractSpecificationImmutable spec) {
var sb = new StringBuilderEx("filled ", order.filled().ToString());
public static string filled_status_description(this OrderRecordImmutable order, ContractSpecificationImmutable spec, uint display_qty_divisor = 1) {
var sb = new StringBuilderEx("filled ", order.filled().safe_divide(display_qty_divisor).ToString());
if (order.filled() > 0)
sb.Add(" @ ", spec.format(order.avg_price()));
sb.Add(", ", camel_to_title_case(order.status().ToString(), TextCapitalizationFormat.NoCaps));
+33 -10
View File
@@ -10,6 +10,7 @@ using bts.spark;
using bts.utils;
using mbts.config;
using mbts.spark.utilities;
using mbts.ui;
using mbts.utils;
using System;
using System.Collections.Generic;
@@ -26,8 +27,9 @@ using OrderUpdatePiq = bts.api.core.TableUpdate<bts.api.orderman.OrderSecurity,
namespace mbts.spark {
[Export]
class OrdersModel : IDisposable {
public readonly MarketDataAuxiliaryModel md_auxiliary_model;
public static readonly bool piq_enabled = Configuration.get<bts.config.toggles.PlacementInQueue>().enabled;
public readonly MarketDataAuxiliaryModel md_auxiliary_model;
public readonly ISecurityAuxiliaryDataModel security_auxiliary_model;
readonly IBus _bus;
readonly OrderFactory _factory;
@@ -44,8 +46,20 @@ namespace mbts.spark {
IDisposable _dispose;
[ImportingConstructor]
OrdersModel(IBus bus, OrderFactory factory, OrderService service, PositionService position_service, MBOService mbo_service,
SpreaderModel spreader, DynamicOrderHelper dynamic_helper, AdminModel admin_model, IAlgoStudioModel algo_model, MarketDataAuxiliaryModel md_auxiliary_model, SecurityStore sstore, ErrorReporter error) {
OrdersModel(IBus bus,
OrderFactory factory,
OrderService service,
PositionService position_service,
MBOService mbo_service,
SpreaderModel spreader,
DynamicOrderHelper dynamic_helper,
AdminModel admin_model,
IAlgoStudioModel algo_model,
ISecurityAuxiliaryDataModel security_auxiliary_model,
MarketDataAuxiliaryModel md_auxiliary_model,
SecurityStore sstore,
ErrorReporter error) {
//
_bus = bus;
_factory = factory;
_service = service;
@@ -57,6 +71,7 @@ namespace mbts.spark {
this.admin_model = admin_model;
this.algo_model = algo_model;
this.md_auxiliary_model = md_auxiliary_model;
this.security_auxiliary_model = security_auxiliary_model;
var scheduler = ThreadPoolScheduler.Instance;
var interval = TimeSpan.FromMilliseconds(100);
on_orders = admin_model
@@ -80,7 +95,10 @@ namespace mbts.spark {
_position_service.subscribei_pnl_data().Naggle(interval, scheduler),
p => p.Pos.AccountId)
.join(sstore, p => p.Pos.SecurityId)
.TableCheckedAutoCorrect(EqualityComparer<(SecurityImmutable? sec, PNLUnitImmutable data)>.Default)
.Select(
tup => (tup.sec, tup.val, new QuantityConversionData(security_auxiliary_model.get_display_quantity_divisor(tup.sec), QuantityConversionData.OpType.DivideActual)),
tup => tup.val.RecordId)
.TableCheckedAutoCorrect(EqualityComparer<(SecurityImmutable?, PNLUnitImmutable, QuantityConversionData)>.Default)
.Where(t => t.has_changes())
.Cache();
var working_orders = on_orders.Filter(tup => tup.order.is_active());
@@ -167,8 +185,8 @@ namespace mbts.spark {
working_account_order_groups,
PNLRowData.key_fn,
acc_orders => acc_orders.group_key,
(pnl_row_data, acc_orders, pnl_key) => pnl_row_data.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order))),
(pnl_row_data, pnl_id) => pnl_row_data,
(row, acc_orders, pnl_key) => row.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order), row.qty_conversion)),
(row, pnl_id) => row,
(acc_orders, pnl_id) => {
string account;
SecurityImmutable? security;
@@ -182,7 +200,9 @@ namespace mbts.spark {
return new PNLRowData(pnl_id,
security: security,
account_name: account,
working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order)));
working: WorkingOrderData.Create(
acc_orders.table.state.Values.Select(t => t.order),
new QuantityConversionData(security_auxiliary_model.get_display_quantity_divisor(security), QuantityConversionData.OpType.DivideActual)));
},
PNLRowData.key_fn)
.Where(t => t.has_changes())
@@ -215,8 +235,11 @@ namespace mbts.spark {
FillTuple.key_fn);
on_fill_tuples_with_root = fill_order_tuples_with_root_join.Cache();
// manual fills
on_manual_fills_securities = _position_service.subscribei_manual_fills()
.Naggle(interval, scheduler)
on_manual_fills_securities = admin_model
.permission_filter(
_position_service.subscribei_manual_fills()
.Naggle(interval, scheduler),
f => f.AccountId)
.Filter(f => f.FillType == ManualFill.Types.FillType.Local)
.join(sstore, mf => mf.SecurityKey)
.Table().Cache();
@@ -279,7 +302,7 @@ namespace mbts.spark {
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples { get; }
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples_with_root { get; }
public IObservable<IReadOnlyDictionary<uint, OrderSecurity>> working_orders { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable), int>> on_pnl { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable, QuantityConversionData), int>> on_pnl { get; }
public IObservable<TableUpdate<PNLRowData, PNLKey>> security_account_pnl { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PositionUnitImmutable), int>> on_positions { get; }
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
@@ -0,0 +1,43 @@
using bts.api;
using bts.utils;
using mbts.config;
using System.Collections.Generic;
using System.ComponentModel.Composition;
namespace mbts.spark {
interface ISecurityAuxiliaryDataModel {
SecurityAuxiliaryData? get_auxiliary_data(SecurityImmutable security);
}
readonly struct SecurityAuxiliaryData {
public SecurityAuxiliaryData(uint quantity_divisor) =>
this.quantity_divisor = quantity_divisor;
public static SecurityAuxiliaryData Default => new SecurityAuxiliaryData(1);
public uint quantity_divisor { get; }
}
[Export(typeof(ISecurityAuxiliaryDataModel))]
class SecurityAuxiliaryDataModel : ISecurityAuxiliaryDataModel {
readonly Dictionary<string, SecurityAuxiliaryData> _auxiliary_data = new Dictionary<string, SecurityAuxiliaryData>();
public SecurityAuxiliaryDataModel() {
var sym_config = Configuration.get<bts.config.SymbolUIConfiguration>();
var sym_config_items = Configuration.get<bts.config.SymbolUIConfiguration>().items;
foreach (var item in sym_config_items)
_auxiliary_data[item.symbol] = new SecurityAuxiliaryData(item.display_quantity_divisor);
}
public SecurityAuxiliaryData? get_auxiliary_data(SecurityImmutable security) =>
_auxiliary_data.TryGetValue(security.Symbol, out var d) ? d : (SecurityAuxiliaryData?)null;
}
static class SecurityAuxiliaryDataModelExtensions {
public static uint get_display_quantity(this ISecurityAuxiliaryDataModel model, SecurityImmutable? security, uint actual_qty) =>
actual_qty.safe_divide(model.get_display_quantity_divisor(security));
public static uint get_display_quantity_divisor(this ISecurityAuxiliaryDataModel model, SecurityImmutable? security) =>
security.HasValue ? model.get_auxiliary_data(security.Value)?.quantity_divisor ?? 1 : 1;
}
}
+1
View File
@@ -12,6 +12,7 @@
<RuntimeIdentifiers>win7-x64</RuntimeIdentifiers>
<PlatformTarget>AnyCPU</PlatformTarget>
<LanguageTargets>$(MSBuildExtensionsPath)\$(VisualStudioVersion)\Bin\Microsoft.CSharp.targets</LanguageTargets>
<LanguageTargets Condition="'$(VisualStudioVersion)'=='16.0'">$(MSBuildToolsPath)\Microsoft.CSharp.targets</LanguageTargets>
<ApplicationIcon>logo.ico</ApplicationIcon>
<OutputType>WinExe</OutputType>
<StartupObject>mbts.ui.shell.App</StartupObject>
@@ -38,9 +38,6 @@ namespace mbts {
public static bool IsNonZero(this double value) =>
value != 0 && !double.IsNaN(value) && !double.IsInfinity(value);
public static bool IsRealNonZero(this double value) =>
value > 0 && value != double.PositiveInfinity;
public static double CoerceNaN(double value) => CoerceNaN(value, 0.0);
public static double CoerceNaN(double value, double if_nan) => double.IsNaN(value) ? if_nan : value;
public static double CoerceNaNOrInfinity(double value) => CoerceNaNOrInfinity(value, 0.0);
@@ -63,5 +60,20 @@ namespace mbts {
public static double? nan_nullable(double value) =>
double.IsNaN(value) ? (double?)null : value;
public static int? to_optional_int32(double value) {
if (double.IsNaN(value) || double.IsInfinity(value))
return null;
if (value > 0) {
value = Math.Floor(value);
if (value <= int.MaxValue)
return (int)value;
} else if (value < 0) {
value = Math.Ceiling(value);
if (value >= int.MinValue)
return (int)value;
}
return null;
}
}
}
@@ -0,0 +1,9 @@
namespace mbts.ui.controls {
class DIYGridColumn : DIYGrid.ColumnBase<string> {
public DIYGridColumn()
: base() {
is_visible = true;
Width = 60;
}
}
}
@@ -38,11 +38,7 @@ namespace mbts.ui.controls {
}
public static ICommand PreviewSoundCommand =>
new DelegateCommand<SoundNotification.SystemSound>(s => {
try {
NAudioPlayer.create(s).play();
} catch (NAudio.MmException) { }
});
new DelegateCommand<SoundNotification.SystemSound>(NAudioPlayer.play);
#region SoundTypes
public static readonly DependencyProperty SoundTypesProperty =
@@ -1,39 +1,43 @@
using System;
using System.Text.RegularExpressions;
using System;
using System.Text.RegularExpressions;
namespace mbts.spark.filters {
static class RegexFilterUtils {
public static Regex build_regex(string str) {
if (str == null)
return null;
str = str.Trim();
if (str == string.Empty)
return null;
return new Regex(str, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
}
}
class RegexFilterVM<TRow> : FilterVMBase<TRow, string, string> {
string _text = string.Empty;
Regex _regex;
public RegexFilterVM(Func<TRow, string> get_row_val, string prop_name = null) : base(get_row_val, prop_name) { }
public string text {
get => _text;
set => _regex = RegexFilterUtils.build_regex(_text = (value ?? string.Empty));
}
public override string get_state() => _text ?? string.Empty;
protected override bool _get_active() => _regex != null;
protected override bool _filter(string value) =>
_regex != null && (
_regex.IsMatch(value) ||
(!string.IsNullOrEmpty(value) && _regex.IsMatch(value.Replace(" ", ""))));
namespace mbts.spark.filters {
static class RegexFilterUtils {
public static Regex build_regex(string str) {
if (str == null)
return null;
str = str.Trim();
if (str == string.Empty)
return null;
return new Regex(str, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
}
}
class RegexFilterVM<TRow> : FilterVMBase<TRow, string, string> {
string _text = string.Empty;
Regex _regex;
public RegexFilterVM(Func<TRow, string> get_row_val, string prop_name = null) : base(get_row_val, prop_name) { }
public string text {
get => _text;
set => _regex = RegexFilterUtils.build_regex(_text = (value ?? string.Empty));
}
public override string get_state() => _text ?? string.Empty;
protected override bool _get_active() => _regex != null;
protected override bool _filter(string value) => _regex != null && _regex.IsMatch(value);
protected override void _load_state_internal(string state) => text = state;
protected override void _clear() {
_close();
_text = string.Empty;
_regex = null;
RaisePropertyChanged(nameof(text));
_raise_filter_changed();
}
}
protected override void _load_state_internal(string state) => text = state;
protected override void _clear() {
_close();
_text = string.Empty;
_regex = null;
RaisePropertyChanged(nameof(text));
_raise_filter_changed();
}
}
}
@@ -1005,6 +1005,8 @@
ContractPickerModel="{Binding contract_picker_model}"
HedgePricing="{Binding hedge_pricing}"
HedgePricingItemsSource="{Binding hedge_pricing_options}"
UseFractionalTicks="{Binding use_fractional_ticks}"
IntegerTickSize="{Binding integer_tick_size}"
/>
<c:DialogSkirt Grid.Row="1" Margin="-4" VerticalAlignment="Top" RightItems="{Binding skirt_items}"/>
</Grid>
@@ -12,6 +12,9 @@ namespace mbts.ui.converters
public Visibility VisibilityForFalse { get { return _visfalse; } set { _visfalse = value; } }
public static BooleanToVisibilityConverter Instance => new BooleanToVisibilityConverter();
public static BooleanToVisibilityConverter HiddenInstance => new BooleanToVisibilityConverter {
VisibilityForFalse = Visibility.Hidden
};
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
bool flag = false;
@@ -0,0 +1,38 @@
using bts.utils;
using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
namespace mbts.ui.converters {
[ValueConversion(typeof(double), typeof(string))]
class DoubleDivisorConverter : IValueConverter {
public double Divisor { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value is double d ? d.safe_divide(Divisor) :
value is int i ? i.safe_divide(Divisor) :
value is uint u ? u.safe_divide(Divisor) :
double.NaN;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
value is double d ? d * Divisor :
value is int i ? (double)i * Divisor :
value is uint u ? (double)u * Divisor :
double.NaN;
}
class DynamicDoubleDivisorConverter : IMultiValueConverter {
public static DynamicDoubleDivisorConverter Instance { get; } = new DynamicDoubleDivisorConverter();
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (ConvertUtils.try_convert_args(values, out double value, out double div)) {
return value.safe_divide(div);
}
return new ValidationResult(false, "unable to convert values to doubles.");
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
}
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
namespace mbts.ui.converters {
@@ -19,4 +20,18 @@ namespace mbts.ui.converters {
value is uint u ? (double)u / Multiplier :
double.NaN;
}
}
class DynamicDoubleMultiplierConverter : IMultiValueConverter {
public static DynamicDoubleMultiplierConverter Instance { get; } = new DynamicDoubleMultiplierConverter();
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (ConvertUtils.try_convert_args(values, out double value, out double mult)) {
return value * mult;
}
return new ValidationResult(false, "unable to convert values to doubles.");
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
}
@@ -183,7 +183,7 @@ namespace mbts.ui.dpi {
}
public static void DrawHorizontalLine(this DrawingContext drawing_context, System.Windows.Media.Pen pen, Point start, double length, bool snap_to_device_pixels) {
if (pen == null || !length.IsRealNonZero()) {
if (pen == null || !length.is_real_positive()) {
// no need to flip out and throw an exception
return;
}
@@ -1,4 +1,5 @@
using System;
using bts.utils;
using System;
using System.ComponentModel;
using System.Windows.Markup;
@@ -19,7 +20,7 @@ namespace mbts.ui {
public double RoundingUnit { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider) =>
RoundingUnit.IsRealNonZero() ? Math.Round(Value * RoundingUnit) / RoundingUnit : Value;
RoundingUnit.is_real_non_zero() ? Math.Round(Value * RoundingUnit) / RoundingUnit : Value;
}
internal class DoubleStringTypeConverter : TypeConverter
+23 -1
View File
@@ -49,6 +49,28 @@ namespace mbts.spark {
public static double parse(this ContractSpecificationImmutable? spec, string text, double display_multiplier, string nan_text) =>
spec.spec_or_default().parse(text, display_multiplier, nan_text);
public static bool is_integer_tick(ContractSpecificationImmutable spec) =>
spec.MainFraction == 0 &&
spec.PriceDisplayFormat == 0 &&
spec.LowTick > 0 &&
MathUtils.is_int(spec.LowTick) &&
spec.HighTick > 0 &&
MathUtils.is_int(spec.HighTick);
public static ContractSpecification create_integer_tick_spec(int low_tick) =>
create_integer_tick_spec(low_tick, low_tick);
public static ContractSpecification create_integer_tick_spec(int low_tick, int high_tick) {
var rv = SecurityDataHelper.Defaults.ContractSpecification;
rv.MainFraction = 0;
rv.SubFraction = 0;
rv.PriceDisplayFormat = 0;
rv.PriceBreak = 0;
rv.LowTick = low_tick;
rv.HighTick = high_tick;
return rv;
}
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double low = bids[bids.Length - 1].price;
if (double.IsNaN(low))
@@ -326,7 +348,7 @@ namespace mbts.spark {
}
static bool _check_tick_size(double tick_size) =>
(tick_size > 0 && tick_size <= 0.5) || tick_size == 1;
tick_size > 0 && (tick_size <= 0.5 || MathUtils.is_int(tick_size));
struct SpecKey : IEquatable<SpecKey> {
public readonly uint denominator, display_digits;
@@ -45,15 +45,16 @@ namespace mbts.spark {
readonly SecurityImmutable _security;
readonly MBOSecurityQueryImmutable _query;
readonly Dictionary<(string,bool), FormattedText> _cache;
readonly Func<int, string> _fmt_mbo_qty;
int _max_order_size = 1;
double _height = 0;
double _number_width = 0;
public MBOPopupModel(MBOService mbo, SecurityImmutable security, MBOSecurityQueryImmutable query) {
public MBOPopupModel(MBOService mbo, SecurityImmutable security, MBOSecurityQueryImmutable query, Func<int, string> fmt_mbo_qty) {
_mbo = mbo;
_security = security;
_query = query;
_fmt_mbo_qty = fmt_mbo_qty ?? new Func<int, string>(v => v.ToString());
// prime the cache with numbers on the left hand side
_cache =
@@ -110,7 +111,7 @@ namespace mbts.spark {
pdp.row_numbers = p.orders.Select((o, i) => format_text((i+1).ToString(), ismine(o))).ToList();
pdp.numbers_width = _number_width = Math.Max(_number_width, (int)pdp.row_numbers.Max(r => r.Width) + text_margin);
pdp.row_labels = p.orders.Select(o => format_text(o.Size.ToString() + (ismine(o) ? "*" : ""), ismine(o))).ToList();
pdp.row_labels = p.orders.Select(o => format_text(_fmt_mbo_qty(o.Size) + (ismine(o) ? "*" : ""), ismine(o))).ToList();
var max_label_width = pdp.row_labels.Max(l => l.Width) + text_margin;
var available_width = pdp.width - pdp.numbers_width - max_label_width;
@@ -7,26 +7,53 @@ using System.Windows.Input;
namespace mbts.spark {
public class CalculatorLogic : DisposableViewModelBase {
readonly UserProperty<uint?> _value;
readonly OptionalDisplayQuantityMultiplier _display_translator = new OptionalDisplayQuantityMultiplier();
IDisposable _prop_changes_echo;
int[] _buttons;
public CalculatorLogic(uint? initial_size) =>
public CalculatorLogic(uint? initial_size) {
_value = new UserProperty<uint?>(backing(nameof(value), initial_size));
_echo_props_changed();
}
public CalculatorLogic(uint? initial_size, IObservable<int[]> buttons_source) : this(initial_size) =>
_dispose = buttons_source.Subscribe(newval => buttons = newval);
public uint? value { get => _value.Value; set => _value.Value = value; }
public uint display_divisor {
get => _display_translator.conversion_data.value;
set => _display_translator.conversion_data = new QuantityConversionData(value, QuantityConversionData.OpType.DivideActual);
}
public uint? value {
get => _display_translator.actual_value;
set => _display_translator.actual_value = value;
}
public uint? display_value {
get => _display_translator.display_value;
set => _display_translator.display_value = value;
}
public bool value_setter_called => _value.setter_called;
public IObservable<uint?> value_observable => _value.Select(vals => vals.newval);
public int[] buttons { get => _buttons; set => Set(ref _buttons, value); }
public ICommand click => new DelegateCommand<uint>(handle_calculator_click);
public ICommand set_value_command => new DelegateCommand<uint>(v => _value.Value = v);
public ICommand clear => new DelegateCommand(() => _value.Value = null);
public ICommand set_display_value_command => new DelegateCommand<uint>(v => _display_translator.display_value = v);
public ICommand set_actual_value_command => new DelegateCommand<uint>(v => _display_translator.actual_value = v);
public ICommand clear => new DelegateCommand(() => _display_translator.actual_value = null);
public IObservable<uint?> observe_current(bool start_immediately = false) =>
_value.observe_current(start_immediately);
public IObservable<uint?> observe_actual_value() =>
_display_translator.subscribe_actual_value();
public void bypass_value_setter(uint value) => _value.bypass_setter(value);
// Don't use a default value for 'is_actual_value.'
// Quietly multiplying (or not multiplying) the size input is dangerous -
// force the caller to be specific.
public void bypass_value_setter(uint value, bool is_actual_value) {
Disposable.dispose(ref _prop_changes_echo);
if (is_actual_value) {
this.value = value;
} else {
display_value = value;
}
if (_prop_changes_echo != null)
_echo_props_changed();
}
public bool validate() {
if (!value.HasValue || value.Value == 0) {
@@ -38,25 +65,34 @@ namespace mbts.spark {
public void flash_error() =>
flash_error(nameof(value)).ignore();
protected override void Dispose(bool disposing) {
Disposable.dispose(ref _prop_changes_echo);
base.Dispose(disposing);
}
void _echo_props_changed() {
// dispose the echo to avoid raising prop changed events more than once
_prop_changes_echo?.Dispose();
_prop_changes_echo = _display_translator.connect_owner(this, nameof(display_value), nameof(value));
}
void handle_calculator_click(uint value) {
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) || value == 0) {
_value.Value = value;
_display_translator.actual_value = value;
} else if (KeyboardExtensions.IsCtrlDown()) {
if (!_value.Value.HasValue)
_value.Value = 0;
else {
if (value > _value.Value)
_value.Value = 0;
else
_value.Value -= value;
if (!_display_translator.actual_value.HasValue) {
_display_translator.actual_value = 0;
} else if (value > _display_translator.actual_value) {
_display_translator.actual_value = 0;
} else {
_display_translator.actual_value -= value;
}
}
else {
if (!_value.Value.HasValue)
_value.Value = value;
} else {
if (!_display_translator.actual_value.HasValue)
_display_translator.actual_value = value;
else
_value.Value += value;
_display_translator.actual_value += value;
}
}
}
}
}
@@ -1,5 +1,7 @@
using mbts.ui.converters;
using System;
using System.Globalization;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
@@ -12,6 +14,31 @@ namespace mbts.ui.utilities {
public static string fmt(string title, KeyGesture key_gesture) =>
key_gesture == null ? title : $"{title} ({key_gesture.GetDisplayStringForCulture(CultureInfo.CurrentCulture)})";
public static IDisposable bind_param_source<T>(ICommand command, IObservable<T> param_source, out ICommand bound_cmd, bool dispose_command_on_dispose = false) {
bool initialized = false;
bool can_execute = false;
T param = default;
var bcmd = new DelegateCommand(() => command.Execute(param), () => can_execute);
bound_cmd = bcmd;
var sub = param_source.Subscribe(p => {
param = p;
if (!initialized) {
initialized = true;
can_execute = command.CanExecute(p);
bcmd.RaiseCanExecuteChanged();
} else if (can_execute != command.CanExecute(p)) {
can_execute = !can_execute;
bcmd.RaiseCanExecuteChanged();
}
});
return dispose_command_on_dispose ?
Disposable.Create(() => {
Disposable.dispose(ref sub);
(command as IDisposable)?.Dispose();
command = null;
}) : sub;
}
#region Paraphrased from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Commands/CommandHelpers.cs
public static bool CanExecuteCommandSource(ICommandSource commandSource) {
@@ -30,6 +30,21 @@ namespace mbts.ui {
public string name { get; }
}
class NamedValueVM : ViewModelBase {
string _name;
object _value;
public NamedValueVM() { }
public NamedValueVM(string name, object value) {
_name = name;
_value = value;
}
public string name { get => _name; set => Set(ref _name, value); }
public object value { get => _value; set => Set(ref _value, value); }
}
static class EnabledValueVMExtensions {
public static T? get_nullable<T>(this EnabledValueVM<T> vm) where T : struct =>
vm.enabled ? vm.value : (T?)null;
@@ -0,0 +1,172 @@
using bts.utils;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
namespace mbts.ui {
interface IDisplayTranslator<T> {
T display_value { get; set; }
T actual_value { get; set; }
event Action value_changed;
}
readonly struct QuantityConversionData : IEquatable<QuantityConversionData> {
public QuantityConversionData(uint value, OpType op) {
this.value = value;
this.op = op;
}
public enum OpType {
None,
MultiplyActual,
DivideActual
}
public uint value { get; }
public OpType op { get; }
public bool Equals(QuantityConversionData other) =>
value == other.value &&
op == other.op;
public override bool Equals(object obj) =>
obj is QuantityConversionData other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.auto_phase(value.GetHashCode(), op.GetHashCode());
public static bool operator ==(QuantityConversionData x, QuantityConversionData y) => x.Equals(y);
public static bool operator !=(QuantityConversionData x, QuantityConversionData y) => !x.Equals(y);
}
class DisplayQuantityMultiplier : IDisplayTranslator<uint> {
uint _actual_value;
QuantityConversionData _conversion_data;
public event Action value_changed;
public uint actual_value {
get => _actual_value;
set {
if (_actual_value != value) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public uint display_value {
get => _conversion_data.to_display(_actual_value);
set => actual_value = _conversion_data.from_display(value);
}
public QuantityConversionData conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
class OptionalDisplayQuantityMultiplier : IDisplayTranslator<uint?> {
uint? _actual_value;
QuantityConversionData _conversion_data;
public event Action value_changed;
public uint? actual_value {
get => _actual_value;
set {
if (_actual_value != value) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public uint? display_value {
get => _actual_value.HasValue ? _conversion_data.to_display(_actual_value.Value) : _actual_value;
set => actual_value = value.HasValue ? _conversion_data.from_display(value.Value) : value;
}
public QuantityConversionData conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
static class DisplayTranslatorExtensions {
public static IDisposable connect_owner<T>(this IDisplayTranslator<T> display_translator, IRaiseNotifyPropertyChanged owner, params string[] properties) {
if (display_translator is null)
throw new ArgumentNullException(nameof(display_translator));
if (owner is null)
throw new ArgumentNullException(nameof(owner));
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
for (int i = 0; i < properties.Length; i++)
owner.raise_notify_property_changed(properties[i]);
}
}
public static IObservable<T> subscribe_actual_value<T>(this IDisplayTranslator<T> display_translator) =>
subscribe_actual_value(display_translator, EqualityComparer<T>.Default);
public static IObservable<T> subscribe_actual_value<T>(this IDisplayTranslator<T> display_translator, IEqualityComparer<T> comparer) {
if (comparer is null)
throw new ArgumentNullException(nameof(comparer));
return Observable.Create<T>(o => {
var curr = display_translator.actual_value;
o.OnNext(curr);
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
var next = display_translator.actual_value;
if (!comparer.Equals(curr, next))
o.OnNext(curr = next);
}
});
}
public static uint to_display(this QuantityConversionData quantity_conversion_data, uint actual_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return actual_value.safe_divide(quantity_conversion_data.value);
case QuantityConversionData.OpType.MultiplyActual: return actual_value * quantity_conversion_data.value;
default: return actual_value;
}
}
public static int to_display(this QuantityConversionData quantity_conversion_data, int actual_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return (int)(actual_value.safe_divide(quantity_conversion_data.value));
case QuantityConversionData.OpType.MultiplyActual: return (int)(actual_value * quantity_conversion_data.value);
default: return actual_value;
}
}
public static double to_display(this QuantityConversionData quantity_conversion_data, double actual_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return actual_value.safe_divide(quantity_conversion_data.value);
case QuantityConversionData.OpType.MultiplyActual: return actual_value * quantity_conversion_data.value;
default: return actual_value;
}
}
public static uint from_display(this QuantityConversionData quantity_conversion_data, uint display_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return display_value * quantity_conversion_data.value;
case QuantityConversionData.OpType.MultiplyActual: return display_value.safe_divide(quantity_conversion_data.value);
default: return display_value;
}
}
}
}
@@ -109,6 +109,9 @@ namespace mbts.ui.spark {
/// These are private members, so any code-readability cost is confined to a limited scope.
delegate void _SizeFormatter(ref uint merged, ref uint implied, ref StringBuilder sb);
delegate void _SizeFormatterNPS(ref NativePriceSize nps, ref StringBuilder sb);
public static Func<uint, string> get_value_formatter(uint divisor) =>
value => (value / divisor).ToString();
public static Func<NativePriceSize, string> get_formatter_nps(MarketSizeDisplay size_display) {
_SizeFormatterNPS implied_absent = _get_formatter_nps(size_display.normal);
@@ -120,6 +123,11 @@ namespace mbts.ui.spark {
};
}
public static Func<NativePriceSize, string> get_formatter_nps(MarketSizeDisplay size_display, uint display_divisor) {
var raw_fmt_size = get_formatter(size_display);
return nps => raw_fmt_size((nps.implied_size + nps.direct_size) / display_divisor, nps.implied_size / display_divisor);
}
public static SizeFormatter get_formatter(MarketSizeDisplay size_display) {
_SizeFormatter implied_absent = _get_formatter(size_display.normal);
_SizeFormatter implied_present = _get_formatter(size_display.implied_present ?? size_display.normal);
@@ -281,4 +289,4 @@ namespace mbts.ui.spark {
public void refresh() => _display.refresh();
}
}
}
}
@@ -39,13 +39,18 @@ namespace bts.api {
static string _format(SecurityImmutable? security) {
switch (security?.ContractCase) {
case ContractOneofCase.Future: return format_future(security?.Symbol, security?.security_desc(), security.Value.Future);
case ContractOneofCase.Future:
if(security?.HasIce == true)
return _format_ICE(security);
return format_future(security?.Symbol, security?.security_desc(), security.Value.Future);
case ContractOneofCase.Spread:
if (security?.HasGlbx == true) {
// In case we ever bring it back, pretty printed spread names can be found here:
// GlbxSecurityFormatter.FormatSpread(security, out string result)
return security?.Glbx.SecurityDesc;
}
else if(security?.HasIce == true)
return _format_ICE(security);
return string.Join("", _format_future_legs(SecurityHelper.get_flat_future_legs(security?.Spread.Legs)));
case ContractOneofCase.Synthetic: return security?.Synthetic.Name;
default: return "Unknown-security";
@@ -79,5 +84,28 @@ namespace bts.api {
default: return $"{fl.quantity:+0;-0}{sym_exp_name}";
}
}
static string _format_ICE (SecurityImmutable? security){
string symbol = security?.Symbol,
strip = security?.Ice.StripName,
md_desc = security?.Ice.MarketDescription,
type_append = "";
if(md_desc.Contains("25k ICE Lots")){
if(md_desc.Contains("TAS"))
type_append = "TAS";
}
else if(md_desc.Contains("ICE Lots")){
if(md_desc.Contains("TAS"))
type_append = "ICE Lots TAS";
else
type_append = "ICE Lots";
}
else if(md_desc.Contains("TAS"))
type_append = "TAS";
type_append = string.IsNullOrEmpty(type_append) ?
" " : $" {type_append} ";
return $"{symbol}{type_append}{strip}";
}
}
}
+13 -11
View File
@@ -83,22 +83,24 @@ namespace bts.api {
}
public static int Compare(SecurityImmutable x, SecurityImmutable y) {
if (x.ContractCase == ContractOneofCase.Future) return y.ContractCase == ContractOneofCase.Future ? Compare(x.Symbol, x.Future, y.Symbol, y.Future) : -1;
else if (y.ContractCase == ContractOneofCase.Future) return 1;
if (x.ContractCase == ContractOneofCase.Future) {
return y.ContractCase == ContractOneofCase.Future ? Compare(x.Symbol, x.Future, y.Symbol, y.Future) : -1;
} else if (y.ContractCase == ContractOneofCase.Future) {
return 1;
}
var xlegs = get_flat_future_legs(x).ToList();
var ylegs = get_flat_future_legs(y).ToList();
int max = Math.Min(xlegs.Count, ylegs.Count);
for (int i = 0; i < max; i++) {
int rv = Compare(xlegs[i].symbol, xlegs[i].future, ylegs[i].symbol, ylegs[i].future);
if (rv != 0) return rv;
}
for (int i = 0; i < max; i++) {
int rv = xlegs[i].quantity.CompareTo(ylegs[i].quantity);
if (rv != 0) return rv;
}
for (int i = 0; i < max; i++) {
int rv = xlegs[i].exchange.CompareTo(ylegs[i].exchange);
if (rv != 0) return rv;
if (rv != 0)
return rv;
rv = xlegs[i].quantity.CompareTo(ylegs[i].quantity);
if (rv != 0)
return rv;
rv = xlegs[i].exchange.CompareTo(ylegs[i].exchange);
if (rv != 0)
return rv;
}
return xlegs.Count.CompareTo(ylegs.Count);
}
+43
View File
@@ -110,6 +110,28 @@ namespace bts.utils {
return epsilon_equals(with_int_value, d);
}
public static bool is_real(this double value) {
switch (value) {
case double.PositiveInfinity:
case double.NegativeInfinity:
case double.NaN:
return false;
default:
return true;
}
}
public static bool is_real_positive(this double value) =>
value > 0 && value != double.PositiveInfinity;
public static bool is_real_negative(this double value) =>
value < 0 && value != double.NegativeInfinity;
public static bool is_real_non_zero(this double value) =>
value > 0 ?
!double.IsPositiveInfinity(value) :
value < 0 && !double.IsNegativeInfinity(value);
public static double factor_out(double value, params double[] factors) {
foreach (double f in factors)
value = factor_out(value, f, out int _);
@@ -129,6 +151,27 @@ namespace bts.utils {
return value;
}
public static int safe_divide(this int value, int divisor) =>
divisor == 0 ? value : value / divisor;
public static int safe_divide(this int value, uint divisor) =>
divisor == 0 ? value : (int)(value / divisor);
public static double safe_divide(this int value, double divisor) =>
divisor.is_real_non_zero() ? value / divisor : value;
public static uint safe_divide(this uint value, uint divisor) =>
divisor == 0 ? value : value / divisor;
public static double safe_divide(this uint value, double divisor) =>
divisor.is_real_non_zero() ? value / divisor : value;
public static ulong safe_divide(this ulong value, uint divisor) =>
divisor == 0 ? value : value / divisor;
public static double safe_divide(this double value, double divisor) =>
divisor.is_real_non_zero() && value.is_real_non_zero() ? value / divisor : value;
/// <summary>
/// Adds <see cref="mod"/> to the modulos result if negative.
/// </summary>
@@ -390,6 +390,15 @@ namespace System.Collections.Generic {
public static ReadOnlyDictionary<TKey, TElement> ToReadOnlyDictionary<T, TKey, TElement>(this IEnumerable<T> enumerable, Func<T, TKey> key_selector, Func<T, TElement> element_selector, IEqualityComparer<TKey> comparer) =>
new ReadOnlyDictionary<TKey, TElement>(enumerable.ToDictionary(key_selector, element_selector, comparer));
public static ReadOnlyDictionary<TKey, TElement> ToReadOnlyDictionaryWhere<TKey, TElement>(this IEnumerable<TKey> keys, TryGetValue<TKey, TElement> value_selector) {
var mdict = new Dictionary<TKey, TElement>();
foreach (var k in keys) {
if (value_selector(k, out var val))
mdict[k] = val;
}
return new ReadOnlyDictionary<TKey, TElement>(mdict);
}
public static ILookup<TKey, TElement> ToFlatLookup<T, TKey, TElement>(this IEnumerable<T> source, Func<T, TKey> key_selector, Func<T, IEnumerable<TElement>> selector) =>
source.SelectMany(v => {
var k = key_selector(v);
@@ -103,6 +103,15 @@ namespace bts.collections {
return array;
}
public static bool SafeTryGetAt<T>(this IList<T> list, int index, out T value) {
if (list != null && index >= 0 && index < list.Count) {
value = list[index];
return true;
}
value = default;
return false;
}
public static bool TryGetAt<T>(this IList<T> list, int index, out T value) {
if (index >= 0 && index < list.Count) {
value = list[index];
@@ -1,11 +1,11 @@
using System;
using System.Globalization;
namespace mbts.utils {
delegate bool TryGetValue<T>(out T value);
delegate bool TryGetValue<TKey, TValue>(TKey key, out TValue value);
namespace bts.utils {
public delegate bool TryGetValue<T>(out T value);
public delegate bool TryGetValue<TKey, TValue>(TKey key, out TValue value);
static class LogicalDelegates {
public static class LogicalDelegates {
public static readonly Func<bool, bool, bool>
And = (a, b) => a && b,
Or = (a, b) => a || b,
@@ -13,7 +13,10 @@ namespace mbts.utils {
None = (a, b) => !a && !b;
}
static class FormattingDelegates {
public static class FormattingDelegates {
public static Func<T, string> default_string<T>() =>
v => v.ToString();
public static class Price {
public static readonly Func<double, string> Value =
p => double.IsNaN(p) ? "" : p.ToString("#,0.00");
@@ -26,4 +29,4 @@ namespace mbts.utils {
p => p.ToString("#,0.00", CultureInfo.CurrentCulture);
}
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.IO;
namespace bts.utils {
public static class FilePathHelper {
public static string GetUniquePathWithDate(string path_root, string ext) =>
GetUniquePath($"{path_root}_{DateTime.Now:MM - dd - yyyy_hh - mm}", ext);
public static string GetUniquePath(string path_root, string ext) {
var path = $"{path_root}.{ext}";
const int max_copies = 2 ^ 14;
int i = 1;
while (File.Exists(path)) {
path = $"{path_root}({i++}).{ext}";
if (i == max_copies)
throw new InvalidOperationException("Unable to make path unique by concating a number.");
}
return path;
}
}
}
@@ -209,7 +209,7 @@ namespace System.Reactive.Linq {
/// <returns>An IDisposable object used to unsubscribe from the source sequence, and dispose the latest value subscription.</returns>
public static IDisposable SubscribeEach<T>(this IObservable<T> source, Func<T, IDisposable> subscribe_value) {
IDisposable value_sub = null;
IDisposable source_sub = source.Select(v => v).Subscribe(on_source);
IDisposable source_sub = source.Subscribe(on_source);
return Disposable.Create(dispose);
// local methods
void on_source(T value) {
+253 -208
View File
@@ -1,208 +1,253 @@
using bts.api.core;
using bts.collections;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
namespace bts.utils {
public static class UpdateExtensions {
public static TableUpdate<T, TKey> with_added<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<T> added) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, added, Enumerable.Empty<Change<T>>(), Enumerable.Empty<T>(), table.key_extractor);
public static TableUpdate<T, TKey> with_removed<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<T> removed) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, Enumerable.Empty<T>(), Enumerable.Empty<Change<T>>(), removed, table.key_extractor);
public static TableUpdate<T, TKey> with_changed<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<T> changed) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, Enumerable.Empty<T>(), changed.Select(v => new Change<T>(table.state[table.key_extractor(v)], v)), Enumerable.Empty<T>(), table.key_extractor);
public static TableUpdate<T, TKey> with_changes<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<Change<T>> changes) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, Enumerable.Empty<T>(), changes, Enumerable.Empty<T>(), table.key_extractor);
public static IObservable<TableUpdate<TResult, TKey>> RootDependent<T, TResult, TKey>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> with_root,
Func<T, TResult> without_root,
Func<TResult, TKey> result_key)
where TKey : IEquatable<TKey> =>
RootDependent(source, key, parent_key, with_root, without_root, result_key, default);
// TODO: unit tests
public static IObservable<TableUpdate<TResult, TKey>> RootDependent<T, TKey, TResult>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> get_result,
Func<T, TResult> get_result_no_parent,
Func<TResult, TKey> result_key,
TKey no_parent_key)
where TKey : IEquatable<TKey> =>
Observable.Create<TableUpdate<TResult, TKey>>(observer => {
var state = DictionaryHelper.empty<TKey, T>();
var output = TableUpdate<TResult, TKey>.mk_empty(result_key);
var parent_to_children = new SetMap<TKey, T>();
return source.Subscribe(update);
//
void update(TableUpdate<T, TKey> input) {
state = input.state;
var builder = output.to_builder();
// execute all parent_to_children changes first, so descendent-propagation will be efficient (otherwise propagation might be needlessly performed on values that are yet to be removed, or performed with the wrong root, if the real root is yet to be added).
foreach (var v in input.removed.Concat(input.changes.Select(ch => ch.Old))) {
var k = input.key_extractor(v);
parent_to_children.remove(parent_key(v), v);
}
foreach (var v in input.added.Concat(input.changed)) {
var pk = parent_key(v);
if (!pk.Equals(no_parent_key))
parent_to_children.add(pk, v);
}
foreach (var v in input.removed) {
var k = input.key_extractor(v);
builder.remove(k);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children) {
var ck = key(child);
builder.add_or_update(ck, get_result_no_parent(child));
foreach (var d in descendents(ck))
builder.add_or_update(key(d), get_result(d, child));
}
}
}
foreach (var v in input.changed) {
var k = input.key_extractor(v);
var root = get_root(v);
TResult result;
if (key(root).Equals(k)) {
result = get_result_no_parent(v);
foreach (var d in descendents(k))
builder.add_or_update(key(d), get_result(d, v));
} else {
result = get_result(v, root);
}
builder.add_or_update(k, result);
}
foreach (var v in input.added) {
var k = input.key_extractor(v);
var pk = parent_key(v);
TResult result;
if (pk.Equals(no_parent_key)) {
result = get_result_no_parent(v);
foreach (var d in descendents(k))
builder.add_or_update(key(d), get_result(d, v));
} else {
parent_to_children.add(pk, v);
var root = get_root(v);
if (key(root).Equals(k)) {
result = get_result_no_parent(v);
foreach (var d in descendents(k))
builder.add_or_update(key(d), get_result(d, v));
} else {
result = get_result(v, root);
}
}
builder.add_or_update(k, result);
}
observer.OnNext(output = builder.to_table());
}
T get_root(T value) {
var pk = parent_key(value);
while (!pk.Equals(no_parent_key) && state.TryGetValue(pk, out var parent)) {
value = parent;
pk = parent_key(value);
}
return value;
}
IEnumerable<T> descendents(TKey k) {
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children) {
yield return child;
foreach (var d in descendents(key(child)))
yield return d;
}
}
}
});
public static IObservable<TableUpdate<T, TKey>> MergeTable<T, TKey>(this IObservable<TableUpdate<T, TKey>> source, IObservable<TableUpdate<T, TKey>> other) where TKey : IEquatable<TKey> =>
source.AggregateLatest(other, _Merge<T, TKey>.source_change_del, _Merge<T, TKey>.other_change_del, () => TableUpdate<T, TKey>.empty, TableUpdate<T, TKey>.empty, TableUpdate<T, TKey>.empty);
public static IObservable<TableUpdate<KeyValuePair<TKey, TResult>, TKey>> ParentDependent<T, TKey, TResult>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> get_result,
Func<T, TResult> get_result_no_parent)
where TResult : IEquatable<TResult>
where TKey : IEquatable<TKey> =>
ParentDependent(source, key, parent_key, get_result, get_result_no_parent, EqualityComparer<TResult>.Default, default);
public static IObservable<TableUpdate<KeyValuePair<TKey, TResult>, TKey>> ParentDependent<T, TKey, TResult>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> get_result,
Func<T, TResult> get_result_no_parent,
IEqualityComparer<TResult> result_comparer,
TKey no_parent_key)
where TKey : IEquatable<TKey> =>
Observable.Create<TableUpdate<KeyValuePair<TKey, TResult>, TKey>>(observer => {
var value_comparer = EqualityComparer.Create<KeyValuePair<TKey, TResult>, TResult>(kv => kv.Value, result_comparer);
var state = DictionaryHelper.empty<TKey, T>();
var output = TableUpdate<KeyValuePair<TKey, TResult>, TKey>.mk_empty(kv => kv.Key);
var parent_to_children = new SetMap<TKey, T>();
return source.Subscribe(update);
//
void update(TableUpdate<T, TKey> input) {
state = input.state;
var builder = output.to_builder();
foreach (var v in input.removed) {
var k = input.key_extractor(v);
builder.remove(k);
parent_to_children.remove(parent_key(v), v);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children)
builder.add_or_update(new KeyValuePair<TKey, TResult>(key(child), get_result_no_parent(child)));
}
}
foreach (var v in input.changed) {
var k = key(v);
builder.add_or_update(new KeyValuePair<TKey, TResult>(k, lookup_result(v)));
parent_to_children.set(parent_key(v), v);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children)
builder.add_or_update(new KeyValuePair<TKey, TResult>(key(child), get_result(child, v)));
}
}
foreach (var v in input.added) {
var k = key(v);
builder.add_or_update(new KeyValuePair<TKey, TResult>(k, lookup_result(v)));
parent_to_children.add(parent_key(v), v);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children)
builder.add_or_update(new KeyValuePair<TKey, TResult>(key(child), get_result(child, v)));
}
}
var result = builder.to_table_checked(value_comparer);
if (result.has_changes())
observer.OnNext(output = result);
}
TResult lookup_result(T child) {
var pk = parent_key(child);
return !pk.Equals(no_parent_key) && state.TryGetValue(pk, out T parent) ? get_result(child, parent) : get_result_no_parent(child);
}
});
static class _Merge<T, TKey> where TKey : IEquatable<TKey> {
public static readonly Func<TableUpdate<T, TKey>, Change<TableUpdate<T, TKey>>, TableUpdate<T, TKey>, TableUpdate<T, TKey>> source_change_del = _source_changed_impl;
public static readonly Func<TableUpdate<T, TKey>, TableUpdate<T, TKey>, Change<TableUpdate<T, TKey>>, TableUpdate<T, TKey>> other_change_del = _other_changed_impl;
static TableUpdate<T, TKey> _source_changed_impl(TableUpdate<T, TKey> target, Change<TableUpdate<T, TKey>> source_change, TableUpdate<T, TKey> other_table) =>
TableUpdate<T, TKey>.update(target, source_change.New.added, source_change.New.changes, source_change.New.removed, source_change.New.key_extractor);
static TableUpdate<T, TKey> _other_changed_impl(TableUpdate<T, TKey> target, TableUpdate<T, TKey> source_table, Change<TableUpdate<T, TKey>> other_change) =>
TableUpdate<T, TKey>.update(target, other_change.New.added, other_change.New.changes, other_change.New.removed, other_change.New.key_extractor);
}
}
}
using bts.api.core;
using bts.collections;
using bts.utils.proto;
using Google.Protobuf;
using mbts;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
namespace bts.utils {
public static class UpdateExtensions {
public static TableUpdate<T, TKey> with_added<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<T> added) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, added, Enumerable.Empty<Change<T>>(), Enumerable.Empty<T>(), table.key_extractor);
public static TableUpdate<T, TKey> with_removed<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<T> removed) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, Enumerable.Empty<T>(), Enumerable.Empty<Change<T>>(), removed, table.key_extractor);
public static TableUpdate<T, TKey> with_changed<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<T> changed) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, Enumerable.Empty<T>(), changed.Select(v => new Change<T>(table.state[table.key_extractor(v)], v)), Enumerable.Empty<T>(), table.key_extractor);
public static TableUpdate<T, TKey> with_changes<T, TKey>(this TableUpdate<T, TKey> table, IEnumerable<Change<T>> changes) where TKey : IEquatable<TKey> =>
TableUpdate<T, TKey>.update(table, Enumerable.Empty<T>(), changes, Enumerable.Empty<T>(), table.key_extractor);
public static IObservable<TableUpdate<TResult, TKey>> RootDependent<T, TResult, TKey>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> with_root,
Func<T, TResult> without_root,
Func<TResult, TKey> result_key)
where TKey : IEquatable<TKey> =>
RootDependent(source, key, parent_key, with_root, without_root, result_key, default);
// TODO: unit tests
public static IObservable<TableUpdate<TResult, TKey>> RootDependent<T, TKey, TResult>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> get_result,
Func<T, TResult> get_result_no_parent,
Func<TResult, TKey> result_key,
TKey no_parent_key)
where TKey : IEquatable<TKey> =>
Observable.Create<TableUpdate<TResult, TKey>>(observer => {
var state = DictionaryHelper.empty<TKey, T>();
var output = TableUpdate<TResult, TKey>.mk_empty(result_key);
var parent_to_children = new SetMap<TKey, T>();
return source.Subscribe(update);
//
void update(TableUpdate<T, TKey> input) {
state = input.state;
var builder = output.to_builder();
// execute all parent_to_children changes first, so descendent-propagation will be efficient (otherwise propagation might be needlessly performed on values that are yet to be removed, or performed with the wrong root, if the real root is yet to be added).
foreach (var v in input.removed.Concat(input.changes.Select(ch => ch.Old))) {
var k = input.key_extractor(v);
parent_to_children.remove(parent_key(v), v);
}
foreach (var v in input.added.Concat(input.changed)) {
var pk = parent_key(v);
if (!pk.Equals(no_parent_key))
parent_to_children.add(pk, v);
}
foreach (var v in input.removed) {
var k = input.key_extractor(v);
builder.remove(k);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children) {
var ck = key(child);
builder.add_or_update(ck, get_result_no_parent(child));
foreach (var d in descendents(ck))
builder.add_or_update(key(d), get_result(d, child));
}
}
}
foreach (var v in input.changed) {
var k = input.key_extractor(v);
var root = get_root(v);
TResult result;
if (key(root).Equals(k)) {
result = get_result_no_parent(v);
foreach (var d in descendents(k))
builder.add_or_update(key(d), get_result(d, v));
} else {
result = get_result(v, root);
}
builder.add_or_update(k, result);
}
foreach (var v in input.added) {
var k = input.key_extractor(v);
var pk = parent_key(v);
TResult result;
if (pk.Equals(no_parent_key)) {
result = get_result_no_parent(v);
foreach (var d in descendents(k))
builder.add_or_update(key(d), get_result(d, v));
} else {
parent_to_children.add(pk, v);
var root = get_root(v);
if (key(root).Equals(k)) {
result = get_result_no_parent(v);
foreach (var d in descendents(k))
builder.add_or_update(key(d), get_result(d, v));
} else {
result = get_result(v, root);
}
}
builder.add_or_update(k, result);
}
observer.OnNext(output = builder.to_table());
}
T get_root(T value) {
var pk = parent_key(value);
while (!pk.Equals(no_parent_key) && state.TryGetValue(pk, out var parent)) {
value = parent;
pk = parent_key(value);
}
return value;
}
IEnumerable<T> descendents(TKey k) {
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children) {
yield return child;
foreach (var d in descendents(key(child)))
yield return d;
}
}
}
});
public static IObservable<TableUpdate<T, TKey>> MergeTable<T, TKey>(this IObservable<TableUpdate<T, TKey>> source, IObservable<TableUpdate<T, TKey>> other) where TKey : IEquatable<TKey> =>
source.AggregateLatest(other, _Merge<T, TKey>.source_change_del, _Merge<T, TKey>.other_change_del, () => TableUpdate<T, TKey>.empty, TableUpdate<T, TKey>.empty, TableUpdate<T, TKey>.empty);
public static IObservable<TableUpdate<KeyValuePair<TKey, TResult>, TKey>> ParentDependent<T, TKey, TResult>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> get_result,
Func<T, TResult> get_result_no_parent)
where TResult : IEquatable<TResult>
where TKey : IEquatable<TKey> =>
ParentDependent(source, key, parent_key, get_result, get_result_no_parent, EqualityComparer<TResult>.Default, default);
public static IObservable<TableUpdate<KeyValuePair<TKey, TResult>, TKey>> ParentDependent<T, TKey, TResult>(
this IObservable<TableUpdate<T, TKey>> source,
Func<T, TKey> key,
Func<T, TKey> parent_key,
Func<T, T, TResult> get_result,
Func<T, TResult> get_result_no_parent,
IEqualityComparer<TResult> result_comparer,
TKey no_parent_key)
where TKey : IEquatable<TKey> =>
Observable.Create<TableUpdate<KeyValuePair<TKey, TResult>, TKey>>(observer => {
var value_comparer = EqualityComparer.Create<KeyValuePair<TKey, TResult>, TResult>(kv => kv.Value, result_comparer);
var state = DictionaryHelper.empty<TKey, T>();
var output = TableUpdate<KeyValuePair<TKey, TResult>, TKey>.mk_empty(kv => kv.Key);
var parent_to_children = new SetMap<TKey, T>();
return source.Subscribe(update);
//
void update(TableUpdate<T, TKey> input) {
state = input.state;
var builder = output.to_builder();
foreach (var v in input.removed) {
var k = input.key_extractor(v);
builder.remove(k);
parent_to_children.remove(parent_key(v), v);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children)
builder.add_or_update(new KeyValuePair<TKey, TResult>(key(child), get_result_no_parent(child)));
}
}
foreach (var v in input.changed) {
var k = key(v);
builder.add_or_update(new KeyValuePair<TKey, TResult>(k, lookup_result(v)));
parent_to_children.set(parent_key(v), v);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children)
builder.add_or_update(new KeyValuePair<TKey, TResult>(key(child), get_result(child, v)));
}
}
foreach (var v in input.added) {
var k = key(v);
builder.add_or_update(new KeyValuePair<TKey, TResult>(k, lookup_result(v)));
parent_to_children.add(parent_key(v), v);
if (parent_to_children.try_lookup(k, out ISet<T> children)) {
foreach (var child in children)
builder.add_or_update(new KeyValuePair<TKey, TResult>(key(child), get_result(child, v)));
}
}
var result = builder.to_table_checked(value_comparer);
if (result.has_changes())
observer.OnNext(output = result);
}
TResult lookup_result(T child) {
var pk = parent_key(child);
return !pk.Equals(no_parent_key) && state.TryGetValue(pk, out T parent) ? get_result(child, parent) : get_result_no_parent(child);
}
});
static class _Merge<T, TKey> where TKey : IEquatable<TKey> {
public static readonly Func<TableUpdate<T, TKey>, Change<TableUpdate<T, TKey>>, TableUpdate<T, TKey>, TableUpdate<T, TKey>> source_change_del = _source_changed_impl;
public static readonly Func<TableUpdate<T, TKey>, TableUpdate<T, TKey>, Change<TableUpdate<T, TKey>>, TableUpdate<T, TKey>> other_change_del = _other_changed_impl;
static TableUpdate<T, TKey> _source_changed_impl(TableUpdate<T, TKey> target, Change<TableUpdate<T, TKey>> source_change, TableUpdate<T, TKey> other_table) =>
TableUpdate<T, TKey>.update(target, source_change.New.added, source_change.New.changes, source_change.New.removed, source_change.New.key_extractor);
static TableUpdate<T, TKey> _other_changed_impl(TableUpdate<T, TKey> target, TableUpdate<T, TKey> source_table, Change<TableUpdate<T, TKey>> other_change) =>
TableUpdate<T, TKey>.update(target, other_change.New.added, other_change.New.changes, other_change.New.removed, other_change.New.key_extractor);
}
}
}
namespace bts.utils.diagnostics {
public static class DiagnosticUpdateExtensions {
// Hard to imagine why this util would be needed outside of diagnosig bugs.
// The #if DEBUG directive is an extra precaution to avoid accidentally writing
// files like this on a client's machine.
#if DEBUG
static readonly string _proto_capture_ext = "proto_capture";
public static IObservable<TableUpdate<T, TKey>> ProtoBufCapture<T, TKey, TProto>(this IObservable<TableUpdate<T, TKey>> source, Func<T, TProto> to_message)
where TKey : IEquatable<TKey>
where TProto : class, IMessage<TProto> =>
ProtoBufCapture(source,
FilePathHelper.GetUniquePathWithDate(Path.Combine(Directory.GetCurrentDirectory(), typeof(TProto).Name), _proto_capture_ext),
to_message);
public static IObservable<TableUpdate<T, TKey>> ProtoBufCapture<T, TKey, TProto>(this IObservable<TableUpdate<T, TKey>> source, string path, Func<T, TProto> to_message)
where TKey : IEquatable<TKey>
where TProto : class, IMessage<TProto> =>
Observable.Create<TableUpdate<T, TKey>>(o => {
// write the state on dispose, and/or on error
IReadOnlyDictionary<TKey, T> state = DictionaryHelper.empty<TKey, T>();
var sub = source.Subscribe(table => {
state = table.state;
o.OnNext(table);
}, ex => {
save(true);
o.OnError(ex);
},
o.OnCompleted);
return Disposable.Create(() => {
save(false);
Disposable.dispose(ref sub);
});
void save(bool has_error) {
if (state.Count > 0)
FileStreamHelper.WriteProtos(has_error ? $"{path}(error)" : path, state.Values.Select(to_message).ExceptNull());
}
});
#endif
}
}
@@ -0,0 +1,13 @@
using System.IO;
using System.Reflection;
namespace bts.resources {
class ResourceHelper {
const string _default_namespace = "spark_test.Resources";
public static Stream LoadResource(string name) {
var ass = Assembly.GetExecutingAssembly();
return ass.GetManifestResourceStream(name) ?? ass.GetManifestResourceStream($"{_default_namespace}.{name}");
}
}
}
@@ -1,13 +1,11 @@
using System;
using bts.api;
using bts.resources;
using bts.test;
using bts.utils.proto;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using bts.api;
using bts.test;
namespace bts.mcore
{
namespace bts.mcore {
[TestGroup]
class SecurityTests {
[Test]
@@ -28,5 +26,23 @@ namespace bts.mcore
Assert.That(dict[b] == 1);
Assert.That(dict[c] == 2);
}
[Test]
public void test_security_compare() {
const string securities_resource = "KrandallOrderBookCrash.security_proto";
// verify that the sample size doesn't shrink for some reason.
// if it grows a bit that's fine; the generic comparison test
// asserts against collections that are too large.
const int min_sample_size = 91;
var securities = StreamHelper.ReadProtos<Security>(
ResourceHelper.LoadResource(securities_resource))
.Select(s => new SecurityImmutable(s))
.ToList();
Assert.IsTrue(securities.Count >= min_sample_size);
ComparisonTests.transitive_comparison(securities, SecurityHelper.Comparer.InstanceImmutable);
ComparisonTests.identity_comparison(securities, SecurityHelper.Comparer.InstanceImmutable);
}
}
}
@@ -0,0 +1,34 @@
using bts.test;
using System.Collections.Generic;
using System.Linq;
namespace bts.mcore {
static class ComparisonTests {
public static void transitive_comparison<T>(IEnumerable<T> items, IComparer<T> comparer) {
// Verify that the sample size isn't too big.
// This test compares each item to every other item
// (technically twice; lhs/rhs is flipped the second time),
// so performance is O(n^2).
const int max_sample_size = 4092;
var sorted_items = items.ToList();
Assert.That(sorted_items.Count <= max_sample_size);
sorted_items.Sort(comparer);
for (int i = 0; i < sorted_items.Count - 1; i++) {
for (int j = i + 1; j < sorted_items.Count; j++) {
assert_comp(i, j);
}
}
void assert_comp(int lhs, int rhs) {
int pos_comp = lhs.CompareTo(rhs);
// hit both sides for good measure
Assert.That(comparer.Compare(sorted_items[lhs], sorted_items[rhs]) == pos_comp);
Assert.That(comparer.Compare(sorted_items[rhs], sorted_items[lhs]) == -pos_comp);
}
}
public static void identity_comparison<T>(IEnumerable<T> items, IComparer<T> comparer) {
foreach (var v in items)
Assert.That(comparer.Compare(v, v) == 0);
}
}
}
@@ -61,7 +61,7 @@ public static async Task<IEnumerable<SecurityImmutable>> get_multiple_securities
return Enumerable.Empty<SecurityImmutable>();
}
public static async Task<IEnumerable<FutureContract>> get_expirations(MarketDataRelay relay, string symbol, SecurityType security_type, MDRequest.Types.Flags flags = MDRequest.Types.Flags.Bbo, Venue v = Venue.Sim) {
public static async Task<(bool, List<FutureContract>)> get_expirations(MarketDataRelay relay, string symbol, SecurityType security_type, MDRequest.Types.Flags flags = MDRequest.Types.Flags.Bbo, Venue v = Venue.Sim) {
var request = new MDRequest() {
Host = _MD_HOST,
Channels = {
@@ -89,7 +89,7 @@ public static async Task<IEnumerable<SecurityImmutable>> get_multiple_securities
}
}
return (IEnumerable<FutureContract>) futures;
return (result.success, futures);
}
}
@@ -22,8 +22,9 @@ namespace bts.tests
public class SpreaderUtilities {
public static async Task<(SecurityImmutable s1, SecurityImmutable s2)> get_securities_for_common_setup_one(MarketDataRelay relay, string symbol) {
var exps = (await MarketDataUtilities.get_expirations(relay, symbol, SecurityType.Future)).ToList();
var (success, exps) = await MarketDataUtilities.get_expirations(relay, symbol, SecurityType.Future);
Assert.IsTrue(success, "Could not reach market data service");
Assert.IsNotNull(exps);
Assert.IsTrue(exps.Count() > 2, "Need at least two expirations");
+3 -1
View File
@@ -14,5 +14,7 @@
<ItemGroup>
<Content Include="../spark_test/system_test_scripts/*.sh" CopyToOutputDirectory="PreserveNewest"></Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\KrandallOrderBookCrash.security_proto" />
</ItemGroup>
</Project>
Loaded 100 of 219 files, more files were not shown because too many files have changed in this diff. Show more