Compare commits

...
156 Commits
Author SHA1 Message Date
John Colvin 9a85a475db fix: Revert accidental change to externals 2023-01-10 09:51:54 -05:00
Taha Afzal 8a1ab0ba9e Updating generated files (and minor code adjustment) for CME schema 8 changes: 2023-01-09 10:28:10 -05:00
John Colvin 57ac54b6c5 build (sparkd): Pipe 'eval' cmd stdout/err back to shell
So we can actually see the build output
2023-01-05 14:03:40 -05:00
johncolvin-og 4a8433246c build: Use unix eol in build_sparkd, exec not eval 2023-01-05 14:03:40 -05:00
John Colvin d6351cd187 Update docker/linux
To get python3-venv
2023-01-05 14:03:40 -05:00
John Colvin eef263f2e7 build: Ensure python virtual env is installed/up-to-date 2023-01-05 14:03:40 -05:00
johncolvin-og 2009027586 build: Port build/publish_spark scripts over from crypto branches 2023-01-05 14:03:40 -05:00
John Colvin feaad6335d build: Set up cmake 'install' build target
The 'install' build target creates a self-contained (deployable) sparkd
package. It copies the sparkd executable and its dependencies into a
single directory (no symlinks), which can then be copied/deployed to
another machine.

Run `cmake --build . --target install` to install sparkd in the
directory defined by the CMAKE_INSTALL_PREFIX variable.
2023-01-05 14:03:40 -05:00
John Colvin b96612d37b fix build (linux): Port py3 build scripts over from spark_crypto.dev 2023-01-05 14:03:40 -05:00
John Colvin 84f9338005 Add .venv dirs to .gitignore 2023-01-05 14:03:40 -05:00
John Colvin 2837f3edfb chore: Add requirements.txt (py venv) 2023-01-05 14:03:40 -05:00
John Colvin a82932652b fix build: Copy a handful of files over from spark_crypto.dev
This fixes the last of the boost-related errors
2022-09-28 19:49:17 -04:00
John Colvin c9b5ff844f fix build (partial): Update externals to get new websocketpp
This fixes some of the boost errors related to the recent boost upgrade
2022-09-28 19:47:39 -04:00
John Colvin b0cbc2cb71 fix build: Disable web build
That's one way to fix it!
2022-09-28 19:33:19 -04:00
BTS User b2a2cae9c1 test 2022-09-28 13:44:48 -04:00
BTS User 93d9a924e9 test 2022-09-28 13:39:09 -04:00
BTS User 9d699711c1 test 2022-09-28 13:31:29 -04:00
BTS User 5951691cbd test 2022-09-28 13:28:20 -04:00
BTS Devs cbff70d5a1 plugin.py edited online with Bitbucket 2022-09-28 16:10:31 +00:00
BTS Devs b59d45ae28 plugin.py edited online with Bitbucket 2022-09-28 16:07:14 +00:00
BTS Devs ea6e158e63 plugin.py edited online with Bitbucket 2022-09-28 15:54:41 +00:00
BTS Devs 55a32935a9 plugin.py edited online with Bitbucket 2022-09-28 15:41:32 +00:00
BTS Devs 9f895df70c audit_ice.py edited online with Bitbucket 2022-09-23 21:25:37 +00:00
BTS Devs 8551936383 audit_upload.sh edited online with Bitbucket 2022-09-20 15:36:21 +00:00
John Colvin cb0378d785 fix: Load FairValueLine price type in settings dialog 2021-07-02 13:55:34 -04:00
John Colvin 489ac45722 Add fair value lines for DJ 2021-06-29 14:07:08 -04:00
John Colvin d910c65e20 fix: ContractPicker syncs selected sec/md w/ selected key 2021-06-29 15:18:06 -04:00
Pedro Pinto cb0e4de6c5 Updated TradingSystemName in IL3 connection. 2021-04-29 15:13:09 -04:00
Pete Fisher 71539424cc updated ilink3 schema to v7 2021-03-25 12:46:49 -04:00
Pete Fisher 602929b945 updating submodule to latest 2021-03-18 12:13:11 -04:00
Pete Fisher 07591f8e28 ice audit scripts 2021-03-18 11:05:34 -04:00
Pedro Pinto 0cb11b7f8d Made parsing of dump security easier to facilitate locating ice symbols. 2021-03-16 11:32:59 -04:00
Pete Fisher 1921f02d30 Merged in DJ_ICE_ManualOrderIndicator (pull request #417)
added ManualOrderIndicator to ICENativeOrder.cpp after merge with Prod
2021-03-02 15:47:34 +00:00
Pete Fisher 2199efc95d added ManualOrderIndicator to ICENativeOrder.cpp after merge with Prod 2021-02-26 15:43:41 -05:00
Pete Fisher 54ff55f22f merge with prod 2021-02-26 14:59:38 -05:00
John Colvin ca00f256e1 ui (DJ): Avoid darkening price text in session range in ladder
Put the session range visual beneath the column backgrounds
(on the z axis), and make the background slightly translucent
while leaving the font color fully opaque.  This effectively
darkens the former without changing the latter.
2021-02-17 12:57:26 -06:00
John Colvin 733ce7dfc6 Set alpha bits to 'FF' in historic ladder settings colors 2021-02-17 12:38:51 -06:00
John Colvin fda812d792 ui (dj): Reduce opacity of session range layer in ladder 2021-02-11 23:03:53 -06:00
John Colvin 1bf9d0aa4a ui (dj): Replace hand cursor with arrow in ladder bid/ask 2021-02-11 22:57:48 -06:00
John Colvin 54eec61f46 ui (dj): Make ladder look more like tt
These changes can be configured via LadderKnobs (defined in UI.proto)
2021-02-11 22:57:40 -06:00
Pete Fisher b84ba1d3f5 changed ICEMain.cpp from NonImplied to FullImplied 2021-02-03 10:26:47 -05:00
John Colvin 70bf5e0c95 fix: Correct visibility binding in ladder session high/low
The other price points visibility bindings were refactored
in a similar manner, for consistency's sake.
2021-01-14 11:06:17 -06:00
John Colvin 2bae938549 feat (DJ): Right align bid, price, and ask column text in ladder
This is hardcoded (not configurable via settings).
2021-01-14 10:12:04 -06:00
John Colvin a67b32fd33 ui-tweak (DJ): Display 'volume' just below 'day change' in ladder 2021-01-07 11:36:35 -06:00
John Colvin 3da8fbd25d feat (DJ): Give bid/ask cells with markets a lighter background in ladder
There is a small chance this will impact performance when
a large number of ladders (50+) are open.  Therefore, this
behavior can be toggled on/off in config, via
LadderKnobs.lighten_market_background.
2021-01-07 11:18:13 -06:00
John Colvin a8cd07cbd2 refactor: Consolidate tick tests after tricky merge
The branches that were merged, DJ_ICE and
DJ_ICE_ladder_price_aggregation, both added new
tick tests.  The former put them in a new file,
"TickTests.cs," while the latter added them
to "SecurityTests.cs."  This commit places all
tick tests in "TickTests.cs."
2021-01-07 10:37:02 -06:00
John Colvin badb463e03 Merge remote-tracking branch 'origin/DJ_ICE_ladder_price_aggregation' into DJ_ICE 2021-01-06 12:08:39 -06:00
John Colvin b6a942d6b2 Add tooltips to ladder left-panel price points 2021-01-06 11:00:51 -06:00
John Colvin 7d8bc13282 Tailor default ladder price point settings for DJ 2021-01-06 11:00:51 -06:00
Pete Fisher cbad79b909 fixed use_onixs 2021-01-06 11:00:51 -06:00
Pedro Pinto 58768567d1 Added operator id to tag 116. 2021-01-06 11:00:51 -06:00
Pedro Pinto 2475ca3662 Used clearing account to populate tag 116 (ATID) 2021-01-06 11:00:51 -06:00
John Colvin f12b8fedc0 feat (DJ): Display settings for ladder left-panel price points
* Add background and font settings for all price points

* Indicate 'day change' sign with modal settings (positive,
  zero, and negative) for background and font color

* Hide 'day change' leading zeros in tick part

* Don't show 'day change' zero integer part (e.g.,
  '25' instead of '0.25')
2021-01-06 11:00:51 -06:00
John Colvin ed920c7c5a Move security/tick data classes to spark_core
* spark_core is the appropriate project for such
  'vocabulary' types.

* unit tests in the spark_tests project can now
  reference these types (this was the immediate
  impetus for the move).
2021-01-04 18:23:27 -06:00
John Colvin db20473949 add some docker scripts (start_docker.sh, docker_bash.sh) 2021-01-04 10:12:20 -06:00
John Colvin 5bf8162fcf vscode (build): change linux build task to run in docker
* prompts user for configuration (debug or release)

* looks for docker instance named "${USER}s_docker"
2021-01-04 10:10:23 -06:00
Taha Afzal 447d3303d1 Time based throttles code complete 2020-09-28 09:57:29 -04:00
Taha Afzal 5693fc4444 Merge branch 'DJ_ICE' of bitbucket.org:bluetradesys/spark into DJ_ICE 2020-09-27 22:48:30 -04:00
Taha Afzal 8896066e2c Time based price throttles 2020-09-25 09:49:19 -04:00
John Colvin 4992f4e0cf config toggle to show/hide ladder scrollbar for dj (defaults to hide) 2020-09-24 16:25:43 -05:00
John Colvin 3503946011 add quote-time-throttle params to SyntheticEditor gui 2020-09-21 19:28:41 -05:00
Pedro Pinto e7806d2fec Merge branch 'master' into prod 2020-09-16 16:28:36 -04:00
John Colvin d31d8648f1 frozen price lines in ladder (for dj) 2020-09-09 16:39:53 -04:00
Taha Afzal 2c15f83c85 merge fix 2020-08-26 09:49:16 -04:00
Taha Afzal f9dfb04fca tooltip change 2020-08-25 23:02:02 -04:00
Taha Afzal 680b19b6fb Enable Hedging and Lean Ticks Offset 2020-08-25 22:58:15 -04:00
John Colvin f790b8f59b RiskManagement view updates when imported row overwrites existing one 2020-08-20 11:14:56 -05:00
John Colvin fbed8e5428 fix AdminModel.delete_risk_limits: don't increment i at end of iteration when an item is removed 2020-08-14 13:15:35 -05:00
John Colvin 524f3be25a RiskManagementWindow - imported rows weren't filtered by account 2020-08-14 11:21:49 -05:00
John Colvin 9823b44083 RiskManagementWindow - hook up menu display vm 2020-08-14 11:04:14 -05:00
John Colvin 30feda72bb Merged DJ_ICE_proprietary_risk into DJ_ICE 2020-08-14 10:54:47 -05:00
John Colvin 2d0e63546d Merge branch 'DJ_ICE' into DJ_ICE_proprietary_risk 2020-08-14 10:53:26 -05:00
John Colvin cc02fa1c84 risk limits import dialog (for dj) 2020-08-13 14:13:44 -05:00
Taha Afzal 6139d53e49 Merge branch 'DJ_ICE' of bitbucket.org:bluetradesys/spark into DJ_ICE 2020-08-12 15:04:34 -04:00
Taha Afzal 7f3c2e75a3 Fixed the qty in the leg fills for spreads with different exchange_qty_mults 2020-08-12 15:04:15 -04:00
John Colvin 0f61e24ae5 fix ColumnBase.SortDirection binding to DIYGridColumnModel 2020-08-12 13:00:03 -05:00
John Colvin 8da2ebee5c SelectionModel should only select the pending selection once (was reselecting it every time the items changed) 2020-08-12 12:59:19 -05:00
Pedro Pinto a1faf685eb CMake, you suck. 2020-08-11 15:59:40 -04:00
Pedro Pinto 9e2b01a723 Cleaned up branch for delivery to DJ 2020-08-11 13:54:27 -04:00
John Colvin d03c74f270 remove unused 'actual_width' prop from DIYGridColumnModel 2020-08-05 15:11:06 -05:00
John Colvin 834a28916b synchronize column model's GridColumnSettings with the column view 2020-08-05 15:10:23 -05:00
John Colvin 43ccb4e3e7 fix risk management thread name typo 2020-08-05 12:20:24 -05:00
Taha Afzal f199cb4157 Fixed the broken test (needed to update the size attribute on the managed ColdEntry 2020-08-05 10:54:43 -04:00
Taha Afzal 5a3bfd7f81 Merge branch 'DJ_ICE_proprietary_risk' of bitbucket.org:bluetradesys/spark into DJ_ICE_proprietary_risk 2020-08-04 15:40:29 -04:00
Taha Afzal bf6461c126 More logs and TAS HNG can not be the master 2020-08-04 15:40:21 -04:00
John Colvin 8892505d11 RiskManagement price cells show 4 decimal places, use commas 2020-08-04 12:11:55 -05:00
John Colvin 5d0a29432a RiskManagementVM - row sorting must refer to immutable data (else binary search can break) 2020-08-04 12:11:55 -05:00
John Colvin cd01c7ae5a RiskManagementVM - remove unused props/methods 2020-08-04 12:11:55 -05:00
Taha Afzal bf19ec0fda Merge branch 'DJ_ICE_proprietary_risk' of bitbucket.org:bluetradesys/spark into DJ_ICE_proprietary_risk 2020-08-04 13:08:14 -04:00
John Colvin 1a0158817e remove size constraints on RiskManagementControl 2020-08-04 11:28:23 -05:00
John Colvin 53853591fe move RiskManagementControl.xaml/cs to 'View' dir 2020-08-04 11:28:23 -05:00
Taha Afzal 80d4899f2c removing fake data functions (when the UI was not around) 2020-08-04 09:51:32 -04:00
Taha Afzal 75f15c50d8 working long/short pos was not getting copied in copy_positions_from 2020-08-03 18:25:40 -04:00
John Colvin f197f213f3 Risk Management ui - open edit-rows dialog near mouse 2020-08-03 15:52:26 -05:00
John Colvin a7e1b71883 TextCell available_size is updated in ArrangeOverride 2020-08-03 15:28:41 -05:00
John Colvin f9f22e1e36 add risk management window 2020-08-03 15:28:41 -05:00
John Colvin cda50f562d AdminModel - add methods to subscribe, and modify proprietary risk limits (dj) 2020-08-03 15:28:41 -05:00
John Colvin 4010298a46 parameterize SecurityStore.join (don't mess with core logic/impl) 2020-08-03 15:28:41 -05:00
John Colvin 48e701ba00 client side data types that mirror api protos (minimize client/server coupling) 2020-08-03 15:28:41 -05:00
John Colvin cb07abbf42 made new SelectorVM (todo: replace refs to old one) 2020-08-03 15:28:41 -05:00
John Colvin 3c8e2c636f add SimpleGridSettingsManagerVM (uses DIYGridColumnModels instead of the view type ColumnBase) 2020-08-03 15:28:41 -05:00
John Colvin 6a3b4ebe75 add DIYGridColumnModel, so the DIYGrid column paradigm is cleanear mvvm 2020-08-03 15:28:41 -05:00
John Colvin 80256fbe12 add get/load_state methods to non-generic IDataGridColumnFilter 2020-08-03 15:28:41 -05:00
John Colvin a0a5d82139 add PropertyEqualityComparerAttribute (supports simple automatic equality impl) 2020-08-03 15:28:41 -05:00
John Colvin ee6b00b9f2 unify SecurityCriteria initialization with securities (use pretty_name if single sec) 2020-08-03 15:28:41 -05:00
Taha Afzal 9a494ce24f Parent Child Secrity Pos Limit 2020-08-03 16:25:24 -04:00
Taha Afzal ae78ebac9a All sec based risk done (master, slave remain) 2020-07-31 17:27:06 -04:00
Taha Afzal 5c11eecbd1 SecurityLimitNode keeps working qty and positions differently 2020-07-31 00:21:17 -04:00
Taha Afzal 78a47c33c7 Fixing order rejection message 2020-07-30 14:55:11 -04:00
Taha Afzal 9dcd80e3db Security level risk controls are passed to update order 2020-07-30 13:02:22 -04:00
Taha Afzal f7563413bd New security level controls hooked to order entry 2020-07-30 11:14:45 -04:00
Taha Afzal 29c418db42 Routed the SecLimit to SecurityLimitNode 2020-07-29 18:15:18 -04:00
Taha Afzal f177efde70 Added price arg to the Limiter's can_send and can_update functions 2020-07-29 14:28:16 -04:00
Taha Afzal 3b83d105a0 Filled in the prop risk limits persistence code 2020-07-29 12:29:56 -04:00
Taha Afzal 34aafd79cd Added new risk proto and Admin API function to update it 2020-07-29 11:30:36 -04:00
Taha Afzal c30bab899f Changing TASCounter to use the new formula 2020-07-29 09:43:00 -04:00
Taha Afzal d3de164c53 Moved the 2500 translation to the end points of the system 2020-07-27 17:05:19 -04:00
Taha Afzal 46544f36f1 Pyramid Algo now sends hedges for fills while a cancel is in flight 2020-07-23 13:13:52 -04:00
John Colvin c7df3ac43c Merge remote-tracking branch 'origin/csv_export' into prod 2020-07-22 10:44:58 -05:00
Taha Afzal 1d1c0d6b85 Jan exps' are showing 12 for the month, adjustment for risk 2020-07-21 12:09:41 -04:00
Taha Afzal 2f127ecc18 Using day positions for risk enforcement 2020-07-20 23:48:35 -04:00
Taha Afzal 3fc080d02c Better position violation comments 2020-07-20 23:21:20 -04:00
Taha Afzal d86543261c Risk enforcement changes for ICE NG OTC markets 2020-07-20 13:01:56 -04:00
John Colvin 8761f313f5 remove with_reguard_to_price param from tick 'format' extension 2020-07-20 10:50:15 -05:00
John Colvin 3906ef6507 fix typo in ConvertUtils.update_value_type (was missing negation '!') 2020-07-19 13:28:09 -05:00
John Colvin 78055d79c6 fix tick unit tests, and reorganize security tests a bit 2020-07-19 13:27:12 -05:00
John Colvin 885a9dd939 add precision param (epsilon) to tickup/down methods 2020-07-19 13:20:00 -05:00
John Colvin 20c449de60 TickFactors are cached in a ThreadStatic dictionary rather than concurrent (no locks required) 2020-07-15 11:22:11 -05:00
John Colvin 52b487946a ticksize extension handles invalid tick props gracefully 2020-07-15 10:50:25 -05:00
John Colvin 98144ea939 remove unused parameter from TickTestHelper.full_ticks_from_zero 2020-07-14 15:46:06 -05:00
John Colvin 5e27a5fa7c fix ladder session low/high grid animation (was not using the ticks per row setting) 2020-07-14 15:39:55 -05:00
John Colvin 6cfc15ccd8 LadderGridHost invalidates '_spec' on connect_vm (not just on settings updates) 2020-07-14 15:38:21 -05:00
John Colvin 2de08c2c48 fix typo in LadderGridHost._spec getter (was accidentally returning the fallback value) 2020-07-14 15:27:03 -05:00
John Colvin fb6e05560b make tick unit tests more robust (test tick_up/down, use these methods to covalidate results) 2020-07-14 12:48:21 -05:00
John Colvin 1abf84337f include Sim ContractSpecs in tick unit tests 2020-07-14 11:53:57 -05:00
John Colvin d691e0aeee ladder settings dialog - fix 'when implied is > 0' label alignment 2020-07-14 11:01:32 -05:00
John Colvin 5e0f1b59fb added tick unit test (namely for 'full_ticks_from_zero' method) 2020-07-14 10:53:30 -05:00
John Colvin a234ecbf26 added 'full_ticks_from' method which accepts an origin price (instead of just an arbitrary 0) 2020-07-14 10:53:27 -05:00
John Colvin 062cabf075 TickUtils.full_ticks_from_zero checks pricebreak 2020-07-13 13:52:29 -05:00
Taha Afzal 1a7fcb2027 Added is_ice_lots to the ICE sec proto 2020-07-12 17:03:37 -04:00
John Colvin 39e1fd4a1a move LadderUtils methods to TickUtils (all were related to ticks) 2020-07-09 12:05:17 -05:00
John Colvin 4d902747df move tick data\utils to spark_core 2020-07-09 11:58:19 -05:00
John Colvin 50d0078556 add 'ticks per row' ladder setting 2020-07-09 11:45:31 -05:00
John Colvin d86b7c6c50 add TickUp/DownCommand dependency props to TickUpDownUInt 2020-07-08 12:48:45 -05:00
Jake Norbut 9d1b03d1e2 fixing merge 2020-03-04 11:51:05 -06:00
Jake Norbut 9a677cf980 merging master into csv_export; taking master 2020-03-04 11:50:56 -06:00
Jake Norbut 25445d58d0 refactoring TaS CSV export 2019-12-18 13:48:14 -06:00
Jake Norbut 0b313a8aa8 refactoring Fills CSV export 2019-12-18 13:47:36 -06:00
Jake Norbut 54f954fd4b adding CSV export to order book 2019-12-18 13:47:03 -06:00
Jake Norbut 980f38ba03 using OrderedDictionary to CSVs are output with a predictable column order 2019-12-18 13:46:22 -06:00
Jake Norbut 9eada2724b Having the Command be on the Window rather than the VM; nice thinking John 2019-12-18 10:11:10 -06:00
Jake Norbut b5f53ee918 adding csv_properties to TimeAndSalesVM.Row 2019-12-18 10:10:04 -06:00
Jake Norbut efc175f654 adding export to csv command to Fills window 2019-12-17 10:32:58 -06:00
Jake Norbut 6074afd999 adding csv export command to fills vm 2019-12-17 10:32:15 -06:00
Jake Norbut 219dff4055 adding new export_csv IDialogCommand which just creates a new file 2019-12-17 10:28:57 -06:00
Jake Norbut 9a3b07b622 adding ToCsv IEnumerable ext method in a new util namespace 2019-12-17 10:21:38 -06:00
388 changed files with 38064 additions and 20003 deletions

No files matched your search

+1
View File
@@ -37,3 +37,4 @@ trace_data/
*.rej
/build
.clangd/
.venv/**
+14 -2
View File
@@ -48,9 +48,9 @@
}
},
{
"label": "linux build (Debug)",
"label": "linux_build",
"type": "shell",
"command": "cd build/ninja_debug; time ninja",
"command": "docker exec -i ${USER}s_docker bash -c \"cd build/ninja_${input:build_configuration} && ninja\"",
"group": {
"kind": "build",
"isDefault": true
@@ -59,5 +59,17 @@
"$gcc"
]
}
],
"inputs": [
{
"id": "build_configuration",
"type": "pickString",
"default": "debug",
"description": "Configuration:",
"options": [
"debug",
"release"
]
}
]
}
+11 -1
View File
@@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.5)
project(spark)
set (USE_ONIX "TRUE")
if (WIN32)
set (platform "windows")
else ()
@@ -40,12 +42,17 @@ else ()
-fPIC -Wall -pedantic -Wextra -Werror \
-Wno-missing-braces -Wno-ignored-qualifiers \
--std=c++17 -fdiagnostics-color")
if (${USE_ONIX})
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DUSE_ONIX")
endif()
set (CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -DNDEBUG -O3")
set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wno-undef")
add_subdirectory(src/server/sparkd)
add_subdirectory(src/lib)
add_subdirectory(src/web)
# add_subdirectory(src/web)
add_subdirectory(doc/user)
add_subdirectory(src/sdk)
add_subdirectory(src/data_files)
@@ -63,4 +70,7 @@ else ()
DEPENDS sparkd data_files_dir)
add_custom_target(server_layout ALL DEPENDS ${CMAKE_BINARY_DIR}/server)
install(FILES "${CMAKE_SOURCE_DIR}/externals/linux/lib/${CMAKE_BUILD_TYPE}/libACE-5.7.5.so" DESTINATION bin)
install(FILES "${CMAKE_SOURCE_DIR}/externals/linux/lib/${CMAKE_BUILD_TYPE}/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.16.0.0" DESTINATION bin)
endif()
+1 -1
View File
@@ -1 +1 @@
set(sources /home/ppinto/Dropbox/bts/spark/doc/user/build.sh;content/_index.md;content/bts_spark/Glossary/_index.md;content/bts_spark/Help/_index.md;content/bts_spark/Help/about_spark.png;content/bts_spark/Help/image39.png;content/bts_spark/Help/image58.png;content/bts_spark/Launcher/_index.md;content/bts_spark/Launcher/image38.png;content/bts_spark/Launcher/image68.png;content/bts_spark/Logging-In-to-the-System/_index.md;content/bts_spark/Logging-In-to-the-System/image3.png;content/bts_spark/Logging-In-to-the-System/image64.png;content/bts_spark/Market/_index.md;content/bts_spark/Market/change_securities.png;content/bts_spark/Market/image11.png;content/bts_spark/Market/image14.png;content/bts_spark/Market/image20.png;content/bts_spark/Market/image21.png;content/bts_spark/Market/image23.png;content/bts_spark/Market/image24.png;content/bts_spark/Market/image34.png;content/bts_spark/Market/image49.png;content/bts_spark/Market/image53.png;content/bts_spark/Market/image66.png;content/bts_spark/Market/image69.png;content/bts_spark/Market/market_view.png;content/bts_spark/Market/settings_icon.png;content/bts_spark/Market/time_and_sales.png;content/bts_spark/My-Windows/_index.md;content/bts_spark/My-Windows/image15.png;content/bts_spark/My-Windows/image16.png;content/bts_spark/My-Windows/image37.png;content/bts_spark/Settings/_index.md;content/bts_spark/Settings/image51.png;content/bts_spark/Settings/image9.png;content/bts_spark/Spreader/_index.md;content/bts_spark/Spreader/image10.png;content/bts_spark/Spreader/image13.png;content/bts_spark/Spreader/image18.png;content/bts_spark/Spreader/image19.png;content/bts_spark/Spreader/image28.png;content/bts_spark/Spreader/image29.png;content/bts_spark/Spreader/image31.png;content/bts_spark/Spreader/image35.png;content/bts_spark/Spreader/image40.png;content/bts_spark/Spreader/image43.png;content/bts_spark/Spreader/image54.png;content/bts_spark/Spreader/image55.png;content/bts_spark/Spreader/image56.png;content/bts_spark/Spreader/image6.png;content/bts_spark/Spreader/image7.png;content/bts_spark/Spreader/image70.png;content/bts_spark/Spreader/image72.png;content/bts_spark/Spreader/image8.png;content/bts_spark/Trading/OrderTypes/index.md;content/bts_spark/Trading/_index.md;content/bts_spark/Trading/algo_studio.png;content/bts_spark/Trading/collapse_all_icon.png;content/bts_spark/Trading/cover_algo_control.png;content/bts_spark/Trading/expand_all_icon.png;content/bts_spark/Trading/image1.png;content/bts_spark/Trading/image17.png;content/bts_spark/Trading/image2.png;content/bts_spark/Trading/image25.png;content/bts_spark/Trading/image27.png;content/bts_spark/Trading/image33.png;content/bts_spark/Trading/image42.png;content/bts_spark/Trading/image44.png;content/bts_spark/Trading/image47.png;content/bts_spark/Trading/image48.png;content/bts_spark/Trading/image5.png;content/bts_spark/Trading/image50.png;content/bts_spark/Trading/image52.png;content/bts_spark/Trading/image57.png;content/bts_spark/Trading/image59.png;content/bts_spark/Trading/image60.png;content/bts_spark/Trading/image61.png;content/bts_spark/Trading/image62.png;content/bts_spark/Trading/image73.png;content/bts_spark/Trading/image74.png;content/bts_spark/Trading/join_algo_control.png;content/bts_spark/Trading/ladder.png;content/bts_spark/Trading/ladder_badges.png;content/bts_spark/Trading/ladder_main_part.png;content/bts_spark/Trading/lean_algo_control.png;content/bts_spark/Trading/mini_orderbook.png;content/bts_spark/Trading/oco_algo_control.png;content/bts_spark/Trading/offset_algo_control.png;content/bts_spark/Trading/pnl_calculation.png;content/bts_spark/Trading/pnl_window.png;content/bts_spark/Trading/recreate_icon.png;content/bts_spark/Trading/settings_icon.png;content/bts_spark/Trading/spark_menu_order_book.png;content/bts_spark/Trading/top_algo_control.png;content/bts_spark/Trading/trigger_algo_control.png;content/bts_spark/_index.md;content/release_notes/_index.md;content/system_requirements/_index.md;static/book.js;static/favicon.ico;static/image.js;templates/index.html;templates/page.html;templates/section.html;templates/shortcodes/back_button.html)
set(sources /home/pfisher/bts/DJ_ICE/doc/user/build.sh;content/_index.md;content/bts_spark/Glossary/_index.md;content/bts_spark/Help/_index.md;content/bts_spark/Help/about_spark.png;content/bts_spark/Help/image39.png;content/bts_spark/Help/image58.png;content/bts_spark/Launcher/_index.md;content/bts_spark/Launcher/image38.png;content/bts_spark/Launcher/image68.png;content/bts_spark/Logging-In-to-the-System/_index.md;content/bts_spark/Logging-In-to-the-System/image3.png;content/bts_spark/Logging-In-to-the-System/image64.png;content/bts_spark/Market/_index.md;content/bts_spark/Market/change_securities.png;content/bts_spark/Market/image11.png;content/bts_spark/Market/image14.png;content/bts_spark/Market/image20.png;content/bts_spark/Market/image21.png;content/bts_spark/Market/image23.png;content/bts_spark/Market/image24.png;content/bts_spark/Market/image34.png;content/bts_spark/Market/image49.png;content/bts_spark/Market/image53.png;content/bts_spark/Market/image66.png;content/bts_spark/Market/image69.png;content/bts_spark/Market/market_view.png;content/bts_spark/Market/settings_icon.png;content/bts_spark/Market/time_and_sales.png;content/bts_spark/My-Windows/_index.md;content/bts_spark/My-Windows/image15.png;content/bts_spark/My-Windows/image16.png;content/bts_spark/My-Windows/image37.png;content/bts_spark/Settings/_index.md;content/bts_spark/Settings/image51.png;content/bts_spark/Settings/image9.png;content/bts_spark/Spreader/_index.md;content/bts_spark/Spreader/image10.png;content/bts_spark/Spreader/image13.png;content/bts_spark/Spreader/image18.png;content/bts_spark/Spreader/image19.png;content/bts_spark/Spreader/image28.png;content/bts_spark/Spreader/image29.png;content/bts_spark/Spreader/image31.png;content/bts_spark/Spreader/image35.png;content/bts_spark/Spreader/image40.png;content/bts_spark/Spreader/image43.png;content/bts_spark/Spreader/image54.png;content/bts_spark/Spreader/image55.png;content/bts_spark/Spreader/image56.png;content/bts_spark/Spreader/image6.png;content/bts_spark/Spreader/image7.png;content/bts_spark/Spreader/image70.png;content/bts_spark/Spreader/image72.png;content/bts_spark/Spreader/image8.png;content/bts_spark/Trading/OrderTypes/index.md;content/bts_spark/Trading/_index.md;content/bts_spark/Trading/algo_studio.png;content/bts_spark/Trading/collapse_all_icon.png;content/bts_spark/Trading/cover_algo_control.png;content/bts_spark/Trading/expand_all_icon.png;content/bts_spark/Trading/image1.png;content/bts_spark/Trading/image17.png;content/bts_spark/Trading/image2.png;content/bts_spark/Trading/image25.png;content/bts_spark/Trading/image27.png;content/bts_spark/Trading/image33.png;content/bts_spark/Trading/image42.png;content/bts_spark/Trading/image44.png;content/bts_spark/Trading/image47.png;content/bts_spark/Trading/image48.png;content/bts_spark/Trading/image5.png;content/bts_spark/Trading/image50.png;content/bts_spark/Trading/image52.png;content/bts_spark/Trading/image57.png;content/bts_spark/Trading/image59.png;content/bts_spark/Trading/image60.png;content/bts_spark/Trading/image61.png;content/bts_spark/Trading/image62.png;content/bts_spark/Trading/image73.png;content/bts_spark/Trading/image74.png;content/bts_spark/Trading/join_algo_control.png;content/bts_spark/Trading/ladder.png;content/bts_spark/Trading/ladder_badges.png;content/bts_spark/Trading/ladder_main_part.png;content/bts_spark/Trading/lean_algo_control.png;content/bts_spark/Trading/mini_orderbook.png;content/bts_spark/Trading/oco_algo_control.png;content/bts_spark/Trading/offset_algo_control.png;content/bts_spark/Trading/pnl_calculation.png;content/bts_spark/Trading/pnl_window.png;content/bts_spark/Trading/recreate_icon.png;content/bts_spark/Trading/settings_icon.png;content/bts_spark/Trading/spark_menu_order_book.png;content/bts_spark/Trading/top_algo_control.png;content/bts_spark/Trading/trigger_algo_control.png;content/bts_spark/_index.md;content/release_notes/_index.md;content/system_requirements/_index.md;static/book.js;static/favicon.ico;static/image.js;templates/index.html;templates/page.html;templates/section.html;templates/shortcodes/back_button.html)
+21
View File
@@ -0,0 +1,21 @@
astroid==2.7.3
autopep8==1.5.7
cffi==1.14.6
colorama==0.4.4
isort==5.9.3
lazy-object-proxy==1.6.0
mccabe==0.6.1
pkg_resources==0.0.0
platformdirs==2.3.0
protobuf==3.19.1
pybars3==0.9.7
pycodestyle==2.7.0
pycparser==2.20
pylint==2.10.2
PyMeta3==0.5.1
termcolor==1.1.0
toml==0.10.2
typed-ast==1.4.3
typing_extensions==4.1.1
wrapt==1.12.1
yapf==0.31.0
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+51
View File
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace RiskUI {
/// <summary>
/// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
///
/// Step 1a) Using this custom control in a XAML file that exists in the current project.
/// Add this XmlNamespace attribute to the root element of the markup file where it is
/// to be used:
///
/// xmlns:MyNamespace="clr-namespace:RiskUI"
///
///
/// Step 1b) Using this custom control in a XAML file that exists in a different project.
/// Add this XmlNamespace attribute to the root element of the markup file where it is
/// to be used:
///
/// xmlns:MyNamespace="clr-namespace:RiskUI;assembly=RiskUI"
///
/// You will also need to add a project reference from the project where the XAML file lives
/// to this project and Rebuild to avoid compilation errors:
///
/// Right click on the target project in the Solution Explorer and
/// "Add Reference"->"Projects"->[Select this project]
///
///
/// Step 2)
/// Go ahead and use your control in the XAML file.
///
/// <MyNamespace:CustomControl1/>
///
/// </summary>
public class CustomControl1 : Control {
static CustomControl1() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
}
}
+24
View File
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net471</TargetFramework>
<RootNamespace>bts</RootNamespace>
<LangVersion>7.2</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<Version>$(major).$(minor).$(patch)</Version>
<Version Condition="'$(version)'=='..'">1.0.0</Version>
</PropertyGroup>
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../spark_api/spark_api.csproj" />
<ProjectReference Include="../Spark/Spark.csproj" />
</ItemGroup>
</Project>
+17
View File
@@ -0,0 +1,17 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RiskUI">
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
+122 -2
View File
@@ -1,24 +1,33 @@
using bts;
using bts.admin.risk;
using bts.api;
using bts.api.admin;
using bts.api.core;
using bts.spark;
using bts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace mbts.spark {
[Export]
class AdminModel : IDisposable {
public readonly UserTuple user;
readonly AdminService _service;
readonly ErrorReporter _error_reporter;
IDisposable _dispose;
IReadOnlyDictionary<uint, string> _account_map = DictionaryHelper.empty<uint, string>();
[ImportingConstructor]
AdminModel(AdminService service, UserTuple user) {
AdminModel(AdminService service, UserTuple user, SecurityStore security_store, ErrorReporter error_reporter) {
this.user = user;
_service = service;
_error_reporter = error_reporter;
// accounts
on_accounts = service.subscribei_admin_data()
.Select(up => up.all.TryGetFirst(out var ad) && ad.Users.Where(u => u.Name == user.name).TryGetSingle(out var usr) && usr.HasUserData && usr.UserData.HasAccountPermissions ?
@@ -31,6 +40,34 @@ namespace mbts.spark {
new Dictionary<uint, string>(0))
.Replay(1)
.RefCount();
// account risk limits
account_security_risk_limits = service.subscribei_admin_data().Where(ad => ad.all.Any())
.Select(upd => {
AdminDataImmutable admin_data = upd.all.First();
return admin_data.Accounts.SelectMany(acc =>
(acc.HasProprietaryRiskLimits ? acc.ProprietaryRiskLimits.SecurityLimits : Enumerable.Empty<SecurityRiskLimitUnitImmutable>())
.Select(api_limit => AccountSecurityRiskLimitsDataHelper.to_client_data(acc.Id, api_limit)))
.ToDictionary(AccountSecurityRiskLimitsDataHelper.key_fn);
})
.Table(AccountSecurityRiskLimitsDataHelper.key_fn);
// this isn't the most efficient join, but the SecurityStore join is more complex than a simple
// left join operation, because missing securities are retrieved asynchronously, then merged back into the output sequence.
// Until it is known that this inefficiency is problematic, best to leave the battle-tested SecurityStore code alone.
account_security_risk_limits = security_store.join<
TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>,
AccountSecurityRiskLimits,
AccountSecurityKey,
TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>>(account_security_risk_limits,
cli_lim => cli_lim.security_key,
table => (table.state.Values, table.added, table.changes, table.removed, AccountSecurityRiskLimitsDataHelper.key_fn),
(all, added, changes, removed, key_fn) =>
new TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>(
ImmutableDictionary.CreateRange<AccountSecurityKey, AccountSecurityRiskLimits>(
all.Select(tup => new KeyValuePair<AccountSecurityKey, AccountSecurityRiskLimits>(tup.val.key, risk_lim_with_sec(tup)))),
added.Select(risk_lim_with_sec),
changes.Select(ch => new Change<AccountSecurityRiskLimits>(risk_lim_with_sec(ch.Old), risk_lim_with_sec(ch.New))),
removed.Select(risk_lim_with_sec),
AccountSecurityRiskLimitsDataHelper.key_fn));
// users
on_users = Observable.Return(DictionaryHelper.wrap_value(user.id, user.name));
all_user_tuples = user.permissions == UserTuple.Permissions.Admin ?
@@ -45,12 +82,16 @@ namespace mbts.spark {
.Cache() :
Observable.Return(TableUpdate<UserTuple, uint>.mk_empty(UserTuple.key_fn).with_added(new[] { user }));
_dispose = on_accounts.Subscribe(map => _account_map = map);
// local methods
AccountSecurityRiskLimits risk_lim_with_sec((SecurityImmutable? sec, AccountSecurityRiskLimits val) tup) =>
tup.val.with(security: tup.sec);
}
public IObservable<IReadOnlyDictionary<uint, string>> on_users { get; }
public IObservable<IReadOnlyDictionary<uint, string>> on_accounts { get; }
public IObservable<TableUpdate<UserTuple, uint>> all_user_tuples { get; }
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToListSorted(a => a.name));
public IObservable<TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>> account_security_risk_limits { get; }
public IObservable<Update<T, TKey>> permission_filter<T, TKey>(IObservable<Update<T, TKey>> positions, Func<T, uint> get_account_id) where TKey : IEquatable<TKey> =>
user.permissions == UserTuple.Permissions.Admin ?
@@ -68,7 +109,86 @@ namespace mbts.spark {
public AccountTuple? get_default_account() =>
_account_map.Select(kv => new AccountTuple(kv.Key, kv.Value)).TryFirst();
public async Task<Result<AdminServiceOperationResult>[]> set_risk_limits(IReadOnlyDictionary<uint, IEnumerable<(uint, RiskLimits)>> state) {
var results = await Task.WhenAll(state.Select(impl));
return results;
async Task<Result<AdminServiceOperationResult>> impl(KeyValuePair<uint, IEnumerable<(uint, RiskLimits)>> kv) {
var prlimits = new ProprietaryRiskLimits();
foreach (var (sec, lim) in kv.Value) {
var api_lim = AccountSecurityRiskLimitsDataHelper.from_client_data(sec, lim);
prlimits.SecurityLimits.Add(api_lim);
}
return await _service.update_proprietary_risk_limits_async(new ProprietaryRiskLimitsAPIArgument {
AccountId = kv.Key,
ProprietaryRiskLimits = prlimits
});
}
}
public Task<(bool success, string error_msg)> add_or_update_risk_limits(uint account_key, IEnumerable<(uint security_key, RiskLimits limits)> new_limits) {
return _modify_risk_limits("Updating Risk Limits", account_key, add_limits);
//
ProprietaryRiskLimits add_limits(ProprietaryRiskLimits prlimits) {
foreach (var (security_key, limits) in new_limits) {
var api_limits = AccountSecurityRiskLimitsDataHelper.from_client_data(security_key, limits);
if (prlimits.SecurityLimits.TryGetFirstIndex(l => l.SecurityKey == security_key, out int index)) {
prlimits.SecurityLimits[index] = api_limits;
} else {
prlimits.SecurityLimits.Add(api_limits);
}
}
return prlimits;
}
}
public Task<(bool success, string error_msg)> delete_risk_limits(uint account_key, IEnumerable<uint> security_keys) {
return _modify_risk_limits("Deleting Risk Limits", account_key, remove_secs);
//
ProprietaryRiskLimits remove_secs(ProprietaryRiskLimits prlimits) {
var sec_key_set = new HashSet<uint>(security_keys);
for (int i = 0; i < prlimits.SecurityLimits.Count;) {
if (sec_key_set.Contains(prlimits.SecurityLimits[i].SecurityKey)) {
prlimits.SecurityLimits.RemoveAt(i);
} else i++;
}
return prlimits;
}
}
public void Dispose() =>
Disposable.dispose(ref _dispose);
async Task<(bool success, string error_msg)> _modify_risk_limits(string op_desc, uint account_key, Func<ProprietaryRiskLimits, ProprietaryRiskLimits> modify) {
var api_limits = new ProprietaryRiskLimits();
var snapshot = await _get_current_admin_data_async();
if (!snapshot.HasValue)
return (false, "Timed out waiting for data from server.");
var mutable_snap = snapshot.Value.CloneToMutable();
if (!snapshot.Value.Accounts.TryGetFirst(acc => acc.Id == account_key, out var target_acci)) {
return (false, $"Account id '{account_key}' not found.");
}
var prlimits = target_acci.HasProprietaryRiskLimits ? target_acci.ProprietaryRiskLimits.CloneToMutable() : new ProprietaryRiskLimits();
prlimits = modify(prlimits);
if (prlimits is null) {
return (false, "Client-side modification function returned null.");
}
var req = new ProprietaryRiskLimitsAPIArgument {
AccountId = account_key,
ProprietaryRiskLimits = prlimits
};
var reply = await _error_reporter.safe_execute<AdminServiceOperationResult>(
() => _service.update_proprietary_risk_limits_async(req),
r => r.IsSuccess,
r => r.ErrorText,
op_desc);
return (reply.success, reply.value.ErrorText);
}
async Task<AdminDataImmutable?> _get_current_admin_data_async() =>
await _service.subscribei_admin_data()
.Where(upd => upd.all.Any())
.Select(upd => (AdminDataImmutable?)upd.all.First())
.FirstOrDefaultAsync(TimeSpan.FromSeconds(5));
//async Task<AdminDataImmutable?>
}
}
}
+183
View File
@@ -0,0 +1,183 @@
using bts.api;
using bts.api.admin;
using bts.utils;
using System;
namespace bts.admin.risk {
readonly struct AccountSecurityKey : IEquatable<AccountSecurityKey> {
public AccountSecurityKey(uint account, uint security) {
this.account = account;
this.security = security;
}
public uint account { get; }
public uint security { get; }
public bool Equals(AccountSecurityKey other) =>
security == other.security &&
account == other.account;
public override bool Equals(object obj) =>
obj is AccountSecurityKey other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.auto_phase(security.GetHashCode(), account.GetHashCode());
public static bool operator ==(AccountSecurityKey a, AccountSecurityKey b) => a.Equals(b);
public static bool operator !=(AccountSecurityKey a, AccountSecurityKey b) => !a.Equals(b);
}
readonly struct AccountSecurityRiskLimits : IEquatable<AccountSecurityRiskLimits> {
readonly AccountTuple? _account;
public AccountSecurityRiskLimits(AccountSecurityKey key, RiskLimits limits, SecurityImmutable? security = null, AccountTuple? account = null) {
this.key = key;
this.limits = limits;
this.security = security;
_account = account;
}
public AccountSecurityKey key { get; }
public uint account_key => key.account;
public uint security_key => key.security;
public string account_name => _account?.name;
public SecurityImmutable? security { get; }
public RiskLimits limits { get; }
public bool Equals(AccountSecurityRiskLimits other) =>
key.Equals(other.key) &&
limits.Equals(other.limits);
public override bool Equals(object obj) =>
obj is AccountSecurityRiskLimits other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.auto_phase(key.GetHashCode(), limits.GetHashCode());
}
readonly struct RiskLimits : IEquatable<RiskLimits> {
public RiskLimits(
bool is_position_limit_enabled,
uint max_long_position,
uint max_short_position,
bool is_clip_limit_enabled,
uint clip_limit,
bool is_price_limit_enabled,
double min_price,
double max_price) {
//
this.is_position_limit_enabled = is_position_limit_enabled;
this.max_long_position = max_long_position;
this.max_short_position = max_short_position;
this.is_clip_limit_enabled = is_clip_limit_enabled;
this.clip_limit = clip_limit;
this.is_price_limit_enabled = is_price_limit_enabled;
this.min_price = min_price;
this.max_price = max_price;
}
// position
public bool is_position_limit_enabled { get; }
public uint max_long_position { get; }
public uint max_short_position { get; }
// clip
public bool is_clip_limit_enabled { get; }
public uint clip_limit { get; }
// price
public bool is_price_limit_enabled { get; }
public double min_price { get; }
public double max_price { get; }
public bool Equals(RiskLimits other) =>
is_position_limit_enabled == other.is_position_limit_enabled &&
max_long_position == other.max_long_position &&
max_short_position == other.max_short_position &&
is_clip_limit_enabled == other.is_clip_limit_enabled &&
clip_limit == other.clip_limit &&
is_price_limit_enabled == other.is_price_limit_enabled &&
min_price == other.min_price &&
max_price == other.max_price;
public override bool Equals(object obj) =>
obj is RiskLimits other && Equals(other);
public override int GetHashCode() {
const int non_bin_bits = 28;
// Evenly split the non-binary props across the first 28 bits.
// The last 3 bits are used to represent each of the 'is_enabled' (binary) props.
var hash = EqualityComparer.auto_phase(new[] {
max_long_position.GetHashCode(),
max_short_position.GetHashCode(),
clip_limit.GetHashCode(),
min_price.GetHashCode(),
max_price.GetHashCode()
}, non_bin_bits);
if (is_position_limit_enabled)
hash |= 1 << (non_bin_bits + 1);
if (is_clip_limit_enabled)
hash |= 1 << (non_bin_bits + 2);
if (is_price_limit_enabled)
hash |= 1 << (non_bin_bits + 3);
return hash;
}
}
static class AccountSecurityRiskLimitsDataHelper {
public static readonly Func<AccountSecurityRiskLimits, AccountSecurityKey> key_fn =
a => a.key;
// Don't make this a constructor. Subscribers to this data are discouraged
// from using the server side data (client/server coupling should be minimal).
public static AccountSecurityRiskLimits to_client_data(uint account_key, SecurityRiskLimitUnitImmutable api_data) =>
new AccountSecurityRiskLimits(
new AccountSecurityKey(account_key, api_data.SecurityKey),
new RiskLimits(
api_data.EnforceNetPosLimits,
api_data.NetLongPositionLimit,
api_data.NetShortPositionLimit,
api_data.EnforceClipLimits,
api_data.ClipSize,
api_data.EnforcePriceBands,
api_data.MinSellPrice,
api_data.MaxBuyPrice));
public static AccountSecurityRiskLimits with(this AccountSecurityRiskLimits cli_data, RiskLimits? limits = null, AccountTuple? account = null, SecurityImmutable? security = null, AccountSecurityKey? key = null) =>
new AccountSecurityRiskLimits(
key ?? cli_data.key,
limits ?? cli_data.limits,
account: account ?? new AccountTuple(cli_data.account_key, cli_data.account_name),
security: security ?? cli_data.security);
public static RiskLimits with(this RiskLimits limits,
bool? is_position_limit_enabled = null,
uint? max_long_position = null,
uint? max_short_position = null,
bool? is_clip_limit_enabled = null,
uint? clip_limit = null,
bool? is_price_limit_enabled = null,
double? min_price = null,
double? max_price = null) =>
new RiskLimits(
is_position_limit_enabled ?? limits.is_position_limit_enabled,
max_long_position ?? limits.max_long_position,
max_short_position ?? limits.max_short_position,
is_clip_limit_enabled ?? limits.is_clip_limit_enabled,
clip_limit ?? limits.clip_limit,
is_price_limit_enabled ?? limits.is_price_limit_enabled,
min_price ?? limits.min_price,
max_price ?? limits.max_price);
public static SecurityRiskLimitUnit from_client_data(uint security_key, RiskLimits limits) =>
new SecurityRiskLimitUnit {
SecurityKey = security_key,
NetLongPositionLimit = limits.max_long_position,
NetShortPositionLimit = limits.max_short_position,
MaxBuyPrice = limits.max_price,
MinSellPrice = limits.min_price,
ClipSize = limits.clip_limit,
EnforceNetPosLimits = limits.is_position_limit_enabled,
EnforceClipLimits = limits.is_clip_limit_enabled,
EnforcePriceBands = limits.is_price_limit_enabled
};
}
}
@@ -276,7 +276,7 @@ namespace mbts.spark {
ob_settings.filters = new bts.shell.orderbook.FilterStates {
statuses = new[] { OrderStatus.Working },
sides = new[] { side },
security_criteria = new SecurityCriteria(security)
security_criteria = SecurityCriteria.from_securities(security)
};
_bus.post(SparkTags.ShowOrderBookTag, ob_settings);
}
@@ -66,7 +66,7 @@ namespace mbts.spark {
if (_sorted_selected_security_criteria_backing != null) {
_selected_security_criteria_backing = _sorted_selected_security_criteria_backing;
} else if (_vm != null)
_selected_security_criteria_backing = new SecurityCriteria(_vm.selected_rows.Select(r => r.security));
_selected_security_criteria_backing = SecurityCriteria.from_securities(_vm.selected_rows.Select(r => r.security));
}
return _selected_security_criteria_backing;
}
@@ -92,7 +92,7 @@ namespace mbts.spark {
}
}
var comp = comp_builder.to_comparer();
_sorted_selected_security_criteria_backing = new SecurityCriteria(_vm.selected_rows.Ordered(comp).Select(r => r.security));
_sorted_selected_security_criteria_backing = SecurityCriteria.from_securities(_vm.selected_rows.Ordered(comp).Select(r => r.security));
}
}
return _sorted_selected_security_criteria_backing;
@@ -1,6 +1,7 @@
using bts.api;
using bts.co2;
using bts.utils;
using bts.utils.csv;
using mbts;
using mbts.spark;
using mbts.ui.controls;
@@ -49,6 +50,18 @@ namespace bts.shell.time_and_sales {
public void raise_invalidated() =>
invalidated?.Invoke();
public static System.Collections.Specialized.OrderedDictionary csv_ordered_properties =>
CsvHelper.create_ordering_maintainer(
typeof(TimeAndSalesRowVM),
new string[] {
nameof(security),
nameof(time),
nameof(price),
nameof(size),
nameof(aggressor_side),
nameof(security_key),
});
public bool Equals(TimeAndSalesRowVM other) =>
other != null &&
row_key == other.row_key;
@@ -2,6 +2,7 @@
using bts.shell.fills;
using bts.spark;
using bts.view;
using bts.utils.csv;
using mbts;
using mbts.co2;
using mbts.persist;
@@ -124,6 +125,8 @@ namespace bts.shell.time_and_sales {
show_in_context_menu = _can_pause_updates
};
_commands = new SparkCommand[] {
// csv export
new SparkCommandCommand("Export to csv", Icons.Export, FileDialogCommand.export_csv(() => _vm.rows.ToCsv(TimeAndSalesRowVM.csv_ordered_properties), "TaS")),
// ladder
SparkCommands.ShowLadderAutoLookup(SparkCommands.KeyGestures.ShowLadder)
.complete(current_sec_key_src, sec_keys => sec_keys.Count > 0),
@@ -135,7 +138,7 @@ namespace bts.shell.time_and_sales {
.complete(selected_rows_src, always_true, get_fills_startup_data),
// mview
SparkCommands.ShowMarketView
.complete(selected_rows_src, always_true, rows => rows.Count > 0 ? new SecurityCriteria(rows.Select(r => r.security)) : _security_criteria),
.complete(selected_rows_src, always_true, rows => rows.Count > 0 ? SecurityCriteria.from_securities(rows.Select(r => r.security)) : _security_criteria),
// pause
toggle_is_paused_cmd,
// change securities
@@ -180,7 +183,7 @@ namespace bts.shell.time_and_sales {
FillsSettings get_fills_startup_data(ImmutableHashSet<TimeAndSalesRowVM> rows) =>
new FillsSettings(new FillsFilterStates {
security = rows.Count > 0 ? new SecurityCriteria(rows.Select(r => r.security)) : _security_criteria
security = rows.Count > 0 ? SecurityCriteria.from_securities(rows.Select(r => r.security)) : _security_criteria
});
string get_is_paused_cmd_title() =>
@@ -114,6 +114,8 @@ namespace bts.shell {
public static QuoteInsideMarketDistanceWithTravel QuoteInsideMarketDistanceWithTravel => new QuoteInsideMarketDistanceWithTravel { Priority = RulePriority };
public static QuoteInsideMarketDistance QuoteInsideMarketDistance => new QuoteInsideMarketDistance { Priority = RulePriority };
public static QuoteInsideMarketDistanceThrottle QuoteInsideMarketDistanceThrottle => new QuoteInsideMarketDistanceThrottle();
public static QuoteTimeBasedThrottle QuoteTimeBasedThrottle => new QuoteTimeBasedThrottle { DelayToImprove = 0, DelayToRun = 0 };
#endregion
#region PreHedge
@@ -79,7 +79,7 @@ namespace mbts.spark {
), SparkCommands.ShowOrders.complete(() => {
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
ob_settings.filters = new bts.shell.orderbook.FilterStates {
security_criteria = new SecurityCriteria(_vm.value.row_vms.Select(r => r.security))
security_criteria = SecurityCriteria.from_securities(_vm.value.row_vms.Select(r => r.security))
};
return ob_settings;
}), SparkCommands.window_settings_command(new DialogCommand(() => {
@@ -10,13 +10,13 @@ namespace mbts.spark {
public override string man_page { get; } = string.Empty;
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.EnableHedging;
leg_tup.leg_params.SendHedge is null || leg_tup.leg_params.SendHedge.Val;
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) =>
leg_tup.leg_params.EnableHedging = value;
public override bool has_errors(IReadOnlyCollection<LegSpreaderParamsTuple> leg_tups) =>
!leg_tups.Any(lt => lt.leg_params.EnableHedging);
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) {
if (leg_tup.leg_params.SendHedge is null)
leg_tup.leg_params.SendHedge = new bts.spark.NullableBoolean();
leg_tup.leg_params.SendHedge.Val = value;
}
}
}
}
}
@@ -0,0 +1,21 @@
using mbts.ui.spark;
using System;
namespace mbts.spark {
partial class SyntheticEditorVM {
class LeanOffsetTicksModel : RowModelBase<int> {
public override string display_name { get; } = "Lean Offset Ticks";
public override string help_text { get; } = "Improvement (in ticks) to the lean market";
public override string man_page { get; } = string.Empty;
public override int get_value(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.QuoteParams?.LeanOffsetTicks ?? 0;
public override void set_value(LegSpreaderParamsTuple leg_tup, int value) {
if (leg_tup.leg_params.QuoteParams is null)
leg_tup.leg_params.QuoteParams = new bts.spark.QuoteParams();
leg_tup.leg_params.QuoteParams.LeanOffsetTicks = value;
}
}
}
}
@@ -0,0 +1,40 @@
using bts.shell;
using bts.spark;
using mbts.ui.spark;
using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteTimeThrottleDelayToImproveModel : RowModelBase<uint> {
readonly IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Delay to Improve (ms)";
public override string help_text { get; } = string.Empty;
public override string man_page { get; } = string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote, Subsections.TimeThrottle };
public void apply_value(LegSpreaderParamsTuple leg_tup) =>
_get_throttle(leg_tup).DelayToImprove = _value_from_store(leg_tup);
public override uint get_value(LegSpreaderParamsTuple leg_tup) =>
_get_throttle(leg_tup) is QuoteTimeBasedThrottle throttle ?
throttle.DelayToImprove :
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.id] = value;
if (_get_throttle(leg_tup) is QuoteTimeBasedThrottle throttle)
throttle.DelayToImprove = value;
}
protected override bool _get_enabled(LegSpreaderParamsTuple leg_tup) =>
_get_throttle(leg_tup) != null;
QuoteTimeBasedThrottle _get_throttle(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.QuoteParams.TimeBasedThrottle;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.QuoteTimeBasedThrottle.DelayToImprove);
}
}
}
@@ -0,0 +1,40 @@
using bts.shell;
using bts.spark;
using mbts.ui.spark;
using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteTimeThrottleDelayToRunModel : RowModelBase<uint> {
readonly IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Delay To Run (ms)";
public override string help_text { get; } = string.Empty;
public override string man_page { get; } = string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote, Subsections.TimeThrottle };
public void apply_value(LegSpreaderParamsTuple leg_tup) =>
_get_throttle(leg_tup).DelayToRun = _value_from_store(leg_tup);
public override uint get_value(LegSpreaderParamsTuple leg_tup) =>
_get_throttle(leg_tup) is QuoteTimeBasedThrottle throttle ?
throttle.DelayToRun :
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.id] = value;
if (_get_throttle(leg_tup) is QuoteTimeBasedThrottle throttle)
throttle.DelayToRun = value;
}
protected override bool _get_enabled(LegSpreaderParamsTuple leg_tup) =>
_get_throttle(leg_tup) != null;
QuoteTimeBasedThrottle _get_throttle(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.QuoteParams.TimeBasedThrottle;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.QuoteTimeBasedThrottle.DelayToRun);
}
}
}
@@ -0,0 +1,38 @@
using bts.shell;
using mbts.ui.spark;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteTimeThrottleEnabledModel : RowModelBase<bool> {
readonly QuoteTimeThrottleDelayToRunModel _delay_to_run;
readonly QuoteTimeThrottleDelayToImproveModel _delay_to_improve;
public QuoteTimeThrottleEnabledModel(QuoteTimeThrottleDelayToRunModel delay_to_run, QuoteTimeThrottleDelayToImproveModel delay_to_improve) {
_delay_to_run = delay_to_run;
_delay_to_improve = delay_to_improve;
}
public override string display_name { get; } = string.Empty;
public override string help_text { get; } = string.Empty;
public override string man_page { get; } = string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote, Subsections.TimeThrottle };
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.QuoteParams.TimeBasedThrottle != null;
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) {
if (value) {
if (leg_tup.leg_params.QuoteParams.TimeBasedThrottle == null) {
leg_tup.leg_params.QuoteParams.TimeBasedThrottle = SyntheticDataHelper.Defaults.QuoteTimeBasedThrottle;
_delay_to_run.apply_value(leg_tup);
_delay_to_improve.apply_value(leg_tup);
}
} else {
leg_tup.leg_params.QuoteParams.TimeBasedThrottle = null;
}
_delay_to_run.set_enabled(leg_tup, value);
_delay_to_improve.set_enabled(leg_tup, value);
}
}
}
}
@@ -108,6 +108,10 @@ namespace mbts.spark {
"Reduces re-quoting updates by defining a region on either side " +
"of the working quoting order, inside which quote updates will not be sent.");
public static ParameterSection TimeThrottle =>
new ParameterSection("Time Throttle", true,
"Reduces re-quoting updates by requiring a minimum amount of time to elapse between quotes.");
public static ParameterSection Conditional =>
new ParameterSection("Conditional", true,
"Reduces re-quoting updates by only quoting if N ticks away from the opposite " +
@@ -196,6 +196,9 @@ namespace mbts.spark {
var quote_throttle_outside_ticks = new QuoteThrottleOutsideTicksModel();
var quote_throttle_inside_ticks = new QuoteThrottleInsideTicksModel();
var quote_time_throttle_delay_to_run = new QuoteTimeThrottleDelayToRunModel();
var quote_time_throttle_delay_to_improve = new QuoteTimeThrottleDelayToImproveModel();
var conditional_quoting_ticks = new ConditionalQuotingTicksModel();
var conditional_quoting_price_type = new ConditionalQuotingPriceTypeModel();
@@ -223,17 +226,24 @@ namespace mbts.spark {
_quantity_model,
_price_mult_model,
is_quote_model,
//new EnableHedgeModel(), //TAHA_SPREADER
new EnableHedgeModel(), //TAHA_SPREADER
new HedgeRatioModel(),
//new MinLeanQtyModel(), //TAHA_SPREADER
new LeanOffsetTicksModel(),
//new QuoteHiddenModel(),
new QuoteQueueHolderModel(),
new MarketDataTypeModel(),
new ShowQuantityEnabledModel(show_quantity_quantity),
show_quantity_quantity,
// Throttle
new QuoteThrottleEnabledModel(quote_throttle_inside_ticks, quote_throttle_outside_ticks),
quote_throttle_inside_ticks,
quote_throttle_outside_ticks,
// Time Throttle
new QuoteTimeThrottleEnabledModel(quote_time_throttle_delay_to_run, quote_time_throttle_delay_to_improve),
quote_time_throttle_delay_to_run,
quote_time_throttle_delay_to_improve,
new ConditionalQuoteEnabledModel(conditional_quoting_ticks, conditional_quoting_price_type),
conditional_quoting_ticks,
//conditional_quoting_price_type,
@@ -169,7 +169,7 @@ namespace mbts.spark {
void _show_order_book() {
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
ob_settings.filters = new bts.shell.orderbook.FilterStates {
security_criteria = new SecurityCriteria(security)
security_criteria = SecurityCriteria.from_securities(security)
};
_bus.post(SparkTags.ShowOrderBookTag, ob_settings);
}
@@ -5,6 +5,7 @@ using bts.collections;
using bts.shell.fills;
using bts.spark;
using bts.utils;
using bts.utils.csv;
using mbts.collections;
using mbts.persist;
using mbts.spark.filters;
@@ -333,6 +334,27 @@ namespace mbts.spark {
public override bool Equals(object obj) => Equals(obj as Row);
public override int GetHashCode() => _fill.FillKey.GetHashCode() ^ _order.Key.GetHashCode();
public static System.Collections.Specialized.OrderedDictionary csv_ordered_properties =>
CsvHelper.create_ordering_maintainer(typeof(Row),
new string[] {
nameof(security),
nameof(order_type),
nameof(root_algo_display_name),
nameof(side),
nameof(fill_time),
nameof(user),
nameof(account),
nameof(exchange),
nameof(time_in_force),
nameof(size),
nameof(price),
nameof(fill_kind),
nameof(fill_status),
nameof(fill_key),
nameof(order_key),
nameof(security_key)
});
public event Action invalidated;
void IRowVM.raise_invalidated() => invalidated?.Invoke();
}
@@ -4,6 +4,7 @@ using bts.shell;
using bts.shell.fills;
using bts.spark;
using bts.view;
using bts.utils.csv;
using mbts.persist;
using mbts.ui;
using mbts.ui.settings;
@@ -61,6 +62,7 @@ namespace mbts.spark {
_view = new FillsView { DataContext = _vm };
_commands = new List<SparkCommand> {
new SparkCommandCommand("Export to csv", Icons.Export, FileDialogCommand.export_csv(() => _vm.rows.ToCsv(FillsVM.Row.csv_ordered_properties), "Fills")),
// wrap the grid settings view factory in a dialogCommand here since the vm is destroyed/recreated when the security criteria changes
SparkCommands.window_settings_command(new DialogCommand(() => {
var vm = new ShellSettingsDialogVM<GridSettings, SimpleGridSettingsVM>(
@@ -6,6 +6,7 @@ using bts.utils;
using mbts.ui;
using mbts.ui.settings;
using mbts.ui.spark;
using mbts.ui.utility.text;
using ProtoBuf;
using System;
using System.Collections.Generic;
@@ -13,6 +14,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace mbts.spark {
// IMPORTANT: BE SURE TO ADD ANY NEW PROPERTIES TO THE EQUALS FUNCTION!
@@ -166,6 +168,10 @@ namespace mbts.spark {
[ProtoContract]
class LadderMarketBreakLine : IEquatable<LadderMarketBreakLine> {
public enum RefreshMode {
Auto,
Manual
}
// this class is 100% mutable (there are literally 0 readonly members),
// so GetHashCode must return the same hash for every instance.
static readonly int _type_hash = typeof(LadderMarketBreakLine).GetHashCode();
@@ -173,6 +179,8 @@ namespace mbts.spark {
public bool show { get; set; }
[ProtoMember(2)]
public uint color { get; set; }
[ProtoMember(3)]
public RefreshMode refresh_mode { get; set; }
public uint? color_nullable {
get {
@@ -185,7 +193,8 @@ namespace mbts.spark {
public bool Equals(LadderMarketBreakLine other) =>
other != null &&
other.show == show &&
other.color == color;
other.color == color &&
other.refresh_mode == refresh_mode;
public override bool Equals(object obj) =>
Equals(obj as LadderMarketBreakLine);
@@ -194,6 +203,37 @@ namespace mbts.spark {
_type_hash;
}
[ProtoContract]
class FairValueLine : IEquatable<FairValueLine> {
public enum PriceType {
Bid,
Ask,
Mid
}
[ProtoMember(1)]
public uint security_key { get; set; }
[ProtoMember(2)]
public PriceType price_type { get; set; }
[ProtoMember(3)]
public LadderMarketBreakLine display { get; set; }
public bool Equals(FairValueLine other) {
return other != null &&
security_key == other.security_key &&
price_type == other.price_type &&
display.safe_equals(other.display);
}
public override bool Equals(object obj) {
return Equals(obj as FairValueLine);
}
public override int GetHashCode() {
return (int)security_key;
}
}
[Flags]
enum LadderFavoriteAlgoFlags {
Sticky = 1,
@@ -317,6 +357,139 @@ namespace mbts.spark {
public static bool operator !=(LadderMiniOrderBookSettings a, LadderMiniOrderBookSettings b) => !a.Equals(b);
}
[ProtoContract]
class LadderPanelPricePointSettings : IEquatable<LadderPanelPricePointSettings> {
public static LadderPanelPricePointSettings historic_default =>
new LadderPanelPricePointSettings {
day_change = new DaySettings {
// (neutral)
font = _default_font,
background_color = Colors.Transparent.rgb_color(),
// negative
negative_font_color = Colors.White.rgb_color(),
negative_background_color = Colors.Red.rgb_color(),
// positive
positive_font_color = Colors.White.rgb_color(),
positive_background_color = Colors.Green.rgb_color()
},
session_low = _default_item,
session_high = _default_item,
open = _default_item,
settlement = _default_item,
volume = _default_item
};
public static LadderPanelPricePointSettings tt_default {
get {
var rv = historic_default;
rv.day_change.background_color = Color.FromRgb(113, 201, 69).rgb_color();
rv.day_change.font.font_color = Colors.Black.rgb_color();
return rv;
}
}
public static LadderPanelPricePointSettings Default => LadderSettings.mimic_tt ?
tt_default : historic_default;
[ProtoMember(1)]
public DaySettings day_change { get; set; }
[ProtoMember(2)]
public ItemSettings session_low { get; set; }
[ProtoMember(3)]
public ItemSettings session_high { get; set; }
[ProtoMember(4)]
public ItemSettings open { get; set; }
[ProtoMember(5)]
public ItemSettings settlement { get; set; }
[ProtoMember(6)]
public ItemSettings volume { get; set; }
public bool Equals(LadderPanelPricePointSettings other) =>
other != null &&
day_change.safe_equals(other.day_change) &&
session_high.safe_equals(other.session_high) &&
session_high.safe_equals(other.session_high) &&
open.safe_equals(other.open) &&
settlement.safe_equals(other.settlement) &&
volume.safe_equals(other.volume);
static FontParamsProto _default_font =>
new FontParamsProto {
font_color = Colors.Black.rgb_color(),
font_size = FontSizes.Regular,
typeface = new TypefaceProto(TextUtils.StandardFontFamilyName, FontWeights.Bold.ToOpenTypeWeight())
};
static ItemSettings _default_item =>
new ItemSettings {
font = _default_font,
background_color = Colors.Transparent.rgb_color()
};
[ProtoContract]
readonly struct SignedColorPair : IEquatable<SignedColorPair> {
[ProtoMember(1)]
public uint positive_color { get; }
[ProtoMember(2)]
public uint negative_color { get; }
public bool Equals(SignedColorPair other) =>
positive_color == other.positive_color &&
negative_color == other.negative_color;
}
[ProtoContract]
public class ItemSettings : IEquatable<ItemSettings> {
[ProtoMember(1)]
public FontParamsProto font { get; set; }
[ProtoMember(2)]
public uint background_color { get; set; }
[ProtoMember(3)]
public bool show_label { get; set; }
public bool Equals(ItemSettings other) =>
other != null &&
font.safe_equals(other.font) &&
background_color == other.background_color &&
show_label == other.show_label;
}
[ProtoContract]
public class DaySettings : IEquatable<DaySettings> {
[ProtoMember(1)]
public FontParamsProto font { get; set; }
// [neutral] background
[ProtoMember(2)]
public uint background_color { get; set; }
[ProtoMember(3)]
public bool show_label { get; set; }
// negative
[ProtoMember(4)]
public uint negative_font_color { get; set; }
[ProtoMember(5)]
public uint negative_background_color { get; set; }
// positive
[ProtoMember(6)]
public uint positive_font_color { get; set; }
[ProtoMember(7)]
public uint positive_background_color { get; set; }
public uint neutral_foreground => font?.font_color ?? 0;
public bool Equals(DaySettings other) =>
other != null &&
font.safe_equals(other.font) &&
background_color == other.background_color &&
show_label == other.show_label &&
negative_font_color == other.negative_font_color &&
negative_background_color == other.negative_background_color &&
positive_font_color == other.positive_font_color &&
positive_background_color == other.positive_background_color;
}
}
// IMPORTANT: BE SURE TO ADD ANY NEW PROPERTIES TO THE EQUALS FUNCTION!
// 1 important reason at this time:
// From the settings dialog, default settings will only be persisted when they are NOT EQUAL to the current default settings.
@@ -334,6 +507,13 @@ namespace mbts.spark {
// this class is 100% mutable (there are literally 0 readonly members),
// so GetHashCode must return the same hash for every instance.
static readonly int _type_hash = typeof(LadderSettings).GetHashCode();
uint _ticks_per_row;
List<FairValueLine> _fair_value_lines;
static LadderSettings() {
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
mimic_tt = knobs.mimic_tt;
}
// Default proto constructor
// Added this so that when clients upgrade to a version with new settings,
@@ -365,8 +545,10 @@ namespace mbts.spark {
enforce_max_clip = _default_enforce_max_clip;
status_message_visible = true;
}
public static bool mimic_tt { get; }
public static LadderSettings Default {
public static LadderSettings historic_default {
get {
LadderColumnSettings
orders_column = LadderColumnSettings.DefaultOrderColumnSettings,
@@ -376,12 +558,12 @@ namespace mbts.spark {
last_column = LadderColumnSettings.Default,
piq_column = LadderColumnSettings.DefaultPiqColumnSettings;
orders_column.background = BTSColors.LadderSpecial.default_column_background;
bid_column.foreground = 0xFFFFFF;
bid_column.foreground = 0xFFFFFFFF;
bid_column.background = BTSColors.MarketData.bid;
ask_column.foreground = 0xFFFFFF;
ask_column.foreground = 0xFFFFFFFF;
ask_column.background = BTSColors.MarketData.ask;
price_column.background = BTSColors.LadderSpecial.default_column_background;
last_column.background = 0xFFFFFF;
last_column.background = 0xFFFFFFFF;
piq_column.background = BTSColors.LadderSpecial.default_column_background;
var rv = new LadderSettings {
row_height = 22,
@@ -425,6 +607,7 @@ namespace mbts.spark {
auto_clear_visible = true,
clear_visible = true,
order_entry_panel_initial_width = _default_order_entry_panel_width,
panel_price_point_settings = LadderPanelPricePointSettings.Default,
zoom_slider_visible = false,
zoom_factor = _default_zoom_factor,
net_change = NetChangeTerms.None,
@@ -437,6 +620,28 @@ namespace mbts.spark {
return rv;
}
}
public static LadderSettings tt_default {
get {
var rv = historic_default;
rv.orders_column.background = Color.FromRgb(223, 223, 223).rgb_color();
// Since the price column sits atop the session range fill in tt
// mode (normally it is the other way around), the former is made
// partially translucent while the latter is fully opaque (obv the
// top layer cannot be opaque, since it would completely hide the
// layer beneath it).
rv.session_range_fill.color = rv.session_range_fill.color.rgb_color().with(255).rgb_color();
rv.price_column.background = Color.FromArgb(226, 166, 166, 166).rgb_color();
rv.price_column.foreground = Colors.White.rgb_color();
rv.bid_column.background = Color.FromRgb(0, 30, 160).rgb_color();
rv.ask_column.background = Color.FromRgb(160, 0, 0).rgb_color();
rv.ask_column.foreground = Colors.Black.rgb_color();
return rv;
}
}
public static LadderSettings Default => mimic_tt ?
tt_default : historic_default;
public enum BadgeStyle { Normal, Alternate }
@@ -603,8 +808,21 @@ namespace mbts.spark {
[ProtoMember(61), DefaultValue(true)]
public bool status_message_visible { get; set; }
[ProtoMember(62)]
public uint ticks_per_row {
get => Math.Max(_ticks_per_row, 1);
set => _ticks_per_row = value;
}
[ProtoMember(900)]
public LadderOrderLimits ladder_order_limits { get; set; }
[ProtoMember(901)]
public LadderPanelPricePointSettings panel_price_point_settings { get; set; }
[ProtoMember(902)]
public List<FairValueLine> fair_value_lines {
get => _fair_value_lines ?? (_fair_value_lines = new List<FairValueLine>());
set => _fair_value_lines = value;
}
public void sanitize() =>
sanitize(this);
@@ -639,6 +857,11 @@ namespace mbts.spark {
s.mini_ob = new LadderMiniOrderBookSettings(s.mini_ob.value.with(sorting_criterion: bts.shell.orderbook.OrderBookStartupData.default_mini_grid.general.sorting_criterion));
#pragma warning restore CS0612 // Type or member is obsolete
s.row_height = LadderRender.measure_row_height(s);
if (s.panel_price_point_settings is null)
s.panel_price_point_settings = LadderPanelPricePointSettings.Default;
if (s.fair_value_lines is null) {
s.fair_value_lines = new List<FairValueLine>(0);
}
return s;
}
@@ -712,7 +935,10 @@ namespace mbts.spark {
a.max_clip == b.max_clip &&
a.price_display_format == b.price_display_format &&
a.status_message_visible == b.status_message_visible &&
a.ladder_order_limits.safe_equals(b.ladder_order_limits);
a.ladder_order_limits.safe_equals(b.ladder_order_limits) &&
a.panel_price_point_settings.safe_equals(b.panel_price_point_settings) &&
a._ticks_per_row == b._ticks_per_row &&
EqualityComparer.safe_sequence_equals(a.fair_value_lines, b.fair_value_lines);
public bool Equals(LadderSettings other) =>
profile_equals(this, other) &&
@@ -208,6 +208,10 @@ namespace bts.shell {
s => {
var svm = new LadderSettingsVM(model.order_gateway, vm.security);
svm.load(s);
svm.SubscribeDisposed(
_bus.subscribe(SparkTags.SecuritiesTag)
.OnDispatcher()
.Subscribe(sec_map => svm.security_map = sec_map));
return svm;
},
_ => _,
@@ -331,4 +335,4 @@ namespace bts.shell {
[ProtoMember(3)]
public uint settings_id;
}
}
}
@@ -0,0 +1,93 @@
using mbts.ui;
using mbts.ui.controls.color_picker;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using System.Windows.Media;
namespace mbts.spark {
class FairValueLineSettingsVM : ui.shell.ViewModelBase {
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
public FairValueLineSettingsVM() {
add_item_cmd = new DelegateCommand(() => items.Add(new FairValueLineItemSettingsVM()));
remove_item_cmd = new DelegateCommand<FairValueLineItemSettingsVM>(item => items.Remove(item));
}
public ObservableCollection<FairValueLineItemSettingsVM> items { get; } =
new ObservableCollection<FairValueLineItemSettingsVM>();
public IReadOnlyDictionary<uint, SecurityMD> security_map {
get => _security_map;
set => Set(ref _security_map, value);
}
public ICommand add_item_cmd { get; }
public ICommand remove_item_cmd { get; }
}
class FairValueLineItemSettingsVM : ui.shell.ViewModelBase {
uint _selected_security_key;
FairValueLine.PriceType _selected_price_type;
Color _color;
bool _show = true;
bool _auto_refresh = true;
public FairValueLineItemSettingsVM() {
palettes = new ObservableCollection<ColorPaletteVM>(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p)));
}
public uint selected_security_key {
get => _selected_security_key;
set => Set(ref _selected_security_key, value);
}
public IEnumerable<FairValueLine.PriceType> price_types =>
ui.utility.EnumHelper.get_values<FairValueLine.PriceType>();
public FairValueLine.PriceType selected_price_type {
get => _selected_price_type;
set => Set(ref _selected_price_type, value);
}
public Color color { get => _color; set => Set(ref _color, value); }
public bool show { get => _show; set => Set(ref _show, value); }
public bool auto_refresh {
get => _auto_refresh; set {
if (Set(ref _auto_refresh, value)) {
RaisePropertyChanged(nameof(refresh_mode));
}
}
}
public LadderMarketBreakLine.RefreshMode refresh_mode {
get => _auto_refresh ? LadderMarketBreakLine.RefreshMode.Auto : LadderMarketBreakLine.RefreshMode.Manual;
set => auto_refresh = value == LadderMarketBreakLine.RefreshMode.Auto;
}
public ObservableCollection<ColorPaletteVM> palettes { get; }
public void assign_from(FairValueLine settings) {
if (settings is null) {
settings = new FairValueLine();
}
selected_security_key = settings.security_key;
selected_price_type = settings.price_type;
var display = settings.display ?? new LadderMarketBreakLine();
show = display.show;
color = display.color.rgb_color();
refresh_mode = display.refresh_mode;
}
public FairValueLine create() {
return new FairValueLine {
security_key = selected_security_key,
price_type = _selected_price_type,
display = new LadderMarketBreakLine {
color = color.rgb_color(),
show = show,
refresh_mode = refresh_mode
}
};
}
}
}
@@ -0,0 +1,121 @@
using mbts.co2;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace mbts.spark {
class FairValueLineVM {
NativePriceSize _bid;
NativePriceSize _ask;
double _settlement;
double _frozen;
ulong _last_update;
public FairValueLineVM(uint sec_id) {
this.sec_id = sec_id;
}
public FairValueLineVM(FairValueLine fvl, SecurityMD? sec_md = null) {
sec_id = fvl.security_key;
price_type = fvl.price_type;
settings = fvl.display;
this.sec_md = sec_md;
}
public FairValueLine.PriceType price_type { get; private set; }
public double price { get; private set; }
public double price_view {
get {
if (!settings.show) {
return double.NaN;
}
switch (settings.refresh_mode) {
case LadderMarketBreakLine.RefreshMode.Auto:
return price;
case LadderMarketBreakLine.RefreshMode.Manual:
return frozen;
default: return double.NaN;
}
}
}
public double frozen {
get {
if (double.IsNaN(_frozen)) {
_frozen = price;
}
return _frozen;
}
}
public Color color { get; set; }
public uint sec_id { get; }
SecurityMD? _sec_md;
public SecurityMD? sec_md {
get => _sec_md;
set {
if (_sec_md == value) {
return;
}
if (value.HasValue && value.Value.security.Id != sec_id) {
throw new ArgumentException("'sec_md' security id must be equal to 'sec_id'", nameof(sec_md));
}
_sec_md = value;
refresh(true);
}
}
public LadderMarketBreakLine settings { get; set; } = new LadderMarketBreakLine();
public void load(LadderMarketBreakLine settings, FairValueLine.PriceType price_type) {
this.settings = settings;
if (this.price_type != price_type) {
this.price_type = price_type;
refresh(true);
}
}
public void refresh(bool update_frozen) {
if (sec_md.HasValue) {
var old_settlement = _settlement;
_settlement = sec_md.Value.md.settlement_price;
if (sec_md.Value.md.get_bbo(ref _last_update, out _bid, out _ask) || _settlement != old_settlement) {
price = _read_price();
}
} else {
price = double.NaN;
}
if (update_frozen) {
_frozen = price;
}
}
double _read_price() {
if (!_sec_md.HasValue) {
return double.NaN;
}
var bid = _bid.price;
var ask = _ask.price;
switch (price_type) {
case FairValueLine.PriceType.Bid:
return LadderPriceLineViewAdapter.bid_line(
_bid.price, _ask.price, _settlement, sec_md.Value.security.ContractSpec);
case FairValueLine.PriceType.Ask:
return _ask.is_nan ? double.NaN : _ask.price;
case FairValueLine.PriceType.Mid:
if (_bid.is_nan) {
if (_ask.is_nan) {
return double.NaN;
}
return _ask.price;
}
if (_ask.is_nan) {
return _bid.price;
}
return (_bid.price + _ask.price) / 2;
default: return double.NaN;
}
}
}
}
@@ -1,13 +1,16 @@
using bts.api;
using bts.api.orderman;
using bts.shell.orderbook;
using bts.utils;
using mbts.persist;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
@@ -52,9 +55,9 @@ namespace mbts.spark {
public event Action piq_changed;
LadderModel model => _owner.model;
Dictionary<uint, (OrderRecordImmutable order, uint? p, AlgoData? algo, AlgoDefinitionMetadata? meta)> _orders = new Dictionary<uint, (OrderRecordImmutable order, uint? p, AlgoData? algo, AlgoDefinitionMetadata? meta)>();
ImmutableDictionary<uint, OrderSecurity> _orders = ImmutableDictionary<uint, OrderSecurity>.Empty;
public LadderOrderVM((OrderRecordImmutable, uint?, AlgoData?, AlgoDefinitionMetadata?) op, double real_price, LadderVM owner, IBus bus, IObservable<LadderSettings.BadgeStyle> bs) {
public LadderOrderVM(ImmutableDictionary<uint, OrderSecurity> orders, double real_price, LadderVM owner, IBus bus, IObservable<LadderSettings.BadgeStyle> bs) {
_owner = owner;
_bus = bus;
_bs = bs;
@@ -78,8 +81,7 @@ namespace mbts.spark {
double get_font_size(LadderSettings s) => s.orders_column.font_size;
Brush get_foreground(LadderSettings s) => s.orders_column.foreground.rgb_brush();
double get_row_height(LadderSettings s) => s.row_height;
//
add_order(op);
_orders = orders;
_dispose = Disposable.Create(
_bs.Subscribe(badge => {
@@ -175,41 +177,6 @@ namespace mbts.spark {
RaisePropertyChanged(nameof(text_lines));
}
/// <summary>
/// Add Order performs an add the first time it's called via the constructor. When it's called, directly
/// it really serves as an update/replace. Also note that since the min_piq is a separate piece of state representing
/// the lowest piq of the orders the badge represents, we must reevaluate if that has changed on said update.
/// </summary>
public void add_order((OrderRecordImmutable order, uint? p, AlgoData? algo, AlgoDefinitionMetadata? meta) new_op) {
_orders[new_op.order.Key] = new_op;
// reevaluate min piq if this order's piq is less
if (new_op.p.HasValue) {
// if this lovm only represents 1 order and the piq is now different, regardless of what it is that's our piq
if (_orders.Count == 1 && new_op.p.Value != min_piq) {
min_piq = new_op.p.Value;
} else if (new_op.p.Value < min_piq) {
min_piq = new_op.p.Value;
}
}
}
public void remove_order(uint order_key) {
_orders.Remove(order_key);
// on a remove, potentially the order w/ the lowest piq has been removed, so
// if the min() is now > than the current min_piq, update the min_piq
var prospective_piq = _orders.Select(op => op.Value.p).Where(piq => piq.HasValue).Min();
if (prospective_piq > min_piq) {
min_piq = prospective_piq;
}
}
//public void update(OrderRecordImmutable neworder) {
// Log.assert(neworder.Key == id, $"OrderVM.update was called with an order with a different Key({neworder.Key}) from original({id})");
// using (detect_property_changes()) {
// _init(neworder);
// }
//}
public uint security_key => _owner.security.Id;
public double price { get => double.IsNaN(_phantom_price) ? _real_price : _phantom_price; }
public uint? min_piq {
@@ -261,9 +228,13 @@ namespace mbts.spark {
public CustomOrderDefinition.CustomOneofCase algo_type { get => _algo_type.Value; private set => _algo_type.Value = value; }
public string algo_studio_abbreviation { get => _algo_studio_abbreviation.Value; private set => _algo_studio_abbreviation.Value = value; }
public IReadOnlyList<uint> order_keys => _orders.Keys.ToReadOnlyList();
public IEnumerable<OrderRecordImmutable> root_orders => _root_nodes();
public void sync_orders(ImmutableDictionary<uint, OrderSecurity> new_orders) {
_orders = new_orders;
min_piq = new_orders.Count == 0 ? null : new_orders.Values.Select(o => o.piq).Min();
}
void _cancel_order() {
foreach (var order in _root_nodes()) {
model.order_gateway.cancel_order(order).ignore();
@@ -296,14 +267,16 @@ namespace mbts.spark {
// I checked with Eric and he guesses that what is happening is that the Popup is throwing away the grid, but the binding doesn't remove the columns from that grid
//
// I think returning a new OrderbookVM is okay at the moment since this Popup window will not be saving any state at this time
var price_filter = new filters.DoubleMinMaxState();
price_filter.minimum.value = real_price;
price_filter.maximum.value = real_price;
var ticks = _owner.spec.full_ticks_from_zero(real_price);
if (!ticks.HasValue) {
// this is an unexpected code path, but not necessarily a reason to crash, so log a warning and return null
return null;
}
var min_price = _owner.spec.tickup(0, ticks.Value);
var max_price = _owner.spec.tickup(min_price, 1);
var state = new bts.shell.orderbook.OrderBookStartupData(
new bts.shell.orderbook.FilterStates {
security_criteria = new SecurityCriteria(_owner.security),
price = price_filter,
security_criteria = SecurityCriteria.from_securities(_owner.security),
statuses = new[] { OrderStatus.Working },
order_types = AlgoID.exchange_algo_keys
},
@@ -311,7 +284,8 @@ namespace mbts.spark {
use_defaults: false
);
var settings_man = new WindowSettingsManager<OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
var obvm = new OrderBookVM(_owner.bus, null, _owner.model.order_gateway, settings_man, true);
var obvm = new OrderBookVM(_owner.bus, null, _owner.model.order_gateway, settings_man, true,
global_filter: o => o.order.order.ui_price() >= min_price - MathUtils.epsilon && o.order.order.ui_price() <= max_price - MathUtils.epsilon);
_owner.settings.Subscribe(s => settings_man.instance_settings = settings_man.instance_settings.with(s.mini_ob.value));
obvm.columns.Insert(0, new OrderBookVM.AttachDetachColumn(typeof(MiniOrderBookView)));
obvm.columns.Insert(0, new OrderBookVM.CommitColumn());
@@ -0,0 +1,116 @@
using bts.proto;
using mbts.ui;
using mbts.ui.shell;
using System.Windows;
using System.Windows.Media;
using mbts.ui.utility.text;
using FontFamily = System.Windows.Media.FontFamily;
namespace mbts.spark {
class LadderPanelPricePointVM : ViewModelBase, IValueFactoryVM<LadderPanelPricePointSettings.ItemSettings> {
Color _background_color = Colors.Transparent, _font_color = Colors.Black;
FontFamily _font_family = TextUtils.StandardFontFamily;
double _font_size = FontSizes.Small;
FontWeight _font_weight = FontWeights.Bold;
bool _show = true, _show_label = true;
public Color background_color { get => _background_color; set => Set(ref _background_color, value); }
public Color font_color { get => _font_color; set => Set(ref _font_color, value); }
public FontFamily font_family { get => _font_family; set => Set(ref _font_family, value); }
public double font_size { get => _font_size; set => Set(ref _font_size, value); }
public FontWeight font_weight { get => _font_weight; set => Set(ref _font_weight, value); }
public bool show {
get => _show;
set {
if (Set(ref _show, value))
RaisePropertyChanged(nameof(actual_show_label));
}
}
public bool show_label {
get => _show_label;
set {
if (Set(ref _show_label, value))
RaisePropertyChanged(nameof(actual_show_label));
}
}
public bool actual_show_label => _show && _show_label;
public LadderPanelPricePointSettings.ItemSettings create() =>
new LadderPanelPricePointSettings.ItemSettings {
font = new FontParamsProto {
font_color = font_color.rgb_color(),
font_size = font_size,
typeface = new TypefaceProto(font_family.name_or_default(), font_weight.ToOpenTypeWeight())
},
background_color = background_color.rgb_color(),
show_label = show_label
};
public void load_from(LadderPanelPricePointSettings.ItemSettings value) {
if (value is null)
return;
load_from(value.font);
show_label = value.show_label;
background_color = value.background_color.argb_color();
}
protected void load_from(FontParamsProto font) {
if (font is null) {
font_weight = FontWeights.Bold;
font_size = FontSizes.Small;
show_label = true;
} else {
font_family = string.IsNullOrEmpty(font.font_family) ?
TextUtils.StandardFontFamily :
new FontFamily(font.font_family);
font_weight = FontWeight.FromOpenTypeWeight(font.font_weight);
font_size = font.font_size;
font_color = font.font_color.argb_color();
}
}
}
class LadderPanelRelativePricePointVM : LadderPanelPricePointVM, IValueFactoryVM<LadderPanelPricePointSettings.DaySettings> {
Color
_negative_font_color = Colors.Red,
_negative_background_color = Colors.Transparent,
_positive_font_color = Colors.Green,
_positive_background_color = Colors.Transparent;
// negative
public Color negative_font_color { get => _negative_font_color; set => Set(ref _negative_font_color, value); }
public Color negative_background_color { get => _negative_background_color; set => Set(ref _negative_background_color, value); }
// positive
public Color positive_font_color { get => _positive_font_color; set => Set(ref _positive_font_color, value); }
public Color positive_background_color { get => _positive_background_color; set => Set(ref _positive_background_color, value); }
public void load_from(LadderPanelPricePointSettings.DaySettings value) {
if (value is null)
return;
load_from(value.font);
show_label = value.show_label;
background_color = value.background_color.argb_color();
// negative
negative_font_color = value.negative_font_color.argb_color();
negative_background_color = value.negative_background_color.argb_color();
// positive
positive_font_color = value.positive_font_color.argb_color();
positive_background_color = value.positive_background_color.argb_color();
}
public new LadderPanelPricePointSettings.DaySettings create() {
var base_settings = base.create();
return new LadderPanelPricePointSettings.DaySettings {
font = base_settings.font,
show_label = base_settings.show_label,
background_color = base_settings.background_color,
// negative
negative_font_color = negative_font_color.rgb_color(),
negative_background_color = negative_background_color.rgb_color(),
// positive
positive_font_color = positive_font_color.rgb_color(),
positive_background_color = positive_background_color.rgb_color()
};
}
}
}
@@ -16,6 +16,7 @@ using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
@@ -31,6 +32,10 @@ using TextUtils = mbts.ui.utility.text.TextUtils;
namespace mbts.spark {
class LadderSettingsVM : DisposableViewModelBase, IWindowSettingsVM<LadderSettings>, IValueFactory<LadderSettings>, IObservable<LadderSettings>, ITemplateSelectorKey {
public static Type PanelPricePointType => typeof(PanelPricePointVM);
public static Type PanelRelativePricePointType => typeof(PanelRelativePricePointVM);
public static Type PanelVisibilityType => typeof(OrderEntryPanelItemVisibilityVM);
readonly Property<LadderSettings.BadgeStyle> _badge_style;
readonly Property<uint> _alternate_order_size, _max_clip;
readonly DelegateProperty<double> _row_height;
@@ -40,6 +45,10 @@ namespace mbts.spark {
readonly ObservableKeyedCollection<AccountTuple?, uint> _accounts = new ObservableKeyedCollection<AccountTuple?, uint>(AccountTuple.nullable_key_fn);
readonly ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey> _algo_definitions = new ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey>(d => d.key);
readonly OrdersModel _model;
readonly SecurityImmutable _security;
IReadOnlyDictionary<uint, SecurityMD> _security_map =
DictionaryHelper.empty<uint, SecurityMD>();
uint? _desired_account;
AlgoID? _desired_algo;
TimeInForce _default_time_in_force;
@@ -48,6 +57,7 @@ namespace mbts.spark {
double _order_entry_panel_width, _zoom_factor;
bool _auto_clear, _show_warn_on_cross, _enforce_max_clip, _enforce_max_settlement_cross_ticks;
int _max_settlement_cross_ticks;
uint _ticks_per_row;
LadderSettings _current;
LadderColumnSettingsVM
_orders = new OrdersColumnSettingsVM(),
@@ -61,17 +71,11 @@ namespace mbts.spark {
_bid_mkt_line = new LadderMarketBreakLineVM("Bid"),
_ask_mkt_line = new LadderMarketBreakLineVM("Ask"),
_mid_mkt_line = new LadderMarketBreakLineVM("Mid"),
_session_low_mkt_line = new LadderMarketBreakLineVM("Session Low", 2, 1),
_session_high_mkt_line = new LadderMarketBreakLineVM("Session High", 2, 1, false),
_session_range_fill = new LadderMarketBreakLineVM("Session Range", 0, 0, false);
_session_low_mkt_line = new LadderMarketBreakLineVM("Session Low", 2, 1, can_edit_refresh_mode: false),
_session_high_mkt_line = new LadderMarketBreakLineVM("Session High", 2, 1, false, can_edit_refresh_mode: false),
_session_range_fill = new LadderMarketBreakLineVM("Session Range", 0, 0, false, can_edit_refresh_mode: false);
OrderEntryPanelItemVisibilityVM
_zoom_slider_visbility = new OrderEntryPanelItemVisibilityVM("Zoom Slider"),
_day_visibility = new OrderEntryPanelItemVisibilityVM("Day Change"),
_high_visibility = new OrderEntryPanelItemVisibilityVM("High"),
_low_visibility = new OrderEntryPanelItemVisibilityVM("Low"),
_open_visibility = new OrderEntryPanelItemVisibilityVM("Open"),
_settle_visibility = new OrderEntryPanelItemVisibilityVM("Settle"),
_volume_visibility = new OrderEntryPanelItemVisibilityVM("Volume"),
_account_visibility = new OrderEntryPanelItemVisibilityVM("Account"),
_tif_visibility = new OrderEntryPanelItemVisibilityVM("Time In Force"),
_order_type_visibility = new OrderEntryPanelItemVisibilityVM("Order Type"),
@@ -85,10 +89,18 @@ namespace mbts.spark {
_cancel_sells_visibility = new OrderEntryPanelItemVisibilityVM("Cancel Sells"),
_cancel_all_visibility = new OrderEntryPanelItemVisibilityVM("Cancel All"),
_status_message_visibility = new OrderEntryPanelItemVisibilityVM("Status Message");
PanelRelativePricePointVM
_day_change = new PanelRelativePricePointVM("Day Change");
PanelPricePointVM
_session_high = new PanelPricePointVM("High"),
_session_low = new PanelPricePointVM("Low"),
_open = new PanelPricePointVM("Open"),
_settlement = new PanelPricePointVM("Settle"),
_volume_panel_item = new PanelPricePointVM("Volume");
public LadderSettingsVM(OrdersModel model, SecurityImmutable security) {
_model = model;
is_synthetic = security.ContractCase == Security.ContractOneofCase.Synthetic;
_security = security;
favorite_algos_vm = new FavoriteAlgosVM(_algo_definitions);
size_presets = new ObservableEditableCollection<IMutableValueWrapperVM<uint>>(_create_size_preset_vm);
size_presets.CollectionChanged += (s, e) => RaisePropertyChanged(nameof(size_presets_text));
@@ -110,6 +122,9 @@ namespace mbts.spark {
accounts_view = new ListCollectionView<AccountTuple?>(_accounts) {
CustomSort = Comparer<AccountTuple>.Default.to_nullable().to_non_generic()
};
// cmds
tick_up_ticks_per_row_cmd = new DelegateCommand(increment_ticks_per_row);
tick_down_ticks_per_row_cmd = new DelegateCommand(decrement_ticks_per_row);
// update
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
mini_ob.result_changed += _update.execute;
@@ -131,17 +146,22 @@ namespace mbts.spark {
_accounts.SyncWithDictionary(MappedDictionary.create(acc_map, account_factory));
_try_select_desired_account();
}),
ObservableAutoWrapper.connect_item_hooks(favorite_algos_vm.items, connect_favorite_algo),
ObservableAutoWrapper.connect_item_hooks(favorite_algos_vm.items, observe_prop_changes),
_model.algo_model.definitions.OnDispatcher().Subscribe(update => {
AlgoCollectionHelper.update_persisted_definitions(_algo_definitions, update, algo_factory);
_try_select_desired_algo();
}),
ObservableAutoWrapper.connect_item_hooks(fair_value_line_settings_vm.items, item => {
_update.execute();
var prop_changes_sub = observe_prop_changes(item);
return Disposable.Create(_update.execute, prop_changes_sub.Dispose);
})
) { dispose_resources };
// local methods
void on_font_size_changed(double oldval, double newval) => _row_height.refresh();
void on_prop_changed(object sender, PropertyChangedEventArgs e) => _update.execute();
void on_favorite_algo_items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => _update.execute();
IDisposable connect_favorite_algo(FavoriteAlgoItem item) {
IDisposable observe_prop_changes(INotifyPropertyChanged item) {
item.PropertyChanged += on_prop_changed;
return Disposable.Create(() => item.PropertyChanged -= on_prop_changed);
}
@@ -155,6 +175,26 @@ namespace mbts.spark {
}
AccountTuple? account_factory(uint key, string name) => new AccountTuple(key, name);
AlgoDefinitionVM algo_factory(AlgoData data) => AlgoDefinitionVM.create(_model, data);
void increment_ticks_per_row() {
var tpr = ticks_per_row;
for (uint i = 1; i < 100; i++) {
var new_tpr = TickUtils.fit_ticks_per_row_to_dollars(security.ContractSpec, ticks_per_row + i);
if (new_tpr != tpr) {
ticks_per_row = new_tpr;
break;
}
}
}
void decrement_ticks_per_row() {
var tpr = ticks_per_row;
for (uint i = 1; i < tpr; i++) {
var new_tpr = TickUtils.fit_ticks_per_row_to_dollars(security.ContractSpec, tpr - i);
if (new_tpr != tpr) {
ticks_per_row = new_tpr;
break;
}
}
}
}
public event Action result_changed;
@@ -177,6 +217,16 @@ namespace mbts.spark {
get => _alternate_order_size.Value;
set => _alternate_order_size.Value = value;
}
public uint ticks_per_row {
get => _ticks_per_row;
set => Set(ref _ticks_per_row, value, dependent_props: nameof(rounded_ticks_per_row));
}
public ICommand tick_up_ticks_per_row_cmd { get; }
public ICommand tick_down_ticks_per_row_cmd { get; }
// since settings profiles may be shared among ladders
// of different securities/tick sizes, the ticks per row setting
// may need to be rounded to a number that is a factor of $1.00
public uint? rounded_ticks_per_row => TickUtils.fit_ticks_per_row_to_dollars(_security.ContractSpec, ticks_per_row);
public MarketSizeDisplayVM size_display { get; } = new MarketSizeDisplayVM();
public SimpleGridSettingsVM mini_ob { get; } = new SimpleGridSettingsVM(OrderBookGridSettingsManager.hard_column_flags);
@@ -209,7 +259,15 @@ namespace mbts.spark {
public FavoriteAlgosVM favorite_algos_vm { get; }
public double row_height => _row_height.Value;
public bool is_synthetic { get; }
public bool is_synthetic => _security.ContractCase == Security.ContractOneofCase.Synthetic;
public IReadOnlyDictionary<uint, SecurityMD> security_map {
get => _security_map;
set {
Set(ref _security_map, value);
fair_value_line_settings_vm.security_map = value;
}
}
public IEnumerable<LadderColumnSettingsVM> column_items {
get {
@@ -248,15 +306,18 @@ namespace mbts.spark {
}
}
public IEnumerable<OrderEntryPanelItemVisibilityVM> order_entry_panel_items {
public FairValueLineSettingsVM fair_value_line_settings_vm { get; } = new
FairValueLineSettingsVM();
public IEnumerable order_entry_panel_items {
get {
yield return _zoom_slider_visbility;
yield return _day_visibility;
yield return _high_visibility;
yield return _low_visibility;
yield return _open_visibility;
yield return _settle_visibility;
yield return _volume_visibility;
yield return _day_change;
yield return _session_high;
yield return _session_low;
yield return _open;
yield return _settlement;
yield return _volume_panel_item;
yield return _account_visibility;
yield return _tif_visibility;
yield return _order_type_visibility;
@@ -295,12 +356,12 @@ namespace mbts.spark {
yield return _session_range_fill;
// left panel
yield return _zoom_slider_visbility;
yield return _day_visibility;
yield return _high_visibility;
yield return _low_visibility;
yield return _open_visibility;
yield return _settle_visibility;
yield return _volume_visibility;
yield return _day_change;
yield return _session_high;
yield return _session_low;
yield return _open;
yield return _settlement;
yield return _volume_panel_item;
yield return _account_visibility;
yield return _tif_visibility;
yield return _order_type_visibility;
@@ -326,6 +387,7 @@ namespace mbts.spark {
_current = settings;
badge_style = settings.badge_style;
alternate_order_size = settings.alternate_order_size;
ticks_per_row = settings.ticks_per_row;
favorite_algos_vm.load_from(settings.favorite_algos);
size_display.load_from(settings.size_display);
size_presets.SetItems(settings.calculator_buttons.empty_if_null().Select(s => _create_size_preset_vm((uint)s)));
@@ -345,6 +407,14 @@ namespace mbts.spark {
_session_low_mkt_line.assign_from(_current.session_low_line);
_session_high_mkt_line.assign_from(_current.session_high_line);
_session_range_fill.assign_from(_current.session_range_fill);
// fair value lines
fair_value_line_settings_vm.items.SetItems(
_current.fair_value_lines.Select(fvl => {
var fvl_vm = new FairValueLineItemSettingsVM();
fvl_vm.assign_from(fvl);
fvl_vm.selected_security_key = fvl.security_key;
return fvl_vm;
}));
// left-panel (defaults)
default_time_in_force = settings.default_time_in_force;
// - account
@@ -359,12 +429,18 @@ namespace mbts.spark {
zoom_factor = settings.zoom_factor;
// left-panel (show/hide)
_zoom_slider_visbility.show = settings.zoom_slider_visible;
_day_visibility.show = settings.day_visible;
_high_visibility.show = settings.high_visible;
_low_visibility.show = settings.low_visible;
_open_visibility.show = settings.open_visible;
_settle_visibility.show = settings.settle_visible;
_volume_visibility.show = settings.volume_visible;
_day_change.show = settings.day_visible;
_day_change.load_from(settings.panel_price_point_settings.day_change);
_session_high.show = settings.high_visible;
_session_high.load_from(settings.panel_price_point_settings.session_high);
_session_low.show = settings.low_visible;
_session_low.load_from(settings.panel_price_point_settings.session_low);
_open.show = settings.open_visible;
_open.load_from(settings.panel_price_point_settings.open);
_settlement.show = settings.settle_visible;
_settlement.load_from(settings.panel_price_point_settings.settlement);
_volume_panel_item.show = settings.volume_visible;
_volume_panel_item.load_from(settings.panel_price_point_settings.volume);
_account_visibility.show = settings.account_visible;
_tif_visibility.show = settings.tif_visible;
_order_type_visibility.show = settings.order_type_visible;
@@ -407,6 +483,7 @@ namespace mbts.spark {
}
clone.badge_style = badge_style;
clone.alternate_order_size = alternate_order_size;
clone.ticks_per_row = ticks_per_row;
clone.favorite_algos = favorite_algos_vm.create();
clone.size_display = size_display.create();
clone.calculator_buttons = size_presets.Select(vm => (int)vm.value).ToArray();
@@ -426,6 +503,20 @@ namespace mbts.spark {
clone.session_low_line = _session_low_mkt_line.get_settings();
clone.session_high_line = _session_high_mkt_line.get_settings();
clone.session_range_fill = _session_range_fill.get_settings();
// fair value lines
clone.fair_value_lines = fair_value_line_settings_vm.items
.Select(fvl => fvl.create())
.Where(fvl => fvl.security_key != 0)
.ToList();
// left-panel price items
clone.panel_price_point_settings = new LadderPanelPricePointSettings {
day_change = _day_change.create(),
open = _open.create(),
session_low = _session_low.create(),
session_high = _session_high.create(),
volume = _volume_panel_item.create(),
settlement = _settlement.create(),
};
// left-panel (defaults)
clone.default_time_in_force = default_time_in_force;
// - account
@@ -440,12 +531,12 @@ namespace mbts.spark {
clone.order_entry_panel_initial_width = _order_entry_panel_width;
clone.zoom_factor = zoom_factor;
// left-panel (components)
clone.day_visible = _day_visibility.show;
clone.high_visible = _high_visibility.show;
clone.low_visible = _low_visibility.show;
clone.open_visible = _open_visibility.show;
clone.settle_visible = _settle_visibility.show;
clone.volume_visible = _volume_visibility.show;
clone.day_visible = _day_change.show;
clone.high_visible = _session_high.show;
clone.low_visible = _session_low.show;
clone.open_visible = _open.show;
clone.settle_visible = _settlement.show;
clone.volume_visible = _volume_panel_item.show;
clone.account_visible = _account_visibility.show;
clone.tif_visible = _tif_visibility.show;
clone.order_type_visible = _order_type_visibility.show;
@@ -569,8 +660,8 @@ namespace mbts.spark {
font_size = settings.font_size;
width = settings.width;
show = settings.show;
foreground = settings.foreground.rgb_color();
background = settings.background.rgb_color();
foreground = settings.foreground.argb_color();
background = settings.background.argb_color();
}
public LadderColumnSettings get_settings() {
@@ -580,8 +671,8 @@ namespace mbts.spark {
font_weight = font_weight.ToOpenTypeWeight(),
width = width,
show = show,
foreground = new BTSColor(foreground).ToUint(),
background = new BTSColor(background).ToUint()
foreground = foreground.rgb_color(),
background = background.rgb_color()
};
}
}
@@ -601,12 +692,14 @@ namespace mbts.spark {
public class LadderMarketBreakLineVM : ViewModelBase {
readonly Property<Color> _color;
readonly Property<bool> _show;
LadderMarketBreakLine.RefreshMode _refresh_mode;
public LadderMarketBreakLineVM(string name, int column_index = 0, int column_span = 5, bool show_session_range = true) {
public LadderMarketBreakLineVM(string name, int column_index = 0, int column_span = 5, bool show_session_range = true, bool can_edit_refresh_mode = true) {
this.name = name;
this.column_index = column_index;
this.column_span = column_span;
this.show_session_range = show_session_range;
this.can_edit_refresh_mode = can_edit_refresh_mode;
palettes = new ObservableCollection<ColorPaletteVM>(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p)));
_color = backing(nameof(color), Colors.Black);
_show = backing(nameof(show), false);
@@ -618,21 +711,25 @@ namespace mbts.spark {
public Color color { get => _color.Value; set => _color.Value = value; }
public bool show { get => _show.Value; set => _show.Value = value; }
public bool show_session_range { get; }
public bool can_edit_refresh_mode { get; }
public LadderMarketBreakLine.RefreshMode refresh_mode { get => _refresh_mode; set => Set(ref _refresh_mode, value); }
public ObservableCollection<ColorPaletteVM> palettes { get; }
public void assign_from(LadderMarketBreakLine settings) {
color = settings.color.rgb_color();
show = settings.show;
refresh_mode = settings.refresh_mode;
}
public LadderMarketBreakLine get_settings() =>
new LadderMarketBreakLine {
color = new BTSColor(color).ToUint(),
show = show
show = show,
refresh_mode = refresh_mode
};
}
public class OrderEntryPanelItemVisibilityVM : ViewModelBase {
public class OrderEntryPanelItemVisibilityVM : ViewModelBase, ITemplateSelectorKey {
readonly Property<bool> _show;
public OrderEntryPanelItemVisibilityVM(string name) {
@@ -643,6 +740,63 @@ namespace mbts.spark {
public string name { get; }
public bool show { get => _show.Value; set => _show.Value = value; }
string ITemplateSelectorKey.key => LadderSettingsPanelTemplateKeys.VisibilityItem;
}
public class PanelPricePointVM : LadderPanelPricePointVM, ITemplateSelectorKey {
ObservableCollection<ColorPaletteVM>
_font_color_palettes,
_background_color_palettes;
public PanelPricePointVM(string name) =>
this.name = name;
public string name { get; }
public ObservableCollection<ColorPaletteVM> font_color_palettes => _font_color_palettes ??
(_font_color_palettes = ColorPalette.all_presets().to_viewmodels());
public ObservableCollection<ColorPaletteVM> background_color_palettes => _background_color_palettes ??
(_background_color_palettes = ColorPalette.all_presets().to_viewmodels());
string ITemplateSelectorKey.key => LadderSettingsPanelTemplateKeys.PricePoint;
}
public class PanelRelativePricePointVM : LadderPanelRelativePricePointVM, ITemplateSelectorKey {
ObservableCollection<ColorPaletteVM>
_negative_font_color_palettes,
_negative_background_color_palettes,
_neutral_font_color_palettes,
_neutral_background_color_palettes,
_positive_font_color_palettes,
_positive_background_color_palettes;
public PanelRelativePricePointVM(string name) =>
this.name = name;
public string name { get; }
//negative
public ObservableCollection<ColorPaletteVM> negative_font_color_palettes => _negative_font_color_palettes ??
(_negative_font_color_palettes = ColorPalette.all_presets().to_viewmodels());
public ObservableCollection<ColorPaletteVM> negative_background_color_palettes => _negative_background_color_palettes ??
(_negative_background_color_palettes = ColorPalette.all_presets().to_viewmodels());
// neutral
public ObservableCollection<ColorPaletteVM> neutral_font_color_palettes => _neutral_font_color_palettes ??
(_neutral_font_color_palettes = ColorPalette.all_presets().to_viewmodels());
public ObservableCollection<ColorPaletteVM> neutral_background_color_palettes => _neutral_background_color_palettes ??
(_neutral_background_color_palettes = ColorPalette.all_presets().to_viewmodels());
// positive
public ObservableCollection<ColorPaletteVM> positive_font_color_palettes => _positive_font_color_palettes ??
(_positive_font_color_palettes = ColorPalette.all_presets().to_viewmodels());
public ObservableCollection<ColorPaletteVM> positive_background_color_palettes => _positive_background_color_palettes ??
(_positive_background_color_palettes = ColorPalette.all_presets().to_viewmodels());
string ITemplateSelectorKey.key => LadderSettingsPanelTemplateKeys.RelativePricePoint;
}
public class VolumeColumnVM : ViewModelBase {
@@ -1,14 +1,18 @@
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.proto;
using bts.shell;
using bts.spark;
using bts.utils;
using mbts.co2;
using mbts.collections;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.user.settings;
using mbts.utils;
using System;
using System.Collections.Generic;
@@ -27,15 +31,25 @@ namespace mbts.spark {
readonly IBus _bus;
readonly LadderModel _model;
readonly Property<double> _high, _low, _change, _open, _settlement, _left_panel_width, _zoom_factor;
//readonly Property<double> _window_width, _window_height;
readonly Property<double> _session_high, _session_low, _day_change, _open, _settlement, _left_panel_width, _zoom_factor;
readonly Property<uint> _orders_column_width, _bid_column_width, _price_column_width, _ask_column_width, _last_column_width, _volume_column_width, _piq_column_width;
readonly Property<ulong> _volume;
readonly Property<bool> _auto_clear;
readonly PropertyAutoWrapper<LadderSettings, bool> _show_day, _show_high, _show_low, _show_open, _show_settle, _show_volume, _show_account,
_show_tif, _show_order_type, _show_size_preset, _show_auto_clear, _show_clear, _show_cancel_buys,
_show_cancel_sells, _show_cancel_all, _show_zoom, _show_status_message;
readonly PropertyAutoWrapper<LadderSettings, bool>
_show_account,
_show_tif,
_show_order_type,
_show_size_preset,
_show_auto_clear,
_show_clear,
_show_cancel_buys,
_show_cancel_sells,
_show_cancel_all,
_show_zoom,
_show_status_message;
readonly PropertyAutoWrapper<LadderSettings, Brush> _bid_background, _bid_foreground, _ask_background, _ask_foreground;
readonly ObservableCollection<FairValueLine> _fair_value_line_inputs =
new ObservableCollection<FairValueLine>();
double
_position,
_day_position,
@@ -49,8 +63,11 @@ namespace mbts.spark {
ulong _last_trade_time = 0;
IEqualityComparer<double> _equality;
Dictionary<double, LadderOrderVM> _slot_price_vs_orders;
ImmutableSortedDictionary<double, LadderOrderVM> _order_price_index;
IReadOnlyDictionary<uint, OrderSecurity> _cache = DictionaryHelper.empty<uint, OrderSecurity>();
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
ImmutableDictionary<AlgoKey, LadderFavoriteAlgoItem> _favorite_algo_map = ImmutableDictionary<AlgoKey, LadderFavoriteAlgoItem>.Empty;
ContractSpecificationImmutable? _spec;
public static LadderVM create(IBus bus, LadderModel model, LadderSettings init, IObservable<LadderSettings> settings) =>
new LadderVM(bus, model, init, settings, new OrderPropertiesEditorVM(model.order_gateway, Observable.Return(model.security_md), settings.Select(s => new InlineOrderProperties(model.order_gateway, s))));
@@ -65,15 +82,16 @@ namespace mbts.spark {
_bus = bus;
_model = model;
current_settings = init;
_order_price_index = ImmutableSortedDictionary.Create<double, LadderOrderVM>(spec.price_comparer());
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);
_low = backing(nameof(low), double.NaN);
_change = backing(nameof(change), double.NaN);
_session_high = backing(nameof(session_high), double.NaN);
_session_low = backing(nameof(session_low), double.NaN);
_day_change = backing(nameof(day_change), double.NaN);
_open = backing(nameof(open), double.NaN);
_settlement = backing(nameof(settlement), double.NaN);
// init window-specific settings: these properties should not observe settings updates; they are independent from the settings dialog, so it doesn't make sense that the settings dialog should cause them to change.
@@ -104,12 +122,6 @@ namespace mbts.spark {
(_ask_background = brush_prop(nameof(ask_background), s => s.ask_column.background)).set,
(_ask_foreground = brush_prop(nameof(ask_foreground), s => s.ask_column.foreground)).set,
// bool visibility props
(_show_day = bool_prop(nameof(show_day), s => s.day_visible)).set,
(_show_high = bool_prop(nameof(show_high), s => s.high_visible)).set,
(_show_low = bool_prop(nameof(show_low), s => s.low_visible)).set,
(_show_open = bool_prop(nameof(show_open), s => s.open_visible)).set,
(_show_settle = bool_prop(nameof(show_settle), s => s.settle_visible)).set,
(_show_volume = bool_prop(nameof(show_volume), s => s.volume_visible)).set,
(_show_account = bool_prop(nameof(show_account), s => s.account_visible)).set,
(_show_tif = bool_prop(nameof(show_tif), s => s.tif_visible)).set,
(_show_order_type = bool_prop(nameof(show_order_type), s => s.order_type_visible)).set,
@@ -122,6 +134,8 @@ namespace mbts.spark {
(_show_zoom = bool_prop(nameof(show_zoom), s => s.zoom_slider_visible)).set,
(_show_status_message = bool_prop(nameof(show_status_message), s => s.status_message_visible)).set,
s => {
// invalidate spec in case 'ticks per row' changed
_spec = null;
if (
Set(ref _show_account_position, s.account_position_visible, nameof(show_account_position)) |
Set(ref _show_day_position, s.day_position_visible, nameof(show_day_position)) |
@@ -139,8 +153,33 @@ namespace mbts.spark {
_piq_column_width.set(s.piq_column.width);
_volume_column_width.set(s.volume_column.general.width);
// left panel/zoom
day_change_style.load_from(s.panel_price_point_settings.day_change);
day_change_style.show = s.day_visible;
// open
open_style.load_from(s.panel_price_point_settings.open);
open_style.show = s.open_visible;
// session low
session_low_style.load_from(s.panel_price_point_settings.session_low);
session_low_style.show = s.low_visible;
// session high
session_high_style.load_from(s.panel_price_point_settings.session_high);
session_high_style.show = s.high_visible;
// settlement
settlement_style.load_from(s.panel_price_point_settings.settlement);
settlement_style.show = s.settle_visible;
// volume
volume_style.load_from(s.panel_price_point_settings.volume);
volume_style.show = s.volume_visible;
// left panel gen display
_left_panel_width.set(s.order_entry_panel_initial_width);
_zoom_factor.set(s.zoom_factor);
RaisePropertyChanged(nameof(day_change_font_color), nameof(day_change_background_color));
// fair value lines
var new_fvls = s.fair_value_lines
.DistinctOn(fvl => fvl.security_key)
.OrderBy(fvl => fvl.security_key)
.ToList();
_fair_value_line_inputs.sync_with(new_fvls);
}
};
PropertyAutoWrapper<LadderSettings, bool> bool_prop(string name, Func<LadderSettings, bool> get_bool) =>
@@ -159,71 +198,46 @@ namespace mbts.spark {
settings_command = new DelegateCommand(() => request_settings_dialog?.Invoke());
orders = new ObservableCollection<LadderOrderVM>();
_day_change.Changed += (o, n) => RaisePropertyChanged(nameof(day_change_font_color), nameof(day_change_background_color));
var orders_source = settings.Select(s => s.ticks_per_row).DistinctUntilChanged()
.SelectManyInSeries(tpr => {
var spec = TickUtils.multiply_tick_size(security.ContractSpec, tpr);
var price_comp = Comparer<double>.Create((a, b) =>
CompareUtils.nullable_compare(spec.full_ticks_from_zero(a), spec.full_ticks_from_zero(b)));
return _order_price_source(model.orders, price_comp, p => TickUtils.normalize_price(spec, p));
});
_dispose = Disposable.Create(
settings.OnDispatcher().Subscribe(settings_observer),
get_order_properties_editor_hooks(),
bus.subscribe(SparkTags.LogActivity).OnDispatcher().Subscribe(status_update),
// we need to hook this up to model.orders which is filtered for just the security
model.orders.OnDispatcher().Subscribe(working => {
bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(on_security_map),
ObservableAutoWrapper.connect(
_fair_value_line_inputs,
fair_value_lines,
fvl => new FairValueLineVM(fvl, _find_sec_md(fvl.security_key))),
orders_source.OnDispatcher().Subscribe(tup => {
// Here we manage N orders to 1 price slot in the ladder
Dictionary<double, HashSet<uint>> orders_used_in_this_update = new Dictionary<double, HashSet<uint>>(_equality);
_cache = working.state;
foreach (var os in _cache.Values) {
var wo = os.order;
var slot_price_target = wo.ui_price();
var order_type = wo.order_type();
var filled = wo.filled();
if (_slot_price_vs_orders.TryGetValue(slot_price_target, out LadderOrderVM slot_vm)) {
slot_vm.add_order((wo, os.piq, os.algo, os.meta));
} else {
slot_vm = new LadderOrderVM((wo, os.piq, os.algo, os.meta), slot_price_target, this, _bus, settings.Select(s => s.badge_style).OnDispatcher());
_slot_price_vs_orders[slot_price_target] = slot_vm;
}
// keep track of slot prices vs orders; we use this below to remove orders from the LadderOrderVMs
// that are not found in this callback run
if (!orders_used_in_this_update.TryGetValue(slot_price_target, out HashSet<uint> orders_at_level)) {
orders_at_level = new HashSet<uint>();
}
orders_at_level.Add(wo.Key);
orders_used_in_this_update[slot_price_target] = orders_at_level;
_cache = tup.map;
/// remove empty price slots (the vm will be disposed automatically when it is removed from <see cref="orders"/>)
var removed_prices = _slot_price_vs_orders.Keys.Except(tup.price_index.Keys).ToList();
foreach (var p in removed_prices)
_slot_price_vs_orders.Remove(p);
// add or update current price slots
foreach (var kv in tup.price_index) {
_slot_price_vs_orders.AddOrUpdate(kv.Key,
order_vm => order_vm.sync_orders(kv.Value),
() => new LadderOrderVM(kv.Value, kv.Key, this, _bus, settings.Select(s => s.badge_style).OnDispatcher()));
}
// remove any orders from the lovm that aren't in here
foreach(var kvp in orders_used_in_this_update) {
var slot_vm = _slot_price_vs_orders[kvp.Key];
foreach(var existing_order_key in slot_vm.order_keys) {
if (!kvp.Value.Contains(existing_order_key)) {
slot_vm.remove_order(existing_order_key);
}
}
}
// update dirty brushes and text
foreach (var lovm in _slot_price_vs_orders.Values) {
lovm.update();
}
// complete removal of slots that were not updated
// orders_used_in_this_update.Keys are the price levels in the update
foreach (var slot_price in _slot_price_vs_orders.Keys.ToList()) {
if (!orders_used_in_this_update.Keys.Contains(slot_price)) {
_slot_price_vs_orders.TryGetValue(slot_price, out LadderOrderVM removal_candidate);
if (removal_candidate != null) {
removal_candidate.Dispose();
}
_slot_price_vs_orders.Remove(slot_price);
}
}
// sync the collection
orders.sync_with(_slot_price_vs_orders.Values);
// update badge colors, text, etc.
foreach (var lovm in orders)
lovm.update();
}),
WorkLoop.Start($"Ladder '{security}'", _tick),
ObservableAutoWrapper.dispose_on_removed(orders, true),
Disposable.Create(() => _on_disposed_impl?.Invoke(true))
);
IDisposable get_order_properties_editor_hooks() {
@@ -256,6 +270,15 @@ namespace mbts.spark {
RaisePropertyChanged(nameof(position_display_text));
}));
}
void on_security_map(IReadOnlyDictionary<uint, SecurityMD> security_map) {
_security_map = security_map;
foreach (var fvl_vm in fair_value_lines) {
if (fvl_vm.sec_md.HasValue) {
continue;
}
fvl_vm.sec_md = _find_sec_md(fvl_vm.sec_id);
}
}
}
public IBus bus => _bus;
@@ -299,17 +322,34 @@ namespace mbts.spark {
remove => _on_disposed_impl -= value;
}
public SecurityImmutable security => _model.security;
public ContractSpecificationImmutable spec => _spec ?? (
_spec = TickUtils.multiply_tick_size(security.ContractSpec, current_settings?.ticks_per_row ?? 1)).Value;
public uint display_quantity_divisor { get; }
public LadderSettings current_settings { get; private set; }
public double high => _high.Value;
public double low => _low.Value;
public double change => _change.Value;
// left panel values
public double session_high => _session_high.Value;
public double session_low => _session_low.Value;
public double day_change => _day_change.Value;
public double open => _open.Value;
public double settlement => _settlement.Value;
public double position => _position;
public double day_position => _day_position;
public double raw_day_position => _raw_day_position;
public ulong volume => _volume.Value;
// left panel styles
public LadderPanelRelativePricePointVM day_change_style { get; } = new LadderPanelRelativePricePointVM();
public Color day_change_font_color => day_change > 0 ?
day_change_style.positive_font_color :
(day_change < 0 ? day_change_style.negative_font_color : day_change_style.font_color);
public Color day_change_background_color => day_change > 0 ?
day_change_style.positive_background_color :
(day_change < 0 ? day_change_style.negative_background_color : day_change_style.background_color);
public LadderPanelPricePointVM open_style { get; } = new LadderPanelPricePointVM();
public LadderPanelPricePointVM settlement_style { get; } = new LadderPanelPricePointVM();
public LadderPanelPricePointVM session_high_style { get; } = new LadderPanelPricePointVM();
public LadderPanelPricePointVM session_low_style { get; } = new LadderPanelPricePointVM();
public LadderPanelPricePointVM volume_style { get; } = new LadderPanelPricePointVM();
//
public bool auto_clear { get => _auto_clear.Value; set => _auto_clear.set(value); }
public CalculatorLogic size_calculator { get; } = new CalculatorLogic(0);
/// Important: <see cref="order_properties_vm"/> can be null.
@@ -324,12 +364,6 @@ namespace mbts.spark {
public Brush bid_foreground => _bid_foreground.view;
public Brush ask_background => _ask_background.view;
public Brush ask_foreground => _ask_foreground.view;
public bool show_day => _show_day.view;
public bool show_high => _show_high.view;
public bool show_low => _show_low.view;
public bool show_open => _show_open.view;
public bool show_settle => _show_settle.view;
public bool show_volume => _show_volume.view;
public bool show_account => _show_account.view;
public bool show_tif => _show_tif.view;
public bool show_order_type => _show_order_type.view;
@@ -344,6 +378,11 @@ namespace mbts.spark {
public bool show_cancel_all => _show_cancel_all.view;
public bool show_zoom => _show_zoom.view;
public bool show_status_message => _show_status_message.view;
public TickDisplaySettings day_change_display_settings =>
TickDisplaySettings.Default.with(
pad_with_leading_zeros: false,
show_zero_integer: false);
// left panel scroll props
public double left_panel_view_port_height {
get => _left_panel_view_port_height;
@@ -389,6 +428,10 @@ namespace mbts.spark {
return rv ?? "";
}
}
public ObservableCollection<FairValueLineVM> fair_value_lines { get; } =
new ObservableCollection<FairValueLineVM>();
//public double window_height { get => _window_height.Value; set => _window_height.set(value); }
//public double window_width { get => _window_width.Value; set => _window_width.set(value); }
public uint orders_column_width { get => _orders_column_width.Value; set => _orders_column_width.set(value); }
@@ -409,11 +452,11 @@ namespace mbts.spark {
double _max_left_panel_scroll_position =>
Math.Max(0, _left_panel_extent_height - _left_panel_view_port_height);
internal OrderRecordImmutable get_root_at_price((OrderRecordImmutable order, uint? _, AlgoData? __, AlgoDefinitionMetadata? meta) op) {
internal OrderRecordImmutable get_root_at_price(OrderSecurity op) {
var order = op.order;
double p = order.ui_price();
double p = _normalize_price(order.ui_price());
foreach (var o in lineage(order.ParentKey)) {
if (_equality.Equals(p, o.ui_price()))
if (_equality.Equals(p, _normalize_price(o.ui_price())))
order = o;
}
return order;
@@ -447,6 +490,14 @@ namespace mbts.spark {
if (order_properties_vm.selected_algo_definition != null)
rv.default_algo = AlgoID.from_algo_key(order_properties_vm.selected_algo_definition.key);
}
rv.panel_price_point_settings = new LadderPanelPricePointSettings {
day_change = day_change_style.create(),
open = open_style.create(),
settlement = settlement_style.create(),
session_low = session_low_style.create(),
session_high = session_high_style.create(),
volume = volume_style.create()
};
return rv;
}
@@ -542,6 +593,64 @@ namespace mbts.spark {
}
}
static IObservable<(ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderSecurity>> price_index, IReadOnlyDictionary<uint, OrderSecurity> map)> _order_price_source(
IObservable<TableUpdate<OrderSecurity, uint>> source,
IComparer<double> price_comp,
Func<double, double> normalize_price) {
//
return Observable.Create<(ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderSecurity>>, IReadOnlyDictionary<uint, OrderSecurity>)>(o => {
var state = ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderSecurity>>(price_comp);
var sub = source.Subscribe(table => {
var bd = state.ToBuilder();
// removes
var removes = table.removed.Concat(table.changes.Where(price_changed).Old());
foreach (var old_order in removes) {
var price = get_price(old_order);
if (bd.TryGetValue(price, out var index)) {
var new_index = index.Remove(old_order.order.Key);
if (new_index.Count == 0) {
bd.Remove(price);
} else {
bd[price] = new_index;
}
}
}
// changes
var changes = table.changes.Where(ch => !price_changed(ch)).New();
foreach (var changed_order in changes) {
var price = get_price(changed_order);
bd[price] = bd[price].SetItem(changed_order.order.Key, changed_order);
}
// adds
var adds = table.added.Concat(table.changes.Where(price_changed).New());
foreach (var new_order in adds) {
var price = get_price(new_order);
if (bd.TryGetValue(price, out var index)) {
bd[price] = index.Add(new_order.order.Key, new_order);
} else {
bd[price] = ImmutableDictionary.CreateRange(
new[] { new KeyValuePair<uint, OrderSecurity>(new_order.order.Key, new_order) });
}
}
o.OnNext((state = bd.ToImmutable(), table.state));
});
return sub;
});
bool price_changed(Change<OrderSecurity> ch) =>
price_comp.Compare(ch.New.order.ui_price(), ch.Old.order.ui_price()) != 0;
double get_price(OrderSecurity order) {
var p = order.order.ui_price();
if (!(normalize_price is null))
p = normalize_price(p);
return p;
}
}
double _normalize_price(double price) =>
TickUtils.normalize_price(spec, price);
bool _validate_price(Side side, double price, out string msg) {
msg = null;
if (!price.is_real()) {
@@ -562,17 +671,17 @@ namespace mbts.spark {
void _update_header() {
bool last_updated = md.get_last_trade(ref _last_trade_time, out NativePriceSize last, out DateTime time, out ulong volume);
if (_misc_time != _model.md.misc_timestamp || last_updated) {
_high.set(md.session_high);
_low.set(md.session_low);
_session_high.set(md.session_high);
_session_low.set(md.session_low);
_open.set(md.open_price);
_settlement.set(md.settlement_price);
if (!double.IsNaN(last.price)) {
_change.set(last.price - md.settlement_price);
_day_change.set(last.price - md.settlement_price);
} else {
ulong unused = 0;
_model.md.get_bbo(ref unused, out NativePriceSize bid, out NativePriceSize ask);
double mid = TickUtils.mid_tick(security.ContractSpec, bid.price, ask.price);
_change.set(mid - md.settlement_price);
_day_change.set(mid - md.settlement_price);
}
_volume.set(volume);
_misc_time = _model.md.misc_timestamp;
@@ -583,7 +692,7 @@ namespace mbts.spark {
void _show_book() {
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
ob_settings.filters = new bts.shell.orderbook.FilterStates {
security_criteria = new SecurityCriteria(security)
security_criteria = SecurityCriteria.from_securities(security)
};
_bus.post(SparkTags.ShowOrderBookTag, ob_settings);
}
@@ -597,5 +706,12 @@ namespace mbts.spark {
_update_header();
on_tick?.Invoke();
}
SecurityMD? _find_sec_md(uint sec_key) {
if (_security_map.TryGetValue(sec_key, out var sec_md)) {
return sec_md;
}
return null;
}
}
}
@@ -0,0 +1,127 @@
<UserControl
x:Class="mbts.ui.spark.FairValueLineSettingsView"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mbts="clr-namespace:mbts"
xmlns:sp="clr-namespace:mbts.spark"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:stg="clr-namespace:mbts.ui.settings"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
>
<UserControl.Resources>
<Thickness x:Key="CellPadding">10,6</Thickness>
<Style x:Key="DisabledColumnHeader" TargetType="GridViewColumnHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewColumnHeader">
<Border HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{x:Static u:BTSColors.header_brush}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<c:DIYGrid
Grid.Row="0"
Grid.IsSharedSizeScope="True"
BorderBrush="Black"
BorderThickness="1"
Rows="{Binding items}"
RowsHorizontalScrollBarVisibility="Collapsed"
SelectionMode="Single"
>
<c:DIYGrid.Columns>
<c:DIYGridColumnCollection>
<usp:FairValueLineSettingsColumn key="Delete" HeaderContainerStyle="{StaticResource DisabledColumnHeader}" Width="23" is_visible="True"/>
<usp:FairValueLineSettingsColumn key="Security" Header="Security" Width="130" is_visible="True"/>
<usp:FairValueLineSettingsColumn key="Show" Header="Show" Width="40" is_visible="True"/>
<usp:FairValueLineSettingsColumn key="AutoRefresh" Header="Auto Refresh" Width="80" is_visible="True"/>
<usp:FairValueLineSettingsColumn key="Color" Header="Color" Width="80" is_visible="True"/>
<usp:FairValueLineSettingsColumn key="PriceType" Header="Type" Width="80" is_visible="True"/>
</c:DIYGridColumnCollection>
</c:DIYGrid.Columns>
<c:DIYGrid.RowTemplate>
<DataTemplate DataType="{x:Type sp:FairValueLineItemSettingsVM}">
<Grid c:DIYGrid.SyncWithColumnGeometry="True">
<!--Delete-->
<Button
c:DIYGrid.ColumnKey="Delete"
HorizontalAlignment="Center"
Command="{Binding DataContext.remove_item_cmd,
RelativeSource={RelativeSource AncestorType=usp:FairValueLineSettingsView}}"
CommandParameter="{Binding}"
Style="{StaticResource RoundCloseButton}"
ToolTip="Delete"
/>
<!--Security-->
<c:ContractPickerPopup
c:DIYGrid.ColumnKey="Security"
Grid.Column="0"
Margin="{StaticResource CellPadding}"
CloseOnOK="True"
CloseOnCancel="True"
IsEditMode="False"
MaxWidth="180"
SecurityMap="{Binding DataContext.security_map,
RelativeSource={RelativeSource AncestorType=usp:FairValueLineSettingsView}}"
SelectedSecurityKey="{Binding selected_security_key}"
Header="{Binding SelectedSecurity,
RelativeSource={RelativeSource Self},
Converter={StaticResource SelectedSecurityTextConverter}}"
/>
<!--Show-->
<CheckBox
c:DIYGrid.ColumnKey="Show"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsChecked="{Binding show}"
/>
<!--AutoRefresh-->
<CheckBox
c:DIYGrid.ColumnKey="AutoRefresh"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsChecked="{Binding auto_refresh}"
/>
<!--Color-->
<c:ColorPickerPopup
c:DIYGrid.ColumnKey="Color"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Margin="{StaticResource CellPadding}"
Palettes="{Binding color_palettes}"
SelectedColor="{Binding color}"
/>
<!--PriceType-->
<ComboBox
c:DIYGrid.ColumnKey="PriceType"
Margin="{StaticResource CellPadding}"
ItemsSource="{Binding price_types}"
SelectedItem="{Binding selected_price_type}"
/>
<Rectangle Grid.Column="0" HorizontalAlignment="Right" Width="1" Fill="Gray"/>
<Rectangle Grid.Column="1" HorizontalAlignment="Right" Width="1" Fill="Gray"/>
<Rectangle Grid.Column="2" HorizontalAlignment="Right" Width="1" Fill="Gray"/>
<Rectangle Grid.Column="3" HorizontalAlignment="Right" Width="1" Fill="Gray"/>
<Rectangle Grid.Column="4" HorizontalAlignment="Right" Width="1" Fill="Gray"/>
</Grid>
</DataTemplate>
</c:DIYGrid.RowTemplate>
</c:DIYGrid>
<TextBlock Grid.Row="1" HorizontalAlignment="Left" Margin="4,8">
<Hyperlink Command="{Binding add_item_cmd}">
Click to Add Item
</Hyperlink>
</TextBlock>
</Grid>
</UserControl>
@@ -0,0 +1,40 @@
using mbts.ui.controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace mbts.ui.spark {
/// <summary>
/// Interaction logic for FairValueLineSettingsView.xaml
/// </summary>
partial class FairValueLineSettingsView : UserControl {
public FairValueLineSettingsView() =>
InitializeComponent();
public enum Columns {
None = 0,
Delete,
Security,
Show,
AutoRefresh,
Color,
PriceType
}
}
class FairValueLineSettingsColumn : DIYGrid.ColumnBase<FairValueLineSettingsView.Columns> {
protected override void on_key_initialized(FairValueLineSettingsView.Columns key) =>
id = key.ToString();
}
}
@@ -29,6 +29,10 @@ using System.Windows.Media.Animation;
namespace mbts.spark {
partial class LadderGridHost : Control {
const int _min_row_count = 501, _max_row_count = 2001;
static readonly bool _show_scrollbar;
static readonly bool _lighten_market_cells;
static readonly Color? _bid_market_cell_background;
static readonly Color? _ask_market_cell_background;
int _row_count = _min_row_count;
@@ -53,7 +57,7 @@ namespace mbts.spark {
readonly RefValueContainer<IEnumerable<VolumeAtPriceLevelImmutable>> _volume_container =
new RefValueContainer<IEnumerable<VolumeAtPriceLevelImmutable>>();
IDisposable _settings_sub, _volume_sub, _tick_sub, _vm_sub;
IDisposable _settings_sub, _volume_sub, _tick_sub, _fair_value_lines_sub, _vm_sub;
ScrollViewer _scrollViewer;
LadderSettings _settings;
LadderVM _vm;
@@ -75,6 +79,19 @@ namespace mbts.spark {
ToolTip tooltip_view = new ToolTip();
IDisposable _parameterized_order_popup;
static LadderGridHost() {
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
_show_scrollbar = knobs.show_scrollbar;
_lighten_market_cells = knobs.lighten_market_cells;
// For market cell backgrounds, 0 means null. Note that the full
// range of colors can still be expressed, as zero is merely one of
// 2^24 possible representations of 'transparent.'
if (knobs.bid_market_cell_background != 0)
_bid_market_cell_background = knobs.bid_market_cell_background.argb_color();
if (knobs.ask_market_cell_background != 0)
_ask_market_cell_background = knobs.ask_market_cell_background.argb_color();
}
public LadderGridHost() {
AllowDrop = true;
UseLayoutRounding = true;
@@ -82,6 +99,8 @@ namespace mbts.spark {
_ordershost = new LadderOrdersHost(this) { HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Stretch };
var scrollViewerFactory = new FrameworkElementFactory(typeof(ScrollViewer));
scrollViewerFactory.SetValue(FocusableProperty, false);
if (!_show_scrollbar)
scrollViewerFactory.SetValue(ScrollViewer.VerticalScrollBarVisibilityProperty, ScrollBarVisibility.Hidden);
Template = new ControlTemplate() {
VisualTree = scrollViewerFactory
};
@@ -296,6 +315,18 @@ namespace mbts.spark {
Column[] columns { get; set; } = new Column[0];
IEnumerable<Column> _visible_columns => columns.Where(c => c.settings.show);
ContractSpecificationImmutable? _spec_backing;
ContractSpecificationImmutable _spec {
get {
if (_spec_backing.HasValue) {
return _spec_backing.Value;
} else if (_vm != null) {
_spec_backing = TickUtils.multiply_tick_size(security.ContractSpec, _settings?.ticks_per_row ?? 1);
return _spec_backing.Value;
}
return SecurityDataHelper.Defaults.ContractSpecificationImmutable;
}
}
public void drag_out_of_bounds() =>
_tactile_adorner?.drag_out_of_bounds();
@@ -303,7 +334,7 @@ namespace mbts.spark {
public void scroll_to_price(double price) {
if (_scrollViewer == null || _price_list.is_null_or_empty())
return;
if (security.ContractSpec.tickdiff(_price_list[0], price) is int target_row) {
if (_spec.tickdiff(_price_list[0], price) is int target_row) {
int visible_row_count = (int)(_scrollViewer.ActualHeight / _settings.row_height);
int curr_row = visible_row_count + (int)(_scrollViewer.VerticalOffset / _settings.row_height);
if ((target_row > curr_row && target_row < _row_count) || (target_row < (curr_row - visible_row_count) && target_row >= 0))
@@ -533,10 +564,10 @@ namespace mbts.spark {
_price_list == null ? double.NaN : _price_list[row];
uint? _bid_size_from_row(int row) =>
_size_from_row(row, _price_list, _bids, _vm.security.ContractSpec);
_size_from_row(row, _price_list, _bids, _spec);
uint? _ask_size_from_row(int row) =>
_size_from_row(row, _price_list, _asks, _vm.security.ContractSpec);
_size_from_row(row, _price_list, _asks, _spec);
uint _volume_from_row(int row) {
const int max_tries = 3;
@@ -622,7 +653,9 @@ namespace mbts.spark {
if (_price_list != null) {
switch (column.column_type) {
case ColumnType.Order: _ordershost.set_col_width(column.settings.width); break;
case ColumnType.Bid: render_market_col(_bids); break;
case ColumnType.Bid:
render_market_col(_bids, _bid_market_cell_background);
break;
case ColumnType.Price:
if (_settings.net_change != LadderSettings.NetChangeTerms.None) {
LadderRender.render_prices(
@@ -634,7 +667,7 @@ namespace mbts.spark {
_vm.md.settlement_price,
_settings.net_change == LadderSettings.NetChangeTerms.Ticks,
column.settings,
security.ContractSpec);
_spec);
} else {
LadderRender.render_prices(
column.visual_tup.visual,
@@ -643,11 +676,13 @@ namespace mbts.spark {
_settings.row_height,
_price_list,
column.settings,
security.ContractSpec,
_spec,
_settings.price_display_format);
}
break;
case ColumnType.Ask: render_market_col(_asks); break;
case ColumnType.Ask:
render_market_col(_asks, _ask_market_cell_background);
break;
case ColumnType.Volume:
_volume_container.mark_dirty();
_refresh_volume_at_price();
@@ -659,9 +694,9 @@ namespace mbts.spark {
if (_mkt_break_lines.high_render_index.HasValue)
_render_session_line(_session_high_vis, _mkt_break_lines.high_render_index.Value, _settings.session_high_line.color);
}
void render_market_col(NativePriceSize[] depth) {
void render_market_col(NativePriceSize[] depth, Color? market_cell_background) {
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_mkt_depth_item);
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, _spec, column.settings, _fmt_mkt_depth_item, _lighten_market_cells, market_cell_background);
}
}
_apply_column_visual_offset();
@@ -696,7 +731,12 @@ namespace mbts.spark {
} else {
_vm.OnDisposed += _on_vm_disposed;
_prices_updated = 0;
_price_comparer = Comparer<double>.Create((x, y) => security.ContractSpec.tick_equals(x, y) ? 0 : y.CompareTo(x));
// invalidate the contract spec here, as well as in the 'settings update' callback.
// While it is assumed that the settings IObservable will always fire at least once (on subscribe),
// this serves as a redundancy/fail-safe; it ensures that the vm's security's
// contract spec will be used, even if the settings sequence is empty.
_invalidate_spec();
_price_comparer = Comparer<double>.Create((x, y) => _spec.tick_equals(_normalize_price(x), _normalize_price(y)) ? 0 : y.CompareTo(x));
_price_dirty = true;
_piq_dirty = true;
// **use local vm to protect against re-assignment,
@@ -713,8 +753,10 @@ namespace mbts.spark {
// with meticulous attention to detail.
_tick_sub = Disposable.Create(() => local_vm.on_tick -= _tick);
_vm_sub = Disposable.Create(() => local_vm.request_recenter -= on_request_recenter);
void on_request_recenter() =>
void on_request_recenter() {
RecenterCommand?.ExecuteIfCan(null);
_refresh_frozen_mkt_lines();
}
}
}
@@ -783,6 +825,15 @@ namespace mbts.spark {
#endregion
void _refresh_frozen_mkt_lines() {
if (_price_list is null)
return;
_mkt_break_lines.refresh_frozen();
_refresh_bid_mkt_break();
_refresh_ask_mkt_break();
_refresh_mid_mkt_break();
_refresh_fair_value_lines(true);
}
void _on_vm_disposed(bool obj) =>
_dispose_resources();
@@ -791,6 +842,7 @@ namespace mbts.spark {
Disposable.dispose(ref _settings_sub);
Disposable.dispose(ref _volume_sub);
Disposable.dispose(ref _tick_sub);
Disposable.dispose(ref _fair_value_lines_sub);
if (_vm != null)
_vm.OnDisposed -= _on_vm_disposed;
}
@@ -814,6 +866,7 @@ namespace mbts.spark {
} else {
_scroll_to_row(_row_count / 2);
}
_refresh_frozen_mkt_lines();
}
}
@@ -846,6 +899,7 @@ namespace mbts.spark {
_refresh_bid_mkt_break();
_refresh_ask_mkt_break();
_refresh_mid_mkt_break();
_refresh_fair_value_lines(true);
_refresh_session_low_line();
_refresh_session_high_line();
_refresh_session_range();
@@ -858,6 +912,7 @@ namespace mbts.spark {
_refresh_ask_mkt_break();
if (_mkt_break_lines.mid_dirty)
_refresh_mid_mkt_break();
_refresh_fair_value_lines(false);
if (_mkt_break_lines.session_low_dirty)
_refresh_session_low_line();
if (_mkt_break_lines.session_high_dirty)
@@ -888,13 +943,36 @@ namespace mbts.spark {
}
void _refresh_bid_mkt_break() =>
_render_mkt_break(_bid_mkt_break_vis, _mkt_break_lines.bid, _settings.bid_break_line.color);
_render_mkt_break(
_bid_mkt_break_vis,
_settings.bid_break_line.refresh_mode == LadderMarketBreakLine.RefreshMode.Manual ?
_mkt_break_lines.frozen_bid :
_mkt_break_lines.bid,
_settings.bid_break_line.color);
void _refresh_ask_mkt_break() =>
_render_mkt_break(_ask_mkt_break_vis, _mkt_break_lines.ask, _settings.ask_break_line.color);
_render_mkt_break(
_ask_mkt_break_vis,
_settings.ask_break_line.refresh_mode == LadderMarketBreakLine.RefreshMode.Manual ?
_mkt_break_lines.frozen_ask :
_mkt_break_lines.ask,
_settings.ask_break_line.color);
void _refresh_mid_mkt_break() =>
_render_mkt_break(_mid_mkt_break_vis, _mkt_break_lines.mid, _settings.mid_break_line.color);
_render_mkt_break(
_mid_mkt_break_vis,
_settings.mid_break_line.refresh_mode == LadderMarketBreakLine.RefreshMode.Manual ?
_mkt_break_lines.frozen_mid :
_mkt_break_lines.mid,
_settings.mid_break_line.color);
void _refresh_fair_value_lines(bool force) {
foreach (var fvl in _mkt_break_lines.fair_value_line_views) {
if (force || fvl.is_dirty) {
_render_mkt_break(fvl.vis, fvl.price_view, fvl.color);
}
}
}
void _refresh_session_low_line() {
if (_settings.session_low_line.show && _mkt_break_lines.low_render_index.HasValue)
@@ -916,14 +994,14 @@ namespace mbts.spark {
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, _fmt_mkt_quantity);
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, origin_index, _price_list, _vm.price_vs_piq, _settings.row_height, _spec, _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_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);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, _spec, _settings.bid_column, _fmt_mkt_depth_item, _lighten_market_cells, _bid_market_cell_background);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, _spec, _settings.ask_column, _fmt_mkt_depth_item, _lighten_market_cells, _ask_market_cell_background);
}
/// <summary>
@@ -951,7 +1029,7 @@ namespace mbts.spark {
_price_list,
vapls,
_settings.row_height,
security.ContractSpec,
_spec,
_settings.volume_column,
last_index,
_get_last_arrow_string(),
@@ -988,8 +1066,8 @@ namespace mbts.spark {
void _refresh_prices() {
var prev_price_list = _price_list;
_price_list = (!(_settings.net_change == LadderSettings.NetChangeTerms.None)) ?
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _vm.md.settlement_price, _settings.net_change == LadderSettings.NetChangeTerms.Ticks, _row_count, _settings.row_height, _settings.price_column, security.ContractSpec) :
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _row_count, _settings.row_height, _settings.price_column, security.ContractSpec, _settings.price_display_format);
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _vm.md.settlement_price, _settings.net_change == LadderSettings.NetChangeTerms.Ticks, _row_count, _settings.row_height, _settings.price_column, _spec) :
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _row_count, _settings.row_height, _settings.price_column, _spec, _settings.price_display_format);
_ordershost.update_settings(_price_list, _price_comparer, _settings.row_height, _settings.orders_column);
if (prev_price_list != null) {
// maintain previous visible price range if possible
@@ -1017,8 +1095,8 @@ namespace mbts.spark {
SparkUtils.lazy_warn(!double.IsNaN(_mkt_break_lines.origin) && _mkt_break_lines.origin >= low && _mkt_break_lines.origin <= high,
() => fmt_log_msg($"market break ({_mkt_break_lines.origin}) is unacceptable for the market range ({low} - {high})"));
int prev_row_count = _row_count;
int n_ticks_to_low = security.ContractSpec.tickdiff(_mkt_break_lines.origin, low) ?? 0;
int n_ticks_to_high = security.ContractSpec.tickdiff(high, _mkt_break_lines.origin) ?? 0;
int n_ticks_to_low = _spec.tickdiff(_mkt_break_lines.origin, low) ?? 0;
int n_ticks_to_high = _spec.tickdiff(high, _mkt_break_lines.origin) ?? 0;
int desired_row_count = Math.Max(n_ticks_to_low, n_ticks_to_high) * 2 + 1;
_row_count = Math.Min(_max_row_count, desired_row_count);
if (_row_count != prev_row_count) {
@@ -1034,7 +1112,7 @@ namespace mbts.spark {
_last_price_range_calc_time = now;
//
string fmt_log_msg(string msg) =>
$"Ladder {security} (id:{security.Id}, low/high tick:{security.ContractSpec.LowTick}/{security.ContractSpec.HighTick}): {msg}.";
$"Ladder {security} (id:{security.Id}, low/high tick:{_spec.LowTick}/{_spec.HighTick}): {msg}.";
}
return prices_changed;
}
@@ -1067,6 +1145,7 @@ namespace mbts.spark {
_refresh_bid_mkt_break();
_refresh_ask_mkt_break();
_refresh_mid_mkt_break();
_refresh_fair_value_lines(true);
_refresh_session_low_line();
_refresh_session_high_line();
_refresh_session_range();
@@ -1082,29 +1161,29 @@ namespace mbts.spark {
double _get_bid_market_break() {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
return double.IsNaN(best_bid) ?
double.IsNaN(best_ask) ? _vm.settlement : security.ContractSpec.tickdown(best_ask) :
best_bid;
return LadderPriceLineViewAdapter.bid_line(best_bid, best_ask, _vm.settlement, _spec);
}
double _normalize_price(double price) =>
TickUtils.normalize_price(_spec, price);
double _get_ask_market_break() {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
return double.IsNaN(best_ask) ?
double.IsNaN(best_bid) ? _vm.settlement : best_bid :
security.ContractSpec.tickdown(best_ask);
return LadderPriceLineViewAdapter.ask_line(best_bid, best_ask, _vm.settlement, _spec);
}
double _get_mid_market_break() {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
return double.IsNaN(best_bid) ?
double.IsNaN(best_ask) ? _vm.settlement : security.ContractSpec.tickdown(best_ask) :
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(security.ContractSpec, best_bid, best_ask);
return LadderPriceLineViewAdapter.mid_line(best_bid, best_ask, _vm.settlement, _spec);
}
double _get_session_low() => _vm.md.session_low;
double _get_session_high() => _vm.md.session_high;
double _get_session_low() => _normalize_price(_get_session_low_raw());
double _get_session_high() => _normalize_price(_get_session_high_raw());
double _get_session_low_raw() => _vm.md.session_low;
double _get_session_high_raw() => _vm.md.session_high;
void _render_mkt_break(DrawingVisual vis, double price, uint color) =>
LadderRender.render_mkt_break(vis, _row_for_price(price), _settings.row_height, color);
@@ -1113,6 +1192,9 @@ namespace mbts.spark {
LadderRender.render_single_grid_line(vis, row, _settings.row_height, color, _price_column.x_left, _settings.price_column.width);
#endregion
void _invalidate_spec() =>
_spec_backing = null;
void _settings_updated(LadderSettings s) {
bool init;
double prev_row_height;
@@ -1126,6 +1208,7 @@ namespace mbts.spark {
/// The viewmodel will always have the most up-to-date settings, so ignore <see cref="s"/> and just get it from <see cref="_vm"/>.
/// This is a workaround for the global settings-defaults issue: when defaults are updated, some properties are inadvertantly overwritten, such as column-widths, etc.
_settings = _vm.get_save_state();
_invalidate_spec();
_bid_vis_tup.glyph_context = glyph_context(s.bid_column);
_ask_vis_tup.glyph_context = glyph_context(s.ask_column);
_price_vis_tup.glyph_context = glyph_context(s.price_column);
@@ -1144,8 +1227,22 @@ namespace mbts.spark {
if (prev_row_height != _settings.row_height)
_reset_height();
_price_dirty = _settings.price_column.show;
Disposable.dispose(ref _fair_value_lines_sub);
_mkt_break_lines.fair_value_line_views.Clear();
// Clear and re-add the columns to the viewer to ensure offset/visibility is correct
_visualHost.Clear();
if (LadderSettings.mimic_tt) {
// The session_range_vis sits beneath the grid backgrounds (on the
// z-axis) to avoid obfuscating the font color (dj wants it to be
// pure white). Historically, it sat atop the price column, with a
// low opacity to slightly darken the range of prices inside of the
// session range (i.e., the price-band of the day's trades). In
// conjuction with this change, the price column's background
// opacity is made slightly less than 1 (in the default settings),
// while the font color remains fully opaque, so this layer will
// darken the background without affecting the font color.
_visualHost.AddVisualChild(_session_range_vis);
}
// Renders the grid lines and column backgrounds
_visualHost.AddVisualChild(_gridvis);
// Orders column
@@ -1170,12 +1267,23 @@ namespace mbts.spark {
}
}
}
_refresh_frozen_mkt_lines();
_visualHost.AddVisualChild(_mid_mkt_break_vis);
_visualHost.AddVisualChild(_ask_mkt_break_vis);
_visualHost.AddVisualChild(_bid_mkt_break_vis);
_fair_value_lines_sub = ui.utility.ObservableAutoWrapper.connect(
_vm.fair_value_lines,
_mkt_break_lines.fair_value_line_views,
vm => {
var with_vis = new FairValueLineWithVis(vm);
_visualHost.AddVisualChild(with_vis.vis);
return with_vis;
},
with_vis => _visualHost.RemoveVisualChild(with_vis.vis));
_visualHost.AddVisualChild(_session_low_vis);
_visualHost.AddVisualChild(_session_high_vis);
_visualHost.AddVisualChild(_session_range_vis);
if (!LadderSettings.mimic_tt)
_visualHost.AddVisualChild(_session_range_vis);
_render_grid_background();
_init_tactile_adorner();
// this effectively sets the background of the "Last" column
@@ -1261,11 +1369,30 @@ namespace mbts.spark {
!(x == y);
}
class FairValueLineWithVis {
readonly FairValueLineVM _vm;
double _price_view = double.NaN;
public FairValueLineWithVis(FairValueLineVM vm) =>
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
public DrawingVisual vis { get; } = new DrawingVisual();
public double price_view => _price_view;
public uint color => _vm.settings.color;
public bool is_dirty { get; private set; }
public void refresh(bool update_frozen) {
_vm.refresh(update_frozen);
is_dirty = ConvertUtils.update_value_type(ref _price_view, _vm.price_view);
}
}
// This class represents interesting price points as they should be rendered on the LadderGridHost
// Note: The values contained here do NOT necessarily represent true data, as they will be NaN/null if their respective visual is hidden in LadderSettings
class MarketBreakLines {
readonly LadderGridHost _owner;
double _bid, _ask, _mid, _origin, _session_low, _session_high;
double _bid = double.NaN, _ask = double.NaN, _mid = double.NaN, _origin, _session_low, _session_high;
double _frozen_bid = double.NaN, _frozen_ask = double.NaN, _frozen_mid = double.NaN, _frozen_origin = double.NaN;
int? _low_index, _high_index;
public MarketBreakLines(LadderGridHost owner) =>
@@ -1279,6 +1406,11 @@ namespace mbts.spark {
public bool session_range_dirty => session_low_dirty || session_high_dirty;
public bool origin_dirty { get; private set; }
public double frozen_bid => double.IsNaN(_frozen_bid) ? (_frozen_bid = _bid) : _frozen_bid;
public double frozen_ask => double.IsNaN(_frozen_ask) ? (_frozen_ask = _ask) : _frozen_ask;
public double frozen_mid => double.IsNaN(_frozen_mid) ? (_frozen_mid = _mid) : _frozen_mid;
public double frozen_origin => double.IsNaN(_frozen_origin) ? (_frozen_origin = _origin) : _frozen_origin;
public double bid => _bid;
public double ask => _ask;
public double mid => _mid;
@@ -1317,17 +1449,30 @@ namespace mbts.spark {
}
}
public ObservableCollection<FairValueLineWithVis> fair_value_line_views { get; } =
new ObservableCollection<FairValueLineWithVis>();
int _get_session_range_size() =>
_owner.security.spec_or_default().tickdiff(_session_high, _session_low) ?? 0;
_owner._spec.tickdiff(_session_high, _session_low) ?? 0;
LadderSettings _settings => _owner._settings;
public void refresh_frozen() {
_refresh(true);
}
public void refresh() {
_refresh(false);
}
void _refresh(bool update_frozen) {
bid_dirty = ConvertUtils.update_value_type(ref _bid, _settings.bid_break_line.show ? _owner._get_bid_market_break() : double.NaN);
ask_dirty = ConvertUtils.update_value_type(ref _ask, _settings.ask_break_line.show ? _owner._get_ask_market_break() : double.NaN);
mid_dirty = ConvertUtils.update_value_type(ref _mid, _settings.mid_break_line.show ? _owner._get_mid_market_break() : double.NaN);
session_low_dirty = ConvertUtils.update_value_type(ref _session_low, (_settings.session_low_line.show || _settings.session_range_fill.show) ? _owner._get_session_low() : double.NaN);
session_high_dirty = ConvertUtils.update_value_type(ref _session_high, (_settings.session_high_line.show || _settings.session_range_fill.show) ? _owner._get_session_high() : double.NaN);
foreach (var fvlv in fair_value_line_views) {
fvlv.refresh(update_frozen);
}
if (session_low_dirty)
_low_index = null;
if (session_high_dirty)
@@ -1342,6 +1487,11 @@ namespace mbts.spark {
if (double.IsNaN(new_origin))
new_origin = _owner._get_bid_market_break();
origin_dirty = ConvertUtils.update_value_type(ref _origin, new_origin);
if (update_frozen) {
_frozen_bid = _bid;
_frozen_ask = _ask;
_frozen_mid = _mid;
}
}
}
}
@@ -130,6 +130,8 @@ namespace mbts.spark {
if (e.ChangedButton == MouseButton.Middle) {
// create the new popup window in here
var obvm = order.create_orderbook_vm();
if (obvm is null)
return;
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
var view = new MiniOrderBookView(obvm);
var popup = new System.Windows.Controls.Primitives.Popup {
@@ -0,0 +1,60 @@
using bts.api;
namespace mbts.spark {
class LadderPriceLineViewAdapter {
public static double bid_line(
double bid, double ask, double settlement, ContractSpecificationImmutable? spec) {
return TickUtils.normalize_price(spec, impl());
double impl() {
if (!double.IsNaN(bid)) {
return bid;
}
if (!double.IsNaN(ask)) {
return _try_tick_down(spec, ask);
}
return settlement;
}
}
public static double ask_line(
double bid, double ask, double settlement, ContractSpecificationImmutable? spec) {
return TickUtils.normalize_price(spec, impl());
double impl() {
if (!double.IsNaN(ask)) {
return _try_tick_down(spec, ask);
}
if (!double.IsNaN(bid)) {
return bid;
}
return settlement;
}
}
public static double mid_line(
double bid, double ask, double settlement, ContractSpecificationImmutable? spec) {
return TickUtils.normalize_price(spec, impl());
double impl() {
if (double.IsNaN(bid)) {
if (double.IsNaN(ask)) {
return settlement;
}
return _try_tick_down(spec, ask);
}
if (double.IsNaN(ask)) {
return bid;
}
if (spec.HasValue) {
return TickUtils.mid_tick(spec.Value, bid, ask);
}
return bid;
}
}
static double _try_tick_down(ContractSpecificationImmutable? spec, double value) {
if (spec.HasValue) {
return spec.Value.tickdown(value);
}
return value;
}
}
}
@@ -43,9 +43,12 @@ namespace mbts.spark {
static class LadderRender {
// Note: the orders-column has a separate "row_padding" value: Badge.DefaultMargin
// (see how it is incorporated in measure_row_height() below)
public static readonly Thickness row_padding = new Thickness(6);
public static Thickness row_padding => LadderSettings.mimic_tt ?
new Thickness(4, 2, 4, 2) : new Thickness(6);
const string tick_separator_period = ".";
const string tick_separator_dash = "-";
const int GRID_LINE_HEIGHT = 1;
// IMPORTANT: this method ONLY WORKS if Badges use the BadgeRender.margin/border_thickness/padding values.
// TODO: elminate the dependency on BadgeRender.margin/border_thickness/padding by integrating them into settings.
@@ -58,11 +61,20 @@ namespace mbts.spark {
public static int index_of(double[] prices, double price, IComparer<double> comparer) =>
Array.BinarySearch(prices, price, comparer);
public static GlyphRun format_text(string text, int row, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, LadderSettings.PriceDisplayFormat price_display_format = LadderSettings.PriceDisplayFormat.Default) =>
public static GlyphRun format_text(
string text,
int row,
double row_height,
LadderColumnSettings settings,
GlyphContext glyph_context,
LadderSettings.PriceDisplayFormat price_display_format = LadderSettings.PriceDisplayFormat.Default) =>
glyph_context.get_glyph_run(
// long term there is probably a better way to get rid of the separator, such as in the TickExtensions._format_tick
price_display_format == LadderSettings.PriceDisplayFormat.Integer ? text.Replace(tick_separator_period, string.Empty).Replace(tick_separator_dash, string.Empty) : text,
settings.font_size, settings.width, row_height, HorizontalAlignment.Center, 0, VerticalAlignment.Center, row * row_height, true);
price_display_format == LadderSettings.PriceDisplayFormat.Integer ?
text.Replace(tick_separator_period, string.Empty).Replace(tick_separator_dash, string.Empty) :
text,
settings.font_size, settings.width, row_height, HorizontalAlignment.Right,
-4, VerticalAlignment.Center, row * row_height, true);
public static double[] gen_prices(double mid, int row_count, ContractSpecificationImmutable spec) {
// require odd number
@@ -85,7 +97,7 @@ namespace mbts.spark {
public static GlyphRun[] format_prices(double[] prices, double reference_price, bool format_in_tick_terms, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, ContractSpecificationImmutable spec) {
var result = new GlyphRun[prices.Length];
Parallel.For(0, prices.Length, i =>
result[i] = format_text(spec.format(prices[i], reference_price, format_in_tick_terms), i, row_height, settings, glyph_context));
result[i] = format_text(spec.format(prices[i] - reference_price, format_in_tick_terms), i, row_height, settings, glyph_context));
return result;
}
public static GlyphRun[] format_prices(double[] prices, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
@@ -149,14 +161,13 @@ namespace mbts.spark {
using (var dc = dv.RenderOpen()) {
// draw column backgrounds
foreach (var col in columns) {
var background = col.settings.background.rgb_brush();
background.Freeze();
var background = col.settings.background.argb_brush();
dc.DrawRectangle(background, null, new Rect(col.x_left, 0, col.settings.width, rowcount * row_height));
}
// draw grid lines
for (int row = 1; row < rowcount; ++row) {
double y = row * row_height;
dc.DrawRectangle(brush, null, new Rect(0, y, 500, 1));
dc.DrawRectangle(brush, null, new Rect(0, y, 500, GRID_LINE_HEIGHT));
}
}
}
@@ -170,9 +181,18 @@ namespace mbts.spark {
double row_height,
ContractSpecificationImmutable spec,
LadderColumnSettings settings,
Func<NativePriceSize, string> fmt
) {
Func<NativePriceSize, string> fmt,
bool lighten_market_cells,
Color? market_cell_background) {
//var background = new SolidColorBrush(settings.background.rgb_color());
Brush market_cell_background_brush;
if (market_cell_background.HasValue) {
market_cell_background_brush = new SolidColorBrush(market_cell_background.Value);
} else if (lighten_market_cells) {
market_cell_background_brush = settings.background.rgb_color().blend_with(Colors.White).to_brush();
} else {
market_cell_background_brush = null;
}
using (var dc = dv.RenderOpen())
using (_draw_with_clip(dc, settings, row_height, prices.Length)) {
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, prices.Length * row_height));
@@ -181,15 +201,32 @@ namespace mbts.spark {
var foreground = settings.foreground.rgb_brush();
int curr_index = origin_index;
double curr_price = prices[curr_index];
for (int i = 0; i < rungs.Length; i++) {
if (rungs[i].is_nan) break;
double price = rungs[i].price;
NativePriceSize curr_size = NativePriceSize.NO_MARKET;
var row_tick_groups = rungs
.TakeWhile(nps => !nps.is_nan)
.AggregateConsecutiveGroups(() => NativePriceSize.NO_MARKET,
(aggr, rung) => rung.with(
direct_size: aggr.direct_size + rung.direct_size,
implied_size: aggr.implied_size + rung.implied_size),
rung => {
//spec.round_to_tick(rung.price);
var ticks = spec.full_ticks_from_zero(rung.price);
return ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.price;
});
foreach (var rung in row_tick_groups) {
//double price = spec.round_to_tick(rung.price);
var ticks = spec.full_ticks_from_zero(rung.price);
var price = ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.price;
// prices are in descending order, so get diff from curr_price - price
int tick_diff = spec.tickdiff_unchecked(curr_price, price);
curr_index += tick_diff;
curr_price = price;
if (curr_index >= 0 && curr_index < prices.Length) {
var gr = format_text(fmt(rungs[i]), curr_index, row_height, settings, glyph_context);
if (market_cell_background_brush != null)
dc.DrawRectangle(market_cell_background_brush, null,
new Rect(0, curr_index * row_height + GRID_LINE_HEIGHT,
settings.width, row_height - GRID_LINE_HEIGHT));
var gr = format_text(fmt(rung), curr_index, row_height, settings, glyph_context);
dc.DrawGlyphRun(foreground, gr);
} else break;
}
@@ -374,7 +411,7 @@ namespace mbts.spark {
public static void render_session_range(DrawingVisual dv, uint color, LadderColumnSettings settings, int low_index, int high_index, double[] prices, double row_height) {
using (var dc = dv.RenderOpen())
using (_draw_with_clip(dc, settings, row_height, prices.Length))
dc.DrawRectangle(new SolidColorBrush(color.rgb_color()) { Opacity = 0.2 }, null, new Rect(0, high_index * row_height, settings.width, (low_index - high_index) * row_height));
dc.DrawRectangle(new SolidColorBrush(color.argb_color()), null, new Rect(0, high_index * row_height, settings.width, (low_index - high_index) * row_height));
}
static GlyphRun[] _format_empty_prices(int row_count, double row_height, LadderColumnSettings settings, GlyphContext glyph_context) {
@@ -50,6 +50,18 @@
<cv:KeyValue Key="{u:Int 0}" Value="{u:Visibility Collapsed}"/>
</cv:ValueMap>
<c:BadgeTextLinesConverter x:Key="BadgeTextLinesConverter"/>
<cv:ValueMap x:Key="RefreshModeToBooleanConverter">
<cv:KeyValue Key="{x:Static Manual, MemberType={u:Nested sp:LadderMarketBreakLine.RefreshMode}}">
<cv:KeyValue.Value>
<sys:Boolean>false</sys:Boolean>
</cv:KeyValue.Value>
</cv:KeyValue>
<cv:KeyValue Key="{x:Static Auto, MemberType={u:Nested sp:LadderMarketBreakLine.RefreshMode}}">
<cv:KeyValue.Value>
<sys:Boolean>true</sys:Boolean>
</cv:KeyValue.Value>
</cv:KeyValue>
</cv:ValueMap>
<Style x:Key="ColumnBackground" TargetType="Rectangle">
<Setter Property="Grid.RowSpan" Value="3"/>
@@ -161,6 +173,191 @@
</Setter>
</Style>
<Style x:Key="LeftPanelPriceFontColorPopup" TargetType="c:PopupControl">
<Setter Property="Width" Value="60"/>
<Setter Property="Margin" Value="4,2"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<Style x:Key="LeftPanelPricePopupLabel" TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="2,0"/>
</Style>
<Style x:Key="LeftPanelPricePopup" TargetType="c:PopupControl">
<Setter Property="Background" Value="{StaticResource ComboBox.Static.Background}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="6,2"/>
<Setter Property="PopupContainerStyle">
<Setter.Value>
<Style TargetType="Border" BasedOn="{StaticResource PopupContainerStyleBase}">
<Setter Property="Padding" Value="4"/>
</Style>
</Setter.Value>
</Setter>
<Setter Property="MinHeight" Value="24"/>
<Setter Property="Header" Value="..."/>
<Setter Property="Content" Value="{Binding}"/>
<Setter Property="ShowExpanderArrow" Value="False"/>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<c:FontPicker
Grid.ColumnSpan="2"
SelectedFontFamily="{Binding font_family}"
SelectedFontSize="{Binding font_size}"
SelectedFontWeight="{Binding font_weight}"
/>
<TextBlock Grid.Row="2" Style="{StaticResource LeftPanelPricePopupLabel}" Margin="4,0">Font Color</TextBlock>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="1"
Palettes="{Binding font_color_palettes}"
SelectedColor="{Binding font_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
<TextBlock Grid.Row="4" Style="{StaticResource LeftPanelPricePopupLabel}">Background Color</TextBlock>
<c:ColorPickerPopup
Grid.Row="4"
Grid.Column="1"
Palettes="{Binding background_color_palettes}"
SelectedColor="{Binding background_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
<CheckBox Grid.Row="6" IsChecked="{Binding show_label}">Show Label</CheckBox>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="LeftPanelRelativePricePopup" TargetType="c:PopupControl" BasedOn="{StaticResource LeftPanelPricePopup}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<c:FontPicker
SelectedFontFamily="{Binding font_family}"
SelectedFontSize="{Binding font_size}"
SelectedFontWeight="{Binding font_weight}"
/>
<GroupBox Grid.Row="2" Header="Negative">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource LeftPanelPricePopupLabel}">Font Color</TextBlock>
<c:ColorPickerPopup
Grid.Column="1"
Palettes="{Binding negative_font_color_palettes}"
SelectedColor="{Binding negative_font_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
<TextBlock Grid.Row="2" Style="{StaticResource LeftPanelPricePopupLabel}">Background Color</TextBlock>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="1"
Palettes="{Binding negative_background_color_palettes}"
SelectedColor="{Binding negative_background_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
</Grid>
</GroupBox>
<GroupBox Grid.Row="4" Header="Zero">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource LeftPanelPricePopupLabel}">Font Color</TextBlock>
<c:ColorPickerPopup
Grid.Column="1"
Palettes="{Binding font_color_palettes}"
SelectedColor="{Binding font_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
<TextBlock Grid.Row="2" Style="{StaticResource LeftPanelPricePopupLabel}">Background Color</TextBlock>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="1"
Palettes="{Binding background_color_palettes}"
SelectedColor="{Binding background_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
</Grid>
</GroupBox>
<GroupBox Grid.Row="6" Header="Positive">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource LeftPanelPricePopupLabel}">Font Color</TextBlock>
<c:ColorPickerPopup
Grid.Column="1"
Palettes="{Binding positive_font_color_palettes}"
SelectedColor="{Binding positive_font_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
<TextBlock Grid.Row="2" Style="{StaticResource LeftPanelPricePopupLabel}">Background Color</TextBlock>
<c:ColorPickerPopup
Grid.Row="2"
Grid.Column="1"
Palettes="{Binding positive_background_color_palettes}"
SelectedColor="{Binding positive_background_color}"
Style="{StaticResource LeftPanelPriceFontColorPopup}"
/>
</Grid>
</GroupBox>
<CheckBox Grid.Row="8" IsChecked="{Binding show_label}">Show Label</CheckBox>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="LadderColumnSample" TargetType="Border">
<Setter Property="Grid.Column" Value="6"/>
<Setter Property="Background" Value="{Binding background, Converter={StaticResource ColorToBrushConverter}}"/>
@@ -455,6 +652,68 @@
</DataTemplate>
</u:TemplateSelectorKeyValue>
</u:KeyedTemplateSelector>
<u:KeyedTemplateSelector x:Key="OrderEntryPanelItemTemplateSelector">
<u:TemplateSelectorKeyValue Key="{x:Static usp:LadderSettingsPanelTemplateKeys.VisibilityItem}">
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="A"/>
<ColumnDefinition Width="*" SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Border Margin="0,5,5,0">
<CheckBox Content="{Binding name}" FlowDirection="RightToLeft" IsChecked="{Binding show}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
</CheckBox.Resources>
</CheckBox>
</Border>
</Grid>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="{x:Static usp:LadderSettingsPanelTemplateKeys.PricePoint}">
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="A"/>
<ColumnDefinition Width="*" SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Border Margin="0,5,5,0">
<CheckBox Content="{Binding name}" FlowDirection="RightToLeft" IsChecked="{Binding show}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
</CheckBox.Resources>
</CheckBox>
</Border>
<c:PopupControl Grid.Column="1" Margin="4,2" Style="{StaticResource LeftPanelPricePopup}"/>
</Grid>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="{x:Static usp:LadderSettingsPanelTemplateKeys.RelativePricePoint}">
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="A"/>
<ColumnDefinition Width="*" SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Border Margin="0,5,5,0">
<CheckBox Content="{Binding name}" FlowDirection="RightToLeft" IsChecked="{Binding show}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
</CheckBox.Resources>
</CheckBox>
</Border>
<c:PopupControl Grid.Column="1" Margin="4,2" Style="{StaticResource LeftPanelRelativePricePopup}"/>
</Grid>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</u:KeyedTemplateSelector>
</UserControl.Resources>
<TabControl>
<TabControl.ItemContainerStyle>
@@ -525,10 +784,12 @@
<ColumnDefinition SharedSizeGroup="c2"/>
<ColumnDefinition SharedSizeGroup="c3"/>
<ColumnDefinition SharedSizeGroup="c4"/>
<ColumnDefinition SharedSizeGroup="c5"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" HorizontalAlignment="Center" Margin="6,0" Text="Show" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" HorizontalAlignment="Center" Margin="6,0" Text="Color" VerticalAlignment="Center"/>
<TextBlock Grid.Column="3" Grid.ColumnSpan="2" HorizontalAlignment="Center" Margin="6,0" Text="Sample" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" HorizontalAlignment="Center" Margin="6,0" Text="Auto Refresh" VerticalAlignment="Center"/>
<TextBlock Grid.Column="3" HorizontalAlignment="Center" Margin="6,0" Text="Color" VerticalAlignment="Center"/>
<TextBlock Grid.Column="4" Grid.ColumnSpan="2" HorizontalAlignment="Center" Margin="6,0" Text="Sample" VerticalAlignment="Center"/>
</Grid>
<ItemsControl Grid.Row="1" ItemsSource="{Binding mkt_break_items}" Margin="8,0">
<ItemsControl.ItemTemplate>
@@ -540,20 +801,28 @@
<ColumnDefinition SharedSizeGroup="c2"/>
<ColumnDefinition SharedSizeGroup="c3"/>
<ColumnDefinition SharedSizeGroup="c4"/>
<ColumnDefinition SharedSizeGroup="c5"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{u:BTSColor header}" Padding="6,3">
<TextBlock VerticalAlignment="Center" Text="{Binding name}" Foreground="{u:BTSColor header_text}"/>
</Border>
<CheckBox Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding show}"/>
<c:ColorPickerPopup
<CheckBox
Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsChecked="{Binding refresh_mode, Converter={StaticResource RefreshModeToBooleanConverter}, Mode=TwoWay}"
IsEnabled="{Binding can_edit_refresh_mode}"
/>
<c:ColorPickerPopup
Grid.Column="3"
BorderBrush="Transparent"
BorderThickness="0"
PopupPlacement="Left"
Palettes="{Binding palettes}"
SelectedColor="{Binding color}"
/>
<Grid Grid.Column="3" Grid.ColumnSpan="2" Height="27">
<Grid Grid.Column="4" Grid.ColumnSpan="2" Height="27">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
@@ -623,13 +892,16 @@
</Rectangle.Fill>
</Rectangle>
</Grid>
<Rectangle Grid.Column="1" Grid.ColumnSpan="5" Fill="DimGray" Height="1" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
<Rectangle Grid.Column="1" Grid.ColumnSpan="6" Fill="DimGray" Height="1" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</TabItem>
<TabItem Header="Fair Value Lines" VerticalAlignment="Stretch">
<usp:FairValueLineSettingsView HorizontalAlignment="Left" MaxHeight="300" DataContext="{Binding fair_value_line_settings_vm}"/>
</TabItem>
<!-- Market Price & Size Tab -->
<TabItem Header="Market Price and Size Display">
<Grid Margin="6" Grid.IsSharedSizeScope="True">
@@ -656,7 +928,7 @@
ItemsSource="{Binding format_options}"
SelectedItem="{Binding selected_format}"
/>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal">
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal" VerticalAlignment="Top">
<TextBlock
Margin="{StaticResource MarginIndentChild}"
Text="when implied is > 0"
@@ -670,6 +942,7 @@
IsEnabled="{Binding can_choose_alternate_implied_format}"
ItemsSource="{Binding alternate_implied_format_options}"
SelectedItem="{Binding selected_alternate_implied_format}"
VerticalAlignment="Top"
/>
</StackPanel>
<Border Grid.Column="2" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="12,4,0,0" Background="Transparent" BorderBrush="DimGray" BorderThickness="1" ToolTip="Sample Output">
@@ -728,7 +1001,38 @@
Visibility="{Binding is_synthetic, Converter={StaticResource NetChangeGroupBoxIsVisibleVisibilityConverter}}"
Padding="4"
>
<c:RadioButtonsControl ItemsSource="{Binding price_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_format}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush2}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock>Ticks Per Row</TextBlock>
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<c:TickUpDownUInt
Maximum="100"
TickUpCommand="{Binding DataContext.tick_up_ticks_per_row_cmd, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}}"
TickDownCommand="{Binding DataContext.tick_down_ticks_per_row_cmd, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}}"
Value="{Binding DataContext.ticks_per_row, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}}"
/>
<TextBlock
Grid.Column="1"
Margin="2,0,0,0"
Text="{Binding DataContext.rounded_ticks_per_row, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}, StringFormat='({0})'}"
ToolTip="The ticks per row parameter may need to be adjusted to fit evenly into dollars."
/>
</Grid>
<c:RadioButtonsControl Grid.Row="2" Grid.ColumnSpan="3" ItemsSource="{Binding price_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_format}"/>
</Grid>
</GroupBox>
<GroupBox
Grid.Row="1"
@@ -909,27 +1213,23 @@
<TextBlock VerticalAlignment="Center">Left Panel Width</TextBlock>
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="{Binding Source={StaticResource SliderValueTextWidth}}"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="2" Minimum="{x:Static sp:LadderView.MinLeftPanelWidth}" Maximum="{x:Static sp:LadderView.MaxLeftPanelWidth}" Value="{Binding order_entry_panel_width}"/>
<TextBlock Grid.Column="4" VerticalAlignment="Center" Text="{Binding order_entry_panel_width, StringFormat='0.00'}"/>
<Slider Grid.Column="0" Minimum="{x:Static sp:LadderView.MinLeftPanelWidth}" Maximum="{x:Static sp:LadderView.MaxLeftPanelWidth}" Value="{Binding order_entry_panel_width}"/>
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding order_entry_panel_width, StringFormat='0.00'}"/>
</Grid>
<!--Zoom Factor-->
<TextBlock Grid.Row="2" VerticalAlignment="Center">Zoom Factor</TextBlock>
<Grid Grid.Row="2" Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="{Binding Source={StaticResource SliderValueTextWidth}}"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="2" Minimum="{x:Static sp:LadderView.MinZoomFactor}" Maximum="{x:Static sp:LadderView.MaxZoomFactor}" Value="{Binding zoom_factor}"/>
<TextBlock Grid.Column="4" VerticalAlignment="Center" Text="{Binding zoom_factor, StringFormat='0.00'}"/>
<Slider Grid.Column="0" Minimum="{x:Static sp:LadderView.MinZoomFactor}" Maximum="{x:Static sp:LadderView.MaxZoomFactor}" Value="{Binding zoom_factor}"/>
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding zoom_factor, StringFormat='0.00'}"/>
</Grid>
<!--
- Note the 'Delays' in the 'SelectedItem' bindings; this is a workaround for a wpf quirk when the DataContext switches (i.e., when the selected Profile changes in the ShellSettingsDialog).
@@ -944,7 +1244,7 @@
<ComboBox Grid.Row="6" Grid.Column="2" ItemsSource="{Binding time_in_force_options}" SelectedItem="{Binding default_time_in_force, Delay=1}"/>
<!--Algo-->
<TextBlock Grid.Row="8" Grid.Column="0" VerticalAlignment="Center">Algo</TextBlock>
<ComboBox Grid.Row="8" Grid.Column="6" ItemsSource="{Binding Source={StaticResource AlgoDefinitionsViewSource}}" SelectedItem="{Binding default_algo, Delay=1}">
<ComboBox Grid.Row="8" Grid.Column="2" ItemsSource="{Binding Source={StaticResource AlgoDefinitionsViewSource}}" SelectedItem="{Binding default_algo, Delay=1}">
<ComboBox.GroupStyle>
<GroupStyle ContainerStyle="{StaticResource ComboBox.DelineatedGroupContainerStyle}"/>
</ComboBox.GroupStyle>
@@ -966,30 +1266,16 @@
</Grid>
</GroupBox>
<GroupBox Grid.Column="2" Header="Show/Hide" Padding="8,4">
<ItemsControl Grid.IsSharedSizeScope="True" ItemsSource="{Binding order_entry_panel_items}">
<ItemsControl
Grid.IsSharedSizeScope="True"
ItemsSource="{Binding order_entry_panel_items}"
ItemTemplateSelector="{StaticResource OrderEntryPanelItemTemplateSelector}"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" Focusable="False"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="A" />
</Grid.ColumnDefinitions>
<Border Margin="0,5,5,0">
<CheckBox Content="{Binding name}" FlowDirection="RightToLeft" IsChecked="{Binding show}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
</CheckBox.Resources>
</CheckBox>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
<!--
@@ -37,4 +37,10 @@ namespace mbts.ui.spark {
public static uint GetVolumeComponentValue(DependencyObject d) => (uint)d.GetValue(VolumeComponentValueProperty);
public static void SetVolumeComponentValue(DependencyObject d, uint value) => d.SetValue(VolumeComponentValueProperty, value);
}
class LadderSettingsPanelTemplateKeys {
public static string VisibilityItem = nameof(VisibilityItem);
public static string PricePoint = nameof(PricePoint);
public static string RelativePricePoint = nameof(RelativePricePoint);
}
}
@@ -126,9 +126,12 @@ namespace mbts.spark {
return Cursors.Arrow;
switch (column.column_type) {
case ColumnType.Bid:
case ColumnType.Ask: return _can_send_orders ? Cursors.Hand : Cursors.No;
default: return Cursors.Arrow;
case ColumnType.Ask:
if (!_can_send_orders)
return Cursors.No;
break;
}
return Cursors.Arrow;
}
#region UI Event Handlers
@@ -37,7 +37,7 @@
<converters:ResponsiveTextConverter x:Key="HighTextConverter" LongText="High:" ShortText="H:" Threshold="99" />
<converters:ResponsiveTextConverter x:Key="LowTextConverter" LongText="Low:" ShortText="L:" Threshold="99" />
<converters:ResponsiveTextConverter x:Key="OpenTextConverter" LongText="Open:" ShortText="O:" Threshold="99" />
<converters:ResponsiveTextConverter x:Key="SettleTextConverter" LongText="Settle:" ShortText="S:" Threshold="99" />
<converters:ResponsiveTextConverter x:Key="SettlementTextConverter" LongText="Settle:" ShortText="S:" Threshold="99" />
<converters:ResponsiveTextConverter x:Key="VolumeTextConverter" LongText="Volume:" ShortText="V:" Threshold="99" />
<converters:DoubleSubtractionConverter x:Key="DoubleSubtractionConverter" Value="4" />
<converters:DoubleSubtractionConverter x:Key="HeightDoubleSubtractionConverter" Value="6" />
@@ -141,7 +141,61 @@
<Setter Property="Margin" Value="2,0,2,5"/>
<Setter Property="Padding" Value="2"/>
</Style>
<Style x:Key="CancelAll" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Grid.Row" Value="2" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center">Cancel All</TextBlock>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Border"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{mbts:BTSColor UI.Button.normal_border}"
Background="{mbts:BTSColor UI.Button.normal_background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
Name="button_content"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{mbts:BTSColor UI.Button.mouseover_background}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{mbts:BTSColor UI.Button.mouseover_border}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TT_CancelAll" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Setter Property="Grid.Row" Value="0"/>
</Style>
<Style x:Key="TT_CancelBuys" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Setter Property="Grid.Row" Value="1" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center">Cancel Buys</TextBlock>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TT_CancelSells" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Setter Property="Grid.Row" Value="2" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center">Cancel Sells</TextBlock>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CancelBuys" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Grid.Row" Value="0" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Foreground="{Binding bid_foreground}">Cancel Buys</TextBlock>
@@ -177,6 +231,7 @@
</Style>
<Style x:Key="CancelSells" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Background" Value="{mbts:BTSColor MarketData.ask}" />
<Setter Property="Grid.Row" Value="1" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Foreground="{Binding ask_foreground}">Cancel Sells</TextBlock>
@@ -210,39 +265,6 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CancelAll" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center">Cancel All</TextBlock>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Border"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{mbts:BTSColor UI.Button.normal_border}"
Background="{mbts:BTSColor UI.Button.normal_background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
Name="button_content"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{mbts:BTSColor UI.Button.mouseover_background}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{mbts:BTSColor UI.Button.mouseover_border}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SettingsButton" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Content">
<Setter.Value>
@@ -277,6 +299,23 @@
</Setter.Value>
</Setter>
</Style>
<!-- Left Panel Price Points -->
<Style x:Key="LeftPanelPricePointLabel" TargetType="TextBlock">
<Setter Property="Margin" Value="0,1,0,4"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style x:Key="LeftPanelPricePointBorder" TargetType="Border">
<Setter Property="Grid.Column" Value="2"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="0,0,0,2"/>
<Setter Property="Padding" Value="1"/>
</Style>
<Style x:Key="LeftPanelPricePointText" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="{mbts:BTSColor UI.default_text}"/>
</Style>
<sys:Double x:Key="MaxZoomFactor">1.5</sys:Double>
<sys:Double x:Key="MinZoomFactor">0.5</sys:Double>
</ResourceDictionary>
@@ -345,107 +384,188 @@
<ColumnDefinition Width="6"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Day -->
<TextBlock Grid.Column="0" Grid.Row="0"
Text="{Binding left_panel_width, Converter={StaticResource DayTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_day, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Grid.Column="2" Grid.Row="0" FontWeight="Bold"
Margin="0,0,0,4"
Foreground="{Binding change, Converter={StaticResource NumericSignBrushConverter}}"
Visibility="{Binding show_day, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="change"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- High -->
<TextBlock Grid.Column="0" Grid.Row="1"
Text="{Binding left_panel_width, Converter={StaticResource HighTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_high, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Grid.Column="2" Grid.Row="1" Foreground="{mbts:BTSColor UI.default_text}" FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_high, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="high"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- Low -->
<TextBlock Grid.Column="0" Grid.Row="2"
Text="{Binding left_panel_width, Converter={StaticResource LowTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_low, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Grid.Column="2" Grid.Row="2" Foreground="{mbts:BTSColor UI.default_text}" FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_low, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="low"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- Open -->
<TextBlock Grid.Column="0" Grid.Row="3"
Text="{Binding left_panel_width, Converter={StaticResource OpenTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_open, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Grid.Column="2" Grid.Row="3" FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_open, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="open"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- Settle -->
<TextBlock Grid.Column="0" Grid.Row="4"
Text="{Binding left_panel_width, Converter={StaticResource SettleTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_settle, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Grid.Column="2" Grid.Row="4" FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_settle, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="settlement"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- Volume -->
<TextBlock Grid.Column="0" Grid.Row="5"
Text="{Binding left_panel_width, Converter={StaticResource VolumeTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<!-- Day Change -->
<TextBlock
Grid.Column="2"
Grid.Row="5"
FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"
Grid.Row="0"
Text="{Binding left_panel_width, Converter={StaticResource DayTextConverter}}"
Visibility="{Binding day_change_style.actual_show_label, Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource LeftPanelPricePointLabel}"
/>
<Border
Grid.Row="0"
Background="{Binding day_change_background_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Visibility="{Binding day_change_style.show, Converter={StaticResource BooleanToVisibilityConverter}}"
ToolTip="Day Change"
Style="{StaticResource LeftPanelPricePointBorder}"
>
<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>
<TextBlock
FontFamily="{Binding day_change_style.font_family}"
FontSize="{Binding day_change_style.font_size}"
FontWeight="{Binding day_change_style.font_weight}"
Foreground="{Binding day_change_font_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Style="{StaticResource LeftPanelPricePointText}"
>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="day_change"/>
<Binding Path="security.ContractSpec"/>
<Binding Path="day_change_display_settings"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Border>
<!-- Volume -->
<TextBlock
Grid.Column="0"
Grid.Row="1"
Text="{Binding left_panel_width, Converter={StaticResource VolumeTextConverter}}"
Visibility="{Binding volume_style.actual_show_label, Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource LeftPanelPricePointLabel}"
/>
<Border
Grid.Row="1"
Background="{Binding volume_style.background_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Visibility="{Binding volume_style.show, Converter={StaticResource BooleanToVisibilityConverter}}"
ToolTip="Volume"
Style="{StaticResource LeftPanelPricePointBorder}"
>
<TextBlock
FontFamily="{Binding volume_style.font_family}"
FontSize="{Binding volume_style.font_size}"
FontWeight="{Binding volume_style.font_weight}"
Foreground="{Binding volume_style.font_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Style="{StaticResource LeftPanelPricePointText}"
>
<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>
</Border>
<!-- High -->
<TextBlock
Grid.Row="2"
Text="{Binding left_panel_width, Converter={StaticResource HighTextConverter}}"
Visibility="{Binding session_high_style.actual_show_label, Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource LeftPanelPricePointLabel}"
/>
<Border
Grid.Row="2"
Background="{Binding session_high_style.background_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Visibility="{Binding session_high_style.show, Converter={StaticResource BooleanToVisibilityConverter}}"
ToolTip="Session High"
Style="{StaticResource LeftPanelPricePointBorder}"
>
<TextBlock
FontFamily="{Binding session_high_style.font_family}"
FontSize="{Binding session_high_style.font_size}"
FontWeight="{Binding session_high_style.font_weight}"
Foreground="{Binding session_high_style.font_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Style="{StaticResource LeftPanelPricePointText}"
>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="session_high"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Border>
<!-- Low -->
<TextBlock
Grid.Row="3"
Text="{Binding left_panel_width, Converter={StaticResource LowTextConverter}}"
Visibility="{Binding session_low_style.actual_show_label, Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource LeftPanelPricePointLabel}"
/>
<Border
Grid.Row="3"
Background="{Binding session_low_style.background_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Visibility="{Binding session_low_style.show, Converter={StaticResource BooleanToVisibilityConverter}}"
ToolTip="Session Low"
Style="{StaticResource LeftPanelPricePointBorder}"
>
<TextBlock
FontFamily="{Binding session_low_style.font_family}"
FontSize="{Binding session_low_style.font_size}"
FontWeight="{Binding session_low_style.font_weight}"
Foreground="{Binding session_low_style.font_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Style="{StaticResource LeftPanelPricePointText}"
>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="session_low"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Border>
<!-- Open -->
<TextBlock
Grid.Row="4"
Text="{Binding left_panel_width, Converter={StaticResource OpenTextConverter}}"
Visibility="{Binding open_style.actual_show_label, Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource LeftPanelPricePointLabel}"
/>
<Border
Grid.Row="4"
Background="{Binding open_style.background_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Visibility="{Binding open_style.show, Converter={StaticResource BooleanToVisibilityConverter}}"
ToolTip="Open"
Style="{StaticResource LeftPanelPricePointBorder}"
>
<TextBlock
FontFamily="{Binding open_style.font_family}"
FontSize="{Binding open_style.font_size}"
FontWeight="{Binding open_style.font_weight}"
Foreground="{Binding open_style.font_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Style="{StaticResource LeftPanelPricePointText}"
>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="open"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Border>
<!-- Settlement -->
<TextBlock
Grid.Row="5"
Text="{Binding left_panel_width, Converter={StaticResource SettlementTextConverter}}"
Visibility="{Binding settlement_style.actual_show_label, Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource LeftPanelPricePointLabel}"
/>
<Border
Grid.Row="5"
Background="{Binding settlement_style.background_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Visibility="{Binding settlement_style.show, Converter={StaticResource BooleanToVisibilityConverter}}"
ToolTip="Settlement"
Style="{StaticResource LeftPanelPricePointBorder}"
>
<TextBlock
FontFamily="{Binding settlement_style.font_family}"
FontSize="{Binding settlement_style.font_size}"
FontWeight="{Binding settlement_style.font_weight}"
Foreground="{Binding settlement_style.font_color, Converter={x:Static converters:ColorToBrushConverter.Instance}}"
Style="{StaticResource LeftPanelPricePointText}"
>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
<Binding Path="settlement"/>
<Binding Path="security.ContractSpec"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Border>
</Grid>
<!-- Account combobox -->
<ComboBox DataContext="{Binding order_properties_vm}" Margin="2,10,2,2"
@@ -571,15 +691,20 @@
</ItemsPanelTemplate>
</uag:AlgoTemplateFavoriteButtonsView.ItemsPanel>
</uag:AlgoTemplateFavoriteButtonsView>
<StackPanel Margin="0,17,0,0" VerticalAlignment="Top">
<Grid Margin="0,17,0,0" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Cancel Buttons -->
<Button Command="{Binding cancel_command}" CommandParameter="Bids" Style="{StaticResource CancelBuys}"
<Button Command="{Binding cancel_command}" CommandParameter="Bids" Style="{StaticResource {x:Static spark:LadderView.CancelBuysButtonStyleKey}}"
Visibility="{Binding show_cancel_buys, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Button Command="{Binding cancel_command}" CommandParameter="Asks" Style="{StaticResource CancelSells}"
<Button Command="{Binding cancel_command}" CommandParameter="Asks" Style="{StaticResource {x:Static spark:LadderView.CancelSellsButtonStyleKey}}"
Visibility="{Binding show_cancel_sells, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Button Command="{Binding cancel_command}" CommandParameter="All" Style="{StaticResource CancelAll}"
<Button Command="{Binding cancel_command}" CommandParameter="All" Style="{StaticResource {x:Static spark:LadderView.CancelAllButtonStyleKey}}"
Visibility="{Binding show_cancel_all, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</StackPanel>
</Grid>
<StackPanel Grid.Row="1" Margin="0,17,0,0" VerticalAlignment="Bottom">
<!-- Settings Button -->
<Button Command="{Binding settings_command}" Style="{StaticResource SettingsButton}"/>
@@ -22,6 +22,22 @@ namespace mbts.spark {
MaxLeftPanelWidth = 150,
MinZoomFactor = 0.5,
MaxZoomFactor = 1.5;
static LadderView() {
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
MimicTT = knobs.mimic_tt;
}
public static bool MimicTT { get; }
public static string CancelBuysButtonStyleKey =>
MimicTT ? "TT_CancelBuys" : "CancelBuys";
public static string CancelSellsButtonStyleKey =>
MimicTT ? "TT_CancelSells" : "CancelSells";
public static string CancelAllButtonStyleKey =>
MimicTT ? "TT_CancelAll" : "CancelAll";
public static IMultiValueConverter PositionToolTipConverter =
ValueConverter.create<AccountTuple?, bool, bool, bool, string>((account, show_day_pos, show_raw_day_pos, show_total_pos) => {
@@ -253,4 +269,4 @@ namespace mbts.spark {
}
}
}
}
@@ -4,6 +4,7 @@ using bts.api.orderman;
using bts.collections;
using bts.spark;
using bts.utils;
using bts.utils.csv;
using mbts.ui;
using mbts.ui.controls;
using System;
@@ -11,6 +12,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Windows.Input;
using System.Windows.Media;
using Columns = mbts.spark.OrderBookGridView.ColumnKeys;
@@ -60,6 +62,35 @@ namespace mbts.spark {
public event Action<(OrderSecurityRootPair oldval, OrderSecurityRootPair newval)> updated;
public event PropertyChangedEventHandler PropertyChanged;
public static System.Collections.Specialized.OrderedDictionary csv_ordered_properties =>
CsvHelper.create_ordering_maintainer(
typeof(OrderBookRowVM),
new string[] {
nameof(security_text),
nameof(symbol),
nameof(time_created),
nameof(last_change),
nameof(user),
nameof(account),
nameof(exchange),
nameof(time_in_force),
nameof(order_type),
nameof(root_algo_display_name),
nameof(order_sub_type),
nameof(side),
nameof(quantity),
nameof(filled_quantity),
nameof(price),
nameof(avg_price),
nameof(last_price),
nameof(piq),
nameof(status),
// last message (could contains commas, so skipping)
nameof(security_key),
nameof(order_key),
nameof(parent_order_key),
});
// Core
public OrderSecurityRootPair order_tuple { get; private set; }
public OrderRecordImmutable order => order_tuple.order.order;
@@ -52,6 +52,7 @@ namespace mbts.spark {
readonly CommonValueMonitor<SecurityImmutable?> _selected_security_mon = new CommonValueMonitor<SecurityImmutable?>();
readonly RightPanelModel _right_panel = new RightPanelModel();
readonly CustomStateSynchronizer _custom_state_sync;
readonly Func<OrderSecurityRootPair, bool> _global_filter;
OrderBookFillsGridSettings _cached_fills_settings = OrderBookStartupData.DefaultFillsGrid;
double
_raw_right_panel_width = OrderBookStartupData.Default.right_panel_width,
@@ -71,8 +72,8 @@ namespace mbts.spark {
FilterData _cached_filter_data;
public OrderBookVM(
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false)
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded) { }
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false, Func<OrderSecurityRootPair, bool> global_filter = null)
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded, global_filter) { }
public OrderBookVM(
IBus bus,
@@ -80,10 +81,12 @@ namespace mbts.spark {
OrdersModel model,
IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source,
IWindowSettingsManager<OrderBookGridSettings> settings_manager,
bool rows_are_initially_expanded = false) {
bool rows_are_initially_expanded = false,
Func<OrderSecurityRootPair, bool> global_filter = null) {
_bus = bus;
_model = model;
_global_filter = global_filter;
_selected_params_mon = new OrderParamsAggregator(model.dynamic_helper, _selection_change_throttle, k => _cached_orders.TryGetValue(k, out var o) ? o.order : (OrderSecurityAlgo?)null);
ladder_vm = new LadderVMContainer(bus, model);
order_mod_vm = new OrderParamsVM(this, AlgoEditingPermissions.AllowEditValues);
@@ -611,7 +614,7 @@ namespace mbts.spark {
// resume column filter updates
_columns.filter_changed += _on_filter_changed;
grid_settings_manager.algo_tree_display_mode_changed += _on_filter_changed;
var new_filter = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states());
var new_filter = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states(), _global_filter);
var new_sort = _columns.get_sorting_data();
if (!new_filter.Equals(_cached_filter_data)) {
_cached_filter_data = new_filter;
@@ -686,7 +689,7 @@ namespace mbts.spark {
}
void _on_filter_changed() {
var fd = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states());
var fd = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states(), _global_filter);
if (!fd.Equals(_cached_filter_data)) {
_cached_filter_data = fd;
_row_collection.filter = fd.build_predicate();
@@ -970,13 +973,25 @@ namespace mbts.spark {
struct FilterData : IEquatable<FilterData> {
public readonly AlgoTreeDisplayMode algo_tree_display_mode;
public readonly FilterStates column_filters;
public readonly Func<OrderSecurityRootPair, bool> global_filter;
public FilterData(AlgoTreeDisplayMode algo_tree_display_mode, FilterStates column_filters) {
public FilterData(AlgoTreeDisplayMode algo_tree_display_mode, FilterStates column_filters, Func<OrderSecurityRootPair, bool> global_filter) {
this.algo_tree_display_mode = algo_tree_display_mode;
this.column_filters = column_filters;
this.global_filter = global_filter;
}
public Func<OrderSecurityRootPair, bool> build_predicate() {
if (global_filter == null)
return _build_predicate_core();
var core_pred = _build_predicate_core();
if (core_pred == null)
return global_filter;
var glbf = global_filter;
return new Func<OrderSecurityRootPair, bool>(o => glbf(o) && core_pred(o));
}
Func<OrderSecurityRootPair, bool> _build_predicate_core() {
var pred = column_filters.build_predicate();
if (algo_tree_display_mode == AlgoTreeDisplayMode.RootAndLeafNodes) {
if (pred == null) {
@@ -6,6 +6,7 @@ using bts.shell.fills;
using bts.shell.orderbook;
using bts.spark;
using bts.view;
using bts.utils.csv;
using mbts.persist;
using mbts.spark.utilities;
using mbts.ui;
@@ -231,6 +232,7 @@ namespace mbts.spark {
toggle_collapse_all_command,
toggle_expand_all_command,
show_grid_settings_command,
new SparkCommandCommand("Export to csv", Icons.Export, FileDialogCommand.export_csv(() => _vm.rows.ToCsv(OrderBookRowVM.csv_ordered_properties), "OrderBook")),
// hidden cmds (i.e., keyboard shortcuts at window level)
hidden_cmd(new SparkCommandCommand(OrderBookView.CommandDescriptions.ModifyOrders, null, _vm.modify_selected_orders_cmd, OrderBookView.KeyboardShortcuts.ModifyOrders)),
hidden_cmd(new SparkCommandCommand(OrderBookView.CommandDescriptions.RevertOrders, null, _vm.revert_selected_orders_cmd, OrderBookView.KeyboardShortcuts.RevertOrders)),
@@ -633,7 +633,7 @@ namespace bts.shell.pnl {
case ColumnKeys.Exchange: filter.exchange = new[] { (Venue)group.Name }; break;
case ColumnKeys.Security:
if (group.Name is SecurityImmutable sec)
filter.security = new SecurityCriteria(sec);
filter.security = SecurityCriteria.from_securities(sec);
break;
default: throw new NotImplementedException();
}
@@ -696,7 +696,7 @@ namespace bts.shell.pnl {
case ColumnKeys.Exchange: filter.exchanges = new[] { (Venue)g.Name }; break;
case ColumnKeys.Security:
if (g.Name is SecurityImmutable sec)
filter.security_criteria = new SecurityCriteria(sec);
filter.security_criteria = SecurityCriteria.from_securities(sec);
break;
default: throw new NotImplementedException();
}
@@ -0,0 +1,105 @@
using bts.admin.risk;
using bts.api.core;
using bts.collections;
using mbts;
using mbts.ui;
using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using Component = bts.shell.risk_management.RiskLimitsImportConflictsView.RiskLimitsComponent;
namespace bts.shell.risk_management {
static class RiskLimitsImporter {
/// <summary>
/// Parses the specified file and passes the results to the <paramref name="on_parsed"/> delegate.
/// If the parser encounters improperly formatted values, a dialog is automatically opened
/// </summary>
/// <returns>An IDisposable that will close the import-conflicts-dialog (if it was launched).</returns>
public static IDisposable import_limits(string file_name, IReadOnlyDictionary<uint, SecurityMD> security_map, IReadOnlyDictionary<uint, string> account_map, Action<IEnumerable<ParsedRiskLimits>> on_parsed) {
if (string.IsNullOrEmpty(file_name))
throw new ArgumentException("File name is null or empty.", nameof(file_name));
if (security_map is null)
throw new ArgumentNullException(nameof(security_map));
if (account_map is null)
throw new ArgumentNullException(nameof(account_map));
if (on_parsed is null)
throw new ArgumentNullException(nameof(on_parsed));
IReadOnlyList<ParsedRiskLimits> parsed_limit_items;
try {
parsed_limit_items = RiskLimitsParser.parse_rows(
File.ReadAllLines(file_name),
MappedDictionary.create(security_map, sec_md => sec_md.security),
account_map)
.ToList();
} catch (Exception ex) {
BTSMessageBox.Error($"Error importing limits: {ex}.", RiskManagementWindow.window_type_display_name);
return null;
}
var conflicts = RiskLimitsParser.get_conflicts(parsed_limit_items);
if (conflicts.Count > 0) {
// init vm
var vm = new RiskLimitsImportVM(parsed_limit_items.Count) {
security_map = security_map
};
// add rows
foreach (var kv in conflicts) {
var resolve_del = RiskLimitsParser.assign_component(kv.Key);
foreach (var text_groups in kv.Value) {
var eval = get_error_value(kv.Key);
var row = new RiskLimitsImportVM.Row(kv.Key, text_groups.Key, text_groups.Value, eval, eval);
vm.rows.Add(row);
vm.import_request += () => {
foreach (var pmf in text_groups.Value) {
if (resolve_del(pmf.result, row.value) is AccountSecurityRiskLimits resolved)
pmf.result = resolved;
}
};
}
}
var win = new RiskLimitsImportConflictsDialog {
DataContext = vm,
Title = RiskManagementWindow.window_type_display_name,
Topmost = true
};
IDisposable dispose = null;
vm.RequestClose += (s, e) => Disposable.dispose(ref dispose);
vm.import_request += () => {
on_parsed(parsed_limit_items);
Disposable.dispose(ref dispose);
};
Disposable.dispose(ref dispose);
vm.accounts.SetItems(account_map.Select(kv => new AccountTuple(kv.Key, kv.Value)).Cast<AccountTuple?>());
dispose = Disposable.Create(
vm,
Disposable.Create(close));
win.move_to_mouse(-200, -75);
win.Show();
return dispose;
void close() => WindowUtilities.force_close(win);
object get_error_value(Component component) {
switch (component) {
case Component.MaxNetLongPosition:
case Component.MaxNetShortPosition:
case Component.ClipLimit:
return 0;
case Component.MinPrice:
case Component.MaxPrice:
return double.NaN;
case Component.IsPositionLimitEnabled:
case Component.IsClipLimitEnabled:
case Component.IsPriceLimitEnabled:
return false;
default: return null;
}
}
} else {
on_parsed(parsed_limit_items);
}
return null;
}
}
}
@@ -0,0 +1,250 @@
using bts.admin.risk;
using bts.api;
using bts.collections;
using bts.utils;
using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using RiskLimitsComponent = bts.shell.risk_management.RiskLimitsImportConflictsView.RiskLimitsComponent;
namespace bts.shell.risk_management {
class ParsedRiskLimits {
public readonly Guid unpersisted_id = Guid.NewGuid();
public AccountSecurityRiskLimits result { get; set; }
public (string text, bool success) security { get; set; }
public (string text, bool success) account { get; set; }
public (string text, bool success) max_long_position { get; set; }
public (string text, bool success) max_short_position { get; set; }
public (string text, bool success) is_position_limit_enabled { get; set; }
public (string text, bool success) clip_limit { get; set; }
public (string text, bool success) is_clip_limit_enabled { get; set; }
public (string text, bool success) min_price { get; set; }
public (string text, bool success) max_price { get; set; }
public (string text, bool success) is_price_limit_enabled { get; set; }
public bool Equals(ParsedRiskLimits other) => other != null && unpersisted_id == other.unpersisted_id;
public override bool Equals(object obj) => Equals(obj as ParsedRiskLimits);
public override int GetHashCode() => unpersisted_id.GetHashCode();
}
static class RiskLimitsParser {
public const string file_extension = "csv";
public static Func<AccountSecurityRiskLimits, object, object> assign_component(RiskLimitsComponent component) {
switch (component) {
case RiskLimitsComponent.Security: return (mf, val) => mf.with(key: new AccountSecurityKey(mf.account_key, ConvertUtils.try_convert<SecurityImmutable?>(val)?.Id ?? 0));
case RiskLimitsComponent.Account: return (mf, val) => mf.with(key: new AccountSecurityKey(ConvertUtils.try_convert<AccountTuple?>(val)?.id ?? 0, mf.key.security));
case RiskLimitsComponent.MaxNetLongPosition: return (mf, val) => mf.with(mf.limits.with(max_long_position: ConvertUtils.try_convert<uint>(val)));
case RiskLimitsComponent.MaxNetShortPosition: return (mf, val) => mf.with(mf.limits.with(max_short_position: ConvertUtils.try_convert<uint>(val)));
case RiskLimitsComponent.IsPositionLimitEnabled: return (mf, val) => mf.with(mf.limits.with(is_price_limit_enabled: ConvertUtils.try_convert<bool>(val)));
case RiskLimitsComponent.ClipLimit: return (mf, val) => mf.with(mf.limits.with(clip_limit: ConvertUtils.try_convert<uint>(val)));
case RiskLimitsComponent.IsClipLimitEnabled: return (mf, val) => mf.with(mf.limits.with(is_price_limit_enabled: ConvertUtils.try_convert<bool>(val)));
case RiskLimitsComponent.MinPrice: return (mf, val) => mf.with(mf.limits.with(min_price: ConvertUtils.try_convert(val, double.NaN)));
case RiskLimitsComponent.MaxPrice: return (mf, val) => mf.with(mf.limits.with(max_price: ConvertUtils.try_convert(val, double.NaN)));
case RiskLimitsComponent.IsPriceLimitEnabled: return (mf, val) => mf.with(mf.limits.with(is_price_limit_enabled: ConvertUtils.try_convert<bool>(val)));
default: throw new NotImplementedException();
}
}
public static ImmutableDictionary<RiskLimitsComponent, ImmutableDictionary<string, ImmutableList<ParsedRiskLimits>>> get_conflicts(IEnumerable<ParsedRiskLimits> parsed_limits) {
// Add all the components up front (a simple optimizatino since there aren't many),
// so there is no need to check for there existence as various conflicts are added.
// After all conflicts have been added, remove the empty entries.
var missing_components = EnumHelper.get_values<RiskLimitsComponent>()
.ToImmutableDictionary(c => c, c => ImmutableDictionary<string, ImmutableList<ParsedRiskLimits>>.Empty)
.ToBuilder();
foreach (var pmf in parsed_limits) {
if (!pmf.security.success) {
add_missing(RiskLimitsComponent.Security, pmf.security.text, pmf);
}
if (!pmf.account.success) {
add_missing(RiskLimitsComponent.Account, pmf.account.text, pmf);
}
if (!pmf.max_long_position.success) {
add_missing(RiskLimitsComponent.MaxNetLongPosition, pmf.max_long_position.text, pmf);
}
if (!pmf.max_short_position.success) {
add_missing(RiskLimitsComponent.MaxNetShortPosition, pmf.max_short_position.text, pmf);
}
if (!pmf.is_position_limit_enabled.success) {
add_missing(RiskLimitsComponent.IsPositionLimitEnabled, pmf.is_position_limit_enabled.text, pmf);
}
if (!pmf.clip_limit.success) {
add_missing(RiskLimitsComponent.ClipLimit, pmf.clip_limit.text, pmf);
}
if (!pmf.is_clip_limit_enabled.success) {
add_missing(RiskLimitsComponent.IsClipLimitEnabled, pmf.is_clip_limit_enabled.text, pmf);
}
if (!pmf.min_price.success) {
add_missing(RiskLimitsComponent.MinPrice, pmf.min_price.text, pmf);
}
if (!pmf.max_price.success) {
add_missing(RiskLimitsComponent.MaxPrice, pmf.max_price.text, pmf);
}
if (!pmf.is_price_limit_enabled.success) {
add_missing(RiskLimitsComponent.IsPriceLimitEnabled, pmf.is_price_limit_enabled.text, pmf);
}
}
// remove empty component conflicts
var empty_keys = missing_components.Where(kv => kv.Value.Count == 0).Select(kv => kv.Key).ToList();
missing_components.RemoveRange(empty_keys);
return missing_components.ToImmutable();
// local methods
void add_missing(RiskLimitsComponent component, string text, ParsedRiskLimits prl) {
if (!missing_components.TryGetValue(component, out var conflicts)) {
conflicts = ImmutableDictionary<string, ImmutableList<ParsedRiskLimits>>.Empty;
}
if (!conflicts.TryGetValue(text, out var affected)) {
affected = ImmutableList<ParsedRiskLimits>.Empty;
}
missing_components[component] = conflicts.SetItem(text, affected.Add(prl));
}
}
// This implementation prioritizes simplicity over speed.
// The security desc lookup could be better optimized, but the speed at which limits are imported is probably not a paramount concern.
public static IEnumerable<ParsedRiskLimits> parse_rows(IEnumerable<string> rows, IReadOnlyDictionary<uint, SecurityImmutable> security_map, IReadOnlyDictionary<uint, string> account_map) =>
parse_rows(rows,
reverse_lookup(MappedDictionary.create(security_map, s => s.ToString())),
reverse_lookup(account_map));
public static IEnumerable<ParsedRiskLimits> parse_rows(IEnumerable<string> rows, Func<string, uint> security_id_lookup, Func<string, uint> account_id_lookup) {
var columns = new[] {
(Headers.Security, Cells.security(security_id_lookup)),
(Headers.Account, Cells.account(account_id_lookup)),
(Headers.MaxNetLongPosition, Cells.max_long_position),
(Headers.MaxNetShortPosition, Cells.max_short_position),
(Headers.IsPositionLimitEnabled, Cells.is_position_limit_enabled),
(Headers.ClipLimit, Cells.clip_limit),
(Headers.IsClipLimitEnabled, Cells.is_clip_limit_enabled),
(Headers.MinPrice, Cells.min_price),
(Headers.MaxPrice, Cells.max_price),
(Headers.IsPriceLimitEnabled, Cells.is_price_limit_enabled)
}.ToDictionary(col => col.Item1, col => col.Item2.with_null_check());
return RowParser.parse_rows(rows, columns, Cells.delimiter);
}
public static Func<string, TKey> reverse_lookup<TKey>(IReadOnlyDictionary<TKey, string> map) {
var lookup = map.ToLookup(kv => kv.Value, kv => kv.Key);
return cell_text => lookup.Contains(cell_text) ? lookup[cell_text].First() : default;
}
static class Headers {
public const string
Security = "Security",
Account = "Account",
MaxNetLongPosition = "Max Long",
MaxNetShortPosition = "Max Short",
IsPositionLimitEnabled = "Enforce Position",
ClipLimit = "Clip",
IsClipLimitEnabled = "Enforce Clip",
MinPrice = "Min Price",
MaxPrice = "Max Price",
IsPriceLimitEnabled = "Enforce Price";
}
static class Cells {
public const char delimiter = ',';
public static RowParser.AssignColumnValue<ParsedRiskLimits> security(Func<string, uint> reverse_id_lookup) {
return impl;
void impl(ref ParsedRiskLimits pmf, string text) {
pmf.result = pmf.result.with(key: new AccountSecurityKey(pmf.result.key.account, reverse_id_lookup(text)));
pmf.security = (text, pmf.result.key.security != 0);
}
}
public static RowParser.AssignColumnValue<ParsedRiskLimits> account(Func<string, uint> reverse_id_lookup) {
return impl;
void impl(ref ParsedRiskLimits pmf, string text) {
pmf.result = pmf.result.with(key: new AccountSecurityKey(reverse_id_lookup(text), pmf.result.key.security));
pmf.account = (text, pmf.result.key.account != 0);
}
}
public static void max_long_position(ref ParsedRiskLimits pmf, string cell_text) {
if (uint.TryParse(cell_text, out uint v)) {
pmf.result = pmf.result.with(pmf.result.limits.with(max_long_position: v));
pmf.max_long_position = (cell_text, true);
} else {
pmf.max_long_position = (cell_text, false);
}
}
public static void max_short_position(ref ParsedRiskLimits pmf, string cell_text) {
if (uint.TryParse(cell_text, out uint v)) {
pmf.result = pmf.result.with(pmf.result.limits.with(max_short_position: v));
pmf.max_short_position = (cell_text, true);
} else {
pmf.max_short_position = (cell_text, false);
}
}
public static void is_position_limit_enabled(ref ParsedRiskLimits pmf, string cell_text) {
if (_try_parse_yes_no(cell_text, out var result)) {
pmf.result = pmf.result.with(pmf.result.limits.with(is_position_limit_enabled: result));
pmf.is_position_limit_enabled = (cell_text, true);
} else {
pmf.is_position_limit_enabled = (cell_text, false);
}
}
public static void clip_limit(ref ParsedRiskLimits pmf, string cell_text) {
if (uint.TryParse(cell_text, out uint v)) {
pmf.result = pmf.result.with(pmf.result.limits.with(clip_limit: v));
pmf.clip_limit = (cell_text, true);
} else {
pmf.clip_limit = (cell_text, false);
}
}
public static void is_clip_limit_enabled(ref ParsedRiskLimits pmf, string cell_text) {
if (_try_parse_yes_no(cell_text, out var result)) {
pmf.result = pmf.result.with(pmf.result.limits.with(is_clip_limit_enabled: result));
pmf.is_clip_limit_enabled = (cell_text, true);
} else {
pmf.is_clip_limit_enabled = (cell_text, false);
}
}
static bool _try_parse_yes_no(string text, out bool result) {
switch (text?.Trim()?.ToLower()) {
case "y": result = true; break;
case "n": result = false; break;
default: result = default; return false;
}
return true;
}
public static void min_price(ref ParsedRiskLimits pmf, string cell_text) {
if (double.TryParse(cell_text, out var v)) {
pmf.result = pmf.result.with(pmf.result.limits.with(min_price: v));
pmf.min_price = (cell_text, true);
} else {
pmf.min_price = (cell_text, false);
}
}
public static void max_price(ref ParsedRiskLimits pmf, string cell_text) {
if (double.TryParse(cell_text, out var v)) {
pmf.result = pmf.result.with(pmf.result.limits.with(max_price: v));
pmf.max_price = (cell_text, true);
} else {
pmf.max_price = (cell_text, false);
}
}
public static void is_price_limit_enabled(ref ParsedRiskLimits pmf, string cell_text) {
if (_try_parse_yes_no(cell_text, out var result)) {
pmf.result = pmf.result.with(pmf.result.limits.with(is_price_limit_enabled: result));
pmf.is_price_limit_enabled = (cell_text, true);
} else {
pmf.is_price_limit_enabled = (cell_text, false);
}
}
}
}
}
@@ -0,0 +1,230 @@
using bts.admin.risk;
using bts.api;
using bts.collections;
using bts.spark;
using bts.view;
using mbts;
using mbts.persist;
using mbts.spark;
using mbts.ui;
using mbts.ui.settings;
using mbts.ui.spark;
using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace bts.shell.risk_management {
class RiskManagementWindow : SparkWindow<RiskManagementSettings>, ISharedWindow {
public const string
window_type_id = "RiskManagement",
window_type_display_name = "Risk Management";
readonly IWindowSettingsManager<GridSettings> _settings_manager;
readonly AdminModel _model;
FrameworkElement _view;
IReadOnlyList<SparkCommand> _commands;
IDisposable _settings_sub, _securities_sub, _cmd_subscriptions;
RiskManagementSettings _settings;
RiskManagementVM _vm;
SimpleGridSettingsManagerVM<RiskManagementControl.Columns> _grid_settings_manager => _vm.grid_settings_manager;
SparkMenuDisplayVM _menu_display_vm;
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
public RiskManagementWindow(
IBus bus,
AdminModel model,
IWindowSettingsManager<GridSettings> settings_manager,
ISaveWindow<RiskManagementSettings> isw,
RiskManagementSettings settings)
: base(window_type_display_name, bus, isw, Icons.Risk) {
//
_settings_manager = settings_manager;
_settings = settings;
_model = model;
thread_id = mbts.config.Configuration.get<bts.config.MultiThreadedWindows>().risk_management ? window_type_id : null;
_securities_sub = bus.subscribe(SparkTags.SecuritiesTag).Subscribe(sec_map => _security_map = sec_map);
}
public override IEnumerable<SparkCommand> Commands => _commands;
public override string window_type => window_type_id;
public override RiskManagementSettings SaveState =>
(_vm?.create() ?? RiskManagementSettings.Default).with(menu_display: _menu_display_vm.safe_create());
public override UIElement Content => _view;
public override double MinWidth =>
RiskManagementSettings.DefaultGrid.columns.Where(c => c.is_visible).Sum(gcs => gcs.width)
+ SPARK_MENU_COLLAPSED_WIDTH
+ LAUNCHER_BUTTON_BORDER_WIDTH
+ EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH;
public string thread_id { get; }
public override void Dispose() {
Disposable.dispose(ref _cmd_subscriptions);
_commands.empty_if_null().OfType<IDisposable>().DisposeAll();
Disposable.dispose(ref _securities_sub);
Disposable.dispose(ref _settings_sub);
Disposable.dispose(ref _vm);
}
public void thread_init() {
// assert nothing has been initialized yet
Log.assert(_menu_display_vm == null, "MenuDisplay viewmodel has already been initialized.");
Log.assert(_vm == null, "Viewmodel has already been initialized.");
Log.assert(_settings != null, "Settings is null.");
Log.assert(_settings_sub == null, "Grid settings subscription has already been initialized.");
// initialize everything: menu_display_vm, vm, settings subscription, and view
_menu_display_vm = _init_menu_display_vm(_settings.menu_display);
_vm = _init_vm(_bus.subscribe(SparkTags.SecuritiesTag), _model, _settings);
_settings_sub = _settings_manager.subscribe_current()
.OnDispatcher()
.Subscribe(gs => _vm.load_from(gs));
(_commands, _cmd_subscriptions) = _mk_commands();
_view = new RiskManagementControl { DataContext = _vm };
}
protected override void OnParentViewCreated(SparkWindowView parent_view) =>
_menu_display_vm.safe_bind_to_view(parent_view);
static SparkMenuDisplayVM _init_menu_display_vm(SparkMenuDisplaySettings settings) {
var vm = new SparkMenuDisplayVM();
vm.load_from(settings);
return vm;
}
static RiskManagementVM _init_vm(IObservable<IReadOnlyDictionary<uint, SecurityMD>> securities_source, AdminModel model, RiskManagementSettings settings) {
var vm = new RiskManagementVM(model.account_security_risk_limits, model.on_account_tuples);
vm.request_update_limit += update_limit;
vm.load_from(settings);
var vm_sec_sub = securities_source.OnDispatcher().Subscribe(sec_map => vm.security_map = sec_map);
vm.OnDisposed += _ => Disposable.dispose(ref vm_sec_sub);
return vm;
void update_limit(AccountSecurityRiskLimits lim) {
model.set_risk_limits(
MappedDictionary.create(
vm.build_account_limits_map(), acc_sec_lims => acc_sec_lims.Select(asl => (asl.security_key, asl.limits))))
.ignore();
}
}
(List<SparkCommand> cmds, IDisposable subscriptions) _mk_commands() {
var subs = new List<IDisposable>();
var cmds = new List<SparkCommand>();
cmds.Add(new SparkCommandCommand("Add New Limits", Icons.AddBoxes, new DialogCommand(() => {
if (_vm is null)
return null;
var account_id = _vm.selected_account?.id ?? 0;
var init_state = new RiskLimitsModification(
is_position_limit_enabled: true,
is_clip_limit_enabled: true,
is_price_limit_enabled: true);
return mk_row_editor_dialog(init_state, commit, true);
void commit(RiskLimitsModification result) {
impl();
async void impl() {
var target_secs = await result.security_criteria.securities(_bus, TimeSpan.FromSeconds(5));
if (target_secs.Count > 0) {
var new_limits = result.apply(new RiskLimits());
await _model.add_or_update_risk_limits(account_id, target_secs.Keys.Select(k => (k, new_limits)));
}
}
}
})));
var edit_cmd = new DialogCommand(() => {
if (!(_vm?.selected_account).HasValue)
return null;
var account_id = _vm.selected_account.Value.id;
var targets = _vm.selected_rows.Select(r => r.data).ToList();
var init_state = RiskLimitsModification.aggregate_common_props(targets);
return mk_row_editor_dialog(init_state, commit, false);
void commit(RiskLimitsModification mod) =>
_vm.modify_rows(targets.WrapSelector(t => t.key, null), mod);
}, () => _vm.selected_rows.Count > 0, true, off_x: -300, off_y: -60);
_vm.selected_rows_changed += edit_cmd.RaiseCanExecuteChanged;
cmds.Add(new SparkCommandCommand("Edit Selected Rows", Icons.ModifyOrder, edit_cmd));
var revert_selected_rows_cmd = ObservantCommand.create(() => _vm?.revert(_vm.selected_rows), out var revert_selected_rows_observer);
subs.Add(_vm.subscribe_selected_rows()
.Select(srs => srs.Count > 0)
.And(_vm.subscribe_has_dirty_rows())
.OnDispatcher()
.Subscribe(revert_selected_rows_observer));
cmds.Add(new SparkCommandCommand("Revert Selected Rows", Icons.UndoCircular, revert_selected_rows_cmd));
cmds.Add(new SparkCommandCommand("Import Limits", Icons.Import,
FileDialogCommand.open_file(file => _vm?.import_limits(file), extensions: RiskLimitsParser.file_extension)));
var save_cmd = new SparkCommandCommand("Save", Icons.Disk, ObservantCommand.create(() => {
//_vm.selected_account.Value.id, _vm.rows.Where(r => r.)
var lims = _vm.build_account_limits_map();
var mlims = MappedDictionary.create(lims, l => l.Select(lim => (lim.security_key, lim.limits)));
_model.set_risk_limits(mlims).ignore();
}, out var save_observer));
cmds.Add(save_cmd);
subs.Add(_vm.subscribe_has_dirty_rows().OnDispatcher().Subscribe(save_observer));
var delete_cmd = new DelegateCommand(() => {
if (!(_vm?.selected_account).HasValue)
return;
var to_remove = _vm.selected_rows;
var account_id = _vm.selected_account.Value.id;
if (BTSMessageBox.Confirm($"Are you sure you want to delete the selected limits ({to_remove.Count})?", "Delete Limits")) {
// remove unpersisted limits first (i.e., imports), then delete the persisted ones.
foreach (var r in to_remove) {
if (!r.is_persisted) {
_vm.rows.RemoveSorted(r, _vm.row_comparer);
to_remove = to_remove.Remove(r);
}
}
_model.delete_risk_limits(account_id, to_remove.Select(r => r.security_key)).ignore();
}
}, () => _vm.selected_rows.Count > 0);
_vm.selected_rows_changed += delete_cmd.RaiseCanExecuteChanged;
cmds.Add(new SparkCommandCommand("Delete", Icons.Trash, delete_cmd));
// wrap the grid settings view factory in a dialogCommand here since the vm is destroyed/recreated when the security criteria changes
cmds.Add(SparkCommands.window_settings_command(new DialogCommand(() => {
if (_vm is null)
return null;
_settings_manager.instance_settings = _vm.create().grid;
var settings_vm = new ShellSettingsDialogVM<GridSettings, SimpleGridSettingsVM>(
_settings_manager,
us => us.risk_management_grid,
UserSettings.FileExtensions.GRID,
_grid_settings_manager.create_vm
);
settings_vm.SubscribeDisposed(literal_title_source.OnDispatcher().Subscribe(t => settings_vm.title = t));
var dialog = new ShellSettingsDialog { DataContext = settings_vm };
settings_vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog);
return dialog;
})));
cmds.Add(_menu_display_vm.spark_cmd);
return (cmds, Disposable.wrap_collection(subs));
// although only the api only requires security keys,
// the actual securities are used in this method so the user can see them in the view
Window mk_row_editor_dialog(RiskLimitsModification init_state, Action<RiskLimitsModification> commit, bool is_add) {
var limits_vm = new RiskLimitsModificationVM(is_add) {
security_map = _security_map
};
limits_vm.load_from(init_state);
limits_vm.request_commit += () => commit(limits_vm.create());
var (title, icon) = is_add ? ("New Limits", Icons.AddBoxes) : ("Edit Limits", Icons.ModifyOrder);
var dialog = new RiskLimitParametersDialog {
DataContext = limits_vm,
Icon = icon,
Title = title
};
dialog.SetBinding(ContentControl.ContentProperty, new Binding());
WindowUtilities.listen_for_close_requests(dialog);
return dialog;
}
}
}
}
@@ -0,0 +1,34 @@
using bts.api;
using mbts.persist;
using mbts.spark;
using mbts.ui;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace bts.shell.risk_management {
class RiskManagementWindowFactory : ShellWindowFactory<RiskManagementSettings> {
readonly IBus _bus;
readonly AdminModel _model;
readonly UserSettingsHub _settings_hub;
public RiskManagementWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, AdminModel model) : base(owner, "RiskManagement") {
_bus = bus;
_model = model;
_settings_hub = settings_hub;
_dispose = bus.subscribe(SparkTags.ShowRiskManagement)
.OnDispatcher()
.Subscribe(show);
async void show(RiskManagementSettings settings) {
var win = await load(settings);
bus.post(SparkTags.ShowWindow, win);
}
}
public override Task<IShellWindow> load(RiskManagementSettings state) =>
state is null ?
throw new ArgumentNullException(nameof(state)) :
Task.FromResult<IShellWindow>(new RiskManagementWindow(_bus, _model, _settings_hub.create_risk_management_settings_manager(state.grid), this, state));
}
}
@@ -0,0 +1,138 @@
using bts.admin.risk;
using bts.utils;
using mbts.spark.filters;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
namespace bts.shell.risk_management {
[ProtoContract]
class RiskManagementFilterSettings : IEquatable<RiskManagementFilterSettings> {
static readonly IEqualityComparer<RiskManagementFilterSettings> _comparer =
ObjectSnapshot.property_equality_comparer<RiskManagementFilterSettings>();
public RiskManagementFilterSettings() { }
public RiskManagementFilterSettings(
string security_regex,
string security_key_regex,
UIntMinMaxState max_long_position,
UIntMinMaxState max_short_position,
bool? is_position_limit_enabled,
UIntMinMaxState clip_limit,
bool? is_clip_limit_enabled,
DoubleMinMaxState min_price,
DoubleMinMaxState max_price,
bool? is_price_limit_enabled) {
// security
this.security_regex = security_regex;
this.security_key_regex = security_key_regex;
// position
this.max_long_position = max_long_position;
this.max_short_position = max_short_position;
this.is_position_limit_enabled = is_position_limit_enabled;
// clip
this.clip_limit = clip_limit;
this.is_clip_limit_enabled = is_clip_limit_enabled;
// price
this.min_price = min_price;
this.max_price = max_price;
this.is_price_limit_enabled = is_price_limit_enabled;
}
// security
[ProtoMember(1)]
public string security_regex { get; }
[ProtoMember(2)]
public string security_key_regex { get; }
// position
[ProtoMember(3)]
public UIntMinMaxState max_long_position { get; }
[ProtoMember(4)]
public UIntMinMaxState max_short_position { get; }
[ProtoMember(5)]
public bool? is_position_limit_enabled { get; }
// clip
[ProtoMember(6)]
public UIntMinMaxState clip_limit { get; }
[ProtoMember(7)]
public bool? is_clip_limit_enabled { get; }
// price
[ProtoMember(8)]
public DoubleMinMaxState min_price { get; }
[ProtoMember(9)]
public DoubleMinMaxState max_price { get; }
[ProtoMember(10)]
public bool? is_price_limit_enabled { get; }
// 'is limit enabled' items (for filter dropdown)
public bool[] is_position_limit_enabled_items => yes_no_filter_items(is_position_limit_enabled);
public bool[] is_clip_limit_enabled_items => yes_no_filter_items(is_clip_limit_enabled);
public bool[] is_price_limit_enabled_items => yes_no_filter_items(is_price_limit_enabled);
public static bool[] yes_no_filter_items(bool? state) =>
state.HasValue ? new[] { state.Value } : Array.Empty<bool>();
public static bool? yes_no_filter_state(IEnumerable<bool> selected_items) {
var set = new HashSet<bool>(selected_items);
return set.Count == 1 ? set.First() : (bool?)null;
}
public Func<AccountSecurityRiskLimits, bool> build_predicate() {
var pb = new ColumnPredicateBuilder<AccountSecurityRiskLimits>();
// security
pb.add_regex(security_regex, l => l.security.ToString());
pb.add_regex(security_key_regex, l => l.security_key.ToString());
// position
pb.add_numeric(max_long_position, l => l.limits.max_long_position);
pb.add_numeric(max_short_position, l => l.limits.max_short_position);
pb.add_discrete_values(is_position_limit_enabled_items, l => l.limits.is_position_limit_enabled);
// clip
pb.add_numeric(clip_limit, l => l.limits.clip_limit);
pb.add_discrete_values(is_clip_limit_enabled_items, l => l.limits.is_clip_limit_enabled);
// price
pb.add_numeric(min_price, l => l.limits.min_price);
pb.add_numeric(max_price, l => l.limits.max_price);
pb.add_discrete_values(is_price_limit_enabled_items, l => l.limits.is_price_limit_enabled);
return pb.to_predicate();
}
public RiskManagementFilterSettings with(
string security_regex = null,
string security_key_regex = null,
UIntMinMaxState max_long_position = null,
UIntMinMaxState max_short_position = null,
OptionalNullable<bool>? is_position_limit_enabled = null,
UIntMinMaxState clip_limit = null,
OptionalNullable<bool>? is_clip_limit_enabled = null,
DoubleMinMaxState min_price = null,
DoubleMinMaxState max_price = null,
OptionalNullable<bool>? is_price_limit_enabled = null) =>
//
new RiskManagementFilterSettings(
// security
security_regex ?? this.security_regex,
security_key_regex ?? this.security_key_regex,
// position
max_long_position ?? this.max_long_position,
max_short_position ?? this.max_short_position,
is_position_limit_enabled.HasValue ? is_position_limit_enabled.Value.value : this.is_position_limit_enabled,
// clip
clip_limit ?? this.clip_limit,
is_clip_limit_enabled.HasValue ? is_clip_limit_enabled.Value.value : this.is_clip_limit_enabled,
// price
min_price ?? this.min_price,
max_price ?? this.max_price,
is_price_limit_enabled.HasValue ? is_price_limit_enabled.Value.value : this.is_price_limit_enabled);
public bool Equals(RiskManagementFilterSettings other) =>
_comparer.Equals(this, other);
public override bool Equals(object obj) =>
obj is RiskManagementFilterSettings other && Equals(other);
public override int GetHashCode() =>
_comparer.GetHashCode(this);
}
}
@@ -0,0 +1,93 @@
using mbts.ui;
using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using Columns = bts.shell.risk_management.RiskManagementControl.Columns;
using ColumnWidths = mbts.ui.controls.DIYGrid.DefaultColumnWidths;
using ColumnHeaders = mbts.ui.controls.DIYGrid.DefaultColumnHeaders;
namespace bts.shell.risk_management {
[ProtoContract]
class RiskManagementSettings {
[ProtoMember(3)]
SortingCriteria[] _sorting_criteria;
public RiskManagementSettings() { }
public RiskManagementSettings(
GridSettings grid = null,
RiskManagementFilterSettings filters = default,
IEnumerable<SortingCriteria> sorting_criteria = null,
uint account_id = 0,
SparkMenuDisplaySettings? menu_display = null) {
//
this.grid = grid;
this.filters = filters ?? new RiskManagementFilterSettings();
_sorting_criteria = sorting_criteria?.ToArray();
this.account_id = account_id;
this.menu_display = menu_display ?? SparkMenuDisplaySettings.Default;
}
public static RiskManagementSettings Default => new RiskManagementSettings(grid: DefaultGrid);
public static GridSettings DefaultGrid => new GridSettings(columns: DefaultColumns.all);
[ProtoMember(1)]
public GridSettings grid { get; }
[ProtoMember(2)]
public RiskManagementFilterSettings filters { get; }
[ProtoMember(4)]
public uint account_id { get; }
[ProtoMember(5)]
public SparkMenuDisplaySettings menu_display { get; }
public IReadOnlyList<SortingCriteria> sorting_criteria =>
_sorting_criteria ?? Array.Empty<SortingCriteria>();
public RiskManagementSettings with(
GridSettings grid = null,
RiskManagementFilterSettings filters = default,
IEnumerable<SortingCriteria> sorting_criteria = null,
uint? account_id = null,
SparkMenuDisplaySettings? menu_display = null) =>
//
new RiskManagementSettings(
grid ?? this.grid,
filters ?? this.filters,
sorting_criteria ?? this.sorting_criteria,
account_id ?? this.account_id,
menu_display ?? this.menu_display);
public static class DefaultColumns {
public static GridColumnSettings security => new GridColumnSettings(Columns.Security.ToString(), ColumnHeaders.Security, width: ColumnWidths.SecurityKey);
public static GridColumnSettings security_key => new GridColumnSettings(Columns.SecurityKey.ToString(), ColumnHeaders.SecurityKey, width: ColumnWidths.SecurityKey, is_visible: false);
// position
public static GridColumnSettings max_long => new GridColumnSettings(Columns.MaxNetLongPosition.ToString(), "Max Long", width: 60, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings max_short => new GridColumnSettings(Columns.MaxNetShortPosition.ToString(), "Max Short", width: 60, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings is_position_limit_enabled => new GridColumnSettings(Columns.IsPositionLimitEnabled.ToString(), "Enforce Pos", width: 70, horizontal_alignment: HorizontalAlignment.Center);
// clip
public static GridColumnSettings clip_limit => new GridColumnSettings(Columns.ClipLimit.ToString(), "Clip Limit", horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings is_clip_limit_enabled => new GridColumnSettings(Columns.IsClipLimitEnabled.ToString(), "Enforce Clip", width: 70, horizontal_alignment: HorizontalAlignment.Center);
// price
public static GridColumnSettings min_price => new GridColumnSettings(Columns.MinPrice.ToString(), "Min Price", horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings max_price => new GridColumnSettings(Columns.MaxPrice.ToString(), "Max Price", horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings is_price_limit_enabled => new GridColumnSettings(Columns.IsPriceLimitEnabled.ToString(), "Enforce Price", width: 75, horizontal_alignment: HorizontalAlignment.Center);
public static IEnumerable<GridColumnSettings> all {
get {
yield return security;
yield return max_long;
yield return max_short;
yield return is_position_limit_enabled;
yield return clip_limit;
yield return is_clip_limit_enabled;
yield return min_price;
yield return max_price;
yield return is_price_limit_enabled;
yield return security_key;
}
}
}
}
}
@@ -0,0 +1,105 @@
using bts.api;
using bts.collections;
using mbts;
using mbts.collections;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Data;
using System.Windows.Input;
using Component = bts.shell.risk_management.RiskLimitsImportConflictsView.RiskLimitsComponent;
namespace bts.shell.risk_management {
class RiskLimitsImportVM : DisposableViewModelBase, IRequestViewClose {
readonly RefCountMap<ParsedRiskLimits> _conflicts = new RefCountMap<ParsedRiskLimits>();
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
public RiskLimitsImportVM(int num_limits) {
rows_view = CollectionViewSource.GetDefaultView(rows);
ok_cmd = new DelegateCommand(() => import_request?.Invoke());
cancel_cmd = new DelegateCommand(() => RequestClose?.Invoke(this, new EventArgs()));
this.num_limits = num_limits;
_dispose = ObservableAutoWrapper.connect_item_hooks(rows, r =>
r.subscribe_has_error().Subscribe(has_error => {
if (has_error) {
if (r.affected_limits.Count(_conflicts.increment_adds_key) == 0)
return;
} else if (r.affected_limits.Count(_conflicts.decrement_removes_key) == 0)
return;
RaisePropertyChanged(nameof(num_successfully_parsed_limits));
}));
}
public event Action import_request;
public event EventHandler RequestClose;
public ObservableCollection<Row> rows { get; } = new ObservableCollection<Row>();
public ICollectionView rows_view { get; }
public int num_limits { get; }
public int num_successfully_parsed_limits => num_limits - _conflicts.Count;
// misc items sources
public IEnumerable<Side> sides => new[] { Side.Buy, Side.Sell };
public ObservableCollection<AccountTuple?> accounts { get; } = new ObservableCollection<AccountTuple?>();
public IReadOnlyDictionary<uint, SecurityMD> security_map {
get => _security_map;
set {
if (!ReferenceEquals(_security_map, value)) {
_security_map = value;
_resolve_securities();
RaisePropertyChanged();
}
}
}
// commands
public ICommand ok_cmd { get; }
public ICommand cancel_cmd { get; }
void _resolve_securities() {
if (_security_map == null)
return;
var reverse_lookup = RiskLimitsParser.reverse_lookup(MappedDictionary.create(_security_map, v => v.security.ToString()));
foreach (var r in rows) {
if (r.component == Component.Security && r.has_error) {
var id = reverse_lookup(r.text);
if (id != 0 && _security_map.TryGetValue(id, out var smd))
r.value = smd.security;
}
}
}
public class Row : ViewModelBase, ITemplateSelectorKey {
readonly DelegateProperty<bool> _has_error;
object _value;
string _text;
public Row(Component component, string text, IReadOnlyCollection<ParsedRiskLimits> affected_limits, object error_value, object value) {
this.component = component;
this.text = text;
this.affected_limits = affected_limits;
_value = value;
_has_error = backing(nameof(has_error), () => Equals(this.value, error_value));
}
public Component component { get; }
public string text { get => _text; set => Set(ref _text, value); }
public IReadOnlyCollection<ParsedRiskLimits> affected_limits { get; }
public object value {
get => _value;
set {
if (Set(ref _value, value))
_has_error.refresh();
}
}
public bool has_error => _has_error.Value;
string ITemplateSelectorKey.key => component.ToString();
public IObservable<bool> subscribe_has_error() => _has_error.observe_current(true);
}
}
}
@@ -0,0 +1,201 @@
using bts.admin.risk;
using bts.api;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace bts.shell.risk_management {
class RiskLimitsModificationVM : ViewModelBase, IValueFactory<RiskLimitsModification>, IRequestViewClose {
readonly DeferrableActionWrapper _result_changed;
RiskLimitsModification _modifications;
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
public RiskLimitsModificationVM(bool can_edit_security) {
_result_changed = new DeferrableActionWrapper(() => result_changed?.Invoke());
this.can_edit_security = can_edit_security;
commit_cmd = new DelegateCommand(commit);
cancel_cmd = new DelegateCommand(close);
//
void commit() {
request_commit?.Invoke();
close();
}
void close() =>
RequestClose?.Invoke(this, EventArgs.Empty);
}
public event Action result_changed, request_commit;
public event EventHandler RequestClose;
public SecurityImmutable? security { get => _modifications.security; set => _set_limits(_modifications.with(security: value)); }
public SecurityCriteria security_criteria {
get => _modifications.security_criteria ?? (security.HasValue ? SecurityCriteria.from_securities(security.Value) : null);
set => _set_limits(_modifications.with(security_criteria: value));
}
public IReadOnlyDictionary<uint, SecurityMD> security_map {
get => _security_map;
set {
_security_map = value;
RaisePropertyChanged();
}
}
public bool can_edit_security { get; }
// position
public uint? max_long_position { get => _modifications.max_long_position; set => _set_limits(_modifications.with(max_long_position: value)); }
public uint? max_short_position { get => _modifications.max_short_position; set => _set_limits(_modifications.with(max_short_position: value)); }
public bool? is_position_limit_enabled { get => _modifications.is_position_limit_enabled; set => _set_limits(_modifications.with(is_position_limit_enabled: value)); }
// clip
public bool? is_clip_limit_enabled { get => _modifications.is_clip_limit_enabled; set => _set_limits(_modifications.with(is_clip_limit_enabled: value)); }
public uint? clip_limit { get => _modifications.clip_limit; set => _set_limits(_modifications.with(clip_limit: value)); }
// price
public double? max_price { get => _modifications.max_price; set => _set_limits(_modifications.with(max_price: value)); }
public double? min_price { get => _modifications.min_price; set => _set_limits(_modifications.with(min_price: value)); }
public bool? is_price_limit_enabled { get => _modifications.is_price_limit_enabled; set => _set_limits(_modifications.with(is_price_limit_enabled: value)); }
public ICommand commit_cmd { get; }
public ICommand cancel_cmd { get; }
public RiskLimitsModification create() =>
_modifications;
public void load_from(RiskLimitsModification value) {
using (_result_changed.defer_execution())
using (detect_property_changes())
_modifications = value;
}
void _set_limits(RiskLimitsModification new_modifications, [CallerMemberName] string prop = "") =>
_set_limits(new_modifications, new[] { prop });
void _set_limits(RiskLimitsModification new_modifications, params string[] invalidate_props) {
if (!_modifications.Equals(new_modifications)) {
_modifications = new_modifications;
RaisePropertyChanged(invalidate_props);
_result_changed.execute();
}
}
}
readonly struct RiskLimitsModification {
public RiskLimitsModification(
SecurityImmutable? security = null,
SecurityCriteria security_criteria = null,
AccountTuple? account = null,
uint? max_long_position = null,
uint? max_short_position = null,
bool? is_position_limit_enabled = null,
uint? clip_limit = null,
bool? is_clip_limit_enabled = null,
double? min_price = null,
double? max_price = null,
bool? is_price_limit_enabled = null) {
//
this.security = security;
this.security_criteria = security_criteria;
this.account = account;
this.max_long_position = max_long_position;
this.max_short_position = max_short_position;
this.is_position_limit_enabled = is_position_limit_enabled;
this.clip_limit = clip_limit;
this.is_clip_limit_enabled = is_clip_limit_enabled;
this.min_price = min_price;
this.max_price = max_price;
this.is_price_limit_enabled = is_price_limit_enabled;
}
public SecurityImmutable? security { get; }
public SecurityCriteria security_criteria { get; }
public AccountTuple? account { get; }
// position
public uint? max_long_position { get; }
public uint? max_short_position { get; }
public bool? is_position_limit_enabled { get; }
// clip
public uint? clip_limit { get; }
public bool? is_clip_limit_enabled { get; }
// price
public double? min_price { get; }
public double? max_price { get; }
public bool? is_price_limit_enabled { get; }
public static RiskLimitsModification aggregate_common_props(IEnumerable<AccountSecurityRiskLimits> limit_items) =>
new RiskLimitsModification(
_common_value(limit_items, l => l.security, s => s),
null,
_common_value(limit_items, l => new AccountTuple(l.account_key, l.account_name)),
_common_value(limit_items, l => l.limits.max_long_position),
_common_value(limit_items, l => l.limits.max_short_position),
_common_value(limit_items, l => l.limits.is_position_limit_enabled),
_common_value(limit_items, l => l.limits.clip_limit),
_common_value(limit_items, l => l.limits.is_clip_limit_enabled),
_common_value(limit_items, l => l.limits.min_price),
_common_value(limit_items, l => l.limits.max_price),
_common_value(limit_items, l => l.limits.is_price_limit_enabled));
public RiskLimits apply(RiskLimits target) =>
new RiskLimits(
is_position_limit_enabled: is_position_limit_enabled ?? target.is_position_limit_enabled,
max_long_position: max_long_position ?? target.max_long_position,
max_short_position: max_short_position ?? target.max_short_position,
is_clip_limit_enabled: is_clip_limit_enabled ?? target.is_clip_limit_enabled,
clip_limit: clip_limit ?? target.clip_limit,
is_price_limit_enabled: is_price_limit_enabled ?? target.is_price_limit_enabled,
min_price: min_price ?? target.min_price,
max_price: max_price ?? target.max_price);
public RiskLimitsModification with(RiskLimitsModification other_mod) =>
with(
other_mod.security.HasValue ? other_mod.security.Value : security,
other_mod.account.HasValue ? other_mod.account.Value : account,
security_criteria: other_mod.security_criteria ?? security_criteria,
max_long_position: other_mod.max_long_position ?? max_long_position,
max_short_position: other_mod.max_short_position ?? max_short_position,
is_position_limit_enabled: other_mod.is_position_limit_enabled ?? is_position_limit_enabled,
clip_limit: other_mod.clip_limit ?? clip_limit,
is_clip_limit_enabled: other_mod.is_clip_limit_enabled ?? is_clip_limit_enabled,
min_price: other_mod.min_price ?? min_price,
max_price: other_mod.max_price ?? max_price,
is_price_limit_enabled: other_mod.is_price_limit_enabled ?? is_price_limit_enabled);
//new RiskLimitsModification(security: other_mod.security ?? security,
// security_criteria: other_mod.security ?? security);
public RiskLimitsModification with(
OptionalNullable<SecurityImmutable>? security = null,
OptionalNullable<AccountTuple>? account = null,
SecurityCriteria security_criteria = null,
OptionalNullable<uint>? max_long_position = null,
OptionalNullable<uint>? max_short_position = null,
OptionalNullable<bool>? is_position_limit_enabled = null,
OptionalNullable<uint>? clip_limit = null,
OptionalNullable<bool>? is_clip_limit_enabled = null,
OptionalNullable<double>? min_price = null,
OptionalNullable<double>? max_price = null,
OptionalNullable<bool>? is_price_limit_enabled = null) =>
//
new RiskLimitsModification(
security.HasValue ? security.Value.value : this.security,
security_criteria ?? this.security_criteria,
account.HasValue ? account.Value.value : this.account,
max_long_position.HasValue ? max_long_position.Value.value : this.max_long_position,
max_short_position.HasValue ? max_short_position.Value.value : this.max_short_position,
is_position_limit_enabled.HasValue ? is_position_limit_enabled.Value.value : this.is_position_limit_enabled,
clip_limit.HasValue ? clip_limit.Value.value : this.clip_limit,
is_clip_limit_enabled.HasValue ? is_clip_limit_enabled.Value.value : this.is_clip_limit_enabled,
min_price.HasValue ? min_price.Value.value : this.min_price,
max_price.HasValue ? max_price.Value.value : this.max_price,
is_price_limit_enabled.HasValue ? is_price_limit_enabled.Value.value : this.is_price_limit_enabled);
static T? _common_value<T>(IEnumerable<AccountSecurityRiskLimits> source, Func<AccountSecurityRiskLimits, T> get) where T : struct =>
_common_value(source, get, common => new T?(common));
static TResult _common_value<T, TResult>(IEnumerable<AccountSecurityRiskLimits> source, Func<AccountSecurityRiskLimits, T> get, Func<T, TResult> convert_common, TResult default_value = default) =>
source.Select(get).Distinct().TryGetSingle(out var common) ? convert_common(common) : default_value;
}
}
@@ -0,0 +1,115 @@
using bts.admin.risk;
using bts.api;
using bts.utils;
using mbts.ui;
using mbts.ui.shell;
using System;
namespace bts.shell.risk_management {
class RiskLimitsVM : ViewModelBase, IEquatable<RiskLimitsVM> {
RiskLimitsVM(AccountSecurityKey id) =>
this.id = id;
public event Action request_commit;
/// The user can modify some props directly in the grid,
/// which may or may not give the row a new sort-position.
/// For this reason, the owner-viewmodel (<see cref="RiskManagementVM"/>)
/// is notified both immediately before, and after the changes are reflected in <see cref="data"/>.
/// By responding to those events, the owner-viewmodel can safely process any
/// sorting-changes without breaking binary search functionality.
/// <see cref="value_changing"/>, <see cref="value_changed"/>
/// <summary>
/// This event is raised just before a new value is assigned to <see cref="data"/>.
/// </summary>
public event Action<(AccountSecurityRiskLimits old_value, AccountSecurityRiskLimits new_value)> value_changing;
/// <summary>
/// This event is raised immediately after a new value is assigned to <see cref="data"/>.
/// </summary>
public event Action<(AccountSecurityRiskLimits old_value, AccountSecurityRiskLimits new_value)> value_changed;
public event Action is_dirty_changed;
public AccountSecurityKey id { get; }
public SecurityImmutable? security => data.security;
public string security_text => is_persisted ? security.ToString() : $"*{security}";
public uint security_key => data.security_key;
public string account_name => data.account_name;
public uint account_key => data.key.account;
// position limit
public bool is_position_limit_enabled { get => _limits.is_position_limit_enabled; set => _modify(new RiskLimitsModification(is_position_limit_enabled: value)); }
public uint max_long_position { get => _limits.max_long_position; set => _modify(new RiskLimitsModification(max_long_position: value)); }
public uint max_short_position { get => _limits.max_short_position; set => _modify(new RiskLimitsModification(max_short_position: value)); }
// clip limit
public bool is_clip_limit_enabled { get => _limits.is_clip_limit_enabled; set => _modify(new RiskLimitsModification(is_clip_limit_enabled: value)); }
public uint clip_limit { get => _limits.clip_limit; set => _modify(new RiskLimitsModification(clip_limit: value)); }
// price limit
public bool is_price_limit_enabled { get => _limits.is_price_limit_enabled; set => _modify(new RiskLimitsModification(is_price_limit_enabled: value)); }
public double min_price { get => _limits.min_price; set => _modify(new RiskLimitsModification(min_price: value)); }
public double max_price { get => _limits.max_price; set => _modify(new RiskLimitsModification(max_price: value)); }
public bool is_persisted => committed_data.HasValue;
public bool is_dirty => !committed_data.HasValue || !committed_data.Value.Equals(data);
public AccountSecurityRiskLimits data { get; private set; }
public AccountSecurityRiskLimits? committed_data { get; private set; }
RiskLimits _limits => data.limits;
public static RiskLimitsVM create_placeholder(AccountSecurityRiskLimits state) {
var vm = new RiskLimitsVM(state.key);
vm.load(state, false);
return vm;
}
public static RiskLimitsVM create_persisted(AccountSecurityRiskLimits state) {
var vm = new RiskLimitsVM(state.key);
vm.load(state, true);
return vm;
}
public void load(AccountSecurityRiskLimits new_state, bool is_committed) {
using (detect_property_changes()) {
bool was_dirty = is_dirty;
if (is_committed)
committed_data = new_state;
if (!data.Equals(new_state)) {
value_changing?.Invoke((data, new_state));
var orig_state = data;
data = new_state;
value_changed?.Invoke((orig_state, new_state));
}
if (is_dirty != was_dirty)
is_dirty_changed?.Invoke();
}
}
public bool Equals(RiskLimitsVM other) =>
other != null && id == other.id;
public override bool Equals(object obj) =>
Equals(obj as RiskLimitsVM);
public override int GetHashCode() =>
id.GetHashCode();
void _modify(RiskLimitsModification mod) {
var new_value = data.with(limits: mod.apply(data.limits));
if (committed_data.HasValue) {
// if the user just toggled one of the limits on/off, request an immediate update to persist the change
// (the on/off toggles are the only props that can be edited directly in the grid,
// the user might find it annoying if they have to click 'Save' just to turn the limits on/off.
var clims = committed_data.Value.limits;
load(new_value, false);
var revert_mod = new RiskLimitsModification(is_position_limit_enabled: clims.is_position_limit_enabled, is_clip_limit_enabled: clims.is_clip_limit_enabled, is_price_limit_enabled: clims.is_price_limit_enabled);
var reverted_lims = new_value = new_value.with(limits: revert_mod.apply(new_value.limits));
if (reverted_lims.limits.Equals(clims)) {
// since only the limit toggle props were reverted,
// the above equality indicates that only the limit toggle props were modified.
request_commit?.Invoke();
}
} else {
load(new_value, false);
}
}
}
}
@@ -0,0 +1,478 @@
using bts.admin.risk;
using bts.api.core;
using bts.collections;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Controls;
using Columns = bts.shell.risk_management.RiskManagementControl.Columns;
namespace bts.shell.risk_management {
class RiskManagementColumnModel : DIYGridColumnModel<Columns, AccountSecurityRiskLimits, RiskManagementFilterSettings> {
public RiskManagementColumnModel(Columns id, IComparer<AccountSecurityRiskLimits> data_comparer, IDataGridColumnFilter<AccountSecurityRiskLimits> filter, string filter_template, Func<RiskManagementFilterSettings, object> get_column_filter_settings, Func<RiskManagementFilterSettings, object, RiskManagementFilterSettings> set_column_filter_settings)
: base(id, data_comparer, filter, filter_template, get_column_filter_settings, set_column_filter_settings) {
}
}
class RiskManagementVM : DisposableViewModelBase, IHandleGridSorting, IHandleRowSelection, IValueFactory<RiskManagementSettings> {
static readonly IComparer<AccountSecurityRiskLimits> _data_row_id_comparer =
new ComparerBuilder<AccountSecurityKey>(new[] {
Comparer<uint>.Default.wrap<uint, AccountSecurityKey>(ask => ask.account),
Comparer<uint>.Default.wrap<uint, AccountSecurityKey>(ask => ask.security)
}).to_comparer()
.wrap<AccountSecurityKey, AccountSecurityRiskLimits>(r => r.key);
static IComparer<RiskLimitsVM> _row_id_comparer =>
_data_row_id_comparer.wrap<AccountSecurityRiskLimits, RiskLimitsVM>(r => r.data);
readonly DeferrableActionWrapper _refresh_filter_changed, _selected_rows_changed, _dirty_rows_changed;
readonly ColumnSortManager _sort_manager;
readonly ObservableCollection<RiskManagementColumnModel> _columns = new ObservableCollection<RiskManagementColumnModel>();
readonly Dictionary<Columns, RiskManagementColumnModel> _column_map = new Dictionary<Columns, RiskManagementColumnModel>();
readonly SelectorVM<AccountTuple?, uint> _accounts_vm = new SelectorVM<AccountTuple?, uint>(acc => acc?.id ?? 0) { comparer = Comparer<AccountTuple>.Default.to_nullable() };
IList<AccountSecurityRiskLimits> _data_rows;
ImmutableSortedSet<RiskLimitsVM> _selected_rows = ImmutableSortedSet.Create(_row_id_comparer);
IComparer<AccountSecurityRiskLimits> _data_comparer = _data_row_id_comparer;
IComparer<RiskLimitsVM> _row_comparer;
IDisposable _import_limits_op;
ImmutableDictionary<AccountSecurityKey, RiskLimitsVM> _dirty_rows = ImmutableDictionary<AccountSecurityKey, RiskLimitsVM>.Empty;
Dictionary<AccountSecurityKey, RiskLimitsVM> _imported_row_map = new Dictionary<AccountSecurityKey, RiskLimitsVM>();
Dictionary<AccountSecurityKey, RiskLimitsVM> _row_cache = new Dictionary<AccountSecurityKey, RiskLimitsVM>();
public RiskManagementVM(IObservable<TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>> data_source, IObservable<IReadOnlyList<AccountTuple>> accounts_source) {
_selected_rows_changed = new DeferrableActionWrapper(() => selected_rows_changed?.Invoke());
_refresh_filter_changed = new DeferrableActionWrapper(() => _filter_changed?.Invoke(_mk_filter()));
_dirty_rows_changed = new DeferrableActionWrapper(() => dirty_rows_changed?.Invoke(dirty_rows));
_sort_manager = new ColumnSortManager(_columns.OfType<ISortableDIYGridColumn>(), null);
_sort_manager.active_sorting_changed += () =>
data_comparer = _sort_manager.BuildComparer(id => _find_column(id)?.row_comparer, _data_comparer);
_data_rows = rows.WrapSelector(r => r.data, null);
_accounts_vm.selected_item_changed += () => {
RaisePropertyChanged(nameof(selected_account));
_refresh_filter_changed.execute();
};
column_map = FunctionalIndex.create<string, RiskManagementColumnModel>(_find_column);
grid_settings_manager = new SimpleGridSettingsManagerVM<Columns>(
_columns, null, ordered_cols => {
CollectionHelper.sync_with_never_duplicate(_columns, ordered_cols, cs => {
throw new InvalidOperationException();
}, c => c.id.ToString(), cs => cs.id);
}, null);
/////////////////
// Columns
/////////////////
// security
mk_column_model(
Columns.Security,
wrap_comp(r => r.security),
new RegexFilterVM<AccountSecurityRiskLimits>(r => r.security.ToString(), null),
DataGridFilterKnownTemplates.Regex,
fs => fs.security_regex,
(fs, v) => fs.with(security_regex: v));
// security key
mk_column_model(
Columns.SecurityKey,
wrap_comp(r => r.security_key),
new RegexFilterVM<AccountSecurityRiskLimits>(r => r.security_key.ToString(), null),
DataGridFilterKnownTemplates.Regex,
fs => fs.security_key_regex,
(fs, v) => fs.with(security_key_regex: v));
// max long
mk_column_model(
Columns.MaxNetLongPosition,
wrap_comp(r => r.limits.max_long_position),
new UIntMinMax<AccountSecurityRiskLimits>(
r => r.limits.max_long_position,
null),
DataGridFilterKnownTemplates.UIntMinMax,
fs => fs.max_long_position,
(fs, v) => fs.with(max_long_position: v));
// max short
mk_column_model(
Columns.MaxNetShortPosition,
wrap_comp(r => r.limits.max_short_position),
new UIntMinMax<AccountSecurityRiskLimits>(
r => r.limits.max_short_position,
null),
DataGridFilterKnownTemplates.UIntMinMax,
fs => fs.max_short_position,
(fs, v) => fs.with(max_short_position: v));
// is position limit enabled
mk_is_enabled_column(
Columns.IsPositionLimitEnabled,
r => r.limits.is_position_limit_enabled,
(fs, yes_no_items) => fs.with(is_position_limit_enabled: RiskManagementFilterSettings.yes_no_filter_state(yes_no_items)),
nameof(RiskLimitsVM.is_position_limit_enabled));
// clip size
mk_column_model(
Columns.ClipLimit,
wrap_comp(r => r.limits.clip_limit),
new UIntMinMax<AccountSecurityRiskLimits>(r => r.limits.clip_limit, null),
DataGridFilterKnownTemplates.UIntMinMax,
fs => fs.clip_limit,
(fs, v) => fs.with(clip_limit: v));
// is clip limit enabled
mk_is_enabled_column(
Columns.IsClipLimitEnabled,
r => r.limits.is_clip_limit_enabled,
(fs, yes_no_items) => fs.with(is_clip_limit_enabled: RiskManagementFilterSettings.yes_no_filter_state(yes_no_items)),
nameof(RiskLimitsVM.is_clip_limit_enabled));
// min price
mk_column_model(
Columns.MinPrice,
wrap_comp(r => r.limits.min_price),
new DoubleMinMax<AccountSecurityRiskLimits>(r => r.limits.min_price, nameof(RiskLimitsVM.min_price)),
DataGridFilterKnownTemplates.DoubleMinMax,
fs => fs.min_price,
(fs, v) => fs.with(min_price: v));
// max price
mk_column_model(
Columns.MaxPrice,
wrap_comp(r => r.limits.max_price),
new DoubleMinMax<AccountSecurityRiskLimits>(r => r.limits.max_price, nameof(RiskLimitsVM.max_price)),
DataGridFilterKnownTemplates.DoubleMinMax,
fs => fs.max_price,
(fs, v) => fs.with(max_price: v));
// is price limit enabled
mk_is_enabled_column(
Columns.IsPriceLimitEnabled,
r => r.limits.is_price_limit_enabled,
(fs, yes_no_items) => fs.with(is_price_limit_enabled: RiskManagementFilterSettings.yes_no_filter_state(yes_no_items)),
nameof(RiskLimitsVM.is_price_limit_enabled));
data_comparer = new ComparerBuilder<AccountSecurityRiskLimits>(new[] {
wrap_comp(r => r.security),
_data_row_id_comparer
}).to_comparer();
_column_map = _columns.ToDictionary(c => c.id);
columns = ObservableAutoWrapper.SelectObservable(_columns, c => c);
var filter_source =
Observable.FromEvent<Func<AccountSecurityRiskLimits, bool>>(cb => _filter_changed += cb, cb => _filter_changed -= cb)
.StartWith(_mk_filter);
var filtered_data_source =
data_source.Filter(filter_source, AccountSecurityRiskLimitsDataHelper.key_fn);
var filter_hooks = _columns.Select(c => {
var f = c.filter;
f.filter_changed += _refresh_filter_changed.execute;
return Disposable.Create(() => f.filter_changed -= _refresh_filter_changed.execute);
}).ToList();
_accounts_vm.selected_item_changed += on_selected_account_changed;
_dispose = Disposable.Create(
ObservableAutoWrapper.connect_item_hooks(rows, subscribe_row_changes),
accounts_source.OnDispatcher().Subscribe(on_accounts),
Disposable.wrap_collection(filter_hooks),
filtered_data_source.OnDispatcher().Subscribe(on_data));
//////////////////
// local methods
//////////////////
IComparer<AccountSecurityRiskLimits> wrap_comp<T>(Func<AccountSecurityRiskLimits, T> get_value) =>
Comparer<T>.Default.wrap<T, AccountSecurityRiskLimits>(get_value);
RiskManagementColumnModel mk_column_model<T>(
Columns id,
IComparer<AccountSecurityRiskLimits> row_comp,
IDataGridColumnPersistentFilter<AccountSecurityRiskLimits, T> filter,
string filter_template,
Func<RiskManagementFilterSettings, T> get_filter_settings,
Func<RiskManagementFilterSettings, T, RiskManagementFilterSettings> set_filter_settings) {
//
var col = new RiskManagementColumnModel(id, row_comp,
filter,
filter_template,
fs => get_filter_settings(fs),
(fs, val) => val is T cfs ? set_filter_settings(fs, cfs) : fs);
_columns.Add(col);
return col;
}
// the 'is enabled' columns are special cases, bc their filters produces an array of bools
// (cheap way to reuse the discrete values filter template for true/false items),
// but the filter protobuf has a nullable<bool>, so an extra conversion must be done explicitly (hence the get_row_val param).
RiskManagementColumnModel mk_is_enabled_column(Columns id, Func<AccountSecurityRiskLimits, bool> get_row_val, Func<RiskManagementFilterSettings, IEnumerable<bool>, RiskManagementFilterSettings> set_filter_prop, string prop) {
var col = new RiskManagementColumnModel(
id,
wrap_comp(get_row_val),
DiscreteValuesFilterVM.Create(new[] { true, false }, get_row_val, prop),
DataGridFilterKnownTemplates.YesNo,
fs => RiskManagementFilterSettings.yes_no_filter_items(fs.is_position_limit_enabled),
(fs, fstate) => set_filter_prop(fs, fstate as IEnumerable<bool>));
_columns.Add(col);
return col;
}
IDisposable subscribe_row_changes(RiskLimitsVM row) {
row.value_changing += on_value_changing;
row.value_changed += on_value_changed;
row.request_commit += on_request_commit;
check_dirty();
return Disposable.Create(() => {
row.value_changing -= on_value_changing;
row.value_changed -= on_value_changed;
row.request_commit -= on_request_commit;
});
void on_value_changing((AccountSecurityRiskLimits old_value, AccountSecurityRiskLimits new_value) change) =>
on_row_data_changed(new Change<AccountSecurityRiskLimits>(change.old_value, change.new_value), false);
void on_value_changed((AccountSecurityRiskLimits old_value, AccountSecurityRiskLimits new_value) change) =>
check_dirty();
void on_request_commit() =>
request_update_limit?.Invoke(row.data);
void check_dirty() {
int dr_count = _dirty_rows.Count;
_dirty_rows = row.is_dirty ? _dirty_rows.Add(row.id, row) : _dirty_rows.Remove(row.id);
if (dr_count != _dirty_rows.Count)
_dirty_rows_changed.execute();
}
}
void on_accounts(IReadOnlyList<AccountTuple> new_accounts) =>
_accounts_vm.update_items(new_accounts.Cast<AccountTuple?>());
void on_data(TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey> table) {
using (_dirty_rows_changed.defer_execution())
using (_selected_rows_changed.defer_execution()) {
foreach (var lim in table.removed) {
if (_row_cache.TryGetValue(lim.key, out var row)) {
_data_rows.RemoveSorted(row.data, data_comparer);
if (row.committed_data.HasValue)
_row_cache.Remove(lim.key);
}
}
foreach (var ch in table.changes) {
on_row_data_changed(ch, true);
}
foreach (var lim in table.added) {
if (_row_cache.TryGetValue(lim.key, out var row)) {
// if row is already here, it must be an import
// overwrite the import with the persisted row (this probably won't happen in practice)
rows.RemoveSorted(row, row_comparer);
_imported_row_map.Remove(lim.key);
}
var new_row = RiskLimitsVM.create_persisted(lim);
_row_cache[lim.key] = new_row;
rows.InsertSorted(new_row, row_comparer);
}
}
}
void on_row_data_changed(Change<AccountSecurityRiskLimits> change, bool is_persisted) {
Log.assert(_row_cache.TryGetValue(change.Old.key, out var row));
int old_index = CollectionHelper.binary_search(rows, row, row_comparer);
Log.assert(old_index >= 0);
int new_index = CollectionHelper.binary_search(_data_rows, change.New, _data_comparer);
// if the change came from the server (i.e., a persisted change that the row-vm doesn't already know about),
// load the new data (current modifications will be overwritten).
// Otherwise, the change is presumably the result of an uncommitted user-edit, which the row must already know about.
if (is_persisted) {
row.load(change.New, true);
if (_dirty_rows.Remove(row.id, out _dirty_rows))
_dirty_rows_changed.execute();
}
if (new_index < 0)
new_index = ~new_index;
if (Math.Abs(new_index - old_index) > 1) {
if (new_index > old_index)
new_index--;
rows.Move(old_index, new_index);
}
}
void on_selected_account_changed() {
foreach (var ir in _imported_row_map.Values) {
if (ir.account_key == selected_account?.id) {
rows.InsertOrReplaceSorted(ir, row_comparer);
} else {
rows.RemoveSorted(ir, row_comparer);
}
}
}
}
public event Action selected_rows_changed;
public event Action<AccountSecurityRiskLimits> request_update_limit;
public event Action<ImmutableDictionary<AccountSecurityKey, RiskLimitsVM>> dirty_rows_changed;
event Action<Func<AccountSecurityRiskLimits, bool>> _filter_changed;
public IReadOnlyIndex<string, RiskManagementColumnModel> column_map { get; }
public bool has_unpersisted_limits => false;
// this collection is needed to control the column order
public IReadOnlyObservableCollection<IDIYGridColumn> columns { get; }
public ObservableCollection<RiskLimitsVM> rows { get; } = new ObservableCollection<RiskLimitsVM>();
/// <summary>
/// The selected rows are provided as an <see cref="ImmutableSortedSet{T}"/> so they can be observed/accessed on different threads.
/// <see cref="RiskManagementVMExtensions.subscribe_selected_rows(RiskManagementVM)"/>.
/// </summary>
public ImmutableSortedSet<RiskLimitsVM> selected_rows => _selected_rows;
/// <summary>
/// The dirty rows are provided as an <see cref="ImmutableDictionary{TKey, TValue}"/> so they can be observed/accessed on different threads.
/// <see cref="RiskManagementVMExtensions.subscribe_has_dirty_rows(RiskManagementVM)(RiskManagementVM)"/>.
/// </summary>
public ImmutableDictionary<AccountSecurityKey, RiskLimitsVM> dirty_rows => _dirty_rows;
public IReadOnlyObservableCollection<AccountTuple?> accounts => _accounts_vm.items;
public AccountTuple? selected_account {
get => _accounts_vm.selected_item ?? default;
set => _accounts_vm.selected_item = value;
}
public SimpleGridSettingsManagerVM<Columns> grid_settings_manager { get; }
public IComparer<AccountSecurityRiskLimits> data_comparer {
get => _data_comparer;
private set {
if (value != null) {
_data_comparer = value;
_row_comparer = null;
rows.sync_with(rows.Ordered(row_comparer));
_selected_rows = _selected_rows.WithComparer(row_comparer);
}
}
}
public IComparer<RiskLimitsVM> row_comparer =>
_row_comparer ?? (_row_comparer = data_comparer?.wrap<AccountSecurityRiskLimits, RiskLimitsVM>(r => r.data));
public IReadOnlyDictionary<uint, SecurityMD> security_map { get; set; } = DictionaryHelper.empty<uint, SecurityMD>();
public IReadOnlyDictionary<uint, IEnumerable<AccountSecurityRiskLimits>> build_account_limits_map() =>
_data_rows.GroupBy(dr => dr.account_key)
.ToDictionary(g => g.Key, g => g.AsEnumerable());
public RiskManagementSettings create() {
var grid = grid_settings_manager.create();
var filters = _columns.Aggregate(new RiskManagementFilterSettings(), (fs, c) => c.assign_filter_settings(fs));
return new RiskManagementSettings(grid, filters, _sort_manager.SortingCriteria, selected_account?.id ?? 0);
}
public void load_from(RiskManagementSettings settings) {
if (settings is null)
throw new ArgumentNullException(nameof(settings));
if (settings.grid != null)
grid_settings_manager.load_from(settings.grid);
_sort_manager.SortingCriteria = settings.sorting_criteria;
using (_refresh_filter_changed.defer_execution()) {
foreach (var col in _columns)
col.load_from(null, settings.filters);
_accounts_vm.try_select(settings.account_id, true);
}
}
public void load_from(GridSettings grid) {
if (grid is null)
throw new ArgumentNullException(nameof(grid));
grid_settings_manager.load_from(grid);
}
public void modify_rows(IEnumerable<AccountSecurityKey> targets, RiskLimitsModification mod) {
foreach (var key in targets) {
if (_row_cache.TryGetValue(key, out var row)) {
var new_data = row.data.with(limits: mod.apply(row.data.limits));
row.load(new_data, false);
}
}
}
public void revert(IEnumerable<RiskLimitsVM> targets) {
foreach (var row in targets) {
if (row.committed_data.HasValue)
row.load(row.committed_data.Value, true);
}
}
public void import_limits(string file_name) {
Disposable.dispose(ref _import_limits_op);
_import_limits_op = RiskLimitsImporter.import_limits(file_name,
security_map,
accounts.Where(a => a.HasValue)
.ToDictionary(a => a.Value.id, a => a.Value.name),
add_imported_rows);
void add_imported_rows(IEnumerable<ParsedRiskLimits> imported_rows) {
foreach (var ir in imported_rows) {
var res = ir.result;
if (!res.security.HasValue && security_map.TryGetValue(res.security_key, out var sec_md))
res = res.with(security: sec_md.security);
if (string.IsNullOrEmpty(res.account_name) && accounts.TryGetFirst(a => a?.id == res.account_key, out var acc))
res = res.with(account: acc);
if (_row_cache.TryGetValue(res.key, out var old_row))
rows.RemoveSorted(old_row, row_comparer);
var new_row = RiskLimitsVM.create_placeholder(res);
_row_cache[res.key] = new_row;
_imported_row_map[res.key] = new_row;
if (new_row.account_key == selected_account?.id)
rows.InsertSorted(new_row, row_comparer);
}
}
}
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
var bd = _selected_rows.ToBuilder();
RowSelectionHelper.apply_changes(bd, e);
_selected_rows = bd.ToImmutable();
_selected_rows_changed.execute();
}
void IHandleGridSorting.on_sorting(object sender, DIYGridSortingEventArgs e) {
_sort_manager.OnColumnClick(e.Column, KeyboardExtensions.IsShiftDown());
e.Handled = true;
}
RiskManagementColumnModel _find_column(string key_str) =>
Enum.TryParse(key_str, true, out Columns key) ? _find_column(key) : null;
RiskManagementColumnModel _find_column(Columns key) =>
_column_map.GetValueOrDefault(key);
Func<AccountSecurityRiskLimits, bool> _mk_filter() {
var col_filter_pred = create().filters.build_predicate();
if (!selected_account.HasValue)
return lim => false;
var acc_id = selected_account.Value.id;
return lim => lim.account_key == acc_id && (col_filter_pred is null || col_filter_pred(lim));
}
}
static class RiskManagementVMExtensions {
public static IObservable<ImmutableSortedSet<RiskLimitsVM>> subscribe_selected_rows(this RiskManagementVM vm) =>
vm is null ?
throw new ArgumentNullException(nameof(vm)) :
Observable.Create<ImmutableSortedSet<RiskLimitsVM>>(o => {
o.OnNext(vm.selected_rows);
vm.selected_rows_changed += on_selected_rows_changed;
return Disposable.Create(() => vm.selected_rows_changed -= on_selected_rows_changed);
void on_selected_rows_changed() =>
o.OnNext(vm.selected_rows);
})
.SubscribeOnDispatcher();
public static IObservable<bool> subscribe_has_dirty_rows(this RiskManagementVM vm) =>
vm is null ?
throw new ArgumentNullException(nameof(vm)) :
Observable.Create<bool>(o => {
bool last_fired = vm.dirty_rows.Count > 0;
o.OnNext(last_fired);
vm.dirty_rows_changed += on_dirty_rows_changed;
return Disposable.Create(() => vm.dirty_rows_changed -= on_dirty_rows_changed);
void on_dirty_rows_changed(ImmutableDictionary<AccountSecurityKey, RiskLimitsVM> drs) {
if (last_fired != drs.Count > 0)
o.OnNext(last_fired = !last_fired);
}
})
.SubscribeOnDispatcher();
}
}
@@ -0,0 +1,111 @@
<Window
x:Class="bts.shell.risk_management.RiskLimitParametersDialog"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:risk="clr-namespace:bts.shell.risk_management"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
u:WindowStyle.AllowResize="False"
u:WindowStyle.SizeToContent="WidthAndHeight"
FocusVisualStyle="{x:Null}"
Style="{StaticResource BTSDialog}"
>
<Window.Resources>
<sys:Double x:Key="HorizontalPush">4</sys:Double>
<sys:Double x:Key="VerticalPush3">8</sys:Double>
<Thickness x:Key="ParamMargin" Left="{StaticResource HorizontalPush}"/>
<Style x:Key="HeaderText" TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{x:Static u:BTSColors.header_text_brush}"/>
<Setter Property="Margin" Value="4"/>
</Style>
<Style x:Key="LimitToggle" TargetType="CheckBox">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</Window.Resources>
<Window.ContentTemplate>
<DataTemplate DataType="{x:Type risk:RiskLimitsModificationVM}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush3}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.ColumnSpan="17" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="{x:Static u:BTSColors.header_brush}"/>
<TextBlock Grid.Column="0" Text="Security" Visibility="{Binding can_edit_security, Converter={StaticResource BooleanToVisibilityConverter}}" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="2" Text="Max Long" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="4" Text="Max Short" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="6" Text="Clip Limit" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="8" Text="Min Price" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="10" Text="Max Price" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="12" Text="Position Limit Enabled" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="14" Text="Clip Limit Enabled" Style="{StaticResource HeaderText}"/>
<TextBlock Grid.Column="16" Text="Price Limit Enabled" Style="{StaticResource HeaderText}"/>
<c:ContractPickerPopup
Grid.Row="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CloseOnCancel="True"
CloseOnOK="True"
Header="{Binding security_criteria, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}}"
Visibility="{Binding can_edit_security, Converter={StaticResource BooleanToVisibilityConverter}}"
IsEditMode="True"
IsLazy="True"
SecurityMap="{Binding security_map}"
SelectedCriteria="{Binding security_criteria}"
ShowDialogSkirt="True"
ToolTip="{Binding security_criteria, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}}"
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityHandler}"
/>
<c:TickUpDownNullableUInt Grid.Row="2" Grid.Column="2" Value="{Binding max_long_position}"/>
<c:TickUpDownNullableUInt Grid.Row="2" Grid.Column="4" Value="{Binding max_short_position}"/>
<c:TickUpDownNullableUInt Grid.Row="2" Grid.Column="6" Value="{Binding clip_limit}"/>
<c:TickUpDownNullableDouble Grid.Row="2" Grid.Column="8" Value="{Binding min_price}"/>
<c:TickUpDownNullableDouble Grid.Row="2" Grid.Column="10" Value="{Binding max_price}"/>
<CheckBox Grid.Row="2" Grid.Column="12" IsChecked="{Binding is_position_limit_enabled}" Style="{StaticResource LimitToggle}"/>
<CheckBox Grid.Row="2" Grid.Column="14" IsChecked="{Binding is_clip_limit_enabled}" Style="{StaticResource LimitToggle}"/>
<CheckBox Grid.Row="2" Grid.Column="16" IsChecked="{Binding is_price_limit_enabled}" Style="{StaticResource LimitToggle}"/>
</Grid>
<c:DialogSkirt Grid.Row="1" Margin="0,6,0,0">
<c:DialogSkirt.RightItems>
<x:Array Type="{x:Type Button}">
<Button Content="OK" Command="{Binding commit_cmd}" IsDefault="True"/>
<Button Content="Cancel" Command="{Binding cancel_cmd}" IsCancel="True"/>
</x:Array>
</c:DialogSkirt.RightItems>
</c:DialogSkirt>
</Grid>
</DataTemplate>
</Window.ContentTemplate>
</Window>
@@ -0,0 +1,11 @@
using System.Windows;
namespace bts.shell.risk_management {
/// <summary>
/// Interaction logic for RiskLimitParametersDialog.xaml
/// </summary>
partial class RiskLimitParametersDialog : Window {
public RiskLimitParametersDialog() =>
InitializeComponent();
}
}
@@ -0,0 +1,39 @@
<Window
x:Class="bts.shell.risk_management.RiskLimitsImportConflictsDialog"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:risk="clr-namespace:bts.shell.risk_management"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
u:WindowStyle.AllowResize="True"
u:WindowStyle.SizeToContent="WidthAndHeight"
u:WindowStyle.TitleContent="Import Limits Conflicts"
Style="{StaticResource BTSDialogTitleTemplate}"
>
<u:WindowStyle.TitleContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Image Height="12" Margin="5,2" Source="{x:Static u:Icons.Import}"/>
<TextBlock Margin="4,0,0,0" Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</u:WindowStyle.TitleContentTemplate>
<Window.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<risk:RiskLimitsImportConflictsView Margin="12,10,12,8"/>
<c:DialogSkirt Grid.Row="1">
<c:DialogSkirt.RightItems>
<x:Array Type="{x:Type Button}">
<Button Command="{Binding ok_cmd}">Continue</Button>
<Button Command="{Binding cancel_cmd}" IsCancel="True">Cancel</Button>
</x:Array>
</c:DialogSkirt.RightItems>
</c:DialogSkirt>
</Grid>
</Window.Content>
</Window>
@@ -0,0 +1,11 @@
using System.Windows;
namespace bts.shell.risk_management {
/// <summary>
/// Interaction logic for RiskLimitsImportConflictsDialog.xaml
/// </summary>
partial class RiskLimitsImportConflictsDialog : Window {
public RiskLimitsImportConflictsDialog() =>
InitializeComponent();
}
}
@@ -0,0 +1,141 @@
<UserControl
x:Class="bts.shell.risk_management.RiskLimitsImportConflictsView"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:risk="clr-namespace:bts.shell.risk_management"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
MinWidth="400"
>
<UserControl.Resources>
<Style x:Key="VerticallyCenteredText" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<DataTemplate x:Key="UIntTemplate">
<c:TickUpDownUInt Value="{Binding value, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
<DataTemplate x:Key="DoubleTemplate">
<c:TickUpDownDouble Value="{Binding value, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
<DataTemplate x:Key="BooleanTemplate">
<CheckBox IsChecked="{Binding value}"/>
</DataTemplate>
<DataTemplate x:Key="PriceReadTemplate">
<TextBlock Text="{Binding value, Converter={StaticResource DoubleTextConverter}}" VerticalAlignment="Center"/>
</DataTemplate>
<!--Value Cell Template Selector-->
<u:KeyedTemplateSelector x:Key="ValueCellTemplateSelector">
<u:KeyedTemplateSelector.Default>
<DataTemplate>
<TextBlock Text="{Binding value}" VerticalAlignment="Center"/>
</DataTemplate>
</u:KeyedTemplateSelector.Default>
<u:TemplateSelectorKeyValue Key="{x:Static MinPrice, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource PriceReadTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static MaxPrice, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource PriceReadTemplate}"/>
</u:KeyedTemplateSelector>
<!--Value Cell Editing Template Selector-->
<u:KeyedTemplateSelector x:Key="ValueCellEditingTemplateSelector">
<u:TemplateSelectorKeyValue Key="{x:Static Security, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}">
<DataTemplate>
<c:ContractPickerPopup
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CloseOnCancel="True"
CloseOnOK="True"
Header="{Binding value}"
IsEditMode="False"
IsLazy="True"
SecurityMap="{Binding DataContext.security_map, RelativeSource={RelativeSource AncestorType=DataGrid}, Mode=OneWay}"
SelectedSecurity="{Binding value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ShowDialogSkirt="True"
/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="{x:Static Account, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}">
<DataTemplate>
<ComboBox ItemsSource="{Binding DataContext.accounts, RelativeSource={RelativeSource AncestorType=DataGrid}}" SelectedItem="{Binding value, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="{x:Static MaxNetLongPosition, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource UIntTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static MaxNetShortPosition, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource UIntTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static IsPositionLimitEnabled, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource BooleanTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static ClipLimit, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource UIntTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static IsClipLimitEnabled, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource BooleanTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static MinPrice, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource DoubleTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static MaxPrice, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource DoubleTemplate}"/>
<u:TemplateSelectorKeyValue Key="{x:Static IsPriceLimitEnabled, MemberType={u:Nested risk:RiskLimitsImportConflictsView.RiskLimitsComponentNames}}" Value="{StaticResource BooleanTemplate}"/>
</u:KeyedTemplateSelector>
</UserControl.Resources>
<!--Content-->
<Grid x:Name="content_root">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image VerticalAlignment="Center" Margin="0,0,8,0">
<Image.Source>
<Binding Source="{x:Static MessageBoxImage.Exclamation}">
<Binding.Converter>
<cv:SystemIconConverter/>
</Binding.Converter>
</Binding>
</Image.Source>
</Image>
<Canvas x:Name="text_canvas" Grid.Column="1" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Column="1" TextWrapping="Wrap" Width="{Binding ActualWidth, ElementName=text_canvas}">
<Run Text="{Binding num_successfully_parsed_limits, Mode=OneWay}"/>
<Run Text="/"/>
<Run Text="{Binding num_limits, Mode=OneWay}"/>
<Run Text=" limit items have been successfully converted."/>
<Run Text="Resolve conflicts below by selecting a value to assign each affected limit item,"/>
<Run Text="or modify individual limit items in the main window."/>
<LineBreak/>
<LineBreak/>
<Run Text="Click 'Continue' to proceed to the main window."/>
</TextBlock>
<DataGrid
x:Name="data_grid"
u:DataGridBehavior.SingleClickEdit="True"
Grid.Row="1"
Grid.ColumnSpan="2"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserSortColumns="True"
HeadersVisibility="All"
ItemsSource="{Binding rows_view}"
Margin="0,8,0,0"
MinRowHeight="17"
RowHeaderWidth="22"
>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell" BasedOn="{StaticResource DataGridCellBaseStyle}">
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
</Style>
</DataGrid.CellStyle>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected" Value="{Binding is_selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource SelectedRowBackground}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Component" Binding="{Binding component}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
<DataGridTextColumn Header="Text" Binding="{Binding text}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
<DataGridTextColumn Header="Affected Limit Items" Binding="{Binding affected_limits.Count}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
<DataGridTemplateColumn Header="Value" CellTemplateSelector="{StaticResource ValueCellTemplateSelector}" CellEditingTemplateSelector="{StaticResource ValueCellEditingTemplateSelector}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
@@ -0,0 +1,38 @@
using System.Windows.Controls;
namespace bts.shell.risk_management {
/// <summary>
/// Interaction logic for RiskLimitsImportConflictsView.xaml
/// </summary>
partial class RiskLimitsImportConflictsView : UserControl {
public RiskLimitsImportConflictsView() =>
InitializeComponent();
public enum RiskLimitsComponent {
Security,
Account,
MaxNetLongPosition,
MaxNetShortPosition,
IsPositionLimitEnabled,
ClipLimit,
IsClipLimitEnabled,
MinPrice,
MaxPrice,
IsPriceLimitEnabled
}
public static class RiskLimitsComponentNames {
public static readonly string
Security = RiskLimitsComponent.Security.ToString(),
Account = RiskLimitsComponent.Account.ToString(),
MaxNetLongPosition = RiskLimitsComponent.MaxNetLongPosition.ToString(),
MaxNetShortPosition = RiskLimitsComponent.MaxNetShortPosition.ToString(),
IsPositionLimitEnabled = RiskLimitsComponent.IsPositionLimitEnabled.ToString(),
ClipLimit = RiskLimitsComponent.ClipLimit.ToString(),
IsClipLimitEnabled = RiskLimitsComponent.IsClipLimitEnabled.ToString(),
MinPrice = RiskLimitsComponent.MinPrice.ToString(),
MaxPrice = RiskLimitsComponent.MaxPrice.ToString(),
IsPriceLimitEnabled = RiskLimitsComponent.IsPriceLimitEnabled.ToString();
}
}
}
@@ -0,0 +1,181 @@
<UserControl
x:Class="bts.shell.risk_management.RiskManagementControl"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:risk="clr-namespace:bts.shell.risk_management"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance risk:RiskManagementVM, IsDesignTimeCreatable=True}"
Background="{StaticResource CommonBackground}"
>
<UserControl.Resources>
<cv:BooleanToObjectConverter x:Key="IsDirtyOpacityConverter">
<cv:BooleanToObjectConverter.ValueForFalse>
<sys:Double>1.0</sys:Double>
</cv:BooleanToObjectConverter.ValueForFalse>
<cv:BooleanToObjectConverter.ValueForTrue>
<sys:Double>0.6</sys:Double>
</cv:BooleanToObjectConverter.ValueForTrue>
</cv:BooleanToObjectConverter>
<Style x:Key="TextCell" TargetType="c:TextCell">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="2,0"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding Path='(c:DIYGrid.Column).horizontal_alignment', RelativeSource={RelativeSource Self}}"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Background" Value="{Binding Path='(c:DIYGrid.Column).background_color', RelativeSource={RelativeSource Self}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
<Setter Property="Foreground" Value="{Binding Path='(c:DIYGrid.Column).font_color', RelativeSource={RelativeSource Self}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
<Setter Property="FontFamily" Value="{Binding Path='(c:DIYGrid.Column).font_family', RelativeSource={RelativeSource Self}}"/>
<Setter Property="FontWeight" Value="{Binding Path='(c:DIYGrid.Column).font_weight', RelativeSource={RelativeSource Self}}"/>
<Setter Property="FontSize" Value="{Binding Path='(c:DIYGrid.Column).font_size', RelativeSource={RelativeSource Self}}"/>
<Setter Property="Opacity" Value="{Binding is_dirty, Converter={StaticResource IsDirtyOpacityConverter}}"/>
</Style>
<Style x:Key="CellBorder" TargetType="Border">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Background" Value="{Binding Path='(c:DIYGrid.Column).background_color', RelativeSource={RelativeSource Self}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
<Setter Property="Padding" Value="2,0"/>
</Style>
<Style x:Key="LimitToggleSwitch" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Foreground" Value="{Binding Path='(c:DIYGrid.Column).font_color', RelativeSource={RelativeSource AncestorType=Border}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
<Setter Property="HorizontalAlignment" Value="{Binding Path='(c:DIYGrid.Column).horizontal_alignment', RelativeSource={RelativeSource AncestorType=Border}}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</UserControl.Resources>
<AdornerDecorator>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Margin="4,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">Account:</TextBlock>
<ComboBox Grid.Column="1" Margin="8,0" ItemsSource="{Binding accounts}" SelectedItem="{Binding selected_account}"/>
</Grid>
<c:DIYGrid
Grid.Row="1"
c:DIYGrid.ColumnModels="{Binding columns}"
c:DIYGrid.SyncWithColumnGeometry="True"
Rows="{Binding rows}"
RowHeight="{u:DPIDouble 21.6}"
>
<c:DIYGrid.Resources>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/FilterTemplates.xaml"/>
</c:DIYGrid.Resources>
<c:DIYGrid.Columns>
<c:DIYGridColumnCollection/>
</c:DIYGrid.Columns>
<c:DIYGrid.CachedColumnParams>
<x:Array Type="{x:Type sys:String}">
<sys:String>font_weight</sys:String>
<sys:String>font_size</sys:String>
<sys:String>font_brush</sys:String>
<sys:String>font_family</sys:String>
<sys:String>horizontal_alignment</sys:String>
</x:Array>
</c:DIYGrid.CachedColumnParams>
<c:DIYGrid.RowTemplate>
<DataTemplate DataType="{x:Type risk:RiskLimitsVM}">
<Grid c:DIYGrid.SyncWithColumnGeometry="True" Height="{Binding RowHeight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}}">
<!--Security-->
<c:TextCell
c:DIYGrid.ColumnKey="Security"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[Security]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Text="{Binding security_text}"
Style="{StaticResource TextCell}"
/>
<c:TextCell
c:DIYGrid.ColumnKey="SecurityKey"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[SecurityKey]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Text="{Binding security_key}"
Style="{StaticResource TextCell}"
/>
<!--Position-->
<c:TextCell
c:DIYGrid.ColumnKey="MaxNetLongPosition"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MaxNetLongPosition]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Text="{Binding max_long_position}"
Style="{StaticResource TextCell}"
/>
<c:TextCell
c:DIYGrid.ColumnKey="MaxNetShortPosition"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MaxNetShortPosition]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Text="{Binding max_short_position}"
Style="{StaticResource TextCell}"
/>
<Border
c:DIYGrid.ColumnKey="IsPositionLimitEnabled"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[IsPositionLimitEnabled]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Style="{StaticResource CellBorder}"
>
<CheckBox
IsChecked="{Binding is_position_limit_enabled}"
Style="{StaticResource LimitToggleSwitch}"
/>
</Border>
<!--Clip-->
<c:TextCell
c:DIYGrid.ColumnKey="ClipLimit"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[ClipLimit]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Text="{Binding clip_limit}"
Style="{StaticResource TextCell}"
/>
<Border
c:DIYGrid.ColumnKey="IsClipLimitEnabled"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[IsClipLimitEnabled]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Style="{StaticResource CellBorder}"
>
<CheckBox
IsChecked="{Binding is_clip_limit_enabled}"
Style="{StaticResource LimitToggleSwitch}"
/>
</Border>
<!--Price-->
<c:TextCell
c:DIYGrid.ColumnKey="MinPrice"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MinPrice]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Text="{Binding min_price, StringFormat='#,0.0000'}"
Style="{StaticResource TextCell}"
/>
<c:TextCell
c:DIYGrid.ColumnKey="MaxPrice"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MaxPrice]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Text="{Binding max_price, StringFormat='#,0.0000'}"
Style="{StaticResource TextCell}"
/>
<Border
c:DIYGrid.ColumnKey="IsPriceLimitEnabled"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[IsPriceLimitEnabled]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Style="{StaticResource CellBorder}"
>
<CheckBox
IsChecked="{Binding is_price_limit_enabled}"
Style="{StaticResource LimitToggleSwitch}"
/>
</Border>
<!--<Border
c:DIYGrid.ColumnKey="IsDirty"
c:DIYGrid.Column="{Binding Path='DataContext.column_map[IsDirty]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
Style="{StaticResource CellBorder}"
>
<Image Source="{x:Static u:Icons.CheckMark}" Visibility="{Binding is_persisted, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Border>-->
</Grid>
</DataTemplate>
</c:DIYGrid.RowTemplate>
</c:DIYGrid>
</Grid>
</AdornerDecorator>
</UserControl>
@@ -0,0 +1,33 @@
using mbts.ui.controls;
using System.Windows.Controls;
namespace bts.shell.risk_management {
/// <summary>
/// Interaction logic for RiskManagementControl.xaml
/// </summary>
partial class RiskManagementControl : UserControl {
public RiskManagementControl() {
InitializeComponent();
}
public enum Columns {
None,
Security,
MaxNetLongPosition,
MaxNetShortPosition,
ClipLimit,
MinPrice,
MaxPrice,
IsPositionLimitEnabled,
IsClipLimitEnabled,
IsPriceLimitEnabled,
SecurityKey
}
}
class RiskManagementColumn : DIYGrid.ColumnBase<RiskManagementControl.Columns> {
protected override void on_key_initialized(RiskManagementControl.Columns key) {
id = key.ToString();
}
}
}
@@ -2,6 +2,7 @@
using bts.api.position;
using bts.shell.fills;
using bts.shell.manual_fills;
using bts.shell.risk_management;
using bts.spark;
using mbts.persist;
using mbts.spark;
@@ -27,6 +28,7 @@ namespace bts.shell {
readonly PNLWindowFactory _plwf;
readonly AlgoStudioWindowFactory _aswf;
readonly ManualFillsWindowFactory _mfwf;
readonly RiskManagementWindowFactory _rmwf;
//
List<SparkCommand> _actions;
@@ -54,6 +56,7 @@ namespace bts.shell {
_old_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
_aswf = new AlgoStudioWindowFactory(this, bus, orders_model, error_reporter);
_mfwf = new ManualFillsWindowFactory(this, bus, orders_model);
_rmwf = new RiskManagementWindowFactory(this, bus, settings_hub, orders_model.admin_model);
}
public ImageSource icon => Icons.Trades;
@@ -77,7 +80,8 @@ namespace bts.shell {
SparkCommands.PickLadder,
SparkCommands.PickOrderTicket,
SparkCommands.ShowAlgoStudio.complete(() => new AlgoStudioWindowFactory.StartupData()),
SparkCommands.ShowManualFills().complete(() => ManualFillsSettings.Default)
SparkCommands.ShowManualFills().complete(() => ManualFillsSettings.Default),
SparkCommands.ShowRiskManagement.complete(() => RiskManagementSettings.Default)
});
}
return _actions;
@@ -95,6 +99,7 @@ namespace bts.shell {
yield return _old_plwf;
yield return _aswf;
yield return _mfwf;
yield return _rmwf;
}
}
+35 -12
View File
@@ -155,9 +155,32 @@ namespace bts {
Disposable.dispose(ref _dispose);
}
public IObservable<Update<(SecurityImmutable? sec, T val), TKey>> join<T,TKey>(
IObservable<Update<T, TKey>> source, Func<T,security_key> skeyfn) where TKey : IEquatable<TKey> {
return Observable.Create<Update<(SecurityImmutable?, T), TKey>>(observer => {
public IObservable<Update<(SecurityImmutable? sec, T val), TKey>> join<T, TKey>(
IObservable<Update<T, TKey>> source, Func<T, security_key> skeyfn) where TKey : IEquatable<TKey> {
return join(source, skeyfn, get_incremental_props, mk_update);
(IEnumerable<T> all, IEnumerable<T> added, IEnumerable<Change<T>> changes, IEnumerable<T> removed, Func<T, TKey> key_extractor) get_incremental_props(Update<T, TKey> upd_in) =>
(upd_in.all, upd_in.added, upd_in.changes, upd_in.removed, upd_in.key_extractor);
Update<(SecurityImmutable? sec, T val), TKey> mk_update(
IEnumerable<(SecurityImmutable? sec, T val)> all,
IEnumerable<(SecurityImmutable? sec, T val)> added,
IEnumerable<Change<(SecurityImmutable? sec, T val)>> changes,
IEnumerable<(SecurityImmutable? sec, T val)> removed,
Func<(SecurityImmutable?, T), TKey> key_extractor) =>
//
new Update<(SecurityImmutable? sec, T val), TKey>(
all, added, changes, removed, key_extractor);
}
public IObservable<TOut> join<TIn, T, TKey, TOut>(
IObservable<TIn> source,
Func<T,security_key> skeyfn,
Func<TIn, (IEnumerable<T> all, IEnumerable<T> added, IEnumerable<Change<T>> changes, IEnumerable<T> removed, Func<T, TKey> key_extractor)> get_incremental_props,
Func<IEnumerable<(SecurityImmutable? sec, T val)>, IEnumerable<(SecurityImmutable? sec, T val)>, IEnumerable<Change<(SecurityImmutable? sec, T val)>>, IEnumerable<(SecurityImmutable? sec, T val)>, Func<(SecurityImmutable?, T), TKey>, TOut> create_result_update)
where TKey : IEquatable<TKey> {
//
return Observable.Create<TOut>(observer => {
var state = ImmutableDictionary<TKey, (SecurityImmutable? s, T t)>.Empty;
var gate = new object();
// We get thie function from the first source update
@@ -186,11 +209,11 @@ namespace bts {
state = builder.ToImmutable();
// fire update
observer.OnNext(
new Update<(SecurityImmutable?, T), TKey>(
state.Values,
Enumerable.Empty<(SecurityImmutable?,T)>(),
changed,
Enumerable.Empty<(SecurityImmutable?,T)>(),
create_result_update(
state.Values,
Enumerable.Empty<(SecurityImmutable?, T)>(),
changed,
Enumerable.Empty<(SecurityImmutable?, T)>(),
rowkeyfn));
}
}
@@ -198,10 +221,11 @@ namespace bts {
return Disposable.Create(
// Fire update when source changes
source.Subscribe((update) => {
source.Subscribe((update_in) => {
var update = get_incremental_props(update_in);
lock (gate) {
rowkeyfn = rowkeyfn ?? (i => update.key_extractor(i.Item2));
var relevant_securities = get_and_ensure_securities(update.added.Concat(update.changed).Select(skeyfn));
var relevant_securities = get_and_ensure_securities(update.added.Concat(update.changes.New()).Select(skeyfn));
var added = new List<(SecurityImmutable?, T)>();
var changed = new List<Change<(SecurityImmutable? s, T t)>>();
@@ -238,8 +262,7 @@ namespace bts {
}
state = builder.ToImmutable();
observer.OnNext(new Update<(SecurityImmutable?, T t), TKey>(
state.Values, added, changed, removed, rowkeyfn));
observer.OnNext(create_result_update(state.Values, added, changed, removed, rowkeyfn));
}
}),
// Also fire update when securities change
@@ -28,6 +28,9 @@ namespace bts.spark {
public static PartialCommand<bts.shell.orderbook.OrderBookStartupData> ShowOrders =>
SparkCommand.create("Order Book", Icons.Book, SparkTags.ShowOrderBookTag);
public static PartialCommand<shell.risk_management.RiskManagementSettings> ShowRiskManagement =>
SparkCommand.create("Risk Management", Icons.Risk, SparkTags.ShowRiskManagement);
// Fills
public static PartialCommand<FillsSettings> ShowFills(KeyGesture key_gesture = null) =>
SparkCommand.create("Fills", Icons.Payment, SparkTags.ShowFillsTag, key_gesture);
+1
View File
@@ -63,6 +63,7 @@ namespace mbts.spark {
public static ITag<IObservable<SecurityMD>> ShowLinkedLadder { get; } = new SparkTag<IObservable<SecurityMD>>("Linked Ladder");
public static ITag<SecurityCriteria> ShowMarketViewTag { get; } = new SparkTag<SecurityCriteria>("Show MarketView");
public static ITag<bts.shell.orderbook.OrderBookStartupData> ShowOrderBookTag { get; } = new SparkTag<bts.shell.orderbook.OrderBookStartupData>(nameof(ShowOrderBookTag));
public static ITag<bts.shell.risk_management.RiskManagementSettings> ShowRiskManagement { get; } = new SparkTag<bts.shell.risk_management.RiskManagementSettings>(nameof(ShowRiskManagement));
public static ITag<PAndLWindowFactory.PAndLStartupData> ShowPAndLTag { get; } = new SparkTag<PAndLWindowFactory.PAndLStartupData>(nameof(ShowPAndLTag));
public static ITag<bts.shell.pnl.PNLSettings> ShowPNLTag { get; } = new SparkTag<bts.shell.pnl.PNLSettings>(nameof(ShowPNLTag));
public static ITag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)> ShowSyntheticCloneTag { get; } = new SparkTag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)>(nameof(ShowSyntheticCloneTag));
@@ -115,7 +115,7 @@ namespace mbts.ui {
public static BTSColor session_low { get; } = new BTSColor(89, 89, 107);
public static BTSColor session_high { get; } = new BTSColor(35, 155, 0);
public static BTSColor session_range_fill { get; } = Colors.Black;
public static BTSColor session_range_fill { get; } = Color.FromArgb(26, 0, 0, 0);
// Alerts
[DisplayInfo(name = "Trade Alert")]
@@ -731,4 +731,4 @@ namespace mbts.ui {
#endregion
}
}
}
@@ -17,6 +17,7 @@ namespace bts.settings {
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
interface IRiskManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
static class SettingsMigrationHelper {
@@ -61,6 +61,8 @@ namespace mbts.spark {
PNLSettings _pnl;
[ProtoMember(14)]
GlobalAlertSettings _alerts;
[ProtoMember(15)]
GridSettings _risk_management_grid;
public UserSettings() { }
@@ -74,6 +76,7 @@ namespace mbts.spark {
public GridSettings market_view_grid => _market_view_grid ?? (_market_view_grid = bts.shell.MarketViewWindowFactory.StartupData.DefaultGridSettings);
public GridSettings time_and_sales_grid => _time_and_sales_grid ?? (_time_and_sales_grid = GridSettings.Default.with(bts.shell.time_and_sales.TimeAndSalesSettings.DefaultColumns.all));
public GridSettings fills_grid => _fills_grid ?? (_fills_grid = bts.shell.fills.FillsSettings.DefaultGridSettings);
public GridSettings risk_management_grid => _risk_management_grid ?? (_risk_management_grid = bts.shell.risk_management.RiskManagementSettings.DefaultGrid);
public GridSettings synthetic_management_grid => _synthetic_management_grid ?? (_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings);
public GridSettings pnl_grid => _pnl_grid ?? (_pnl_grid = bts.shell.PAndLWindowFactory.PAndLStartupData.DefaultGridSettings);
public PNLSettings pnl => _pnl ?? (_pnl = PNLSettings.Default);
@@ -91,7 +94,8 @@ namespace mbts.spark {
GridSettings provided_pnl_gridsettings = null,
PNLSettings pnl = null,
bts.shell.orderbook.OrderBookGridSettings order_book_grid = null,
GlobalAlertSettings alerts = null) {
GlobalAlertSettings alerts = null,
GridSettings risk_management_grid = null) {
var us = new UserSettings(ladder_profiles ?? _ladder_profiles) {
_ladder = ladder ?? _ladder,
_notification = notification ?? _notification,
@@ -102,7 +106,8 @@ namespace mbts.spark {
_pnl_grid = provided_pnl_gridsettings ?? _pnl_grid,
_pnl = pnl ?? _pnl,
_order_book_grid = order_book_grid ?? _order_book_grid,
_alerts = alerts ?? _alerts
_alerts = alerts ?? _alerts,
_risk_management_grid = risk_management_grid ?? _risk_management_grid
};
us._last_ladder_profile_id = Math.Max(_last_ladder_profile_id, us.ladder_profiles.Select(p => p.id).MaxOrDefault());
return us;
@@ -26,6 +26,7 @@ namespace mbts.persist {
_fills_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.fills_grid, (us, gs) => us.with(provided_fills_gridsettings: gs)),
_mview_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.market_view_grid, (us, gs) => us.with(provided_market_view_gridsettings: gs)),
_pandl_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs)),
_risk_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.risk_management_grid, (us, gs) => us.with(risk_management_grid: gs)),
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs));
static readonly IImmutableProperty<UserSettings, PNLSettings>
@@ -37,6 +38,7 @@ namespace mbts.persist {
readonly IEnumerable<IMarketViewGridSettingsMigrationRule> _mview_migration_rules;
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
readonly IEnumerable<IRiskManagementMigrationRule> _risk_migration_rules;
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
readonly IUserPersist _persist;
@@ -49,6 +51,7 @@ namespace mbts.persist {
[ImportMany]IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
[ImportMany]IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
[ImportMany]IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules,
[ImportMany]IEnumerable<IRiskManagementMigrationRule> risk_migration_rules,
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules) {
//
_persist = persist;
@@ -56,6 +59,7 @@ namespace mbts.persist {
_mview_migration_rules = mview_migration_rules ?? Enumerable.Empty<IMarketViewGridSettingsMigrationRule>();
_pandl_migration_rules = pandl_migration_rules ?? Enumerable.Empty<IPAndLGridSettingsMigrationRule>();
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
_risk_migration_rules = risk_migration_rules ?? Enumerable.Empty<IRiskManagementMigrationRule>();
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
init();
async void init() {
@@ -68,6 +72,7 @@ namespace mbts.persist {
_mview_migration_rules.merge().wrap(_mview_propi),
_pandl_migration_rules.merge().wrap(_pandl_propi),
_pnl_migration_rules.merge().wrap(_pnl_propi),
_risk_migration_rules.merge().wrap(_risk_propi),
_synth_migration_rules.merge().wrap(_synth_propi)
};
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
@@ -142,6 +147,12 @@ namespace mbts.persist {
_pnl_migration_rules,
_pnl_propi);
public IWindowSettingsManager<GridSettings> create_risk_management_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_risk_migration_rules,
_risk_propi);
public IWindowSettingsManager<GridSettings> create_synth_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
@@ -170,4 +181,4 @@ namespace mbts.persist {
}
}
}
}
}
@@ -3,6 +3,7 @@ using mbts;
using mbts.ui.shell;
using mbts.utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -10,6 +11,13 @@ using System.Reactive.Linq;
using System.Reflection;
namespace bts.utils {
class PropertyEqualityComparerAttribute : Attribute {
public PropertyEqualityComparerAttribute(Type comparer_type) =>
this.comparer_type = comparer_type ?? throw new ArgumentNullException(nameof(comparer_type));
public Type comparer_type { get; }
}
interface IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
IDisposable detect_changes(T target);
}
@@ -68,6 +76,42 @@ namespace bts.utils {
public static IObservable<bool> dirty_monitor<T>(T instance, PropertyInfo[] props) where T : INotifyPropertyChanged =>
_dirty_monitor(instance, props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance))));
public static Func<T, int> property_hash_code_delegate<T>(string prop) =>
DelegateStore<T>.property_hash_code(prop);
public static Func<T, int> property_hash_code_delegate<T>(IEnumerable<string> props) {
var prop_hash_code_dels = props.Select(DelegateStore<T>.property_hash_code).ExceptNull().ToArray();
return impl;
int impl(T a) =>
EqualityComparer.auto_phase(prop_hash_code_dels.Select(peq => peq(a)));
}
public static Func<T, T, bool> property_equals_delegate<T>(string prop) =>
DelegateStore<T>.property_equals(prop);
public static Func<T, T, bool> property_equals_delegate<T>(IEnumerable<string> props) {
var prop_equals_dels = props.Select(DelegateStore<T>.property_equals).ExceptNull().ToArray();
return impl;
bool impl(T a, T b) =>
prop_equals_dels.All(peq => peq(a, b));
}
public static IEqualityComparer<T> property_equality_comparer<T>() {
var prop_infos = PropertyInfoStore<T>.all_props;
return property_equality_comparer<T>(
prop_infos.Select(p => p.Name),
// hash code should NEVER include mutable props
prop_infos.Where(pi => !pi.CanWrite).Select(p => p.Name));
}
public static IEqualityComparer<T> property_equality_comparer<T>(IEnumerable<string> props) =>
property_equality_comparer<T>(props, props);
public static IEqualityComparer<T> property_equality_comparer<T>(IEnumerable<string> equals_props, IEnumerable<string> hash_code_props) =>
EqualityComparer.Create(
property_equals_delegate<T>(equals_props),
property_hash_code_delegate<T>(hash_code_props));
static IObservable<bool> _dirty_monitor<T>(T instance, Dictionary<string, PropNode> prop_map) where T : INotifyPropertyChanged =>
Observable.Create<bool>(o => {
var dirty_props = new HashSet<string>();
@@ -106,6 +150,8 @@ namespace bts.utils {
PropertyInfo prop_info { get; }
Func<T, object> getter { get; }
Action<T, object> setter { get; }
Func<T, T, bool> property_equals { get; }
Func<T, int> property_hash_code { get; }
(Action<T> initialize, Func<T, bool> refresh) create_snapshot();
(Action initialize, Func<bool> refresh) create_snapshot(T source);
}
@@ -139,6 +185,15 @@ namespace bts.utils {
public static Action<T, object> setter(string prop_name) =>
accessor_data.TryGetValue(prop_name, out var dels) ? dels.setter : null;
public static Func<T, T, bool> property_equals(string prop_name) =>
accessor_data.TryGetValue(prop_name, out var dels) ? dels.property_equals : null;
public static Func<T, int> property_hash_code(string prop_name) =>
accessor_data.TryGetValue(prop_name, out var dels) ? dels.property_hash_code : null;
public static IPropertyAccessorData<T> get_accessor(string prop_name) =>
accessor_data.GetValueOrDefault(prop_name);
static DelegateStore() {
var prop_dels = new Dictionary<string, IPropertyAccessorData<T>>();
var accessor_type_cache = new Dictionary<Type, Type>();
@@ -185,6 +240,7 @@ namespace bts.utils {
class PropertyAccessorData<T, TValue> : IPropertyAccessorData<T, TValue> {
readonly Func<T, object> _ng_get;
readonly Action<T, object> _ng_set;
IEqualityComparer<TValue> _property_equals_comp;
public PropertyAccessorData(PropertyInfo prop_info) {
this.prop_info = prop_info;
@@ -200,11 +256,27 @@ namespace bts.utils {
setter = set_method.CreateDelegate(typeof(Action<T, TValue>)) as Action<T, TValue>;
_ng_set = (src, val) => setter(src, (TValue)val);
}
if (prop_info.GetCustomAttribute<PropertyEqualityComparerAttribute>()?.comparer_type is Type comp_type) {
_property_equals_comp = Activator.CreateInstance(comp_type) as IEqualityComparer<TValue>;
}
if (_property_equals_comp is null) {
_property_equals_comp = EqualityComparer<TValue>.Default;
}
}
public Func<T, TValue> getter { get; }
public Action<T, TValue> setter { get; }
public PropertyInfo prop_info { get; }
public Func<T, T, bool> property_equals {
get {
return impl;
bool impl(T a, T b) =>
_property_equals_comp.Equals(getter(a), getter(b));
}
}
public Func<T, int> property_hash_code =>
a => _property_equals_comp.GetHashCode(getter(a));
Func<T, object> IPropertyAccessorData<T>.getter => _ng_get;
Action<T, object> IPropertyAccessorData<T>.setter => _ng_set;
@@ -1,6 +1,11 @@
using System;
using bts.collections;
using bts.utils;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
namespace mbts.ui.utility {
@@ -153,6 +158,95 @@ namespace mbts.ui.utility {
_try_match_item(_item_to_value(item), out match);
}
class SelectorVM<T, TKey> {
readonly IEqualityComparer<T> _eq_comparer;
readonly IEqualityComparer<TKey> _key_comparer;
readonly Func<T, TKey> _key_fn;
readonly ObservableCollection<T> _items = new ObservableCollection<T>();
readonly Dictionary<TKey, T> _map;
/// not as noisy as <see cref="ObservableCollection{T}.CollectionChanged"/>,
/// this event is only raised after a batch collection change has been completed.
/// <see cref="update_items(IEnumerable{T})"/>.
IComparer<T> _comparer;
T _selected_item;
TKey _pending_selected_item_key;
bool _is_pending_selection;
public SelectorVM(Func<T, TKey> key_fn)
: this(key_fn, EqualityComparer<TKey>.Default) { }
public SelectorVM(Func<T, TKey> key_fn, IEqualityComparer<TKey> key_comparer) {
_key_fn = key_fn ?? throw new ArgumentNullException(nameof(key_fn));
_key_comparer = key_comparer ?? throw new ArgumentNullException(nameof(key_comparer));
_eq_comparer = EqualityComparer.Create(_key_fn, _key_comparer);
_map = new Dictionary<TKey, T>(key_comparer);
items = ObservableAutoWrapper.create_read_only_wrapper(_items);
}
public event Action selected_item_changed;
public IReadOnlyObservableCollection<T> items { get; }
public T selected_item {
get => _selected_item;
set {
if (!_eq_comparer.Equals(_selected_item, value)) {
_selected_item = value;
selected_item_changed?.Invoke();
}
}
}
public IComparer<T> comparer {
get => _comparer;
set {
_comparer = value;
if (value != null)
_update_items(items, false);
}
}
public bool try_select(T item, bool wait_for_item_to_arrive = false) =>
try_select(_key_fn(item), wait_for_item_to_arrive);
public bool try_select(TKey item_key, bool wait_for_item_to_arrive = false) {
if (_map.TryGetValue(item_key, out var item)) {
selected_item = item;
return true;
} else if (wait_for_item_to_arrive) {
_pending_selected_item_key = item_key;
_is_pending_selection = true;
}
return false;
}
public void update_items(IEnumerable<T> items) =>
_update_items(items, true);
void _update_items(IEnumerable<T> items, bool map_changed) {
// The currently selected item may be removed during this collection change.
// After the items have been changed, attempt to re-select the last selected item;
// if there is already a particular 'pending selection,' attempt to select that instead.
var target_key = _is_pending_selection ? _pending_selected_item_key : _key_fn(_selected_item);
if (map_changed)
_map.Clear();
if (items is null) {
_items.Clear();
} else {
if (_comparer != null) {
items = items.Ordered(_comparer);
}
_items.sync_with(items);
}
if (map_changed) {
foreach (var i in this.items)
_map[_key_fn(i)] = i;
}
if (_map.Count != this.items.Count)
throw new InvalidOperationException("Map and items are out of sync (different item counts).");
if (try_select(target_key, _is_pending_selection || _items.Count == 0) && _is_pending_selection)
_is_pending_selection = false;
}
}
static class SelectionModelExtensions {
public static IObservable<TValue> observe_selected_item<TItem, TValue>(this SelectionModel<TItem, TValue> selection_model, bool notify_on_subscribe = false) =>
Observable.Create<TValue>(o => {
@@ -166,4 +260,4 @@ namespace mbts.ui.utility {
return Disposable.Create(() => selection_model.Changed -= callback);
}
}
}
}
@@ -112,6 +112,14 @@ namespace mbts.ui.shell {
protected bool Set<T>(ref T backing, T value, [CallerMemberName]string name = "") =>
Set(ref backing, value, EqualityComparer<T>.Default, name);
protected bool Set<T>(ref T backing, T value, [CallerMemberName] string name = "", params string[] dependent_props) {
if (Set(ref backing, value, EqualityComparer<T>.Default, name)) {
RaisePropertyChanged(dependent_props);
return true;
}
return false;
}
protected bool Set<T>(ref T backing, T value, IEqualityComparer<T> comparer, [CallerMemberName]string name = "") {
if (!comparer.Equals(backing, value)) {
backing = value;
@@ -1,28 +0,0 @@
using bts.api;
using System.ComponentModel.Composition;
namespace mbts.user.settings
{
[InheritedExport]
internal interface ISecurityBasedSettingsFactory
{
SecurityBasedSettings get(Venue exchange, SecurityType type, string symbol);
}
internal interface ISecurityBasedSettingsFactory<T> : ISecurityBasedSettingsFactory where T : class
{
}
[ProtoBuf.ProtoContract]
[ProtoBuf.ProtoInclude(100, typeof(SecurityTickSettings))]
internal abstract class SecurityBasedSettings
{
protected SecurityBasedSettings() { }
protected SecurityBasedSettings(SecurityTuple security) {
this.security = security;
}
[ProtoBuf.ProtoMember(1)]
public SecurityTuple security;
}
}
@@ -2,6 +2,7 @@
using mbts.spark.utilities;
using ProtoBuf;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Media;
@@ -119,4 +120,9 @@ namespace mbts.ui.controls.color_picker {
};
#endregion
}
static class ColorPaletteExtensions {
public static ObservableCollection<ColorPaletteVM> to_viewmodels(this IEnumerable<ColorPalette> source) =>
new ObservableCollection<ColorPaletteVM>(source.empty_if_null().Select(cp => new ColorPaletteVM(cp)));
}
}
@@ -229,7 +229,10 @@ namespace mbts.ui.controls {
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
#endregion
protected override Size ArrangeOverride(Size finalSize) => finalSize;
protected override Size ArrangeOverride(Size finalSize) {
_gcr.available_size = finalSize;
return finalSize;
}
protected override Size MeasureOverride(Size availableSize) {
_gcr.available_size = availableSize;
@@ -237,6 +237,15 @@ namespace mbts.ui.controls {
Color? _background_color_override;
object _header_copy;
bool _is_visible;
IDisposable _model_hooks;
static ColumnBase() {
WidthProperty.OverrideMetadata(typeof(ColumnBase),
new PropertyMetadata((d, e) => {
if (d is ColumnBase cb)
cb.width_changed?.Invoke((double)e.NewValue);
}));
}
protected ColumnBase() {
var gs = GridSettings.Default;
@@ -256,10 +265,25 @@ namespace mbts.ui.controls {
get => GetValue(SortDirectionProperty) as ListSortDirection?;
set => SetValue(SortDirectionProperty, value);
}
ListSortDirection? ISortableDIYGridColumn.SortDirection {
get => SortDirection;
set => SetCurrentValue(SortDirectionProperty, value);
}
#region IsVisible
public static readonly DependencyProperty IsVisibleProperty =
WPFHelper.create_dp<bool, ColumnBase>(nameof(IsVisible), (o, v) => o.is_visible = v);
public bool IsVisible {
get => (bool)GetValue(IsVisibleProperty);
set => SetValue(IsVisibleProperty, value);
}
#endregion
public event Action<ColumnBase, ListSortDirection?> sort_direction_changed;
public event Action font_params_changed;
public event Action<bool> is_visible_changed;
public event Action<double> width_changed;
public string id { get; set; }
public string sort_member_path { get; set; }
@@ -273,6 +297,7 @@ namespace mbts.ui.controls {
_is_visible = value;
is_visible_changed?.Invoke(value);
raise_prop_changed();
SetCurrentValue(IsVisibleProperty, value);
}
}
}
@@ -297,6 +322,29 @@ namespace mbts.ui.controls {
set => base.Header = (_header_copy = value);
}
public void set_model(IDIYGridColumnModel model) {
Disposable.dispose(ref _model_hooks);
model.width = Width;
width_changed += on_width_changed;
model.width_changed += on_model_width_changed;
model.result_changed += on_result_changed;
_model_hooks = Disposable.Create(() => {
width_changed -= on_width_changed;
model.width_changed -= on_model_width_changed;
});
void on_model_width_changed(double value) {
if (!DoubleUtil.AreClose(value, Width))
SetCurrentValue(WidthProperty, value);
}
void on_width_changed(double value) {
model.width_changed -= on_model_width_changed;
model.width = value;
model.width_changed += on_model_width_changed;
}
void on_result_changed() =>
load_from(model.create());
}
/// important: don't access any dependency props in here.
/// this may be called from a thread other than that of this dispatcher (i.e., when <see cref="IShellWindow.GetState(System.IO.Stream)"/> is called).
public GridColumnSettings create() =>
@@ -327,9 +375,8 @@ namespace mbts.ui.controls {
}
if (original_width != settings.width) {
original_width = settings.width;
Width = original_width;
SetCurrentValue(WidthProperty, original_width);
}
//
}
// 'fallback' font-params (aka 'grid-level' font-params)
@@ -9,10 +9,10 @@ using System.Reactive.Linq;
namespace mbts.ui {
class ColumnSortManager {
readonly List<ISortableDIYGridColumn> _visible_sorting = new List<ISortableDIYGridColumn>();
readonly IList<ISortableDIYGridColumn> _columns;
readonly IEnumerable<ISortableDIYGridColumn> _columns;
readonly string _primary_key;
public ColumnSortManager(IList<ISortableDIYGridColumn> columns, string primary_key) {
public ColumnSortManager(IEnumerable<ISortableDIYGridColumn> columns, string primary_key) {
_columns = columns;
_primary_key = primary_key;
}
@@ -1,4 +1,18 @@
namespace mbts.ui.controls {
using bts.utils;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui.settings;
using mbts.ui.shell;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
namespace mbts.ui.controls {
class DIYGridColumn : DIYGrid.ColumnBase<string> {
public DIYGridColumn()
: base() {
@@ -6,4 +20,194 @@
Width = 60;
}
}
interface IDIYGridColumnModel : DIYGrid.IFilterableColumn, IValueFactoryVM<GridColumnSettings> {
string id { get; }
string column_template { get; }
IComparer row_comparer { get; }
double width { get; set; }
event Action<double> width_changed;
event Action result_changed;
}
interface IDIYGridColumnFactory {
DIYGrid.ColumnBase create_view();
}
class DIYGridColumnModel<TKey> : ViewModelBase, IDIYGridColumnModel, ISortableDIYGridColumn<TKey>, ISortableDIYGridColumn, IValueFactoryVM<GridColumnSettings> where TKey : struct {
readonly DeferrableActionWrapper _result_changed;
FontWeight _fallback_font_weight;
Color _fallback_font_color;
Color _fallback_background_color;
double _fallback_font_size;
FontFamily _fallback_font_family;
double _width = 60;
bool _is_visible = true;
ListSortDirection? _sort_direction;
object _custom_header;
string _display_name;
HorizontalAlignment _horizontal_alignment = HorizontalAlignment.Left;
Color? _background_color_override, _font_color_override;
FontWeight? _font_weight_override;
public DIYGridColumnModel(TKey id, IComparer row_comparer, IDataGridColumnFilter filter, string filter_template = null, string column_template = null) {
this.id = id;
this.row_comparer = row_comparer;
this.filter = filter;
this.filter_template = filter_template;
this.column_template = column_template;
_result_changed = new DeferrableActionWrapper(() => result_changed?.Invoke());
}
public event Action result_changed;
public event Action<double> width_changed;
public TKey id { get; }
string IDIYGridColumnModel.id => id.ToString();
public IComparer row_comparer { get; }
public string display_name {
get => _display_name;
set {
if (value != _display_name) {
_display_name = value;
_result_changed.execute();
RaisePropertyChanged();
if (_custom_header is null) {
RaisePropertyChanged(nameof(header));
}
}
}
}
public object header => _custom_header ?? display_name;
public object custom_header { get => _custom_header;
set {
if (value != _custom_header) {
_custom_header = value;
RaisePropertyChanged(nameof(custom_header), nameof(header));
}
}
}
public double width {
get => _width;
set {
if (_set(ref _width, value, DoubleUtil.AreClose))
width_changed?.Invoke(value);
}
}
public bool is_visible { get => _is_visible; set => _set(ref _is_visible, value); }
public ListSortDirection? sort_direction { get => _sort_direction; set => Set(ref _sort_direction, value); }
public Color? background_color_override { get => _background_color_override; set => _set(ref _background_color_override, value); }
public Color? font_color_override { get => _font_color_override; set => _set(ref _font_color_override, value); }
public FontWeight? font_weight_override { get => _font_weight_override; set => _set(ref _font_weight_override, value); }
public FontFamily font_family => _fallback_font_family;
public FontWeight font_weight => _font_weight_override ?? _fallback_font_weight;
public Color font_color => font_color_override ?? _fallback_font_color;
public Color background_color => background_color_override ?? _fallback_background_color;
public double font_size => _fallback_font_size;
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => _set(ref _horizontal_alignment, value); }
public IDataGridColumnFilter filter { get; }
public string column_template { get; }
public string filter_template { get; }
string IDIYGridColumn.id => id.ToString();
ListSortDirection? ISortableDIYGridColumn.SortDirection { get => sort_direction; set => sort_direction = value; }
public GridColumnSettings create() =>
new GridColumnSettings(
id: id.ToString(),
display_name: display_name,
is_visible: is_visible,
font_color_override: font_color_override,
font_weight_override: font_weight_override,
width: width,
background_color_override: background_color_override,
horizontal_alignment: horizontal_alignment);
public void set_fallback_font_params(FontFamily font_family, double font_size, Color font_color, FontWeight font_weight, Color background_color) {
using (ReflectionHelper.detect_property_changes(this, RaisePropertyChanged, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight), nameof(background_color))) {
_fallback_font_family = font_family;
_fallback_font_size = font_size;
_fallback_font_color = font_color;
_fallback_background_color = background_color;
_fallback_font_weight = font_weight;
}
}
public void load_from(GridColumnSettings value) {
if (!value.strong_id<TKey>().Equals(id))
throw new ArgumentException("Column id in settings object does not match the column model's id.", nameof(value));
using (_result_changed.defer_execution())
using (_detect_result_changed())
using (detect_property_changes(nameof(font_color), nameof(font_weight), nameof(background_color))) {
display_name = value.display_name;
is_visible = value.is_visible;
font_color_override = value.font_color_override;
font_weight_override = value.font_weight_override;
background_color_override = value.background_color_override;
width = value.width;
horizontal_alignment = value.horizontal_alignment;
}
}
IDisposable _detect_result_changed() {
var orig_state = create();
return Disposable.Create(() => {
var curr_state = create();
if (!orig_state.Equals(curr_state))
result_changed?.Invoke();
});
}
bool _set<T>(ref T backing, T new_value, [CallerMemberName]string prop = null) =>
_set(ref backing, new_value, EqualityComparer<T>.Default, prop);
bool _set<T>(ref T backing, T new_value, Func<T, T, bool> equals, [CallerMemberName] string prop = null) =>
// Technically, the GetHashCode impl could disagree with the equals delegate.
// However, this doesn't matter because the _set method only uses IEqualityComparer.Equals (not GetHashCode).
// It didn't seem worth the trouble of changing the method signature, as there is no conceivable reason why that method
// would ever be changed to use GetHashCode.
_set(ref backing, new_value, EqualityComparer.Create(equals, a => a.GetHashCode()), prop);
bool _set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = null) {
if (Set(ref backing, new_value, comparer, prop)) {
_result_changed.execute();
return true;
}
return false;
}
}
class DIYGridColumnModel<TKey, TRow, TGridFilter> : DIYGridColumnModel<TKey> where TKey : struct {
readonly Func<TGridFilter, object> _get_column_filter_settings;
readonly Func<TGridFilter, object, TGridFilter> _set_column_filter_settings;
public DIYGridColumnModel(TKey id, IComparer<TRow> row_comparer, IDataGridColumnFilter<TRow> filter, string filter_template, Func<TGridFilter, object> get_column_filter_settings, Func<TGridFilter, object, TGridFilter> set_column_filter_settings)
: base(id, row_comparer.to_non_generic(), filter, filter_template) {
_get_column_filter_settings = get_column_filter_settings;
_set_column_filter_settings = set_column_filter_settings;
this.row_comparer = row_comparer;
this.filter = filter;
}
public new IComparer<TRow> row_comparer { get; }
public new IDataGridColumnFilter<TRow> filter { get; }
public void load_from(GridColumnSettings column_settings, TGridFilter grid_filter_settings) {
if (column_settings != null)
load_from(column_settings);
if (_get_column_filter_settings != null && grid_filter_settings != null)
filter?.load_state(_get_column_filter_settings(grid_filter_settings));
}
public TGridFilter assign_filter_settings(TGridFilter target) {
if (_set_column_filter_settings != null) {
var filter_settings = filter.get_state();
target = _set_column_filter_settings(target, filter_settings);
}
return target;
}
}
}
@@ -1,4 +1,5 @@
using bts;
using bts.collections;
using bts.utils;
using mbts.spark.filters;
using mbts.ui.controls.adorners;
@@ -867,6 +868,83 @@ namespace mbts.ui.controls {
public static void SetColumnKey(DependencyObject d, object value) => d.SetValue(ColumnKeyProperty, value);
#endregion
#region Column
public static readonly DependencyProperty ColumnProperty =
DependencyProperty.RegisterAttached("Column", typeof(object), typeof(DIYGrid), new PropertyMetadata());
public static object GetColumn(DependencyObject d) => d.GetValue(ColumnProperty);
public static void SetColumn(DependencyObject d, object value) => d.SetValue(ColumnProperty, value);
#endregion
#region ColumnModels
public static readonly DependencyProperty ColumnModelsProperty =
DependencyProperty.RegisterAttached("ColumnModels", typeof(IReadOnlyObservableCollection<IDIYGridColumn>),
typeof(DIYGrid),
new PropertyMetadata(null, OnColumnModelsChanged));
static void OnColumnModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
IDisposable hook = null;
if (e.NewValue is IEnumerable<IDIYGridColumnModel> col_models) {
resort();
if (e.NewValue is INotifyCollectionChanged col_models_notifier) {
hook = CollectionHelper.buffer_collection_changed_callback(col_models_notifier, resort, TimeSpan.FromSeconds(0.3), TimeSpan.Zero);
}
void resort() {
if (d is DIYGrid grid) {
var cols = grid.Columns;
if (cols != null) {
var col_map = cols.ToDictionary(c => c.id);
var sorted_cols = col_models.Select(cm => {
if (col_map.TryGetValue(cm.id, out var cb))
return cb;
if (cm is IDIYGridColumnFactory col_factory) {
cb = col_factory.create_view();
} else if (!string.IsNullOrEmpty(cm.column_template)) {
cb = grid.TryFindResource(cm.column_template) as ColumnBase;
}
if (cb is null) {
cb = new DIYGridColumn { id = cm.id };
BindingOperations.SetBinding(cb, ColumnBase.HeaderProperty, new Binding(nameof(DIYGridColumnModel<int>.header)) { Source = cm });
BindingOperations.SetBinding(cb, ColumnBase.SortDirectionProperty, new Binding(nameof(DIYGridColumnModel<int>.sort_direction)) { Source = cm, Mode = BindingMode.TwoWay });
cb.set_model(cm);
BindingOperations.SetBinding(cb, ColumnBase.IsVisibleProperty, new Binding(nameof(DIYGridColumnModel<int>.is_visible)) { Source = cm });
if (cm is IFilterableColumn fc) {
DataGridFilterProperties.SetFilter(cb, fc.filter);
DataGridFilterProperties.SetFilterTemplate(cb, grid.FindResource(fc.filter_template) as DataTemplate);
BindingOperations.SetBinding(cb, DataGridFilterProperties.FilterActiveProperty, new Binding(nameof(IDataGridColumnFilter.active)) { Source = fc.filter });
BindingOperations.SetBinding(cb, DataGridFilterProperties.FilterOpenProperty, new Binding(nameof(IDataGridColumnFilter.open)) { Source = fc.filter });
}
}
return cb;
}).ToList();
CollectionHelper.sync_with_never_duplicate(cols, sorted_cols);
}
}
}
}
SetColumnModelsHooks(d, hook);
}
public static IReadOnlyObservableCollection<IDIYGridColumn> GetColumnModels(DependencyObject d) =>
d.GetValue(ColumnModelsProperty) as IReadOnlyObservableCollection<IDIYGridColumn>;
public static void SetColumnModels(DependencyObject d, IReadOnlyObservableCollection<IDIYGridColumn> value) =>
d.SetValue(ColumnModelsProperty, value);
static readonly DependencyPropertyKey ColumnModelsHooksPropertyKey =
DependencyProperty.RegisterAttachedReadOnly("ColumnModelsHooks", typeof(IDisposable),
typeof(DIYGrid),
new PropertyMetadata(null, FrameworkElementLifeDependencyHelper.DisposeOld));
internal static DependencyProperty ColumnModelsHooksProperty => ColumnModelsHooksPropertyKey.DependencyProperty;
internal static IDisposable GetColumnModelsHooks(DependencyObject d) =>
d.GetValue(ColumnModelsHooksProperty) as IDisposable;
static void SetColumnModelsHooks(DependencyObject d, IDisposable value) =>
d.SetValue(ColumnModelsHooksPropertyKey, value);
#endregion
#region SyncWithColumnGeometrySource
public static readonly DependencyProperty SyncWithColumnGeometrySourceProperty =
DependencyProperty.RegisterAttached(
@@ -1,5 +1,6 @@
using bts.utils;
using mbts.spark;
using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.utils;
@@ -121,4 +122,111 @@ namespace mbts.ui {
}
}
}
class SimpleGridSettingsManagerVM<TColumnKey> : ViewModelBase, IObserver<GridSettings> where TColumnKey : struct {
readonly IReadOnlyList<DIYGridColumnModel<TColumnKey>> _columns;
readonly Property<GridSettings> _grid_settings;
readonly DelegateProperty<FontFamily> _font_family;
readonly DelegateProperty<FontWeight> _font_weight;
readonly DelegateProperty<double> _font_size;
readonly DelegateProperty<SolidColorBrush> _font_color, _selection_highlight, _background_color;
readonly IReadOnlyDictionary<string, ImmutableColumnSettingStore> _column_flags;
readonly Action<IEnumerable<GridColumnSettings>> _sync_column_order;
readonly Action _invalidate_rows;
public SimpleGridSettingsManagerVM(
IReadOnlyList<DIYGridColumnModel<TColumnKey>> columns,
IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> column_flags,
Action<IEnumerable<GridColumnSettings>> sync_column_order,
Action invalidate_rows) {
//
_columns = columns;
_sync_column_order = sync_column_order;
_invalidate_rows = invalidate_rows;
_column_flags = (column_flags ?? DictionaryHelper.empty<string, GridColumnSettingsFeatureFlags>())
.ToDictionary(kv => kv.Key, kv => ImmutableColumnSettingStore.create(kv.Value));
_grid_settings = backing(nameof(grid_settings), GridSettings.Default);
_font_family = backing(nameof(font_family), () => new FontFamily(grid_settings.font_family), nameof(grid_settings));
_font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings));
_font_size = backing(nameof(font_size), () => grid_settings.font_size, nameof(font_size));
_font_color = backing(nameof(font_color), () => grid_settings.font_color.rgb_solid_brush().frozen(), nameof(grid_settings));
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight.to_brush().frozen(), nameof(grid_settings));
_background_color = backing(nameof(background_color), () => grid_settings.background_color.argb_color().to_solid_brush().frozen(), nameof(grid_settings));
}
public GridSettings grid_settings => _grid_settings.Value;
public FontFamily font_family => _font_family.Value;
public FontWeight font_weight => _font_weight.Value;
public double font_size => _font_size.Value;
public SolidColorBrush font_color => _font_color.Value;
public SolidColorBrush selection_highlight => _selection_highlight.Value;
public SolidColorBrush background_color => _background_color.Value;
public SimpleGridSettingsVM create_vm(GridSettings settings) {
var vm = new SimpleGridSettingsVM(_column_flags.ToDictionary(kv => kv.Key, kv => kv.Value.flags));
vm.load_from(settings);
return vm;
}
public GridSettings create() {
// IMPORTANT: don't overwrite current sorting
_grid_settings.set(grid_settings.with(_columns.Select(c => {
var col_settings = c.create();
if (_column_flags.TryGetValue(c.id.ToString(), out ImmutableColumnSettingStore store))
col_settings = store.apply(col_settings);
return col_settings;
})));
return grid_settings;
}
/// <summary>
/// This will overwrite the columns' immutable-value store (unlike the IObserver&lt;<see cref="GridSettings"/>&gt; 'OnNext' implementation)
/// </summary>
public void load_from(GridSettings grid) => _load_from_core(grid, true);
void IObserver<GridSettings>.OnCompleted() { }
void IObserver<GridSettings>.OnError(Exception error) { }
void IObserver<GridSettings>.OnNext(GridSettings grid) {
if (grid.columns != null) {
var cols_with_applied_flags = grid.columns.ToList();
for (int i = 0; i < cols_with_applied_flags.Count; i++) {
if (_column_flags.TryGetValue(cols_with_applied_flags[i].id, out ImmutableColumnSettingStore store)) {
cols_with_applied_flags[i] = store.apply(cols_with_applied_flags[i]);
}
}
grid = grid.with(cols_with_applied_flags);
}
_load_from_core(grid, false);
}
void _load_from_core(GridSettings grid, bool reset_immutable_flag_stores) {
grid = grid ?? GridSettings.Default;
_grid_settings.set(grid);
foreach (var col in _columns) {
col.set_fallback_font_params(new FontFamily(grid.font_family), grid.font_size, grid.font_color.rgb_color(), grid.font_weight, grid.background_color.argb_color());
}
// ** in the unfortunate event that we botch backwards compatability, and the columns don't bear a 1:1 relationship with the column-settings
// don't add insult to injury by crashing **
if (grid_settings.columns != null) {
if (reset_immutable_flag_stores) {
foreach (var kv in _column_flags) {
if (grid.columns.TryGetFirst(c => c.id == kv.Key, out GridColumnSettings cs))
kv.Value.store(cs);
}
}
if (_columns != null) {
var col_settings_id_map = grid_settings.columns.ToDictionary(cs => cs.id);
var columns_id_map = _columns.ToDictionary(c => c.id.ToString());
if (col_settings_id_map.Count == columns_id_map.Count && col_settings_id_map.Count == grid_settings.columns.Count)
_sync_column_order?.Invoke(grid_settings.columns);
// ** don't assume that columns:column_settings have a perfect 1:1 relationship (they should, but it is possible we accidentally break the settings as grids/uis change) **
foreach (var col in _columns) {
if (col_settings_id_map.TryGetValue(col.id.ToString(), out GridColumnSettings cs))
col.load_from(cs);
}
}
_invalidate_rows?.Invoke();
}
}
}
}
@@ -60,6 +60,7 @@ namespace mbts.spark.filters {
TimeFilter = "TimeFilterTemplate",
DiscreteValues = "DiscreteValuesTemplate",
DiscreteValuesCollectionView = "DiscreteValuesCollectionViewTemplate",
SecurityCriteriaCriteria = "SecurityCriteriaCriteria",
EnumValues = "EnumValuesTemplate",
Regex = "RegexTemplate",
YesNo = "YesNoTemplate";
@@ -12,6 +12,8 @@ namespace mbts.spark.filters {
ICommand cancel { get; }
bool filter(object row);
event Action filter_changed;
void load_state(object state);
object get_state();
}
interface IDataGridColumnFilter<in TRow> : IDataGridColumnFilter {
@@ -20,6 +22,6 @@ namespace mbts.spark.filters {
interface IDataGridColumnPersistentFilter<in TRow, TState> : IDataGridColumnFilter<TRow> {
void load_state(TState state);
TState get_state();
new TState get_state();
}
}
}
@@ -1,5 +1,6 @@
using mbts.ui;
using mbts.ui.shell;
using mbts.utils;
using System;
using System.Windows.Input;
@@ -53,6 +54,15 @@ namespace mbts.spark.filters {
_raise_filter_changed();
}
object IDataGridColumnFilter.get_state() =>
get_state();
void IDataGridColumnFilter.load_state(object state) {
if (state is TState fstate)
load_state(fstate);
else if (state != null) throw ExceptionBuilder.Argument.not_of_type<TState>(nameof(state), state?.GetType());
}
protected abstract bool _get_active();
protected abstract bool _filter(TValue value);
protected abstract void _load_state_internal(TState state);
@@ -111,4 +121,4 @@ namespace mbts.spark.filters {
bool IDataGridColumnFilter.filter(object row) =>
filter(ConvertUtils.try_convert<TRow>(row));
}
}
}
@@ -1,5 +1,7 @@
using bts.api;
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark.filters {
class SecurityCriteriaFilter<TRow> : FilterVMBase<TRow, SecurityImmutable, SecurityCriteria>, IDisposable {
@@ -43,4 +45,50 @@ namespace mbts.spark.filters {
void _set_default_values() =>
_security_criteria = model.result_criteria;
}
}
class SecurityCriteriaCriteriaFilter<TRow> : FilterVMBase<TRow, SecurityCriteria, SecurityCriteria>, IDisposable {
SecurityCriteria _security_criteria;
public SecurityCriteriaCriteriaFilter(Func<TRow, SecurityCriteria> get_row_val, string prop_name)
: base(get_row_val, prop_name) { }
public ContractPickerModel model { get; }
public void Dispose() =>
model.security_criteria_clause_changed -= _on_security_criteria_clause_changed;
public override SecurityCriteria get_state() =>
_security_criteria;
protected override bool _filter(SecurityCriteria value) {
if ((_security_criteria?.clauses).is_null_or_empty())
return true;
if (!_security_criteria.is_defined())
return true;
if ((value?.clauses).is_null_or_empty())
return false;
var result = value.clauses.Any(c => c.is_defined && _security_criteria.clauses.Any(fclause => c.is_subset_of(fclause)));
return result;
}
protected override void _load_state_internal(SecurityCriteria state) =>
_security_criteria = state;
protected override void _clear() {
_close();
using (_clear_properties())
_set_default_values();
}
protected override bool _get_active() =>
_security_criteria != null && _security_criteria.clauses.Length > 0;
void _on_security_criteria_clause_changed(object sender, SecurityCriteriaClauseChangedEventsArgs e) {
_security_criteria = model.result_criteria;
_raise_filter_changed();
}
void _set_default_values() =>
_security_criteria = model.result_criteria;
}
}
@@ -1,6 +1,7 @@
using bts;
using mbts.ui;
using mbts.ui.shell;
using mbts.utils;
using System;
using System.Windows.Input;
@@ -60,16 +61,24 @@ namespace mbts.spark.filters {
public TCompoundState get_state() =>
_get_state(_filter1.get_state(), _filter2.get_state());
object IDataGridColumnFilter.get_state() =>
get_state();
public void load_state(TCompoundState state) {
_filter1.load_state(_get_filter1_state(state));
_filter2.load_state(_get_filter2_state(state));
}
void IDataGridColumnFilter.load_state(object state) {
if (state is TCompoundState cstate)
load_state(cstate);
else throw ExceptionBuilder.Argument.not_of_type<TCompoundState>(nameof(state), state?.GetType());
}
protected void _raise_request_update() =>
RequestUpdate?.Invoke();
bool IDataGridColumnFilter.filter(object row) =>
filter(ConvertUtils.try_convert<TRow>(row));
}
}
}
@@ -165,6 +165,18 @@
/>
</usp:FilterContentControl>
</DataTemplate>
<DataTemplate x:Key="{x:Static filters:DataGridFilterKnownTemplates.SecurityCriteriaCriteria}">
<usp:FilterContentControl
ApplyCommandParameter="{Binding}"
ApplyCommand="{x:Static c:DataGridFilterProperties.CommitCommand}"
ClearCommandParameter="{Binding}"
ClearCommand="{x:Static c:DataGridFilterProperties.ClearCommand}"
>
<msp:ContractPickerView DataContext="{Binding model}">
</msp:ContractPickerView>
</usp:FilterContentControl>
</DataTemplate>
<DataTemplate x:Key="AlgoTypeTemplate">
<usp:FilterContentControl
ApplyCommandParameter="{Binding}"
@@ -93,6 +93,25 @@ namespace mbts.ui.controls {
public uint Increment { get { return (uint)GetValue(IncrementProperty); } set { SetValue(IncrementProperty, value); } }
#region TickUpCommand
public static readonly DependencyProperty TickUpCommandProperty =
TickUpDown.TickUpCommandProperty.AddOwner(typeof(TickUpDownUInt));
public ICommand TickUpCommand {
get => GetValue(TickUpCommandProperty) as ICommand;
set => SetValue(TickUpCommandProperty, value);
}
#endregion
#region TickDownCommand
public static readonly DependencyProperty TickDownCommandProperty =
TickUpDown.TickDownCommandProperty.AddOwner(typeof(TickUpDownUInt));
public ICommand TickDownCommand {
get => GetValue(TickDownCommandProperty) as ICommand;
set => SetValue(TickDownCommandProperty, value);
}
#endregion
// explicit props
public bool EnableArrowKeyTick { get => _enable_arrow_key_tick.value; set => _enable_arrow_key_tick.value = value; }
public ModifierKeys ArrowKeyTickModifiers { get => _arrow_key_tick_modifiers.value; set => _arrow_key_tick_modifiers.value = value; }
@@ -120,13 +139,19 @@ namespace mbts.ui.controls {
}
void _tick_up() {
if (TickUpDownHelper.tick_up(Value, Increment, out uint result))
if (TickUpCommand is ICommand cmd) {
cmd.ExecuteIfCan(null);
} else if (TickUpDownHelper.tick_up(Value, Increment, out uint result)) {
SetCurrentValue(ValueProperty, result);
}
}
void _tick_down() {
if (TickUpDownHelper.tick_down(Value, Increment, out uint result))
if (TickDownCommand is ICommand cmd) {
cmd.ExecuteIfCan(null);
} else if (TickUpDownHelper.tick_down(Value, Increment, out uint result)) {
SetCurrentValue(ValueProperty, result);
}
}
}
}
@@ -5,6 +5,8 @@ using System.Windows.Media;
namespace mbts.ui.converters {
class ColorToBrushConverter : IValueConverter {
public static ColorToBrushConverter Instance => new ColorToBrushConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is Color color) return new SolidColorBrush(color);
if (value is BTSColor bts_color) return new SolidColorBrush(bts_color);
@@ -6,10 +6,12 @@ using System.Windows.Data;
namespace mbts.ui.converters {
[ValueConversion(typeof(SecurityTuple), typeof(string))]
public class SecurityDisplayTextConverter : IValueConverter {
public string TextForNull { get; set; } = "Unknown";
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is Security s)
return SecurityFormatter.format(s);
return value?.ToString() ?? "Unknown";
return value?.ToString() ?? TextForNull;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
@@ -1,4 +1,5 @@
using bts.api;
using mbts;
using mbts.spark;
using System;
using System.Globalization;
@@ -17,7 +18,8 @@ namespace mbts.ui.spark.converters {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
var price = get_val<double>(values, 0);
var spec = get_val<ContractSpecificationImmutable?>(values, 1) ?? SecurityDataHelper.Defaults.ContractSpecificationImmutable;
return spec.format(price);
var settings = get_val<mbts.user.settings.TickDisplaySettings>(values, 2);
return spec.format(price, settings);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
@@ -64,6 +64,25 @@ namespace mbts.ui {
}
}, can_execute == null ? null : new Func<object, bool>(_ => can_execute()));
public static IDialogCommand export_csv(
Func<string> get_file_content,
string source_window_name) =>
new AnonymousFileDialogCommand<object>(_ =>
_create_save_file_dialog(
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
$"BTS_Spark_{source_window_name}_{DateTime.Now.ToString("yyyyMMdd_HHmm")}.csv",
new[] { ("CSV (comma delimited)", new string[] { "csv" }) }
),
(_, file) => {
try {
File.WriteAllText(file, get_file_content.Invoke());
} catch (Exception ex) {
BTSMessageBox.Show($"Error exporting data to csv: {ex}.", "CSV Export Error", BTSMessageBoxButtons.OK, System.Windows.MessageBoxImage.Error, true, false, true);
}
},
// we are always able to perform an export, even when no rows (it will just be a header)
(_) => true);
static TDialog _create_file_dialog<TDialog>(string initial_dir = null, IReadOnlyList<(string name, string[] extensions)> filter_items = null) where TDialog : FileDialog, new() {
var rv = new TDialog();
if (!string.IsNullOrEmpty(initial_dir))
Loaded 100 of 388 files, more files were not shown because too many files have changed in this diff. Show more