Compare commits

...
125 Commits
Author SHA1 Message Date
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
207 changed files with 8306 additions and 6176 deletions

No files matched your search

+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"
]
}
]
}
+7
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,6 +42,11 @@ 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")
+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)
+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);
@@ -317,6 +326,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 +476,12 @@ 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;
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 +513,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 +526,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 +575,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 +588,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 +776,16 @@ 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; }
public void sanitize() =>
sanitize(this);
@@ -639,6 +820,8 @@ 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;
return s;
}
@@ -712,7 +895,9 @@ 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;
public bool Equals(LadderSettings other) =>
profile_equals(this, other) &&
@@ -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,7 @@ 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;
uint? _desired_account;
AlgoID? _desired_algo;
TimeInForce _default_time_in_force;
@@ -48,6 +54,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 +68,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 +86,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 +119,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;
@@ -155,6 +167,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 +209,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 +251,7 @@ 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 IEnumerable<LadderColumnSettingsVM> column_items {
get {
@@ -248,15 +290,15 @@ namespace mbts.spark {
}
}
public IEnumerable<OrderEntryPanelItemVisibilityVM> order_entry_panel_items {
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 +337,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 +368,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)));
@@ -359,12 +402,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 +456,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 +476,15 @@ 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();
// 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 +499,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 +628,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 +639,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 +660,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 +679,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 +708,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,4 +1,5 @@
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.proto;
using bts.shell;
@@ -9,6 +10,8 @@ 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,14 +30,22 @@ 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;
double
_position,
@@ -49,8 +60,10 @@ 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>();
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 +78,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 +118,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 +130,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 +149,27 @@ 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));
}
};
PropertyAutoWrapper<LadderSettings, bool> bool_prop(string name, Func<LadderSettings, bool> get_bool) =>
@@ -159,71 +188,41 @@ 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 => {
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() {
@@ -299,17 +298,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 +340,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 +354,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;
@@ -409,11 +424,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 +462,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 +565,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 +643,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 +664,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);
}
@@ -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;
@@ -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,14 @@ 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();
}
void _on_vm_disposed(bool obj) =>
_dispose_resources();
@@ -814,6 +864,7 @@ namespace mbts.spark {
} else {
_scroll_to_row(_row_count / 2);
}
_refresh_frozen_mkt_lines();
}
}
@@ -888,13 +939,28 @@ 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_session_low_line() {
if (_settings.session_low_line.show && _mkt_break_lines.low_render_index.HasValue)
@@ -916,14 +982,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 +1017,7 @@ namespace mbts.spark {
_price_list,
vapls,
_settings.row_height,
security.ContractSpec,
_spec,
_settings.volume_column,
last_index,
_get_last_arrow_string(),
@@ -988,8 +1054,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 +1083,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 +1100,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;
}
@@ -1080,31 +1146,46 @@ namespace mbts.spark {
prices != null && prices.Length > 0 ? prices[0].price : double.NaN;
double _get_bid_market_break() {
var raw_bid_break = _get_bid_market_break_raw();
return _normalize_price(raw_bid_break);
}
double _normalize_price(double price) =>
TickUtils.normalize_price(_spec, price);
double _get_bid_market_break_raw() {
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) ? _vm.settlement : _spec.tickdown(best_ask) :
best_bid;
}
double _get_ask_market_break() {
var raw_ask_break = _get_ask_market_break_raw();
return _normalize_price(raw_ask_break);
}
double _get_ask_market_break_raw() {
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);
_spec.tickdown(best_ask);
}
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);
double.IsNaN(best_ask) ? _vm.settlement : _spec.tickdown(best_ask) :
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(_spec, best_bid, best_ask);
}
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 +1194,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 +1210,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);
@@ -1146,6 +1231,18 @@ namespace mbts.spark {
_price_dirty = _settings.price_column.show;
// 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,14 @@ 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);
_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
@@ -1265,7 +1364,8 @@ namespace mbts.spark {
// 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 +1379,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;
@@ -1318,11 +1423,18 @@ namespace mbts.spark {
}
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);
@@ -1342,6 +1454,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 {
@@ -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,7 +892,7 @@
</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>
@@ -656,7 +925,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 +939,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 +998,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 +1210,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 +1241,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 +1263,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);
@@ -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))
@@ -77,6 +77,16 @@ namespace mbts.ui {
var bytes = BitConverter.GetBytes(color);
return Color.FromArgb(bytes[3], bytes[2], bytes[1], bytes[0]);
}
/// <summary>
/// Creates a blended color, whose a, r, g, b channels are the average of two colors.
/// </summary>
public static Color blend_with(this Color color, Color other) =>
Color.FromArgb(
(byte)(((ushort)color.A + (ushort)other.A) / 2),
(byte)(((ushort)color.R + (ushort)other.R) / 2),
(byte)(((ushort)color.G + (ushort)other.G) / 2),
(byte)(((ushort)color.B + (ushort)other.B) / 2));
public static SolidColorBrush to_brush(this BTSColor color) =>
new SolidColorBrush(color);
@@ -87,6 +97,12 @@ namespace mbts.ui {
return brush;
}
public static Brush argb_brush(this uint color) {
var brush = new SolidColorBrush(argb_color(color));
brush.Freeze();
return brush;
}
public static SolidColorBrush rgb_solid_brush(this uint color) =>
new SolidColorBrush(rgb_color(color));
@@ -9,19 +9,13 @@ using System.Reactive.Linq;
namespace mbts.spark {
[ProtoContract]
class SecurityCriteria : IEquatable<SecurityCriteria> {
public SecurityCriteria()
{
public SecurityCriteria() {
clauses = new SecurityCriteriaClause[] { };
}
public SecurityCriteria(IEnumerable<SecurityCriteriaClause> c)
{
public SecurityCriteria(IEnumerable<SecurityCriteriaClause> c) {
clauses = c.ToArray();
}
internal SecurityCriteria(IEnumerable<SecurityImmutable> securities) {
clauses = new[] { new SecurityCriteriaClause(securities) };
}
internal SecurityCriteria(params SecurityImmutable[] securities) : this(securities.AsEnumerable()) { }
[ProtoMember(1)]
public SecurityCriteriaClause[] clauses;
@@ -31,8 +25,11 @@ namespace mbts.spark {
new SecurityCriteriaClause (security_keys: keys.ToArray())
});
internal static SecurityCriteria from_securities(ISet<SecurityImmutable> securities) =>
new SecurityCriteria(securities ?? Enumerable.Empty<SecurityImmutable>());
internal static SecurityCriteria from_securities(params SecurityImmutable[] securities) =>
from_securities(securities.AsEnumerable());
internal static SecurityCriteria from_securities(IEnumerable<SecurityImmutable> securities) =>
new SecurityCriteria(new[] { SecurityCriteriaClause.from_securities(securities) });
internal static Func<SecurityCriteria, IObservable<IEnumerable<SecurityImmutable>>> get_to_securities(IBus bus) =>
new Func<SecurityCriteria, IObservable<IEnumerable<SecurityImmutable>>>(sc => to_securities(sc, bus));
@@ -61,8 +58,27 @@ namespace mbts.spark {
this.pretty_name = pretty_name;
}
public SecurityCriteriaClause(IEnumerable<SecurityImmutable> securities)
: this(security_keys: securities.Select(s => s.Id).ToArray()) { }
public static SecurityCriteriaClause from_securities(IEnumerable<SecurityImmutable> securities) {
var num_secs = securities.SoftCount();
uint[] keys;
string pretty_name = null;
if (num_secs.HasValue) {
keys = new uint[num_secs.Value];
int i = 0;
if (keys.Length == 1) {
foreach (var s in securities) {
keys[i++] = s.Id;
pretty_name = s.ToString();
}
} else {
foreach (var s in securities)
keys[i++] = s.Id;
}
} else {
keys = securities.Select(s => s.Id).ToArray();
}
return new SecurityCriteriaClause(security_keys: keys, pretty_name: pretty_name);
}
[ProtoMember(1)]
public readonly string pretty_name;
@@ -24,6 +24,12 @@ namespace mbts.spark {
return self == null ? all_secs : all_secs.Values.Where(s => ContractPickerModel.security_in_criteria(self, s)).ToDictionary(key => key.Id);
}
public static async Task<IReadOnlyDictionary<uint,SecurityImmutable>> securities(this SecurityCriteria self, IBus bus, TimeSpan timeout) {
var md_map = await bus.subscribe(SparkTags.SecuritiesTag).FirstOrDefaultAsync(timeout, DictionaryHelper.empty<uint, SecurityMD>());
var all_secs = md_map.Values.Select(md => md.security).ToDictionary(key => key.Id);
return self == null ? all_secs : all_secs.Values.Where(s => ContractPickerModel.security_in_criteria(self, s)).ToDictionary(key => key.Id);
}
public static IObservable<IEnumerable<SecurityImmutable>> subscribe(this SecurityCriteria self, IBus bus) {
var md_source = bus.subscribe(SparkTags.SecuritiesTag);
return self == null ? md_source.Select(kv => kv.Values.Select(smd => smd.security)) :
@@ -1,388 +0,0 @@
using bts.api;
using bts.utils;
using mbts.co2;
using mbts.user.settings;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
static class TickUtils {
internal static ContractSpecificationImmutable spec_or_default(this ContractSpecificationImmutable? spec) =>
spec.HasValue ? spec.Value : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
internal static ContractSpecificationImmutable spec_or_default(this SecurityImmutable security) =>
security.HasContractSpec ? security.ContractSpec : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
internal static ContractSpecificationImmutable spec_or_default(this SecurityImmutable? security) =>
security.HasValue && security.Value.HasContractSpec ? security.Value.ContractSpec : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
public static ContractSpecificationImmutable spec_or_default(this SecurityMD? security_md) =>
(security_md?.security).spec_or_default();
public static double ticksize(this ContractSpecificationImmutable spec, double price) =>
Math.Abs(price) > spec.PriceBreak ? spec.HighTick : spec.LowTick;
public static string format(this ContractSpecificationImmutable spec, double price, double with_regard_to_price, bool in_tick_terms) {
double net_change = price - with_regard_to_price;
return (in_tick_terms) ?
TickStore.get(spec).format_without_decimal_part((net_change) / spec.ticksize(net_change)) :
TickStore.get(spec).format(net_change);
}
public static string format(this ContractSpecificationImmutable spec, double price) =>
TickStore.get(spec).format(price);
public static string format(this ContractSpecificationImmutable? spec, double price) =>
spec.spec_or_default().format(price);
public static string format(this ContractSpecificationImmutable spec, double price, double display_multiplier, bool show_commas, string nan_text) =>
TickStore.get(spec).format(price, display_multiplier, show_commas, nan_text);
public static string format(this ContractSpecificationImmutable? spec, double price, double display_multiplier, bool show_commas, string nan_text) =>
spec.spec_or_default().format(price, display_multiplier, show_commas, nan_text);
public static double parse(this ContractSpecificationImmutable spec, string text, double display_multiplier, string nan_text) =>
TickStore.get(spec).parse(text, display_multiplier, nan_text);
public static double parse(this ContractSpecificationImmutable? spec, string text, double display_multiplier, string nan_text) =>
spec.spec_or_default().parse(text, display_multiplier, nan_text);
public static bool is_integer_tick(ContractSpecificationImmutable spec) =>
spec.MainFraction == 0 &&
spec.PriceDisplayFormat == 0 &&
spec.LowTick > 0 &&
MathUtils.is_int(spec.LowTick) &&
spec.HighTick > 0 &&
MathUtils.is_int(spec.HighTick);
public static ContractSpecification create_integer_tick_spec(int low_tick) =>
create_integer_tick_spec(low_tick, low_tick);
public static ContractSpecification create_integer_tick_spec(int low_tick, int high_tick) {
var rv = SecurityDataHelper.Defaults.ContractSpecification;
rv.MainFraction = 0;
rv.SubFraction = 0;
rv.PriceDisplayFormat = 0;
rv.PriceBreak = 0;
rv.LowTick = low_tick;
rv.HighTick = high_tick;
return rv;
}
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double low = bids[bids.Length - 1].price;
if (double.IsNaN(low))
low = bids.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(low))
low = asks[0].price;
return low;
}
public static double high_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double high = asks[asks.Length - 1].price;
if (double.IsNaN(high))
high = asks.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(high))
high = bids[0].price;
return high;
}
public static int? tickdiff(this ContractSpecificationImmutable spec, double x, double y) {
if (get_regional_ticks(spec).try_get_tickdiff(x, y, out int n_low, out int n_high))
return n_low + n_high;
return null;
}
public static int tickdiff_unchecked(this ContractSpecificationImmutable spec, double x, double y) {
(int n_low, int n_high) = get_regional_ticks(spec).tickdiff_unchecked(x, y);
return n_low + n_high;
}
public static double mid_tick(ContractSpecificationImmutable spec, double bid, double ask) {
double raw_mid;
if (double.IsNaN(bid)) {
if (double.IsNaN(ask)) return double.NaN;
raw_mid = ask;
} else if (double.IsNaN(ask)) {
raw_mid = bid;
} else {
raw_mid = (bid + ask) / 2.0;
}
double tick_size = spec.ticksize(raw_mid);
return raw_mid < 0 ?
raw_mid + (raw_mid % tick_size) :
raw_mid - (raw_mid % tick_size);
}
public static TickFactors deduce_ticks(IEnumerable<ContractSpecification> contract_specs) {
var ticks = new HashSet<TickFactors>(contract_specs.Select(TickStore.get).SelectMany(rtf => new[] { rtf.low_tick, rtf.high_tick }));
switch (ticks.Count) {
case 0: return TickStore.default_rtf.low_tick;
case 1: return ticks.First();
default:
double tick_base_lcm = MathUtils.LCM(ticks.Select(t => (int)t.tick_base));
double tick_unit_gcd = MathUtils.GCD(ticks.Select(t => t.tick_unit * (tick_base_lcm / t.tick_base)));
return new TickFactors(tick_base_lcm, tick_unit_gcd);
}
}
public static RegionalTickFactors get_tick_factors(ContractSpecificationImmutable spec) =>
TickStore.get(spec);
public static double tickup(this ContractSpecificationImmutable spec, double value) =>
TickStore.get(spec).tickup(value);
public static double tickup(this ContractSpecificationImmutable spec, double value, int count) {
if (count == 0) return value;
if (count < 0) return TickStore.get(spec).tickdown(value, (uint)(-count));
else return TickStore.get(spec).tickup(value, (uint)count);
}
public static double tickdown(this ContractSpecificationImmutable spec, double value) =>
TickStore.get(spec).tickdown(value);
public static double tickdown(this ContractSpecificationImmutable spec, double value, int count) {
if (count == 0) return value;
if (count < 0) return TickStore.get(spec).tickup(value, (uint)(-count));
else return TickStore.get(spec).tickdown(value, (uint)count);
}
public static bool tick_equals(this ContractSpecificationImmutable contract, double x, double y) =>
MathUtils.RoundFractionalUnits(x, contract.LowTick) == MathUtils.RoundFractionalUnits(y, contract.LowTick);
public static IEqualityComparer<double> tick_equality_comparer(this ContractSpecificationImmutable contract) =>
EqualityComparer.Create((double a, double b) => tick_equals(contract, a, b), x => MathUtils.RoundFractionalUnits(x, contract.LowTick));
public static int compare_tick_fallback_to_double(this ContractSpecificationImmutable contract, double x, double y) =>
tick_equals(contract, x, y) ? 0 : x.CompareTo(y);
public static double round_to_tick(this ContractSpecificationImmutable spec, double value) {
double intpart = (long)value;
double floatpart = value % 1;
double tick = spec.ticksize(value);
floatpart = ((int)(floatpart / tick)) * tick;
return intpart + floatpart;
}
public static decimal round_to_tick(this ContractSpecificationImmutable spec, decimal value) {
decimal intpart = (long)value;
decimal floatpart = value % 1;
decimal tick = (decimal)spec.ticksize((double)value);
floatpart = ((int)(floatpart / tick)) * tick;
return intpart + floatpart;
}
public static RegionalTickFactors get_regional_ticks(ContractSpecificationImmutable? spec) =>
spec.HasValue ? TickStore.get(spec.Value) : TickStore.default_rtf;
public static RegionalTickFactors get_regional_ticks(SecurityImmutable security) =>
get_regional_ticks(security.spec_or_default());
static TickFactors _inflate_to_base(TickFactors tick_factors, double new_base) =>
new TickFactors(new_base, tick_factors.tick_unit * new_base / tick_factors.tick_base);
public static class CompareUtils {
public static IComparer<double> base_comparer =>
Comparer<double>.Create(_compare_base);
public static IComparer<double> unit_comparer =>
Comparer<double>.Create(_compare_unit);
public static IComparer<TickFactors> base_unit_comparer =>
Comparer<TickFactors>.Create(bts.utils.CompareUtils.safe_comparison<TickFactors>(compare_base_unit));
public static int compare_base_unit(TickFactors a, TickFactors b) {
int comp = _compare_base(a.tick_base, b.tick_base);
return comp == 0 ? a.tick_unit.CompareTo(b.tick_unit) : comp;
}
static int _compare_base(double a, double b) {
double reduced_a = MathUtils.factor_out(a, 10);
double reduced_b = MathUtils.factor_out(b, 10);
int comp = reduced_a.CompareTo(reduced_b);
return comp == 0 ? a.CompareTo(b) : comp;
}
static int _compare_unit(double a, double b) =>
_compare_unit(a, b, 0, 0);
static int _compare_unit(double a, double b, int a_mult10, int b_mult10) {
// fractional-coins are a special case, as they may not be an integer to begin with
if (!MathUtils.is_int(a))
return _compare_unit(a * 10, b, ++a_mult10, b_mult10);
if (!MathUtils.is_int(b))
return _compare_unit(a, b * 10, a_mult10, ++b_mult10);
double reduced_a = a_mult10 > 0 ? a : MathUtils.factor_out(a, 10, 5);
double reduced_b = b_mult10 > 0 ? b : MathUtils.factor_out(b, 10, 5);
int comp = reduced_a.CompareTo(reduced_b);
return comp == 0 ? (a * Math.Pow(10, -a_mult10)).CompareTo(b * Math.Pow(10, -b_mult10)) : comp;
}
}
static class TickStore {
public static readonly RegionalTickFactors default_rtf = new RegionalTickFactors(new TickFactors(1000, 1), new TickFactors(1000, 1), double.MaxValue);
static readonly ConcurrentDictionary<SpecKey, RegionalTickFactors> _exchange_ticks = new ConcurrentDictionary<SpecKey, RegionalTickFactors>();
static readonly bool _som_ticks_fix_enabled = config.Configuration.get<bts.config.toggles.SOMTicksFix>().enabled;
public static RegionalTickFactors get(ContractSpecification spec) =>
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
public static RegionalTickFactors get(ContractSpecificationImmutable spec) =>
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
public static RegionalTickFactors get(SecurityImmutable security) =>
security.HasContractSpec ? get(security.ContractSpec) : default_rtf;
public static RegionalTickFactors get(SecurityImmutable? security) =>
security.HasValue ? get(security.Value) : default_rtf;
static RegionalTickFactors _create_rtf(SpecKey key) {
double tick_base = key.denominator;
if (tick_base == 0) {
// indicates tick is a decimal
tick_base = 100;
}
return _create_rtf(key.low_tick, key.high_tick, key.price_break, tick_base, key.display_digits);
}
static RegionalTickFactors _create_rtf(double low_tick, double high_tick, double price_break, double reduced_target_base, uint display_digits_override) {
var low_tf = _try_create_tf(low_tick, reduced_target_base, display_digits_override);
var high_tf = _try_create_tf(high_tick, reduced_target_base, display_digits_override);
if (low_tf == null) {
low_tf = high_tf ?? (high_tf = default_rtf.low_tick);
} else if (high_tf == null)
high_tf = low_tf;
else {
// if low/high tick-bases do not match, inflate the smaller base so they do
// Note: this assumes the tick-bases are related by factors of 10 (i.e. they have all the same prime factors except for a different number of pairs of 5 and 2, e.g. 64 and 640)
if (low_tf.tick_base > high_tf.tick_base) {
high_tf = _inflate_to_base(high_tf, low_tf.tick_base);
} else if (low_tf.tick_base < high_tf.tick_base) {
low_tf = _inflate_to_base(low_tf, high_tf.tick_base);
}
}
return new RegionalTickFactors(low_tf, high_tf, price_break);
}
/// <summary>
/// Creates TickFactors with a base equal to &quot;<paramref name="tick_base"/> * 10^n,&quot; where 'n' is a natural number (including 0).
/// <para>Note: If <paramref name="display_digits_override"/> is non-zero, 'n' will be &quot;<paramref name="display_digits_override"/> - Math.Ceiling(Log10(<paramref name="tick_base"/>))&quot;</para>
/// </summary>
/// <param name="tick_size">The raw tick-size.</param>
/// <param name="tick_base">The raw tick-base.</param>
/// <returns>Well-defined TickFactors</returns>
static TickFactors _try_create_tf(double tick_size, double tick_base, uint display_digits_override) {
var tf = _try_create_tf(tick_size, tick_base);
if (tf != null && display_digits_override > 0) {
int display_digit_diff = (int)display_digits_override - tf.display_digits();
if (_som_ticks_fix_enabled) {
// we can always add display_digits (i.e. dislpay_digit_diff > 0), of course the ticks can be expressed precisely with extra 0s.
// but if we are going to remove display_digits (i.e display_digit_diff < 0), we must make sure the tick_unit is large enough to be expressed precisely with that many fewer digits
// - "-display_digit_diff <= Math.Ceiling(Math.Log10(tf.tick_unit))" will tell us if the digits can be removed without compromising precision
// - unfortunately, we cannot rely on the exchange's ContractSpec for this, there is 1 known case where the data they give us is incorrect
// SOM spreads ContractSpec (from the exchange):
// LowTick: 0.0025 (aka 1/400, or 2/800)
// MainFraction: 8
// SubFraction: 0
// PriceDisplayFormat: 1
// - as you can see, this cannot possibly be formatted with 1 display digit, it ticks in increments of 1/400
// - in practice, to maintain the '8' base, we format this in increments of 2/800 (e.g. 1-000, 1-002, 1-004, and so on up to 1-778, 2-000)
if (display_digit_diff > 0 || (display_digit_diff < 0 && -display_digit_diff <= Math.Ceiling(Math.Log10(tf.tick_unit))))
tf = _inflate_to_base(tf, tf.tick_base * Math.Pow(10, display_digit_diff));
} else if (display_digit_diff != 0) {
tf = _inflate_to_base(tf, tf.tick_base * Math.Pow(10, display_digit_diff));
}
}
return tf;
}
/// <summary>
/// Creates TickFactors with a base equal to &quot;reduced_target_base * 10^n,&quot; where 'n' is a natural number (including 0).
/// </summary>
/// <param name="tick_size">The raw tick-size.</param>
/// <param name="reduced_target_base">The smallest acceptable tick-base.</param>
/// <returns>Well-defined TickFactors</returns>
static TickFactors _try_create_tf(double tick_size, double reduced_target_base) {
if (!_check_tick_size(tick_size))
return null;
double raw_base = 1 / tick_size;
double raw_base_log10 = Math.Log10(raw_base);
double reduced_target_base_log10 = Math.Log10(reduced_target_base);
double log10_diff = raw_base_log10 - reduced_target_base_log10;
if (MathUtils.is_int(log10_diff))
return raw_base > reduced_target_base ? new TickFactors(raw_base, 1) : new TickFactors(reduced_target_base, Math.Pow(10, -log10_diff));
int count = 0;
double factored_base = log10_diff > 0 ? reduced_target_base * Math.Pow(10, Math.Ceiling(log10_diff)) : reduced_target_base;
do {
// Note that the modulus operator is not used here due to rounding errors.
// Such a rounding errror was the cause of the bug https://bluetradesys.atlassian.net/browse/SP-283.
if (MathUtils.is_int(factored_base / raw_base))
return new TickFactors(factored_base, factored_base / raw_base);
factored_base *= 10;
} while (++count < 10);
return null;
}
static TickFactors _create_tf(double tick_size) {
if (!_check_tick_size(tick_size))
return default_rtf.low_tick;
double matched_unit = double.MinValue;
TickFactors matched_tf = null;
foreach (var kv in _exchange_ticks) {
if (kv.Key.high_tick == tick_size) {
return kv.Value.high_tick;
} else if (kv.Key.low_tick == tick_size) {
return kv.Value.low_tick;
} else if (is_better_match(kv.Key.high_tick)) {
matched_unit = kv.Key.high_tick;
matched_tf = kv.Value.high_tick;
} else if (is_better_match(kv.Key.low_tick)) {
matched_unit = kv.Key.low_tick;
matched_tf = kv.Value.low_tick;
}
}
return matched_tf ?? new TickFactors(1.0 / tick_size, 1);
bool is_better_match(double known_unit) =>
known_unit < tick_size && known_unit > matched_unit && tick_size % known_unit == 0;
}
static bool _check_tick_size(double tick_size) =>
tick_size > 0 && (tick_size <= 0.5 || MathUtils.is_int(tick_size));
struct SpecKey : IEquatable<SpecKey> {
public readonly uint denominator, display_digits;
public readonly double low_tick, high_tick, price_break;
public SpecKey(ContractSpecification spec)
: this(spec.MainFraction, spec.PriceDisplayFormat, spec.LowTick, spec.HighTick, spec.PriceBreak) { }
public SpecKey(ContractSpecificationImmutable spec)
: this(spec.MainFraction, spec.PriceDisplayFormat, spec.LowTick, spec.HighTick, spec.PriceBreak) { }
public SpecKey(uint denominator, uint display_digits, double low_tick, double high_tick, double price_break) {
this.denominator = denominator;
this.display_digits = display_digits;
this.low_tick = low_tick;
this.high_tick = high_tick;
this.price_break = price_break;
}
public bool Equals(SpecKey other) =>
denominator == other.denominator &&
display_digits == other.display_digits &&
low_tick == other.low_tick &&
high_tick == other.high_tick &&
price_break == other.price_break;
public override bool Equals(object obj) =>
obj is SpecKey sk && Equals(sk);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash((int)denominator, (int)display_digits, low_tick.GetHashCode(), high_tick.GetHashCode(), price_break.GetHashCode());
}
}
}
}
@@ -0,0 +1,20 @@
using System;
namespace mbts.ui {
interface IValueChangedEventArgs<out T> {
T value { get; }
}
static class ValueChangedEventArgs {
public static IValueChangedEventArgs<T> create<T>(Func<T> get_value) =>
new LazyImpl<T>(new Lazy<T>(get_value));
class LazyImpl<T> : IValueChangedEventArgs<T> {
readonly Lazy<T> _value;
public LazyImpl(Lazy<T> value) =>
_value = value;
public T value => _value.Value;
}
}
}
@@ -122,6 +122,9 @@ namespace mbts.ui.utility.text {
public static string name(this System.Windows.Media.FontFamily font_family) =>
font_family.FamilyNames.Values.FirstOrDefault() ?? string.Empty;
public static string name_or_default(this System.Windows.Media.FontFamily font_family) =>
font_family?.FamilyNames?.Values?.FirstOrDefault() ?? StandardFontFamilyName;
public static string display_name(this System.Windows.Media.FontFamily family, double size) =>
$"{family}, {size} pt";
+3 -3
View File
@@ -12,7 +12,7 @@ using System.Threading.Tasks;
using security_key = System.UInt32;
namespace mbts.co2 {
unsafe class MDConsumerHandle {
public unsafe class MDConsumerHandle {
// Once is_initialized goes true it never goes false
public bool is_heart_beating { get { return Co2NativeMethods.md_consumer_is_heart_beating(_header); } }
@@ -260,13 +260,13 @@ namespace mbts.co2 {
}
// If Size is wrong remember to update from
[StructLayout(LayoutKind.Sequential, Size = 704)]
[StructLayout(LayoutKind.Sequential, Size = 712)]
internal struct ColdEntry {
public int exchange_status;
// Last trade
public AtomicPriceSize last_trade;
public ulong total_volume;
public utils.LLDateTime last_trade_time;
public utils.LLDateTime last_trade_time;
//// Settlement
public UInt64 misc_timestamp;
+5 -1
View File
@@ -112,5 +112,9 @@ namespace mbts.co2 {
return !a.Equals(b);
}
}
static class NativePriceSizeExtensions {
public static NativePriceSize with(this NativePriceSize nps, uint? direct_size = null, uint? implied_size = null) =>
new NativePriceSize(nps.int_price, direct_size ?? nps.direct_size, implied_size ?? nps.implied_size);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ using mbts.co2;
using System;
namespace mbts {
readonly struct SecurityMD : IEquatable<SecurityMD> {
public readonly struct SecurityMD : IEquatable<SecurityMD> {
public static readonly Func<SecurityMD, uint> key_fn = smd => smd.security.Id;
public SecurityImmutable security { get; }
@@ -24,7 +24,7 @@ static class ExchangeSpecificUitlities{
}
public static bool is_ice_lots(SecurityImmutable sec){
return sec.Ice.MarketDescription.Contains("ICE Lots");
return sec.Ice.IsIceLots;
}
public static bool is_tas(SecurityImmutable sec){
@@ -33,25 +33,36 @@ static class ExchangeSpecificUitlities{
#endregion
#region CPP
#region CPP
public static double get_future_cpp (SecurityImmutable sec){
if(sec.Symbol == "HHD")
// only futures in HHD are the dailies, everything
// else is a strip or spread
return 1;
// TODO, this should be part of the security
if(sec.Symbol == "HHD")
return 2500;
if(is_ice_lots(sec))
return 2500;
else{
var days_in_month = DateTime.DaysInMonth(Convert.ToInt32(sec.Future.ExpirationYear),
Convert.ToInt32(sec.Future.ExpirationMonth));
return days_in_month; // because the size is multiplied by 2500 already
// other futures - HNG, HHD, HEN - are like daily
// strips
UInt32 days_in_month = 31;
var mm = sec.Future.ExpirationMonth;
if(mm <=11)
days_in_month = Convert.ToUInt32(
DateTime.DaysInMonth(
Convert.ToInt32(sec.Future.ExpirationYear),
// expects 1-12
Convert.ToInt32(sec.Future.ExpirationMonth+1)));
else
// TODO : sometimes Jan is 12 !
days_in_month = 31;
return days_in_month * 2500;
}
}
public static double get_spread_cpp(SecurityImmutable sec){
// for purpose of pnl we are 0'ing out the spread pnl
return 0;
}
public static double get_strip_cpp(PNLUnitImmutable pnl,
SecurityImmutable sec){
// add up all the legs
@@ -77,16 +77,17 @@ namespace bts.utils {
public static int auto_phase(params int[] hash_codes) =>
auto_phase(Array.AsReadOnly(hash_codes));
public static int auto_phase<T>(IEnumerable<T> items) =>
auto_phase(items.Take(_max_auto_phase_buckets).ToList());
public static int auto_phase<T>(IReadOnlyList<T> hash_codes) {
public static int auto_phase<T>(IReadOnlyList<T> hash_codes, int bit_span = 32) {
Log.assert(bit_span > 0 && bit_span <= 32);
// don't divide by zero
if (hash_codes.Count == 0)
return 0;
int size = Math.Min(hash_codes.Count, _max_auto_phase_buckets);
int bits_per_hash = 32 / size;
int bits_per_hash = bit_span / size;
int phase = 1 << (bits_per_hash - 1);
int rv = hash_codes[0].GetHashCode() % phase;
/// The idea is to split the 32 to bits into evenly sized buckets for each hash code item.
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more