Compare commits

...
121 Commits
Author SHA1 Message Date
John Colvin fc1cc34257 fix flickering pnl hyperlink hover background 2019-11-18 17:35:55 -06:00
John Colvin 0359a9d840 fix flickering mview hyperlink hover background 2019-11-18 17:19:16 -06:00
John Colvin ad6e1e04ad Merge remote-tracking branch 'origin/master' into mview-popups2 2019-11-18 16:13:42 -06:00
Eric Harding 6aad329e09 Include ice symbol list in defaults for windows 2019-11-18 13:04:00 -05:00
Jake Norbut 0641cf45d9 Merged in SP-261_bye_bye_butterfly (pull request #366)
RTM - no more butterfly defaults

Approved-by: Jake Norbut <jake.norbut@bluetradesys.com>
2019-11-18 16:34:35 +00:00
Jake Norbut 1fe0efe5c3 Merged in SP-264_price_display_format (pull request #374)
RTM - SP-264 drop tick separator in ladder prices

Approved-by: John Colvin <john.colvin@bluetradesys.com>
2019-11-18 16:17:31 +00:00
Jake Norbut 93938b6ef2 fix merge 2019-11-18 10:15:19 -06:00
Jake Norbut 818806a4e1 Merged in SP-263_recenter_on_spacebar (pull request #371)
RTM - SP-263 recenter on spacebar

Approved-by: Eric Harding <eric.harding@bluetradesys.com>
2019-11-18 15:35:59 +00:00
Jake Norbut 261bf3e475 Merged in SP-260_local_max_clip (pull request #370)
RTM SP-260 local max clip

Approved-by: Eric Harding <eric.harding@bluetradesys.com>
2019-11-18 15:34:30 +00:00
John Colvin 8074e0d9e6 remove unused file (SWP.cs) 2019-11-15 17:05:29 -06:00
Eric Harding c3299232f1 Update release notes 2019-11-14 17:44:24 -05:00
Jake Norbut e2b80942df adding manual test 2019-11-14 15:58:59 -06:00
Jake Norbut 51fb068469 adding manual test 2019-11-14 15:51:40 -06:00
John Colvin b310b461ea PNLView - HyperlinkCell mouse hover animation 2019-11-14 15:15:35 -06:00
John Colvin e16864d2d6 PNLView - set Hyperlink/TextCell 'SnapToDevicePixels' to true 2019-11-14 15:13:52 -06:00
John Colvin ccb192887a PNLView - get rid of magic const row height 2019-11-14 15:13:40 -06:00
John Colvin 5e6b048013 PNLView.xaml - factor out common TextCell props into base style 2019-11-14 14:51:51 -06:00
John Colvin 29f230507c PNLView.xaml - factor out common HyperlinkCell props into base style 2019-11-14 14:51:48 -06:00
Eric Harding 81b36edfe3 Merged in subprocess (pull request #375)
[RTM] Simple sparkd subprocess runner
2019-11-14 20:26:38 +00:00
Eric Harding 90976ffeba Merged in init_spark_plugin (pull request #372)
[RTM] Always init_spark_plugin() for libspark.so
2019-11-14 20:24:50 +00:00
Eric Harding 43aad8b53f merge master 2019-11-14 15:24:36 -05:00
Eric Harding 5434647ecb Merged in SP-262-read_latest_crash_dump-crahes (pull request #368)
[RTM] SP-262 check for directory exists in keep_n_matching_files

Approved-by: Jake Norbut <jake.norbut@bluetradesys.com>
2019-11-14 20:22:53 +00:00
Eric Harding 7c4aa11c69 Merged in SP-259-elliots-tas-algo-not-firing (pull request #369)
[RTM] SP-259 - Fix md exchange status transitions.

Approved-by: Jake Norbut <jake.norbut@bluetradesys.com>
2019-11-14 20:22:44 +00:00
Eric Harding 176c1f8d92 Update sdk overview.md with blog edits 2019-11-14 15:22:01 -05:00
John Colvin a54830cde3 set HyperlinkCell 'HyperlinkClickMode' default to 'Anywhere' 2019-11-14 13:10:51 -06:00
John Colvin e210c987e7 remove HyperlinkColumn props 'padding' and 'HyperlinkClickMode' (callers should set them through the ElementStyle prop) 2019-11-14 13:09:23 -06:00
John Colvin 52e9380a0f add 'AffectsRender' flag to Hyperlink/TextCell HorizontalContentAlignment dps 2019-11-14 12:05:43 -06:00
John Colvin 6cdd637251 fix Hyperlink/TextCell padding (actually use it) 2019-11-14 11:36:21 -06:00
Jake Norbut 8b0c9e7722 adding some TAS-style symbols so we get md recording on them 2019-11-14 07:56:27 -06:00
John Colvin 96f030f4dc after opening, mview remembers subsequent security criteria changes in workspace 2019-11-13 23:32:53 -06:00
John Colvin 64b1b903a1 mview synth fills popup only shows fills of synth root algos 2019-11-13 23:15:00 -06:00
John Colvin 788eb90715 mview mini ob only shows working orders 2019-11-13 21:29:46 -06:00
John Colvin 69738643e5 mview mini orderbook only shows exchange orders for outrights, spreader orders for synths 2019-11-13 21:29:24 -06:00
John Colvin 6c25d16822 fix mview sorting (use new ColumnSortManager) 2019-11-13 21:08:27 -06:00
John Colvin 2ba10d1a97 MarketViewVM - move local methods (in constructor) to bottom 2019-11-13 21:08:27 -06:00
John Colvin 28f7b4e892 mview order ticket popup closes on send 2019-11-13 21:08:27 -06:00
John Colvin 0090a1ca36 mview toolbar menu items open regular windows instead of minis (for now) 2019-11-13 21:08:27 -06:00
John Colvin 5885dd4f6a mview fills/orderbook toolbar menu items open the new popups 2019-11-13 21:08:27 -06:00
John Colvin 7fb254f373 DIYGridRowsPresenter initialization in Loaded callback is idempotent (Loaded event not always followed by Unloaded event) 2019-11-13 21:08:27 -06:00
John Colvin 765d0cb63f mview popups use AutoHideScrollBar to prevent scrollbars from covering data 2019-11-13 21:08:27 -06:00
John Colvin 4770f875d0 mview 'Position' column shows day position (uses position service) 2019-11-13 21:08:27 -06:00
John Colvin 534f248fbf tweak HyperlinkCell focus/mouse capture logic 2019-11-13 21:08:27 -06:00
John Colvin 3d27735006 clean up DIYGrid TextColumn (mimic recent changes in HyperlinkColumn) 2019-11-13 21:08:27 -06:00
John Colvin 1bbe8d0af3 fix TextCell alignment issues 2019-11-13 21:08:27 -06:00
John Colvin 87ab2c1ca2 SP-257 mview working orders popup 2019-11-13 21:08:27 -06:00
John Colvin 17b40452b5 hamburger spruce 2019-11-13 21:08:27 -06:00
John Colvin 4bb159311f add 'Freeze' prop to BrushExtension 2019-11-13 21:08:26 -06:00
John Colvin 06935d525a add ITimeAndSalesGridSettingsMigrationRule 2019-11-13 21:08:26 -06:00
John Colvin 97df614bc0 DIYGridCellHoverAdorner 2019-11-13 21:08:26 -06:00
John Colvin cd1fab327e refactor DIYGrid.SelectionHighlight to be Color instead of Brush (also fixes drag rect adorner bug, where the color wouldn't respond to settings changes) 2019-11-13 21:08:26 -06:00
John Colvin ec1dde8342 HighPerformanceRow does not depend on TColumn 2019-11-13 21:08:26 -06:00
John Colvin 2a10b83d19 fix typo in ColumnGeometry (was assigning 'left' to 'unclicpped_left') 2019-11-13 21:08:26 -06:00
John Colvin c88d773734 add 'HyperlinkClickMode' to HyperlinkCell to support cmd on click anywhere 2019-11-13 21:08:26 -06:00
John Colvin 82b199412e fix HyperlinkCell HorizontalAlignment binding 2019-11-13 21:08:26 -06:00
John Colvin 870b892b8f market view row 'inside working orders' comparison to bbo was backwards 2019-11-13 21:08:26 -06:00
John Colvin 128e0e0877 MarketViewVM does not derive from SecurityMDGrid 2019-11-13 21:08:26 -06:00
John Colvin 1673763937 clip mview animations to scroll content presnter bounds 2019-11-13 21:08:26 -06:00
John Colvin ee16430896 factor out pnl auxiliary popups for general use (mview) 2019-11-13 21:08:26 -06:00
John Colvin 424c83bdfd introduce WindowStyle.UnsafeWindow dp 2019-11-13 21:08:26 -06:00
John Colvin f28884e4c6 refactor ChildBounds extensions to be for UIElement 2019-11-13 21:08:26 -06:00
John Colvin 3bcfee558d OrderTypeItem template uses Rectangle instead of Badge (the latter has some unresolved rounding issues) 2019-11-13 21:07:07 -06:00
John Colvin ec9967f9f0 WPFHelper.measure_content() rounds to layout correctly 2019-11-13 21:07:07 -06:00
John Colvin 0191856733 fix WPFHelper.measure_visual_content - 'left edge' calculation for right align; 'bottom edge' for bottom align 2019-11-13 21:07:07 -06:00
John Colvin c26c145b2f fix HyperlinkCell Alignment issues 2019-11-13 21:07:07 -06:00
John Colvin 077b8d615c DIYGrid CellRender.draw_text with layout rounding 2019-11-13 21:07:07 -06:00
Eric Harding a5c2e8b287 SP-230 Disable GMI_import for sdk 2019-11-13 15:37:38 -05:00
Eric Harding 24e766ba59 SP-230 include futures.coconut in dockerfile 2019-11-13 14:45:34 -05:00
Eric Harding ddd81ea93a Allow sdk shell to always skip stunnel 2019-11-13 14:45:06 -05:00
Eric Harding 5481cce34c Simple sparkd subprocess runner 2019-11-13 13:50:21 -05:00
Jake Norbut 58d39c4833 rendering ladder prices approparitely based on price_display_format 2019-11-13 10:38:26 -06:00
Jake Norbut 367d9be724 adding price_display_format to settings view 2019-11-13 10:38:04 -06:00
Jake Norbut 99f2ab7c8a adding price_display_format to vm 2019-11-13 10:37:46 -06:00
Jake Norbut 8b091b39eb adding PriceDisplayFormat enum to proto 2019-11-13 10:37:24 -06:00
Eric Harding 9d1f0e5538 Remove bad symlink breaking build. 2019-11-12 16:58:30 -05:00
Eric Harding 02ade1e0c9 Merged in SP-230-api-experience (pull request #373)
SP-230 api experience
2019-11-12 21:36:11 +00:00
Eric Harding 296749bae3 remote docker part from udpate_github 2019-11-12 16:34:29 -05:00
Eric Harding 49ec9885eb Merge branch 'master' of bitbucket.org:bluetradesys/spark into SP-230-api-experience 2019-11-12 13:28:28 -05:00
Eric Harding 04264e70b9 add spark_client.cmd 2019-11-12 11:54:15 -05:00
Eric Harding eaadc74242 SP-230 overview/blog 2019-11-12 10:56:29 -05:00
Eric Harding 6760ca1898 Always init_spark_plugin() for libspark.so 2019-11-12 08:50:31 -05:00
Eric Harding 5f2d3c7c26 SP-230 algo overview draft 2019-11-11 16:49:12 -05:00
Eric Harding 326532ed5c Urls change /algo/ to /server/ 2019-11-11 11:43:31 -05:00
Eric Harding 5cfb4a65e9 SP-230 add faq/join 2019-11-11 10:19:17 -05:00
Eric Harding f955fef680 SP-230 orders 2019-11-08 17:31:54 -05:00
Eric Harding 2214988442 SP-230 rename Algo sdk to Server sdk 2019-11-08 16:51:25 -05:00
Eric Harding e80ab05969 SP-230 subscriptions 2019-11-08 16:26:51 -05:00
Eric Harding 92bdbd460a initialization : editing 2019-11-08 15:43:31 -05:00
Jake Norbut a357d978ca misc string formatting fix; this is only used for debugging focus issues 2019-11-08 13:45:03 -06:00
Jake Norbut dfb673bd63 recenters all ladders via posting to appropriate tag to bus when spacebar is pressed over a ladder 2019-11-08 13:43:29 -06:00
Jake Norbut 24f366491b update view with appropriate controls for enforce_max_clip and max_clip 2019-11-08 10:44:03 -06:00
Jake Norbut 55134bf172 check max clip for crossing market dialog as well 2019-11-08 10:42:23 -06:00
Jake Norbut b6b33f14ef enforce max clip at order entry 2019-11-08 10:40:43 -06:00
Jake Norbut 12d532a17f adding enforce_max_clip and max_clip to settings vm 2019-11-08 10:39:38 -06:00
Jake Norbut 09679ff494 adding enforce_max_clip and max_clip to proto 2019-11-08 10:38:10 -06:00
Eric Harding 781e7c98a0 SP-230 initialization 2019-11-08 10:09:33 -05:00
Eric Harding 4189b05665 SP-262 check for directory exists in keep_n_matching_files 2019-11-07 18:08:13 -05:00
Eric Harding ff5b04f8ee SP-259 - Fix md exchange status transitions. 2019-11-07 17:52:10 -05:00
Jake Norbut 4180b9a8ec removing price and quantity special case when legs = 3 2019-11-06 11:45:42 -06:00
Eric Harding 5ba244c982 Editing initialization 2019-10-31 17:24:20 -04:00
Eric Harding 4029a6a58a registration.md 2019-10-31 16:48:06 -04:00
Eric Harding c03d05285b Copy images locally so Michael doesn't steal them 2019-10-28 17:15:18 -04:00
Eric Harding 78323d09e8 SP-230 Development with other editor 2019-10-25 17:40:10 -04:00
Eric Harding f2621da423 SP-230 Development with VScode 2019-10-25 12:19:38 -04:00
Eric Harding cf2bc03dd1 SP-230 Editing 2019-10-24 17:36:27 -04:00
Eric Harding 88e17743ef Disable onixs for sdk sparkd 2019-10-24 11:28:04 -04:00
Eric Harding 950073703f SP-230 Scripts to deploy to api.bluetradesys.com 2019-10-23 19:40:11 -04:00
Eric Harding 504d09eb6c SP-230 devcontainers are pretty neat 2019-10-22 17:36:51 -04:00
Eric Harding c2ea5aa36f ServiceContext needs to include port when deciding to connect to sparkvm:8000 for debuggin 2019-10-22 16:35:00 -04:00
Eric Harding c367488844 sdk: refactor example build 2019-10-22 13:02:10 -04:00
Eric Harding fb19dd8ce5 Add sdk dockerfile and create it on build 2019-10-17 17:43:19 -04:00
Eric Harding c0a2c6a24a Split examples into separate directories and add devcontainer 2019-10-17 17:42:30 -04:00
Eric Harding 7cbd5adccc Advance linux docker to include 'zip' executable 2019-10-17 17:28:38 -04:00
Eric Harding 3b8a4f89cc Rename Log subsystem ClientSdk to just Sdk 2019-10-17 17:25:20 -04:00
Eric Harding 04f165b1a3 api docs: change info icon 2019-10-10 17:12:25 -04:00
Eric Harding 893aa7a370 api sdk - development 2019-10-09 20:01:56 -04:00
Eric Harding 245b5d0f99 sdk docs - downloading 2019-10-09 18:56:30 -04:00
Eric Harding d5225aa75b spark sdk Installing 2019-10-09 12:21:03 -04:00
Eric Harding 48e6e1e139 client sections need work 2019-10-08 17:39:51 -04:00
Eric Harding 1ebd0be57c api docs outline 2019-10-08 17:24:01 -04:00
Eric Harding 28779d333c api docs "landing page" 2019-10-08 16:53:40 -04:00
Eric Harding c9387a92be Add docs/api 2019-10-08 16:41:59 -04:00
214 changed files with 8213 additions and 1209 deletions

No files matched your search

-1
View File
@@ -58,7 +58,6 @@ else ()
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.13.0.0 ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libACE-5.7.5.so ${CMAKE_BINARY_DIR}/server
COMMAND mkdir ARGS -p ${CMAKE_BINARY_DIR}/server/plugins
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/sdk/examples/order_plugin_example.so ${CMAKE_BINARY_DIR}/server/plugins/
COMMENT "Creating server runtime layout"
DEPENDS sparkd data_files_dir)
+1
View File
@@ -0,0 +1 @@
public
+15
View File
@@ -0,0 +1,15 @@
# The URL the site will be built for
base_url = "https://api.bluetradesys.com"
title = "BTS Spark API"
description = "Documentation for the BTS Spark API"
compile_sass = true
highlight_code = true
highlight_theme = "kronuz"
insert_anchor_links = true
build_search_index = true
[extra]
author = "Blue Trading Systems"
+5
View File
@@ -0,0 +1,5 @@
+++
title = "BTS Spark API"
+++
This is index
+7
View File
@@ -0,0 +1,7 @@
+++
title="Client API"
template="client.html"
sort_by="weight"
+++
@@ -0,0 +1,7 @@
+++
title="Getting Started"
template="client.html"
sort_by="weight"
weight = 1
+++
@@ -0,0 +1,9 @@
+++
title = "Install Prerequisites"
template="client.html"
weight = 1
+++
## Coming Soon
* Install .net core
+25
View File
@@ -0,0 +1,25 @@
+++
title="Algo API"
template="documentation.html"
redirect_to = "server/getting-started/overview/"
sort_by="weight"
+++
Getting Started
- Install (docker, editor)
- Downloading
- Write your first algo
- Testing
Reference
- Subscriptions
- Child Orders
- Detaching
- Attaching
- ...
Samples
- Iceberg
- Pro-rata
@@ -0,0 +1,7 @@
+++
title="Getting Started"
template="documentation.html"
sort_by="weight"
weight = 1
+++
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@@ -0,0 +1,138 @@
+++
title = "Development"
weight = 3
+++
# Visual Studio Code
In this section we're going to use the Remote - Containers feature of <a href="https://code.visualstudio.com/" target="_blank">Visual Studio Code<i class="fa fa-fw fa-window-restore" aria-hidden="true"></i></a> to simplify the development experience. If you are using a different editor skip to the <a href="#other-editors">other editor</a> section below.
## Remote - Containers
The Visual Studio Code Remote containers extension allows it to run a complete development environment inside the Spark SDK Docker container. The editor UI runs as a thin client on your desktop and connects to a headless instance running inside the Spark SDK Docker environment. This configuration allows us to ship a pre-configured set of [Tasks](https://code.visualstudio.com/Docs/editor/tasks) for building and testing your code.
Start Visual Studio Code and click on the puzzle piece icon on the left-hand side to open the extensions panel. Use the search box at the top to find the "Remote - Containers" extension and install it.
<img src="../vscode_remote_containers.png" width="640" />
## Into the Code
Open the examples/algo_template directory using the File->Open Folder menu option. If you opted to add it to your path during install, you could run `code .` from your command prompt.
When the workspace opens, you should see a notification offering to let you re-open the workspace inside the container.
<img src="../vscode_notification.png" width="480" />
The first time you open the workspace inside the container it will take a few seconds to configure the spark_sdk docker container. While the workspace is open in Visual Studio Code the docker image will also be running in the background. The container will stop automatically when you close the editor.
Open algo_template.cpp from the file browser on the left or by pressing ctrl-p and typing the name. Each Algo is a standard C++ class which implements **IAlgoOrder**. Placing your cursor on IAlgoOrder and pressing F12 will take you to the IAlgoOrder.h header file where you can browse the API. To learn more about how an Algo is instantiated see the [Algo Initialization](/server/ref/initialization).
## Compiling
To build press "Ctrl+Shift+B". You should see the build output at the bottom of your screen. If the build was successful the resulting algo_template.so will be automatically copied to the plugins directory where it will be loaded by the Spark server process on start.
<img src="../compiled.png" width="640" />
## Debugging
To debug your Algo set a breakpoint (F9) and open the Debug panel (Ctrl+Shift+D) and choose "(gdb) Launch". This will launch the Spark Server inside the container with the debugger attached.
<img src="../gdb_launch.png" width="380" />
With sparkd running you can launch the Spark Client (spark_client.cmd) and it will connect to the running server. Log in with the user "bts" and no password and open a ladder. You can now select your Algo on the order type dropdown and activate it by clicking on a price on the ladder.
<img src="../myalgo.png" width="150" />
While sparkd is running you can also access the Spark Server Admin interface on [http://localhost:9000](http://localhost:9000). Use the same "bts" login to access the admin interface. Here you can manage user and account permissions and risk limits.
## Starting Spark Server
To start the Spark Server without attaching a debugger press "Ctrl+Shift+P" and select "Tasks: Run Task" and then "Start Sparkd" and "Never scan task output" This will open a new terminal where you can see any messages logged with the **Sdk** subsystem as well as any warnings or errors. To stop the Spark server simply kill the terminal (trash can icon) or run the "Stop Sparkd" task.
## Next Steps
If you would like to understand more about how Docker is working behind the scenes continue to the [next section](#other-editors). Otherwise, continue to [Algo Registration](/server/ref/registration) to learn how Algos are registered with the Spark.
<hr>
# Other Editors
In this section you will be more exposed to the details of how Docker works and how to compile and test your Algo using the command line.
## Docker
Spark Server is built and runs only on Linux so you need a Linux development environment to effectively develop plug-ins for it. Docker lets you take the spark_sdk.img disk image and run it inside a virtual Linux machine running on your Windows desktop.
First, some terminology. A Docker "image" is a disk image containing a complete filesystem. The spark_sdk.img file you imported earlier is an image. A Docker "container" is what you get when you get when you attach this image to a running Linux system. You can connect to a running container and use it to run commands like the C++ compiler or the Spark server. Directories from your local filesystem can be mapped into the Docker container when it starts so you can easily share files.
### Start
The first step is to start the spark_sdk Docker container with the examples directory mapped so we can compile the examples inside the container. In your command prompt cd to the *examples* directory that you unzipped from the spark_sdk.zip file. Then run the following command to start the container.
Windows Command Prompt:
{% clipable() %}
docker run --rm -it -v %CD%:/examples -u bts -w /examples --shm-size 1024m --name spark_sdk -h spark_sdk --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 9000:9000 -p 9001:9001 spark_sdk bash
{% end %}
**Powershell** / Linux / Mac:
{% clipable() %}
docker run --rm -it -v ${PWD}:/examples -u bts -w /examples --shm-size 1024m --name spark_sdk -h spark_sdk --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 9000:9000 -p 9001:9001 spark_sdk bash
{% end %}
### Attach
The container will remain as long as this prompt is open. You can attach an additional to the running container with the following command:
{% clipable() %}
docker exec -it spark_sdk bash
{% end %}
## Compiling
Let's look at the algo_template sample first. All of the samples are laid out similarly with a `build.sh` script for compiling. As long as you are running build.sh inside the spark Docker container, it automatically copies the build output to /sparkbin/plugins/ where it needs to be to get loaded by the Spark server.
```
bts@spark_sdk:/examples$ cd algo_template/
bts@spark_sdk:/examples/algo_template$ ./build.sh
algo_template.so copied to /sparkbin/plugins/
Restart sparkd to see changes.
```
## Starting Spark Server
To start the Spark server `cd /sparkbin/` and run `./sparkd run`. Press Ctrl+C to stop it.
```
bts@spark_sdk:/sparkbin$ ./sparkd run
2019-10-25 16:21:09.383626|262552152803600|Gen|Warn |1 |main|132 |Run.cpp:53 |dev|
------------------------------------ Sparkd launched --------------------------------
```
You will see any Sdk log messages as well as any log messages at or above Warning level from other parts of the system. If you need additional diagnostic information more details logs are stored in /sparkdata/data/logging/log. These logs will be automatically deleted when you stop the Docker container.
With sparkd running you can launch the Spark Client (spark_client.cmd) and it will connect to the running server. Log in with the user "bts" and no password and open a ladder. You can now select your Algo on the order type dropdown and activate it by clicking on a price on the ladder.
<img src="../myalgo.png" width="150" />
While sparkd is running you can also access the Spark Server Admin interface on [http://localhost:9000](http://localhost:9000). Use the same "bts" login to access the admin interface. Here you can manage user and account permissions and risk limits.
## Debugging
You can debug your algo using gdb from the bash command prompt.
```bts@spark_sdk:/sparkbin$ gdb --args ./sparkd run
GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
...
Reading symbols from ./sparkd...done.
(gdb) break algo_template.cpp:45
No source file named algo_template.cpp.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (algo_template.cpp:45) pending.
(gdb) run
Starting program: /sparkbin/sparkd run
```
If your editor supports debugging over the ssh protocol you can start sshd from inside the container.
## Next
Continue to [Algo Registration](/server/ref/registration) to learn how Algos are registered with the Spark.
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -0,0 +1,79 @@
+++
title = "Download SDK"
weight = 2
+++
## Request Access 🔑
To start using the Server SDK you will need to request access though our sales department. Please email <a href="mailto:info@bluetradesys.com">info@bluetradesys.com</a> or call <a href="tel:919-913-0850">(919) 913-0850</a> and we will provide download credentials.
## Download
Download the latest SDK image here: [spark_sdk.zip](https://api.bluetradesys.com/download/spark_sdk.zip)
Unzip that folder into a working directory. Inside you should see something like this:
```
spark_sdk.img
client/
spark_client.cmd
examples/
```
## Import
Import the spark_sdk image into docker using the `docker load` command.
{% clipable() %}
docker load < spark_sdk.img
{% end %}
<div class="info">
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
<strong>If you see this error it means Docker Desktop is not running:</strong>
error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.40/images/json: open //./pipe/docker_engine: The system cannot find the file specified. In the default daemon configuration on Windows, the docker client must be run elevated to connect. This error may also indicate that the docker daemon is not running.
</div>
Confirm that the import worked by running `docker images` which should list the new image.
```
c:\bts> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
bluetradingsystems/spark_sdk latest e2be89436e73 4 hours ago 3.89GB
```
<!--
## Run
Start the docker container by running
```
docker run -d -v %CD%:/work --name spark_sdk --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8000:8000 -p 8001:8001 -p 8008:8008 bluetradingsytems/spark_sdk
```
<div class="linux">
<i class="fa fa-linux fa-fw" aria-hidden="true"></i>
If you are using Linux or Mac replace <strong>%CD%</strong> with <strong>$PWD</strong>.
</div>
The first time you invoke `docker run` you may be prompted to "share" your filesystem with Docker. This usually works fine but if you run into trouble, see <a href="">Docker Desktop for Windows tips</a> on avoiding common problems.
## Success 🎉
Docker is now running the spark SDK image. If you are on Windows or Mac it is running inside a virtual Linux machine. You can see that is is running by typing `docker ps` to list running docker containers. If you restart the Docker Desktop application you will need to start the spark_sdk image again using `docker start spark_sdk`.
You should now be able to access an admin interface at http://localhost:8008/ which will let you stop and start the spark server process.
-->
## Update
To update your Spark SDK simply download an unzip a new [spark_sdk.zip](https://api.bluetradesys.com/download/spark_sdk.zip) and use `docker load < spark_sdk.img` to overwrite the old image with the new one.
To preserve the old image so you can go back to it later run `docker tag spark_sdk spark_sdk:your_custom_tag` before loading the new SDK image.
<!--
Then stop, remove and restart spark_sdk.
```
c:\bts> docker stop spark_sdk
c:\bts> docker rm spark_sdk
c:\bts> docker run -d -v %cd%:/sdk --name spark_sdk bluetradingsytems/spark_sdk
``` -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

@@ -0,0 +1,57 @@
+++
title = "Install Prerequisites"
weight = 1
+++
These instructions assume you are using Microsoft Windows but you should be able to follow them on a Linux or Mac with minor modifications.
{% linux() %}
When there is a significant difference on non-Windows platforms it will be called out in a box like this one.
{% end %}
{% info() %}
Tips or solutions to common problems will be highlighted in a box like this.
{% end %}
The minimum hardware requirements for using the SDK are:
- **12 GB** RAM (preferably 16 GB or more)
- 4 CPU cores
- 20 GB free hard disk space
The Spark SDK uses a tool called Docker to distribute all of the tools you need to build and test your trading algorithm ("Algo"). Once you have Docker installed you can download and import the Spark SDK image and be ready to start developing your Algo in a few minutes.
## Docker
Download and install <a href="https://www.docker.com/products/docker-desktop" target="_blank">Docker Desktop for Windows/Mac. <i class="fa fa-fw fa-window-restore" aria-hidden="true"></i></a>
<div class="linux">
<i class="fa fa-linux fa-fw" aria-hidden="true"></i>
If you are using Linux follow the <a href="https://docs.docker.com/install/#supported-platforms">official install instructions for Docker for your distribution.</a>
</div>
During the install Docker may ask you to enable virtualization support and reboot.
Once Docker Desktop is installed and running it will create an icon in your system tray area that looks like the Docker logo.
![](../docker_tray.png)
Right click on the tray icon and choose 'Settings' and choose the 'Advanced' tab.
Ensure that the docker image has 8192 MB of memory and at least 2 cores. Memory that you assign to the docker image will not be available to Windows/Mac while Docker is running. CPU cores are shared with the host OS. Since you are going to use this image to compile your Algo it makes sense to allow Docker to use all available cores.
<img src="../docker_config.png" width="640" >
## Sharing
You need to give Docker access to the Windows drive where your source code is kept.
Open Settings -> Shared Drives and check the drive where you intend to keep your Algo source code.
<div class="info">
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
If you run into trouble with drive sharing, see <a href="https://code.visualstudio.com/docs/remote/troubleshooting#_docker-desktop-for-windows-tips">Docker Desktop for Windows tips</a> on avoiding common problems.
</div>
## Editor
The Server SDK includes a complete configuration for C++ development with the <a href="https://code.visualstudio.com/" target="_blank">Visual Studio Code <i class="fa fa-fw fa-window-restore" aria-hidden="true"></i></a> editor. If you are a new to C++ development on a Linux system we recommend you give this a try as it's the easiest way to get up and running quickly. Of course, If you have a different editor that you prefer we fully support that as well.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,44 @@
+++
title = "Overview"
weight = 0
+++
Using the **Spark Server SDK**, users can create new order types ("Algos"). In this case, users implement the new functionality in C++, producing a shared library that is loaded at run time by our Spark Server.
If you are interested in accessing Spark server remotely, please see the [Spark Client SDK](/client/).
## Speed
The Spark platform offers a sub-10 microsecond socket to socket response times for algorithmic trading. With the **Spark Server SDK**, you can create your order types in high-performance C++ and load them into the Spark server.
Since your order is running inside the Spark Server, there is virtually no overhead allowing your algorithm to take full advantage of the speed Spark offers. Hosting a high-performance language like C++ directly in the Spark server process allows it to respond to the market thousands of times faster than an Algo built using a proprietary drag and drop tool.
## Easy to Get Started
If you are not actively developing with C++, it can be time-consuming to set up a complete development environment from scratch. That is why we provide a full development environment out of the box with [Docker](https://docker.io) so you can download and try the Server SDK quickly and easily.
Docker works across Windows, macOS, and Linux, so you don't need to change your development environment to try the Spark API. Docker also ensures that all of the dependencies you need are packaged together in one image, so you know it will work out of the box, no matter the platform.
## Implementation
Creating a new Algo begins with implementing a standard C++ class derived from IAlgoOrder. Start from the [algo_template](/server/samples/template) example to get going quickly.
The first call your new Algo receives is [do_before_launch](/server/ref/initialization#before-launch), which is an opportunity to validate parameters and allocate any long-lived resources needed during execution. Next, it receives either [do_launch](/server/ref/initialization#launch-launch-attached) in the case of a new order or [do_launch_attached](/server/ref/initialization#launch-launch-attached) if the order is attaching to an existing order.
The Algo receives a reference to [IAlgoServices](/server/ref/subscriptions/), which is how it calls out to the Spark system. It will use [IAlgoServices](/server/ref/subscriptions/) to subscribe to market events, launch and control child orders, and communicate its state to the larger Spark system.
## Fully Integrated
When a Spark Server plugin is loaded, it registers its exposed Algos with the Spark Server though the [init_spark_plugin](/server/ref/registration) callback. This callback allows the Algo to describe the parameters it expects so they can be automatically entered in the [Spark Client](https://www.bluetradingsystems.com/bts-spark/).
<img src="../myalgo.png" width="150" />
<img src="../paramsui.png" width="460" />
Compose your Algo with out of the box Algos in the [Spark Algo Studio](https://www.bluetradingsystems.com/news/155-bts-algo-studio-provides-algo-library-with-customization-functionality) to create new functionality on the fly.
<img src="../algostudio.png" width="548" /><br />
<img src="../iced_algo.png" width="460" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

+7
View File
@@ -0,0 +1,7 @@
+++
title="Reference"
template="documentation.html"
sort_by="weight"
weight = 2
+++
+8
View File
@@ -0,0 +1,8 @@
+++
title = "Questions"
weight = 40
+++
## I need to make a call that might take a few seconds. What is the best way to do this?
Create a background thread in [do_before_launch](/server/ref/initialization#before-launch) or [do_launch](/server/ref/initialization#launch-launch-attached) to do the work. Use [on_timer](/server/ref/subscriptions#timer) to poll until the result is available.
@@ -0,0 +1,97 @@
+++
title = "Initialization"
weight = 10
+++
Each Algo derives from IAlgoOrder and overrides virtual methods to receive callbacks from the Spark system.
## Before Launch
When an order is activated by a user or by another Algo, the first call it receives is do_before_launch. This call happens outside of the latency-sensitive path, so it's the place to validate parameters and set up data structures that the Algo might need.
This call returns an OrderOpResult, which allows it to fail and return an error message to its creator. If a user created this order, this message is displayed in the [Activity Log](https://www.bluetradingsystems.com/docs/bts_spark/Help/#activity-log) and across the bottom of the ladder.
Every SDK Algo is a "Dynamic" Algo since it's parameters are defined during [registration](/server/ref/registration/). Use the `get_if<T>` helper to retrieve a named parameter from the `order_params` collection as a `std::option<T>`. If a parameter has a valid default value you can use `std::option::value_or(...)` to set default values for missing parameters. If a parameter *must* exist or must be within a certain valid range, return an error to the caller.
```cpp
OrderOpResult MyAlgo::do_before_launch(IBeforeLaunchAlgoServices& serv) {
auto& def = serv.definition();
auto& params = def.custom_def().dynamic().order_params();
auto mqi = get_if<uint32_t>(params, "Min Quantity");
if (!mqi || mqi.value() == 0)
return OrderOpResult{
ORDER_ERROR_INVALID_ORDER_DEFINITON,
"Min Quantity parameter must be defined and non zero"};
}
```
<div class="info">
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
The parameters are defined during <a href="/server/ref/registration/">registration</a> by the <a href="/server/ref/registration/#dynamicorderdescriptor">DynamicOrderDescriptor</a>.
</div>
### IBeforeLaunchAlgoServices
IBeforeLaunchAlgoServices is passed to `do_before_launch` and allows the Algo to query its environment.
```cpp
class IBeforeLaunchAlgoServices {
public:
// The OrderDefinition for this algo
virtual api::orderman::OrderDefinition const &definition() const = 0;
// The security this algo is paramterized with (also available from
// definition())
virtual api::Security const &security() const = 0;
// The unique order key for this algo
virtual OrderKey order_key() const = 0;
// The parent key for this algo's parent or null_order_key() if the
// algo does not have a parent
virtual OrderKey parent_order_key() const = 0;
// The security identified by k or nullptr if no such security exists
virtual api::Security const *lookup_security(SecurityKey k) const = 0;
// Wheter this algo is being attached to an existing order.
// If an algo does not support attaching it can return an error
// if is_attaching() == true from do_before_launch.
virtual bool is_attaching() const = 0;
// Build a child template that can later can be used to quickly
// launch child orders. The reserve parameter indicates how many
// orders should be created ahead of time, before they are
// launched. This is usefull to remove the relatively expensive
// order creation operation to a background thread.
virtual ChildTemplate &
make_child_template(api::orderman::OrderDefinition const &child_def,
size_t reserve = 1) = 0;
// Get the current time
virtual TimeStamp get_time() const;
};
```
## Launch / Launch Attached
Depending on the state of `is_attaching()` the next call will either be `do_launch` or `do_launch_attached`. The only difference is that in `do_launch_attached` the existing OrderKey and ChildState are provided.
`do_launch` and `do_launch_attached` are in the latency-sensitive path and should not do more work than needed. These functions get a reference to `IAlgoServices`, which is a superset of the functionality available on `IBeforeLaunchAlgoServices`. Most Algos should store a pointer or reference to this object for managing child orders (it has a stable address).
Now is the time to use `IAlgoServices` to subscribe to market events that are relevant to your Algo and create any child orders that need to exist at the start of your Algo. You can safely call IAlgoServices from any `IAlgoOrder` callback.
To learn more about subscribing to events see [subscriptions](/server/ref/subscriptions).
### Things to do in the launch callback
☑ Store a reference to IAlgoServices
☑ Subscribe to relevant market events
☑ Launch a child order?
## Update
If the user or parent Algo changes the order parameters (price, size, or the custom parameters defined during registration), Spark calls the `do_update` callback with the new parameters. It is up to the Algo to decide how to apply the new parameters to update the running state. It can do this by canceling, changing, or adding new child orders.
+88
View File
@@ -0,0 +1,88 @@
+++
title = "Child Orders"
weight = 30
+++
Every Algo must, at some point, launch a child order to be useful. Each child order can be another Algo or regular exchange order. When the child order is updated or filled, those changes are also sent to the parent.
# Templates
Since launching a child order is one of the most latency-sensitive things an Algo can do, the Server SDK enables you to create a child template. An order template prepares an order based on a known OrderDefinition and gets it ready to send on a background thread. When the Algo determines it is time to send the order, it only needs to provide the price and quantity to complete and send the order. If an Algo expects to send multiple child orders, it can reserve more than one. Preparing a child template is typically done in [do_before_launch](http://127.0.0.1:1111/server/ref/initialization/#before-launch) callback.
```cpp
virtual ChildTemplate&
IBeforeLaunchAlgoServices::make_child_template(
api::orderman::OrderDefinition const &child_def,
size_t reserve = 1);
```
# Launch
IAlgoServices provides two overloads for launching child orders. By default, launching a child order automatically subscribes the Algo to its state changes.
Launch a child order from a template if you have one prepared. This is the fastest option.
```cpp
OrderKey IAlgoServices::launch_child(ChildTemplate&, Price, Quantity, bool subscribe = true);
```
Launch a child order by constructing an OrderDefinition as needed.
```cpp
OrderKey launch_child(OrderDefinition const &, Price, Quantity, bool subscribe = true);
```
When a child is launched its `OrderKey` is returned to the caller. This ID can be used to update, cancel, or detach the child order.
## Update
To update an existing child order, call update_child and pass an Update. The Update type lets you change the price, size or the entire OrderDefinition. You only need to specify the fields that you intend to update so only set the price() field if that is what you are changing.
```cpp
void IAlgoServices::update_child(OrderKey child, api::orderman::Update const &);
```
## Cancel
To cancel an existing child order call `cancel_child`. When the order is canceled you will receive an update though the `on_child_change` callback.
```cpp
void IAlgoServices::cancel_child(OrderKey child);
```
## Detach
The lifetime of a child order is tied to its parent. If a parent is canceled or completed child orders are automatically stopped as well. If the Algo needs to launch an order that will continue after the Algo ends use `detach_child` to sever the connection. This order hierarchy can be viewed in the Spark [Order Book](https://www.bluetradingsystems.com/docs/bts_spark/Trading/#order-book).
```cpp
void IAlgoServices::detach_child(OrderKey child);
```
To detach all children at once use `detach_all_children`.
```cpp
void IAlgoServices::detach_all_children();
```
# Child Updated
When a child order is updated the parent receives the `on_child_change` callback with an updated ChildState object.
```cpp
void on_child_change(OrderKey, ChildState const &) override;
struct ChildState {
Price price;
Quantity quantity;
Quantity total_fills;
Price avg_price;
Quantity last_fills;
Price last_price;
ChildStatus status;
Quantity fills_at_attach_time;
};
```
<!--
To talk about:
- updating fills (update_fill_state)
- handling child finished (unexpected cancel)
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

@@ -0,0 +1,92 @@
+++
title = "Registration"
weight = 1
+++
When Spark Server starts, it loads all plugins listed in the /sparkbin/plugins directory and calls an exported function named `init_spark_plugin` which must have the following signature.
```cpp
extern "C" void init_spark_plugin(bts::SparkServices &serv) { }
```
The `init_spark_plugin` function registers the Algo with a DynamicOrderDescriptor. Once registered, the Algo appears in the [Spark ladder](https://www.bluetradingsystems.com/docs/bts_spark/Trading/#ladder) and the [Algo Studio](https://www.bluetradingsystems.com/docs/bts_spark/Trading/#algo-studio).
To register an Algo call `register_dynamic_algo_order` on the provided bts::SparkServices instance. This function takes the DynamicOrderDescriptor and a function that is called to construct the IAlgoOrder. A single init_spark_plugin can call register_dynamic_algo_order multiple times to register multiple Algos.
The DynamicOrderDescriptor must have a dynamic_order_type (name), or the registration will fail.
```cpp
DynamicOrderDescriptor desc;
desc.set_dynamic_order_type("My Algo");
auto result =
serv.register_dynamic_algo_order(
desc, []() { return std::make_unique<MyAlgo>(); });
if (result != bts::AlgoRegistrationResult::SUCCESS) {
// Algo failed to register
}
```
## DynamicOrderDescriptor
The DynamicOrderDescriptor describes the type of parameters that the Algo accepts. These inputs can come either from the user manually entering them or from a parent Algo. Spark Algos are composable, so one Algo can activate another. See [Child Orders](/server/ref/orders) for more on how to activate custom order types as child orders.
Add parameters to the DynamicOrderDescriptor by calling `add_param_desc()` and setting the name and parameter type.
```cpp
*desc.add_param_desc() = param_bool("Enable Feature");
*desc.add_param_desc() = param_uint32("Feature Quantity");
```
The above parameters look like this when rendered in the Spark ladder.
<img src="../paramsui.png" width="460" />
And like this when viewed in the Spark order book.
<img src="../orderbook_truncated.png" width="988" />
Each descriptor can be one of the following types:
```
// Primitive
int32
uint32
double
string
bool
// Complex
Order
Array
Struct
```
**Currently the Spark UI only renders parameters with primitive types.** The complex types can only be provided by a parent order.
### Order
This parameter type indicates that another order type parameterizes this Algo. When the Algo needs to create a child order, it can use this parameter type to create a custom order type instead of defaulting to a limit order.
For example, an Algo might be designed to wait for a specific market condition before putting in an order. Instead of placing a limit order when the condition is met, the user can specify that it should an IceBerg order. The Algo doesn't need special logic to handle different child order types.
### Array
An array parameter is a collection of the same type of parameter. If your Algo needed to behave differently depending on a table of prices, you could send that list in though an array parameter.
```cpp
*desc.add_param_desc() =
param_array("Price Points",
param_double("unused").param_type());
```
### Struct
A struct parameter is a set of named parameter types similar to the entire DynamicOrderDescriptor. The struct parameter type can be combined with the array type to handle tabular data.
```cpp
*desc.add_param_desc() =
param_array("Step Table",
param_struct("", {
param_uint32("Market Quantity"),
param_uint32("Order Quantity")
}).param_type());
```
@@ -0,0 +1,69 @@
+++
title = "Subscriptions"
weight = 20
+++
An Algo exists to observe changes in the market and respond to them. When the Algo is [launched](1/server/ref/initialization/#launch-launch-attached) it uses IAlgoServices to subscribe to the events it needs to operate. Each subscribe call is paired with a matching unsubscribe call, which takes the same argument to unsubscribe.
# Book
Subscribe to book updates for the specified security call use subscribe_book and unsubscribe book.
```cpp
// Subscribe to book updates for the specified security
void IAlgoServices::subscribe_book(SecurityKey);
void IAlgoServices::unsubscribe_book(SecurityKey);
// You implement
virtual void on_book(SecurityKey, Book const &) override;
```
Get the `SecurityKey` of the current security using:
```cpp
services.subscribe_book(SecurityKey(algo_services().security().id()));
```
If your Algo needs to subscribe to other securities, their keys must be passed in as parameters.
To get the details of a security use
```cpp
api::Security const* IBeforeLaunchAlgoServices::lookup_security(SecurityKey k) const = 0;
```
# Timer
To handle time-based logic, an Algo can subscribe to a common timer event that is invoked once every 10 milliseconds. Use the TimeStamp class from `bts_chrono.h` to efficiently handle time calculations.
```cpp
// Subscribe to a periodic 10ms timer
void IAlgoServices::subscribe_timer();
// You implement
virtual void on_timer() override;
```
# Ticker
The status callback is called when the market for this security opens, closes, or has some other state transition.
```cpp
// Subscribe to trade ticker
void IAlgoServices::subscribe_ticker(SecurityKey);
// You implement
void on_ticker(SecurityKey, TradeVector const&) override;
```
# Trading Status
The status callback is called when the market for this security opens, closes, or has some other state transition.
```cpp
// Subscribe to security trading status
void IAlgoServices::subscribe_status(SecurityKey);
// You implement
virtual void on_status(SecurityKey, co2::TradingStatus) override;
```
# Child Orders
When an Algo launches a child order, it is subscribed to the changes on that child automatically. If the Algo unsubscribes from those changes, it can re-subscribe using this method.
```cpp
// Subscribe to child order updates for the specified child order.
void IAlgoServices::subscribe_child_change(OrderKey);
// You implement
virtual void on_child_change(OrderKey, ChildState const &) override;
```
+7
View File
@@ -0,0 +1,7 @@
+++
title="Samples"
template="documentation.html"
sort_by="weight"
weight = 3
+++
+354
View File
@@ -0,0 +1,354 @@
+++
title = "Join"
weight = 10
+++
Join starts working N ticks away from the market (usually) and then watches for the opposite inside to become same side inside market; if there is then enough size on that market, it will join.
<div class="info">
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
This sample is part of the Server SDK examples included in the <a href="/server/getting-started/download/">spark_sdk.zip download</a>.
</div>
```cpp
#include "spark/lib/config/config.h"
#include "spark/lib/config/protos/system.pb.h"
#include "spark/lib/domain_model/Security.h"
#include "spark/lib/orderman/orders/server/IAlgoOrder.h"
#include "spark/lib/orderman/orders/server/IAlgoServices.h"
#include "spark/lib/plugin/SparkServices.h"
#include "spark/lib/orderman/order_factory/IOrderFactory.h"
#include "spark/lib/orderman/orderman_utils/Book.h"
#include "spark/lib/orderman/orderman_utils/orderman_utils.h"
#include "spark/lib/utils/PBUtils.h"
using namespace bts::api::orderman;
using namespace bts::orderman;
namespace bts {
namespace order_example {
// Join starts working N ticks away from the market (usually) and then
// watches for the opposite inside to become same side inside market;
// if there is then enough size on that market, it will join.
class DynamicJoin : public IAlgoOrder {
public:
DynamicJoin();
~DynamicJoin();
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
void do_launch(IAlgoServices &) override;
void do_launch_attached(IAlgoServices &, OrderKey, ChildState) override;
void do_update(api::orderman::Update const &) override;
void on_book(SecurityKey, Book const &) override;
void on_child_change(OrderKey, ChildState const &) override;
private:
enum class Status;
struct PImpl;
std::unique_ptr<PImpl> me;
IAlgoServices &algo_services() const;
void try_to_join();
};
// The three stages our algo goes through. We start by waiting to
// observe the book for our security (WAITING_FOR_MD). Once we have a
// book we use it to derive our target price and we continue to
// monitor the book to see if we should join the market
// (WAITING_TO_JOIN). Once the size and price constraints are met we
// jump to the target price (JOINED). We remain in the JOINED state
// until our order is filled or canceled.
enum class DynamicJoin::Status { WAITING_FOR_MD, WAITING_TO_JOIN, JOINED };
// All state information needed for this algo
struct DynamicJoin::PImpl {
IAlgoServices *algo_services;
Quantity min_quantity_to_join;
Status status = Status::WAITING_FOR_MD;
OrderKey child_key = null_order_key();
ChildTemplate *child_template = nullptr;
Book book_at_inception;
Book last_book;
};
DynamicJoin::DynamicJoin() : me(new PImpl()) {}
DynamicJoin::~DynamicJoin() {}
IAlgoServices &DynamicJoin::algo_services() const { return *me->algo_services; }
// This is called outside of the latency sensitive path
// once. Expensive initialization should be done here, rather than
// in do_launch()
OrderOpResult DynamicJoin::do_before_launch(IBeforeLaunchAlgoServices &serv) {
auto &def = serv.definition();
bts_assert(def.has_custom_def());
bts_assert(def.custom_def().has_dynamic());
// Validate our parameters, returning an error if we find a
// problem. If an error is returned here the order will be
// abandoned.
auto &params = def.custom_def().dynamic().order_params();
auto mqi = get_if<uint32_t>(params, "Min Quantity");
if (!mqi || mqi.value() == 0) {
return OrderOpResult{
ORDER_ERROR_INVALID_ORDER_DEFINITON,
"Min Quantity parameter must be defined and non zero"};
}
me->min_quantity_to_join = Quantity(mqi.value());
if (!serv.is_attaching()) {
// If we are not attaching, create a child template so we can
// quickly launch our child when the time comes.
// We will be sending a limit order with the same side,
// security, trader, account, etc, as this algo order, so just
// copy our definition to the child definition to start with.
auto child_def = def;
// Now add a limit order component to mark our child as a limit
// order. Limit orders do not require any extra parameters
// beyond those already inherited from this algo order
// (i.e. beyond what we obtained through serv.definition()) so
// we can just use a default constructed LimitOrderDefinition.
*child_def.mutable_custom_def()->mutable_limit() = LimitOrderDefinition{};
// Now just build the child template and cache it for future use
me->child_template = &(serv.make_child_template(child_def));
} else {
// If we are attaching to an existing order then we do not need
// to spawn a new child, so there is no need to setup a child
// template
}
// All is good, return a default constructed OrderOpResult which
// indicates success.
return {};
}
// Called in the latency sensitive path, once, when the algo is first
// launched.
void DynamicJoin::do_launch(IAlgoServices &services) {
// Cache our algo services
me->algo_services = &services;
LOG(OrderMan, Diag, "Lauching DynamicJoin k: {} ",
algo_services().order_key().value());
// Launch our child order
me->child_key =
services.launch_child(*me->child_template, algo_services().price(),
algo_services().quantity());
// Subscribe to market data
services.subscribe_book(SecurityKey(algo_services().security().id()));
}
// Called in the latency sensitive path, once, when the algo is first
// launched. In this case the algo is provided with an existing order
// it is now responsible for managing.
void DynamicJoin::do_launch_attached(IAlgoServices &services,
OrderKey attached_order, ChildState st) {
// The order was launched and attached to an existing order, with
// key 'attached_order' and current state 'st'
// Cache our algo services
me->algo_services = &services;
LOG(OrderMan, Diag,
"Lauching DynamicJoin k: {} Attached to existing order: {}",
algo_services().order_key().value(), attached_order.value());
// Cache the key of the attached order we will be managing
me->child_key = attached_order;
// Subscribe to market data
services.subscribe_book(SecurityKey(services.security().id()));
// Simulate a child change so we can update our own state to
// reflect that of the child order we attached to. From this point
// forward there is no distinction between an algo instance that
// spawned a new child order and one that attached to an existing
// order.
on_child_change(attached_order, st);
}
// Called when a human (or a supervising algo) updates a parameter for
// this algo. The implementation validates the update and depending on
// the validation outcome either applies the update or terminates the
// algo.
void DynamicJoin::do_update(api::orderman::Update const &update) {
LOG(OrderMan, Diag, "DynamicJoin k: {} receiving update: {}",
algo_services().order_key().value(), utils::pb::to_string(update));
// Validate update
if (update.quantity() == 0) {
// An invalid update has been detected so we terminate the
// algo, signaling an error. Any child orders launched by
// this algo will also be terminated.
algo_services().terminate_error("Invalid update");
return;
}
// Updates have two parts, a standard part consisting of price and
// size and present in all updates, and an optional part that is
// specific to the algo in question. Below we check for the
// existance of this optional part and validate if present.
bool has_new_min_qty = false;
if (update.has_custom()) {
auto &params = update.custom().dynamic().order_params();
auto mqi = params.find("Min Quantity");
if (mqi == params.end() || mqi->second.as_uint32() == 0) {
algo_services().terminate_error(
"Minimum quantity to join must be greater than 0");
return;
}
has_new_min_qty = true;
me->min_quantity_to_join = Quantity(mqi->second.as_uint32());
}
// Update is good, forward it to our child
Update child_update;
child_update.set_price(update.price());
child_update.set_quantity(update.quantity());
algo_services().update_child(me->child_key, child_update);
// Try to join again if conditions changed
if (me->status == Status::WAITING_TO_JOIN && has_new_min_qty)
try_to_join();
}
// Called when the book for the specified security has changed
void DynamicJoin::on_book(SecurityKey, Book const &b) {
// Cache the book, so we can reffer to it in the event of a
// parameter change that might allow us to join
me->last_book = b;
LOG(OrderMan, Diag, "DynamicJoin order {} on book. BID: {}@{} ASK: {}@{}",
algo_services().order_key().value(), b.side(api::SIDE_BUY)[0].size(),
b.side(api::SIDE_BUY)[0].price(), b.side(api::SIDE_SELL)[0].size(),
b.side(api::SIDE_SELL)[0].price());
switch (me->status) {
case Status::WAITING_FOR_MD: {
auto other_side =
securities::other_side(algo_services().definition().side());
auto &other_side_top = me->last_book.side(other_side)[0];
if (other_side_top.size() > 0) {
// First time we get the opposite side market, we will use
// this price as the target price for the join
me->status = Status::WAITING_TO_JOIN;
me->book_at_inception = b;
LOG(OrderMan, Diag,
"JoinOrder order {} receiving first book: {} Target: {}",
algo_services().order_key().value(),
me->min_quantity_to_join.value(), other_side_top.price());
try_to_join();
}
} break;
case Status::JOINED:
// Nothing to do at this point
break;
case Status::WAITING_TO_JOIN:
// Check if we reached the target and join if so
try_to_join();
break;
}
}
// Given the current book, our target price and our min quantity
// parameter, join if appropriate
void DynamicJoin::try_to_join() {
bts_assert(me->status == Status::WAITING_TO_JOIN);
auto my_side = algo_services().definition().side();
auto other_side = securities::other_side(my_side);
auto &my_side_psz = me->last_book.side(my_side)[0];
auto &opposite_at_inception_psz = me->book_at_inception.side(other_side)[0];
if (my_side_psz.size() &&
my_side_psz.size() >= me->min_quantity_to_join.value() &&
securities::is_equal_or_better(
my_side, Price(my_side_psz.price()),
Price(opposite_at_inception_psz.price()))) {
// Time to update our child to join the market
Update child_update;
child_update.set_price(my_side_psz.price());
child_update.set_quantity(algo_services().quantity().value());
algo_services().update_child(me->child_key, child_update);
me->status = Status::JOINED;
LOG(OrderMan, Diag, "DynamicJoin order {} joined: {}@{}",
algo_services().order_key().value(),
algo_services().quantity().value(), my_side_psz.price());
} else {
LOG(OrderMan, Diag,
"JoinOrder order {} still waiting to join. Order: {}@{} Top of "
"the book: {}@{} Target: {}@{}",
algo_services().order_key().value(),
algo_services().quantity().value(), algo_services().price().value(),
my_side_psz.size(), my_side_psz.price(),
me->min_quantity_to_join.value(), opposite_at_inception_psz.price());
}
}
// Our child reported a change:
void DynamicJoin::on_child_change(OrderKey, ChildState const &st) {
// Our fill quantity and price is always a reflection of our
// child's state
algo_services().update_fill_state(st.total_fills, st.last_price);
switch (st.status) {
case ChildStatus::WORKING:
// Child order is still working, nothing to do
return;
case ChildStatus::COMPLETED:
// Child order completed, we should also complete
algo_services().complete();
return;
case ChildStatus::CANCELED:
// Child order unexpectedly canceled, signal an error and
// terminate
algo_services().terminate_error("Unexpected child cancel");
return;
}
}
// This is called by the spark daemon at startup. In this call we
// describe our algo's parameters and register a factory function to
// create an instance of the algo.
extern "C" void init_spark_plugin(bts::SparkServices &serv) {
using namespace bts;
DynamicOrderDescriptor join_desc;
// Start by naming our order type
join_desc.set_dynamic_order_type("dyn join");
// Now add a single unsigned int parameter, Min Quantity, that will
// determine the minimum quantity that must exist in the book in
// order for us to join the market.
*join_desc.add_param_desc() = param_uint32("Min Quantity");
// Associate the descriptor with a builder function and register
// that with the Spark server.
auto result =
serv.register_dynamic_algo_order(
join_desc, []() { return std::make_unique<DynamicJoin>(); });
if (result != bts::AlgoRegistrationResult::SUCCESS) {
// The join_desc has errors
// Check the error log for more details
}
}
} // namespace order_example
} // namespace bts
```
+146
View File
@@ -0,0 +1,146 @@
+++
title = "Template"
weight = 5
+++
A mostly blank Algo template that can be used as a jump-start for developing a new Algo.
<div class="info">
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
This sample is part of the Server SDK examples included in the <a href="/server/getting-started/download/">spark_sdk.zip download</a>.
</div>
```cpp
#include "spark/lib/config/config.h"
#include "spark/lib/config/protos/system.pb.h"
#include "spark/lib/domain_model/Security.h"
#include "spark/lib/orderman/orders/algo/IAlgoOrder.h"
#include "spark/lib/orderman/orders/algo/IAlgoServices.h"
#include "spark/lib/plugin/SparkServices.h"
#include "spark/lib/orderman/order_factory/IOrderFactory.h"
#include "spark/lib/orderman/orderman_utils/Book.h"
#include "spark/lib/orderman/orderman_utils/orderman_utils.h"
#include "spark/lib/utils/PBUtils.h"
using namespace bts::api::orderman;
using namespace bts::orderman;
namespace bts {
namespace order_example {
// MyAlgo is a blank IAlgoOrder which you can copy when starting a new Algo
class MyAlgo : public IAlgoOrder {
public:
MyAlgo();
~MyAlgo();
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
void do_launch(IAlgoServices &) override;
void do_launch_attached(IAlgoServices &, OrderKey, ChildState) override;
void do_update(api::orderman::Update const &) override;
void on_book(SecurityKey, Book const &) override;
void on_child_change(OrderKey, ChildState const &) override;
private:
IAlgoServices* algo_services;
ChildTemplate *child_template = nullptr;
OrderKey child_key = null_order_key();
};
MyAlgo::MyAlgo() : algo_services(nullptr) {}
MyAlgo::~MyAlgo() {}
// This is called outside of the latency sensitive path.
// Expensive initialization should be done here, rather than in do_launch()
OrderOpResult MyAlgo::do_before_launch(IBeforeLaunchAlgoServices &serv) {
LOG(Sdk,Info, "MyAlgo: do_before_launch");
// If we are not attaching, create a child template so we can
// quickly launch our child when the time comes.
if (!serv.is_attaching()) {
// We will be sending a limit order with the same side,
// security, trader, account, etc, as this algo order, so just
// copy our definition to the child definition to start with.
auto child_def = serv.definition();
// Now add a limit order component to mark our child as a limit
// order. Limit orders do not require any extra parameters
// beyond those already inherited from this algo order
// (i.e. beyond what we obtained through serv.definition()) so
// we can just use a default constructed LimitOrderDefinition.
*child_def.mutable_custom_def()->mutable_limit() = LimitOrderDefinition{};
// Now just build the child template and cache it for future use
child_template = &(serv.make_child_template(child_def));
}
// Default constructed result is successful
return {};
}
// Called in the latency sensitive path, once, when the algo is first launched.
// If the Algo is attaching to an existing order do_launch_attached is called instead.
void MyAlgo::do_launch(IAlgoServices &services) {
algo_services = &services;
// Subscribe to book updates for this security
services.subscribe_book(SecurityKey(services.security().id()));
// Launch a child limit order
child_key =
services.launch_child(*child_template, services.price(), services.quantity());
LOG(Sdk,Info, "MyAlgo: do_launch");
}
// Called in the latency sensitive path, once, when the algo is first
// launched. In this case the algo is provided with an existing order
// it is now responsible for managing.
void MyAlgo::do_launch_attached(IAlgoServices &services,
OrderKey attached_order, ChildState st) {
algo_services = &services;
// Subscribe to book updates for this security
services.subscribe_book(SecurityKey(services.security().id()));
// Keep track of the child key
child_key = attached_order;
LOG(Sdk,Info, "MyAlgo: do_launch_attached");
}
// Called when a human (or a supervising algo) updates a parameter for
// this algo. The implementation validates the update and depending on
// the validation outcome either applies the update or terminates the algo.
void MyAlgo::do_update(api::orderman::Update const &update) {
}
// Called when the book for the specified security has changed
void MyAlgo::on_book(SecurityKey, Book const &b) {
}
// Our child reported a change
void MyAlgo::on_child_change(OrderKey, ChildState const &st) {
}
// This is called by the spark daemon at startup. In this call we
// describe our algo's parameters and register a factory function to
// create an instance of the algo.
extern "C" void init_spark_plugin(bts::SparkServices &serv) {
using namespace bts;
DynamicOrderDescriptor desc;
// Start by naming our order type
desc.set_dynamic_order_type("My Algo");
*desc.add_param_desc() = param_bool("Enable Feature");
*desc.add_param_desc() = param_uint32("Feature Quantity");
// Associate the descriptor with a builder function and register
// that with the Spark server.
auto result =
serv.register_dynamic_algo_order(
desc, []() { return std::make_unique<MyAlgo>(); });
if (result != bts::AlgoRegistrationResult::SUCCESS) {
// The desc has errors
// Check the error log (/sparkdata/data/logging/log) for more details
}
}
} // namespace order_example
} // namespace bts
```
+9
View File
@@ -0,0 +1,9 @@
#/bin/bash
# ../../externals/linux/bin/zola/zola build
zola.exe build
rsync -avzL public/ bts@api.bluetradesys.com:/data/sdk/public/
rm -rf ./public/
+97
View File
@@ -0,0 +1,97 @@
html, body {
font-size: 16px;
height: 100%;
}
body {
font-family: "Source Sans Pro", "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
line-height: 1.6;
// from http://www.heropatterns.com
background-color: #0072a2;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%234b4b4b' fill-opacity='0.4'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
color: $foreground;
// for sticky footer
display: flex;
flex-direction: column;
min-height: 100vh;
}
a {
color: $link-color;
text-decoration: none;
cursor: pointer;
border-bottom: 1px solid rgba(0, 124, 188, 0.25);
&:hover {
border-bottom: 1px solid $link-color;
}
&:visited {
color: $link-color;
}
// totally taken from stripe
&.button {
outline: none;
white-space: nowrap;
display: inline-block;
height: 40px;
line-height: 40px;
padding: 0 14px;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
background: #fff;
border-radius: 4px;
font-size: 15px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .025em;
color: #191919;
text-decoration: none;
transition: all .15s ease;
border-bottom: none;
&:hover {
transform: translateY(-1px);
text-decoration: none;
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08);
}
&:active {
background-color: #f6f9fc;
transform: translateY(1px);
}
}
&.white {
color: white;
border-bottom-color: rgba(255, 255, 255, 0.25);
&:hover {
border-bottom-color: white;
}
&:visited {
color: white;
}
}
}
pre {
padding: 1rem;
overflow: auto;
}
p code, li code {
// color: #e6e1dc;
// background-color: #383838;
color: #191919;
background-color: #f5f5f5;
white-space: pre-wrap;
padding: 3px;
border-radius: 3px;
font-size: 0.85rem;
font-family: monospace;
// box-shadow: 0 1px 3px rgba(0,0,0,.1), 0 1px 1px rgba(0,0,0,.1), 0 2px 1px -1px rgba(0,0,0,.12);
}
+96
View File
@@ -0,0 +1,96 @@
.documentation {
padding: 3rem;
display: flex;
&__sidebar {
margin-right: 2rem;
ul {
padding-left: 0;
list-style: none;
}
> ul {
> li {
margin-bottom: 1rem;
}
.documentation__sidebar__title {
font-size: 1.25rem;
font-weight: bold;
}
ul {
margin-top: 0.25rem;
li {
border-left: 1px solid $background;
padding-left: 0.75rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
li.active a {
font-weight: bold;
border-bottom: 1px solid $link-color;
}
}
}
}
&__content {
width: 60%;
// Same as the anchor absolute left to not overlap
margin-left: 2rem;
h1, h2, h3, h4, h5, h6 {
.zola-anchor {
font-size: 1.25rem;
visibility: hidden;
margin-left: -2rem;
text-decoration: none;
border-bottom-color: transparent;
cursor: pointer;
}
&:hover {
.zola-anchor {
visibility: visible;
}
}
}
blockquote {
border-left: 10px solid black;
padding: 0.25rem 0.5rem;
margin-left: 0;
font-style: italic;
}
}
iframe {
width: 100%;
min-height: 400px;
}
}
@media only screen and (max-width: 1000px) {
.documentation {
flex-direction: column;
padding: 2rem;
&__content {
width: 100%;
margin-left: 0;
}
&__sidebar {
border-top: 1px solid $background;
order: 1;
margin-right: 0;
margin-top: 1rem;
}
}
}
+33
View File
@@ -0,0 +1,33 @@
header {
.header__logo {
border-bottom: none;
}
nav a {
margin-right: 2rem;
}
.header__logo {
font-size: 2rem;
font-weight: bold;
&:hover {
text-decoration: none;
}
}
}
@media only screen and (max-width: 1000px) {
header {
padding: 1rem 0;
nav {
text-align: center;
}
.header__logo {
// to force menu links to be on a line below
display: block;
}
}
}
+71
View File
@@ -0,0 +1,71 @@
.inverted-colours {
background: $foreground;
color: $background;
}
.hero {
width: 60%;
margin: 0 auto;
margin-top: 2rem;
margin-bottom: 4rem;
text-align: center;
&__tagline {
margin-bottom: 2rem;
}
}
.disabled {
opacity: 0.5;
}
.selling-points {
padding: 3rem;
&__content {
display: flex;
flex-wrap: wrap;
}
}
.selling-point {
// 2 selling points per row on desktop
width: 50%;
padding: 2rem;
padding-top: 1rem;
}
.selling-header {
width: 100%;
padding: 1rem;
padding-top: 0rem;
display: flex;
align-items: center;
justify-content: center;
img {
align-self: center;
max-width: 70%;
}
}
// and 1 on mobile
@media only screen and (max-width: 1000px) {
.hero {
width: 90%;
margin-top: 2rem;
margin-bottom: 4rem;
}
.selling-points {
padding: 2rem;
}
.selling-point {
width: 100%;
padding: 1rem;
h2 {
text-align: center;
}
}
}
+43
View File
@@ -0,0 +1,43 @@
header {
padding: 2rem 3rem;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
@media only screen and (max-width: 1000px) {
.container {
max-width: 90%;
}
}
.content {
flex-grow: 1;
&--reversed {
background: $foreground;
color: $background;
}
}
.linux {
padding: 25px 15px;
background: #eff0f1;
border-radius: 5px;
border: 1px solid #0072a2;
}
.info {
padding: 25px 15px;
background: #ffffe0;
border-radius: 5px;
border: 1px solid #383838;
}
footer {
text-align: center;
padding: 2rem 3rem;
}
+447
View File
@@ -0,0 +1,447 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}
+47
View File
@@ -0,0 +1,47 @@
.search-container {
display: inline-block;
position: relative;
width: 300px;
input {
width: 100%;
padding: 0.5rem;
}
}
.search-results {
display: none;
position: absolute;
background: white;
color: black;
padding: 1rem;
box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.5);
max-height: 500px;
overflow: auto;
width: 150%;
right: 0;
&__items {
list-style: none;
}
li {
margin-top: 1rem;
border-bottom: 1px solid #ccc;
font-size: 0.9rem;
&:first-of-type {
margin-top: 0;
}
}
&__item {
margin-bottom: 1rem;
a {
font-size: 1.2rem;
display: inline-block;
margin-bottom: 0.5rem;
}
}
}
+79
View File
@@ -0,0 +1,79 @@
.themes-container {
padding: 3rem;
width: 80%;
margin: 0 auto;
img {
max-width: 100%;
}
}
@media only screen and (max-width: 1000px) {
.themes-container {
width: 100%;
margin: 0 1rem;
}
}
.themes {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.theme {
width: 45%;
text-decoration: none;
cursor: pointer;
margin-bottom: 2rem;
border-bottom: none;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
padding: 10px;
img {
width: 100%;
height: 90%;
}
span {
display: block;
text-align: center;
font-size: 1.1rem;
}
}
}
.theme-info {
display: flex;
align-items: flex-start;
.thumb {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
width: 400px;
margin-right: 2rem;
}
h1, p {
margin: 0;
}
padding: 1rem;
}
@media only screen and (max-width: 1000px) {
.themes .theme {
width: 100%;
}
}
@media only screen and (max-width: 1000px) {
.theme-info {
flex-direction: column;
align-items: center;
img {
margin-bottom: 1rem;
}
}
}
+19
View File
@@ -0,0 +1,19 @@
@charset "utf-8";
@import "normalize";
*, *:before, *:after {
box-sizing: border-box;
}
$background: #191919;
$foreground: white;
$link-color: #007CBC;
@import "base";
@import "layout";
@import "header";
@import "index";
@import "docs";
@import "themes";
@import "search";
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 523.438 166.055" version="1.1" viewBox="0 0 523.44 166.06" xmlns="http://www.w3.org/2000/svg"><path d="m297.43 153.29c-18.051-6.698-91.506-40.523-160.87-72.832-69.291-32.28-134.17-62.929-134.17-62.929-2.082-0.983-2.971-3.466-1.987-5.549 0.981-2.079 3.466-2.97 5.548-1.988 0.037 0.02 16.216 7.661 41.09 19.354 24.874 11.695 58.412 27.425 93.046 43.557 69.192 32.242 143.09 66.232 160.25 72.574 20.188 7.443 37.922 12.204 80.113 12.239 30.434 0 73.407-2.559 138.44-8.568 2.288-0.211 4.319 1.474 4.531 3.765 0.208 2.29-1.478 4.324-3.771 4.536-65.152 6.014-108.31 8.602-139.2 8.605h-0.423c-42.393-1e-3 -61.964-5.092-82.59-12.764z" fill="#B3B3B3"/><path d="m45.824 145.9v-138.39h101.28c27.03 0 44.924 12.184 44.924 35.789 0 17.513-8.947 26.27-19.226 29.506 12.564 3.996 21.702 15.039 21.702 31.22 0 25.319-17.516 41.88-44.545 41.88h-104.13zm41.692-60.155v-20.37h59.774c13.707 0 20.179-4.758 20.179-16.751 0-14.658-11.613-16.943-29.507-16.943h-67.96v90.234h70.052c17.325 0 29.889-4.759 29.889-18.276 0-11.423-7.423-17.894-20.939-17.894h-61.488z" fill="#0086BF"/><path d="m149.96 153.4h-104.13v-7.502h7.501-7.501v7.502h-7.504v-153.4h108.78c14.604 0.018 27.473 3.236 37.154 10.583 9.724 7.301 15.384 19.035 15.273 32.71 0.018 10.01-2.623 18.337-7.222 24.554-1.308 1.779-2.755 3.368-4.306 4.774 2.395 1.964 4.557 4.235 6.422 6.792 4.847 6.626 7.594 15.107 7.58 24.606 0.044 14.363-5.105 27.096-14.494 35.896-9.347 8.809-22.486 13.486-37.463 13.486-0.028 1e-3 -0.058 1e-3 -0.088 1e-3zm0-15.007c12.016-0.018 21.114-3.62 27.266-9.402 6.125-5.814 9.736-14.02 9.777-24.973-0.015-6.687-1.835-11.817-4.698-15.757-2.875-3.934-6.891-6.753-11.775-8.315l-22.267-7.085c-0.322 7e-3 -0.645 0.011-0.97 0.012h-52.271v5.367h53.988c7.892 0 15.036 1.876 20.422 6.475 5.429 4.589 8.092 11.651 8.015 18.922 0.016 4.524-1.103 8.88-3.35 12.497-2.236 3.632-5.46 6.38-9.02 8.3-7.172 3.825-15.677 4.938-25.019 4.979h-77.553v-105.24h75.46c6.958 0.014 13.253 0.288 19.035 1.724 5.677 1.383 11.425 4.37 14.807 10.025 2.23 3.702 3.174 8.068 3.171 12.698-5e-3 3.663-0.489 7.129-1.729 10.368-1.045 2.753-2.714 5.301-4.853 7.329l2.154-0.677c3.906-1.243 7.213-3.358 9.68-6.692 2.445-3.351 4.276-8.156 4.297-15.657-0.111-9.936-3.399-16.094-9.377-20.786-6.028-4.645-15.62-7.52-28.046-7.501l-93.773-3e-3v123.39h96.629v-1e-3zm-72.453-23.984h62.551c5.981 0.011 11.112-0.651 14.744-1.85 3.688-1.218 5.575-2.753 6.528-4.306 0.647-1.077 1.099-2.388 1.113-4.618-0.073-4.151-1.124-6.039-2.785-7.541-1.701-1.492-5.034-2.853-10.648-2.853h-61.49v-7.501h7.501-7.501v7.501h-7.504v-35.372h67.277c4.502 0.018 7.602-0.643 9.234-1.431 1.634-0.854 2.056-1.329 2.718-2.838 0.402-1.025 0.728-2.648 0.728-4.979-0.01-2.704-0.518-4.121-1.023-4.967-0.526-0.837-1.154-1.457-2.696-2.209-3.023-1.521-9.667-2.335-18.288-2.268h-60.459v75.232z" fill="#fff"/><path d="M251.22,145.899V31.68h-52.54V7.505h129.256V31.68h-52.539v114.22H251.22z" fill="#0086BF"/><path d="m251.22 153.4v-7.499h7.501-7.501v7.499h-7.504l-2e-3 -114.22h-52.538v-39.175h144.26v39.175h-52.541v114.22h-31.678zm16.673-15.001v-114.22h52.538v-9.171h-114.25v9.171h52.542v114.22h9.171v-1e-3z" fill="#fff"/><path d="m340.11 145.9v-24.176h103.74c12.942 0 20.177-7.233 20.177-18.083 0-11.802-7.234-17.893-20.177-17.893h-63.58c-26.075 0-42.257-16.562-42.257-39.404 0-22.271 15.038-38.831 42.639-38.831h99.365v24.174h-99.365c-11.042 0-17.514 6.473-17.514 16.94 0 10.469 6.661 16.751 17.324 16.751h63.388c27.98 0 43.02 13.134 43.02 39.974 0 23.226-14.089 40.546-43.02 40.546h-103.74z" fill="#383431"/><path d="m443.85 153.4h-103.74v-7.499h7.498-7.498v7.499h-7.505v-39.176h111.25c5.118-0.02 8.116-1.385 9.849-2.984 1.71-1.622 2.774-3.8 2.823-7.597-0.071-4.45-1.162-6.342-2.67-7.727-1.55-1.385-4.646-2.668-10.002-2.668h-63.58c-14.603 0.028-27.235-4.711-36.097-13.218-8.89-8.476-13.698-20.511-13.666-33.685-0.032-12.792 4.428-24.712 13.274-33.177 8.826-8.496 21.605-13.184 36.87-13.155h106.86v39.176h-106.86c-4.11 0.028-6.327 1.125-7.658 2.411-1.313 1.309-2.315 3.29-2.354 7.027 0.043 3.691 1.042 5.592 2.354 6.89 1.338 1.278 3.562 2.338 7.469 2.36h63.388c15.081-7e-3 27.896 3.536 37.065 11.888 9.185 8.367 13.511 20.865 13.455 35.59 0.022 13.078-4.037 25.357-12.824 34.248-8.729 8.882-21.678 13.796-37.497 13.796-0.066 1e-3 -0.135 1e-3 -0.2 1e-3zm-96.246-14.999 96.246-4e-3c13.017-0.037 21.505-3.779 27.018-9.329 5.488-5.576 8.475-13.569 8.494-23.712-0.052-12.118-3.246-19.606-8.572-24.519-5.35-4.923-14.046-7.951-26.939-7.956h-63.388c-6.76 0.022-13.199-2.056-17.858-6.541-4.686-4.459-7.014-10.932-6.972-17.712-0.038-6.729 2.191-13.218 6.88-17.764 4.659-4.564 11.2-6.707 18.14-6.678h91.856v-9.174l-91.856 4e-3c-12.341 0.026-20.88 3.617-26.474 8.971-5.574 5.382-8.632 12.875-8.667 22.358 0.035 9.665 3.317 17.334 9.038 22.85 5.746 5.486 14.244 9.029 25.721 9.049h63.58c7.58 0 14.573 1.767 19.88 6.38 5.34 4.606 7.87 11.665 7.799 19.018 0.048 7.05-2.504 13.916-7.652 18.62-5.117 4.73-12.205 6.982-20.026 6.965l-96.246-6e-3v9.18h-2e-3z" fill="#fff"/><path d="m45.824 145.9v-138.39h101.27c6Line truncated

After

Width:  |  Height:  |  Size: 7.6 KiB

+180
View File
@@ -0,0 +1,180 @@
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
};
}
// Taken from mdbook
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <b>.
function makeTeaser(body, terms) {
var TERM_WEIGHT = 40;
var NORMAL_WORD_WEIGHT = 2;
var FIRST_WORD_WEIGHT = 8;
var TEASER_MAX_WORDS = 30;
var stemmedTerms = terms.map(function (w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var termFound = false;
var index = 0;
var weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
var sentences = body.toLowerCase().split(". ");
for (var i in sentences) {
var words = sentences[i].split(" ");
var value = FIRST_WORD_WEIGHT;
for (var j in words) {
var word = words[j];
if (word.length > 0) {
for (var k in stemmedTerms) {
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
value = TERM_WEIGHT;
termFound = true;
}
}
weighted.push([word, value, index]);
value = NORMAL_WORD_WEIGHT;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
}
index += 1; // because we split at a two-char boundary '. '
}
if (weighted.length === 0) {
return body;
}
var windowWeights = [];
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
// We add a window with all the weights first
var curSum = 0;
for (var i = 0; i < windowSize; i++) {
curSum += weighted[i][1];
}
windowWeights.push(curSum);
for (var i = 0; i < weighted.length - windowSize; i++) {
curSum -= weighted[i][1];
curSum += weighted[i + windowSize][1];
windowWeights.push(curSum);
}
// If we didn't find the term, just pick the first window
var maxSumIndex = 0;
if (termFound) {
var maxFound = 0;
// backwards
for (var i = windowWeights.length - 1; i >= 0; i--) {
if (windowWeights[i] > maxFound) {
maxFound = windowWeights[i];
maxSumIndex = i;
}
}
}
var teaser = [];
var startIndex = weighted[maxSumIndex][2];
for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
var word = weighted[i];
if (startIndex < word[2]) {
// missing text from index to start of `word`
teaser.push(body.substring(startIndex, word[2]));
startIndex = word[2];
}
// add <em/> around search terms
if (word[1] === TERM_WEIGHT) {
teaser.push("<b>");
}
startIndex = word[2] + word[0].length;
teaser.push(body.substring(word[2], startIndex));
if (word[1] === TERM_WEIGHT) {
teaser.push("</b>");
}
}
teaser.push("…");
return teaser.join("");
}
function formatSearchResultItem(item, terms) {
return '<div class="search-results__item">'
+ `<a href="${item.ref}">${item.doc.title}</a>`
+ `<div>${makeTeaser(item.doc.body, terms)}</div>`
+ '</div>';
}
function initSearch() {
var $searchInput = document.getElementById("search");
var $searchResults = document.querySelector(".search-results");
var $searchResultsItems = document.querySelector(".search-results__items");
var MAX_ITEMS = 10;
var options = {
bool: "AND",
fields: {
title: {boost: 2},
body: {boost: 1},
}
};
var currentTerm = "";
var index = elasticlunr.Index.load(window.searchIndex);
$searchInput.addEventListener("keyup", debounce(function() {
var term = $searchInput.value.trim();
if (term === currentTerm || !index) {
return;
}
$searchResults.style.display = term === "" ? "none" : "block";
$searchResultsItems.innerHTML = "";
if (term === "") {
return;
}
var results = index.search(term, options);
if (results.length === 0) {
$searchResults.style.display = "none";
return;
}
currentTerm = term;
for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
var item = document.createElement("li");
item.innerHTML = formatSearchResultItem(results[i], term.split(" "));
$searchResultsItems.appendChild(item);
}
}, 150));
}
if (document.readyState === "complete" ||
(document.readyState !== "loading" && !document.documentElement.doScroll)
) {
initSearch();
} else {
document.addEventListener("DOMContentLoaded", initSearch);
}
+31
View File
@@ -0,0 +1,31 @@
{% extends "index.html" %}
{% block extra_nav_class %}{% endblock extra_nav_class %}
{% block extra_content_class %}content--reversed{% endblock extra_content_class %}
{% block content %}
{% set section = get_section(path="client/_index.md") %}
<div class="documentation">
<aside class="documentation__sidebar">
<ul>
{% for p in section.subsections %}
{% set subsection = get_section(path=p) %}
<li>
<span class="documentation__sidebar__title">{{ subsection.title }}</span>
<ul>
{% for page in subsection.pages %}
<li class="{% if current_path == page.path %}active{% endif %}">
<a href="{{page.permalink}}">{{page.title}}</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</aside>
<div class="documentation__content">
{% block doc_content %}
{% endblock doc_content %}
</div>
</div>
{% endblock content %}
+36
View File
@@ -0,0 +1,36 @@
{% extends "index.html" %}
{% block extra_nav_class %}{% endblock extra_nav_class %}
{% block extra_content_class %}content--reversed{% endblock extra_content_class %}
{% block content %}
{%if page.extra.section %}
{% set section = get_section(path="{{page.extra.section}}/_index.md") %}
{% else %}
{% set section = get_section(path="server/_index.md") %}
{% endif %}
<div class="documentation">
<aside class="documentation__sidebar">
<ul>
{% for p in section.subsections %}
{% set subsection = get_section(path=p) %}
<li>
<span class="documentation__sidebar__title">{{ subsection.title }}</span>
<ul>
{% for page in subsection.pages %}
<li class="{% if current_path == page.path %}active{% endif %}">
<a href="{{page.permalink}}">{{page.title}}</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</aside>
<div class="documentation__content">
{% block doc_content %}
{% endblock doc_content %}
{% block next_link %} {% endblock next_link %}
</div>
</div>
{% endblock content %}
+101
View File
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en-gb">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600" rel="stylesheet" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% block description %}{{ config.description }}{% endblock description %}">
<meta name="keywords" content="Futures, Options, Derivatives, Trade, Trading, Professional, options trading, futures trading, Software, API, SDK" />
<meta name="rights" content="© Blue Trading Systems" />
<meta name="author" content="{{ config.extra.author }}">
<title>{% block title %}{{ config.title }}{% endblock title %}</title>
<link rel="stylesheet" href="{{ get_url(path="site.css") }}"/>
<link rel="icon" href="{{ get_url(path="favicon.ico") }}">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<header>
<nav class="{% block extra_nav_class %}container{% endblock extra_nav_class %}">
<a class="header__logo white" href="{{ config.base_url }}">
<img width="100" src="/images/logo.svg">
</a>
<a class="white" href="{{ get_url(path="@/server/_index.md") }}" class="nav-link">Server SDK</a>
<a class="white" href="{{ get_url(path="@/client/_index.md") }}" class="nav-link">Client SDK</a>
<a class="white" href="https://www.bluetradingsystems.com/bts-spark" class="nav-link">About Spark</a>
<div class="search-container">
<input id="search" type="search" placeholder="🔎 Search">
<div class="search-results">
<div class="search-results__items"></div>
</div>
</div>
</nav>
</header>
<div class="content {% block extra_content_class %}{% endblock extra_content_class %}">
{% block content %}
<div class="hero">
<h1>Spark SDK</h1>
<p class="hero__tagline">
Spark consists of a client UI
(<a class="white" href="https://www.bluetradingsystems.com/bts-spark#ladder-algo-screen">Spark Client</a>),
a high-performance professional futures front end, connected via our proprietary protocol over TCP to our Spark Server, which in turn connects to execution venues via their native protocols.
Use the Spark SDK to extend Spark Server with your custom Algos, then use the Client SDK to launch and control them.
</p>
</div>
<div class="inverted-colours selling-points">
<div class="selling-points__content container">
<div class="selling-header">
<img src="/images/API.png" width="100%" />
</div>
<div class="selling-point">
<h2>
Server SDK
<i class="fa fa-tachometer" aria-hidden="true"></i>
</h2>
<p>
Leverage the ultra-low latency Spark C++ API to develop and run your custom Algo. Have your first sub-10 microsecond trading algorithm running in under an hour.
</p>
<a href="{{ get_url(path="@/server/_index.md") }}" class="button">Get started</a>
</div>
<div class="selling-point disabled">
<h2>
Client SDK
<i class="fa fa-gamepad" aria-hidden="true"></i>
</h2>
<p>
The Spark client SDK allows you to integrate spark with your existing ecosystem. The Spark Client is built entirely on this simple, powerful API.
</p>
<a href="" class="button" disabled>Get started</a>
</div>
<!-- <div class="selling-point disabled">
<h2>
Microsoft Excel
<i class="fa fa-table" aria-hidden="true"></i>
</h2>
<p>
More of your business is built on excel than probably should be. Continue this worrying trend with the Spark Excel API.
</p>
<a href="" class="button" disabled>Get started</a>
</div> -->
</div>
</div>
{% endblock content %}
</div>
<footer>
© Blue Trading Systems
</footer>
<script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") }}"></script>
<script type="text/javascript" src="{{ get_url(path="search_index.en.js") }}"></script>
<script type="text/javascript" src="{{ get_url(path="search.js") }}"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{% extends "documentation.html" %}
{% block title %}{{ page.title }} | {{ super() }} {% endblock title %}
{% block doc_content %}
<h1>{{page.title}}</h1>
{{page.content | safe}}
{% endblock doc_content %}
{% block next_link %}
<p>
{% if page.lighter %}
<a href="{{ page.lighter.permalink }}">
<i class="fa fa-chevron-left fa-fw" aria-hidden="true"></i>
Previous
</a>
{% endif %}
&nbsp;
{% if page.heavier %}
<a href="{{ page.heavier.permalink }}">
Next
<i class="fa fa-chevron-right fa-fw" aria-hidden="true"></i>
</a>
{% endif %}
</p>
{% endblock next_link %}
@@ -0,0 +1,15 @@
<pre
{% if background %}
style="background-color:{{background}};"
{% else %}
style="background-color:#383838;"
{% endif %}
>
<span
{% if color %}
style="color:{{color}};"
{% else %}
style="color:#e6e1dc;"
{% endif %}
><a onclick="navigator.clipboard.writeText('{{body}}')"><i class="fa fa-clipboard fa-underline" style="color: white; text-decoration: none;"></i></a> {{body}}</span>
</pre>
@@ -0,0 +1,7 @@
{% for asset in page.assets -%}
{%- if asset is matching("[.](jpg|png)$") -%}
<a href="{{ get_url(path=asset) | safe }}" target="_blank">
<img src="{{ resize_image(path=asset, width=240, height=180, op="fill", format="auto") | safe }}" />
</a>
{%- endif %}
{%- endfor %}
+4
View File
@@ -0,0 +1,4 @@
<div class="info">
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
{{body}}
</div>
+4
View File
@@ -0,0 +1,4 @@
<div class="linux">
<i class="fa fa-linux fa-fw" aria-hidden="true"></i>
{{body}}
</div>
@@ -0,0 +1 @@
<img src="{{ resize_image(path=path, width=width, height=height, op=op) }}" />
+6
View File
@@ -4,6 +4,12 @@ weight = 3
sort_by = "weight"
insert_anchor_links = "right"
+++
## 2.14.0
### User Visible Changes
### Bug Fixes
- Fixed an issue where market transition were not observed by IAlgoOrder
- Fixed a bug in error reporting
## 2.13.0
### User Visible Changes
- Added the name of the spreader to the parent algo badge (for spreader child orders), in the fills, orderbook and floating orderbook windows
+4
View File
@@ -1,3 +1,7 @@
2.14.0
- Load Algos in libspark.so automatically (no symlink needed)
- Specify commands to run when sparkd starts. These will automatically be killed when sparkd terminates if they are still running.
1.5.7
- Fixed Aggressor coloring and ladder high/low coloring in SIM
+5 -2
View File
@@ -136,8 +136,11 @@ namespace mbts.ui.shell
try {
if (argv.Contains("-multiple") || _mutex == null || _mutex.WaitOne(TimeSpan.Zero, true))
{
if(_mutex == null || _mutex.WaitOne(TimeSpan.Zero, true))
init_stunnel(argv);
if(_mutex == null || _mutex.WaitOne(TimeSpan.Zero, true)) {
if (!argv.Contains("-nostunnel")) {
init_stunnel(argv);
}
}
return base.Run();
}
else {
+2
View File
@@ -40,6 +40,7 @@ namespace mbts.ui {
EditDocumentAltGeometryKey = nameof(EditDocumentAltGeometryKey),
EditDocumentGeometryKey = nameof(EditDocumentGeometryKey),
GearGeometryKey = nameof(GearGeometryKey),
HamburgerMenuGeometryKey = nameof(HamburgerMenuGeometryKey),
HornGeometryKey = nameof(HornGeometryKey),
KeyGeometryKey = nameof(KeyGeometryKey),
LetterAKey = nameof(LetterAKey),
@@ -172,6 +173,7 @@ namespace mbts.ui {
public static ImageSource FilterActive { get { return load_resource_and_freeze("FilterActive"); } }
public static ImageSource FilterError { get { return load_resource_and_freeze("FilterError"); } }
public static ImageSource Gear => create_icon(GearGeometryKey);
public static ImageSource HamburgerMenu => create_icon(HamburgerMenuGeometryKey);
public static ImageSource Horn => create_icon(HornGeometryKey);
public static ImageSource Import => load_resource_and_freeze("Import");
public static ImageSource Key => create_icon(KeyGeometryKey);
@@ -272,6 +272,10 @@
M16.000015,9.6000061C12.500012,9.6000061 9.5999844,12.5 9.5999844,16 9.5999844,19.5 12.500012,22.399994 16.000015,22.399994 19.500019,22.399994 22.400015,19.5 22.400015,16 22.400015,12.5 19.500019,9.6000061 16.000015,9.6000061z M14.199995,0L17.800005,0C18.199999,0,18.599993,0.29998779,18.599993,0.79998779L18.599993,4.7999878C19.900013,5.1000061,21.000019,5.6000061,22.099997,6.2999878L25.000023,3.3999939C25.099999,3.2999878 25.300011,3.1999817 25.500025,3.1999817 25.700007,3.1999817 25.900019,3.2999878 26.000025,3.3999939L28.600033,6C28.900021,6.2999878,28.900021,6.7999878,28.600033,7.1000061L25.700007,10C26.400019,11.100006,26.900019,12.299988,27.200007,13.5L31.20001,13.5C31.600037,13.5,31.999998,13.799988,31.999998,14.299988L31.999998,17.899994C31.999998,18.299988,31.700012,18.699982,31.20001,18.699982L27.200007,18.699982C26.900019,20,26.400019,21.100006,25.700007,22.199982L28.600033,25.100006C28.700009,25.199982 28.799984,25.399994 28.799984,25.600006 28.799984,25.799988 28.700009,26 28.600033,26.100006L26.000025,28.600006C25.900019,28.699982 25.700007,28.799988 25.500025,28.799988 25.300011,28.799988 25.099999,28.699982 25.000023,28.600006L22.099997,25.699982C21.000019,26.399994,19.800007,26.899994,18.599993,27.199982L18.599993,31.199982C18.599993,31.600006,18.300005,32,17.800005,32L14.199995,32C13.8,32,13.400006,31.699982,13.400006,31.199982L13.400006,27.199982C12.099987,26.899994,11.000011,26.399994,9.9000027,25.699982L7.0000064,28.600006C6.9000003,28.699982 6.6999881,28.799988 6.5000059,28.799988 6.2999937,28.799988 6.099981,28.699982 6.0000054,28.600006L3.3999967,26C3.0999784,25.699982,3.0999784,25.199982,3.3999967,24.899994L6.2999937,22C5.5999805,20.899994,5.0999801,19.699982,4.7999923,18.5L0.79998828,18.5C0.39999409,18.5,-1.933513E-07,18.199982,0,17.699982L0,14.100006C-1.933513E-07,13.699982,0.2999879,13.299988,0.79998828,13.299988L4.7999923,13.299988C5.0999801,12,5.5999805,10.899994,6.2999937,9.7999878L3.3999967,7C3.2999908,6.8999939 3.1999845,6.6999817 3.1999845,6.5 3.1999845,6.2999878 3.2999908,6.1000061 3.3999967,6L6.0000054,3.3999939C6.099981,3.2999878 6.2999937,3.1999817 6.5000059,3.1999817 6.6999881,3.1999817 6.9000003,3.2999878 7.0000064,3.3999939L9.9000027,6.2999878C11.000011,5.6000061,12.199993,5.1000061,13.400006,4.7999878L13.400006,0.79998779C13.400006,0.29998779,13.8,0,14.199995,0z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.HamburgerMenuGeometryKey}">
M2.384186E-06,24L32.000004,24 32.000004,26 2.384186E-06,26z M0,11.999998L32,11.999998 32,13.999998 0,13.999998z M2.384186E-06,0L32.000004,0 32.000004,2 2.384186E-06,2z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.HornGeometryKey}">
M4.0499969,7.0870913C2.9519978,7.0870913 2.0249984,8.4780906 2.0249987,10.125091 2.0249984,11.771091 2.9519978,13.162091 4.0499969,13.162091 5.1479961,13.162091 6.0749954,11.771091 6.0749955,10.125091 6.0749954,8.4780906 5.1479961,7.0870913 4.0499969,7.0870913z M29.975016,3.316155C23.883831,8.2016557,11.829888,8.9691842,8.1184387,9.0896781L8.0151367,9.0928462 8.0175586,9.1059306C8.0716033,9.4352315 8.0999939,9.7760909 8.0999942,10.125091 8.0999939,10.473966 8.0716033,10.814716 8.0175586,11.143921L8.0098858,11.185364 8.193316,11.195448C12.163719,11.420399,23.994581,12.458921,29.975016,17.005057z M31.176758,0.00019051835C31.280359,-0.0022594665 31.391857,0.01889403 31.514975,0.065146674 31.908983,0.21413228 31.999985,0.59114302 31.999985,1.0121596L31.999985,19.236969C31.999985,19.653958 31.913988,20.028956 31.525991,20.179955 31.405998,20.226952 31.450981,20.250023 31.32699,20.250023 31.050991,20.250023 30.778992,20.13601 30.583986,19.925928 25.229074,14.140572 9.6001122,13.269665 7.2865356,13.172559L7.2813506,13.172345 7.1739591,13.343056C6.4304952,14.468533 5.3060585,15.187091 4.0499969,15.187091 1.8169985,15.187091 0,12.916091 0,10.125091 0,7.3330911 1.8169985,5.0620912 4.0499969,5.0620912 5.3060585,5.0620912 6.4304952,5.78065 7.1739591,6.9063042L7.2852713,7.0832756 7.3087114,7.0828188C9.6077337,7.036146 25.141474,6.4939421 30.567997,0.34315929 30.75913,0.12521249 30.948835,0.005580858 31.176758,0.00019051835z
</sys:String>
@@ -0,0 +1,13 @@
using mbts.spark;
namespace mbts.ui.spark {
readonly struct MarketViewCell {
public readonly MarketViewVM.Row row;
public readonly MarketViewView.Columns column;
public MarketViewCell(MarketViewVM.Row row, MarketViewView.Columns column) {
this.row = row;
this.column = column;
}
}
}
@@ -5,6 +5,7 @@ using mbts.co2;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
@@ -18,7 +19,7 @@ using System.Windows.Input;
namespace mbts.spark {
partial class MarketViewVM {
public class Row : SecurityMDRow, IDisposable, IEquatable<Row>, DIYGrid.IRowVM, DIYGrid.IActionableRowVM {
public class Row : DisposableViewModelBase, ISecurityMDRow, IEquatable<Row>, DIYGrid.IRowVM, DIYGrid.IActionableRowVM {
readonly IBus _bus;
readonly OrdersModel _orders_model;
readonly AnimationManager _animation = new AnimationManager();
@@ -27,7 +28,6 @@ namespace mbts.spark {
readonly string _security_text, _security_key_text;
readonly Property<double> _bid_price, _ask_price, _last_price, _day_change, _settlement, _session_high, _session_low;
readonly Property<bool> _last_price_from_spread;
readonly CellProperty<int> _position;
readonly CellProperty<uint> _last_size;
readonly CellProperty<ulong> _volume;
readonly CellProperty<bool> _inside_bids, _inside_asks;
@@ -37,17 +37,18 @@ namespace mbts.spark {
readonly MyOrderSummarizer _order_summarizer = new MyOrderSummarizer();
readonly MDDisplayHelper.SizeFormatter _size_formatter = MDDisplayHelper.get_formatter(MarketSizeDisplay.get_default());
Property<uint> _bid_direct_size, _bid_merged_size, _ask_direct_size, _ask_merged_size;
double _raw_day_position;
double _day_position, _raw_day_position;
string _bid_merged_size_text, _ask_merged_size_text;
ulong _bbo_ref, _last_ref;
bool _is_visible;
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) : base(smd) {
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) {
_bus = bus;
security_md = smd;
_orders_model = orders_model;
_security_text = smd.security.ToString();
_security_key_text = security_key.ToString();
string format_price(double price) => security.ContractSpec.format(price);
_position = new CellProperty<int>(this, nameof(position), 0);
_bid_price = backing(nameof(bid_price), double.NaN);
_ask_price = backing(nameof(ask_price), double.NaN);
_last_price = backing(nameof(last_price), double.NaN);
@@ -91,9 +92,17 @@ namespace mbts.spark {
void on_animations_changed() => RaisePropertyChanged(nameof(animations));
_dispose = Disposable.Create(
_orders_model.on_security_root_orders(security.Id).OnDispatcher().Subscribe(_on_orders),
_position.OnDispatcher().Subscribe(v => _animation.animate_position(v.newval > v.oldval)),
_orders_model.on_security_positions(security.Id).OnDispatcher().Subscribe(t => {
if (Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position)))
bool day_pos_dirty = false;
var new_day_pos = t.state.Values.Sum(p => p.Item2.day_pos());
if (new_day_pos != _day_position) {
if (!double.IsNaN(new_day_pos) && !double.IsNaN(_day_position))
_animation.animate_position(new_day_pos > _day_position);
_day_position = new_day_pos;
RaisePropertyChanged(nameof(day_position));
day_pos_dirty = true;
}
if (day_pos_dirty | Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position)))
raise_invalidated();
}),
_inside_bids.OnDispatcher().Subscribe(v => _animation.animate_working_bids_inside(v.newval)),
@@ -105,10 +114,20 @@ namespace mbts.spark {
public event Action invalidated;
public SecurityMD security_md { get; }
public SecurityImmutable security => security_md.security;
public uint security_key => security_md.security.Id;
SecurityMD ISecurityMDRow.md => security_md;
public bool is_visible {
get => _is_visible;
set => Set(ref _is_visible, value);
}
#region Bound Properties
public bool inside_bids => _inside_bids.value;
public bool inside_asks => _inside_asks.value;
public int position => _position.value;
public double day_position => _day_position;
public double raw_day_position => _raw_day_position;
public double last_price => _last_price.Value;
public bool last_price_from_spread => _last_price_from_spread.Value;
@@ -158,29 +177,30 @@ namespace mbts.spark {
public IReadOnlyList<DIYGrid.IGridCellStore> cell_stores => new DIYGrid.IGridCellStore[0];
#endregion
MDConsumerHandle _md => security_md.md;
public void raise_invalidated() => invalidated?.Invoke();
public void refresh() {
bool dirty = false;
if (md.md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
if (_md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
_bid_price.set(b.price);
refresh_size(ref b, _bid_direct_size, _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
_ask_price.set(a.price);
refresh_size(ref a, _ask_direct_size, _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
_inside_bids.on_next_distinct(_bid_price.Value >= _working_best_bid.value, ref dirty);
_inside_asks.on_next_distinct(_ask_price.Value <= _working_best_ask.value, ref dirty);
_refresh_inside_working_orers(ref dirty);
}
if (md.md.get_last_trade(ref _last_ref, out NativePriceSize ps, out DateTime ts, out ulong v, out bool from_spread)) {
if (_md.get_last_trade(ref _last_ref, out NativePriceSize ps, out DateTime ts, out ulong v, out bool from_spread)) {
_last_price.set(ps.price);
_last_price_from_spread.set(from_spread);
_last_size.on_next_distinct(ps.size, ref dirty);
_volume.on_next_distinct(v, ref dirty);
}
dirty |= _settlement.set_new(md.md.settlement_price);
dirty |= _session_low.set_new(md.md.session_low);
dirty |= _session_high.set_new(md.md.session_high);
dirty |= _day_change.set_new(last_price - settlement);
_volume.on_next_distinct(md.md.total_volume, ref dirty);
dirty |=
_settlement.set_new(_md.settlement_price) |
_session_low.set_new(_md.session_low) |
_session_high.set_new(_md.session_high) |
_day_change.set_new(last_price - settlement);
if (dirty)
invalidated?.Invoke();
@@ -206,7 +226,7 @@ namespace mbts.spark {
security_key.GetHashCode();
#region Show Windows
void _show_ladder() => _bus.post(SparkTags.ShowLadderTag, md);
void _show_ladder() => _bus.post(SparkTags.ShowLadderTag, security_md);
void _show_order_book(MarketViewView.Columns col) {
switch (col) {
@@ -275,14 +295,11 @@ namespace mbts.spark {
}
void _refresh_orders(ref bool dirty) {
_position.on_next_distinct(_order_summarizer.position, ref dirty);
_working_bid_volume.on_next_distinct(_order_summarizer.working_bid_qty, ref dirty);
_working_ask_volume.on_next_distinct(_order_summarizer.working_ask_qty, ref dirty);
_working_best_bid.on_next_distinct(_order_summarizer.best_working_bid, ref dirty);
_working_best_ask.on_next_distinct(_order_summarizer.best_working_ask, ref dirty);
_inside_bids.on_next_distinct(_order_summarizer.best_working_bid >= bid_price, ref dirty);
_inside_asks.on_next_distinct(_order_summarizer.best_working_ask <= ask_price, ref dirty);
_refresh_inside_working_orers(ref dirty);
RaisePropertyChanged(
// Implied might not be ever set, so it probably does not RaisePropertyChanged either
// This might be part of the filter funk?
@@ -292,6 +309,11 @@ namespace mbts.spark {
nameof(working_asks_text)
);
}
void _refresh_inside_working_orers(ref bool dirty) {
_inside_bids.on_next_distinct(!double.IsNaN(working_best_bid) && OrderDataHelper.PriceComparer.Compare(working_best_bid, bid_price) >= 0, ref dirty);
_inside_asks.on_next_distinct(!double.IsNaN(working_best_ask) && OrderDataHelper.PriceComparer.Compare(working_best_ask, ask_price) <= 0, ref dirty);
}
#endregion
class MyOrderSummarizer {
@@ -367,4 +389,4 @@ namespace mbts.spark {
}
}
}
}
}
@@ -1,15 +1,30 @@
using bts;
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.collections;
using bts.utils;
using mbts.persist;
using mbts.spark.filters;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using static bts.shell.MarketViewWindowFactory;
@@ -17,21 +32,61 @@ using static mbts.ui.controls.DIYGrid;
using static mbts.ui.spark.MarketViewView;
namespace mbts.spark {
partial class MarketViewVM : SecurityMDGrid<MarketViewVM.Row, SecurityMD> {
/// <summary>
/// Viewmodel class designed for <see cref="ui.spark.MarketViewView"/>
/// </summary>
partial class MarketViewVM : DisposableViewModelBase, IHandleRowSelection {
static readonly IReadOnlyList<Func<MarketViewVM, IDataGridColumnFilter<Row>>> _filter_getters =
ObjectSnapshot.property_delegates_of_type<MarketViewVM, IDataGridColumnFilter<Row>>().ToReadOnlyList();
static readonly IComparer<SecurityMD> _sec_md_key_comparer = Comparer<uint>.Default.wrap(SecurityMD.key_fn);
static readonly IComparer<Row> _row_key_comparer = _sec_md_key_comparer.wrap<SecurityMD, Row>(r => r.security_md);
static readonly string _security_delimiter = $" {SpecialCharacters.Dot} ";
readonly IBus _bus;
readonly OrdersModel _orders_model;
IWindowSettingsManager<GridSettings> _settings_manager;
readonly ObservableCollection<Row> _rows = new ObservableCollection<Row>();
readonly IList<SecurityMD> _wrapped_rows;
readonly ListCollectionView _rows_view;
readonly Dictionary<uint, Row> _cache = new Dictionary<uint, Row>();
readonly Property<Row> _current_row;
readonly DeferrableActionWrapper _refresh_filter;
Func<Row, bool> _filter;
IComparer<Row> _comparer;
Color _selection_highlight;
public MarketViewVM(IBus bus, OrdersModel orders_model, IObservable<StartupData> state, IWindowSettingsManager<GridSettings> settings_manager )
: this(bus, orders_model, state, state.SelectManyInSeries(s => s.security_criteria.subscribe_md(bus)), settings_manager) { }
// securities_connectable should NOT be connected already.
MarketViewVM(IBus bus, OrdersModel orders_model, IObservable<StartupData> state, IObservable<IEnumerable<SecurityMD>> securities_source, IWindowSettingsManager<GridSettings> settings_manager)
: base(RXCollection.Create(securities_source, SecurityDataHelper.EqualityComparer.MDComparer), null) {
/// <summary>
/// Initializes an instance of the <see cref="ui.spark.MarketViewView"/> class.
/// </summary>
/// <param name="bus"></param>
/// <param name="orders_model"></param>
/// <param name="security_mds_source"></param>
/// <param name="use_mouse_state_transitions">Indicates whether or not hyperlink cells should use visual states to animate mouse over (seems to be slower than style triggers, should investigate to be certain).</param>
public MarketViewVM(
IBus bus,
OrdersModel orders_model,
IObservable<TableUpdate<SecurityMD, uint>> security_mds_source,
bool use_mouse_state_transitions = false) {
_bus = bus;
_orders_model = orders_model;
security_filter = new RegexFilterVM<Row>(r => r.security.ToString(), nameof(Row.security));
_settings_manager = settings_manager;
// rows
_wrapped_rows = _rows.WrapSelector(r => r.security_md, s => {
var r = new Row(_bus, s, _orders_model);
r.is_visible = _filter == null || _filter(r);
return r;
});
_current_row = backing<Row>(nameof(current_row));
_rows_view = new ListCollectionView(_rows) {
Filter = obj => obj is Row r && r.is_visible,
IsLiveFiltering = true
};
_rows_view.LiveFilteringProperties.Add(nameof(Row.is_visible));
// Important: these selected_security/md wrappers should be instantiated in the constructor (rather than lazily),
// to prevent situations where callers subscribe up to the selected_rows.CollectionChanged event before the wrappers do.
// This way, the wrappers are protected from unintended side effects of the foreign callbacks, as they will always receive CollectionChange notifications first.
selected_securities = selected_rows.SelectObservable(r => r.security);
selected_security_mds = selected_rows.SelectObservable(r => r.security_md);
// Columns
const string merged_size_col_header = "Qty", direct_size_col_header = "Direct", implied_size_col_header = "Implied", working_col_header = "Working";
const double working_orders_col_width = 105;
@@ -39,24 +94,55 @@ namespace mbts.spark {
add_col(Columns.Security, "Security", r => r.security.ToString(), nameof(Row.security), DefaultColumnWidths.Security, security_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Volume, "Volume", r => r.volume.ToString(), nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
add_col(Columns.LastSpd, "Spd", r => r.last_price_from_spread ? "S" : "", nameof(Row.last_price_from_spread), DefaultColumnWidths.Side, last_price_from_spread_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Last, "Last", r => r.format_price(r.last_price), nameof(Row.last_price), DefaultColumnWidths.Price, last_price_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.RawDayPosition, "Raw Day Pos", r => SparkUtils.fmt_position(r.raw_day_position), nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingBids, working_col_header, nameof(Row.working_bids_text), nameof(Row.working_best_bid), nameof(Row.working_bids_click), working_bid_volume_filter, BTSColors.MarketData.bid, System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.BidMergedSize, merged_size_col_header, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), nameof(Row.bid_merged_size_click), bid_merged_size_filter, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), nameof(Row.bid_price_click), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), nameof(Row.ask_price_click), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.AskMergedSize, merged_size_col_header, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), nameof(Row.ask_merged_size_click), ask_merged_size_filter, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingAsks, working_col_header, nameof(Row.working_asks_text), nameof(Row.working_best_ask), nameof(Row.working_asks_click), working_ask_volume_filter, BTSColors.MarketData.ask, System.Windows.HorizontalAlignment.Right);
add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.SessionLow, "Low", r => r.format_price(r.session_low), nameof(Row.session_low), DefaultColumnWidths.Price, session_low_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.SessionHigh, "High", r => r.format_price(r.session_high), nameof(Row.session_high), DefaultColumnWidths.Price, session_high_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.Last, "Last", r => r.format_price(r.last_price), nameof(Row.last_price), DefaultColumnWidths.Price, last_price_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax);
add_hl_col(
Columns.Position,
"Day Pos",
new Binding(nameof(Row.day_position)),
nameof(Row.day_position),
day_position_filter,
DataGridFilterKnownTemplates.IntMinMax,
DefaultColumnWidths.Size,
tooltip: $"Show {_fills_title(Columns.Position).desc}");
add_hl_col(
Columns.RawDayPosition,
"Raw Day Pos",
new Binding(nameof(Row.raw_day_position)) {
Converter = ValueConverter.create<double, string>(SparkUtils.fmt_position)
},
nameof(Row.raw_day_position),
raw_day_position_filter,
DataGridFilterKnownTemplates.DoubleMinMax,
DefaultColumnWidths.Size,
tooltip: $"Show {_fills_title(Columns.RawDayPosition).desc}");
add_hl_working_orders_col(
Columns.WorkingBids,
working_col_header,
nameof(Row.working_bids_text),
nameof(Row.working_best_bid),
working_bid_volume_filter,
BTSColors.MarketData.bid);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid);
add_hl_merged_size_col(Columns.BidMergedSize, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), bid_merged_size_filter, BTSColors.MarketData.bid);
add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid);
add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask);
add_hl_merged_size_col(Columns.AskMergedSize, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), ask_merged_size_filter, BTSColors.MarketData.ask);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask);
add_hl_working_orders_col(
Columns.WorkingAsks,
working_col_header,
nameof(Row.working_asks_text),
nameof(Row.working_best_ask),
working_ask_volume_filter,
BTSColors.MarketData.ask);
add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true);
add_col(Columns.SessionLow, "Low", r => r.format_price(r.session_low), nameof(Row.session_low), DefaultColumnWidths.Price, session_low_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.SessionHigh, "High", r => r.format_price(r.session_high), nameof(Row.session_high), DefaultColumnWidths.Price, session_high_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.SecurityKey, "Security Key", r => r.security_key.ToString(), nameof(Row.security_key), DefaultColumnWidths.SecurityKey, security_key_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.MarketDescription, "Market Description", r => r.security.market_description(), nameof(Row.market_description), DefaultColumnWidths.SecurityKey, market_description_filter, DataGridFilterKnownTemplates.Regex);
@@ -65,9 +151,32 @@ namespace mbts.spark {
add_col(Columns.HubAlias, "Hub Alias", r => r.security.hub_alias(), nameof(Row.hub_alias), DefaultColumnWidths.SecurityKey, hub_alias_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.StripName, "Strip Name", r => r.security.strip_name(), nameof(Row.strip_name), DefaultColumnWidths.SecurityKey, strip_name_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.MicCode, "Mic Code", r => r.security.mic_code(), nameof(Row.mic_code), DefaultColumnWidths.SecurityKey, mic_code_filter, DataGridFilterKnownTemplates.Regex);
//
void add_col(Columns col_key, string header, Func<Row, string> get_text, string prop, double width, IDataGridColumnFilter filter, string filter_template, bool is_animatable = false, Color? font_color = null, System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
columns.Add(new RenderTextColumn<Row, Columns>(col_key, get_text, filter, filter_template) {
// row vm filter
// Althoguh securities are filtered at the row 'model' level (SecurityMD), the column base filters have to be applied at the row vm level,
// because they access market data, which is polled periodically and cached in the row vm.
_refresh_filter = new DeferrableActionWrapper(() => _filter_changed?.Invoke());
var row_filter_source = Observable.Create<Func<Row, bool>>(o => {
notify();
_filter_changed += notify;
return Disposable.Create(() => _filter_changed -= notify);
void notify() =>
o.OnNext(get_filter_states().build_predicate());
});
foreach (var f in _all_filters) {
f.filter_changed += _refresh_filter.execute;
}
_dispose = Disposable.Create(
row_filter_source.Subscribe(f => {
// the rows are polled every 100 ms, just set the filter and let the refresh take care of the rest.
_filter = f;
}),
security_mds_source.Subscribe(_on_securities),
ObservableAutoWrapper.connect_item_hooks(_rows, r => r, true),
WorkLoop.Start("MarketView", _refresh)
);
// local methods
RenderTextColumn<Row> add_col(Columns col_key, string header, Func<Row, string> get_text, string prop, double width, IDataGridColumnFilter filter, string filter_template, bool is_animatable = false, Color? font_color = null, HorizontalAlignment h_alignment = HorizontalAlignment.Right) {
var col = new RenderTextColumn<Row, Columns>(col_key, get_text, filter, filter_template) {
// it's necessary to have this id populated for grid settings
id = bts.shell.MarketViewWindowFactory.col_id(col_key),
Header = header,
@@ -76,31 +185,91 @@ namespace mbts.spark {
padding = is_animatable ? AnimatableCellPadding : DefaultCellPadding,
font_color_override = font_color,
horizontal_alignment = h_alignment
});
};
columns.Add(col);
return col;
}
void add_hl_price_col(Columns col_key, string header, string prop, string command_prop, IDataGridColumnFilter filter, Color foreground, Color? background, System.Windows.HorizontalAlignment h_alignment) =>
add_hl_col(col_key, header, price_binding(prop), prop, command_prop, filter, DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.MViewPrice, foreground, background, h_alignment);
void add_hl_price_col(Columns col_key, string header, string prop, IDataGridColumnFilter filter, Color foreground, Color? background) =>
add_hl_col(
col_key,
header,
price_binding(prop),
prop,
filter,
DataGridFilterKnownTemplates.DoubleMinMax,
DefaultColumnWidths.MViewPrice,
foreground,
background,
tooltip: _order_ticket_tooltip(col_key));
void add_hl_merged_size_col(Columns col_key, string header, string sort_prop, string text_prop, string command_prop, IDataGridColumnFilter filter, Color foreground, System.Windows.HorizontalAlignment h_alignment) =>
add_hl_col(col_key, header, new Binding(text_prop), sort_prop, command_prop, filter, DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.ImpliedDirectMergedSize, foreground, null, h_alignment);
void add_hl_merged_size_col(Columns col_key, string sort_prop, string text_prop, IDataGridColumnFilter filter, Color foreground) =>
add_hl_col(
col_key,
merged_size_col_header,
new Binding(text_prop),
sort_prop,
filter,
DataGridFilterKnownTemplates.UIntMinMax,
DefaultColumnWidths.ImpliedDirectMergedSize,
foreground,
tooltip: _order_ticket_tooltip(col_key));
void add_hl_working_orders_col(Columns col_key, string header, string text_prop, string sort_prop, string command_prop, IDataGridColumnFilter filter, Color foreground, System.Windows.HorizontalAlignment h_alignment) =>
add_hl_col(col_key, header, new Binding(text_prop), sort_prop, command_prop, filter, DataGridFilterKnownTemplates.IntMinMax, working_orders_col_width, foreground, null, h_alignment);
HyperlinkElementColumn<Columns> add_hl_working_orders_col(Columns col_key, string header, string text_prop, string sort_prop, IDataGridColumnFilter filter, Color foreground) =>
add_hl_col(
col_key,
header,
new Binding(text_prop),
sort_prop,
filter,
DataGridFilterKnownTemplates.IntMinMax,
working_orders_col_width,
foreground,
tooltip: $"Show {_orderbook_title(col_key).desc}");
void add_hl_col(
Columns col_key, string header, BindingBase binding, string prop, string command_prop,
IDataGridColumnFilter filter, string filter_template, double width, Color foreground, Color? background, System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
columns.Add(new HyperlinkElementColumn<Row, Columns>(
col_key, binding, new Binding(command_prop), filter, filter_template) {
// it's necessary to have this id populated for grid settings
id = bts.shell.MarketViewWindowFactory.col_id(col_key),
Header = header,
sort_member_path = prop,
font_color_override = foreground,
background_color_override = background,
Width = DefaultColumnWidths.Price,
horizontal_alignment = h_alignment
});
HyperlinkElementColumn<Columns> add_hl_col(
Columns col_key,
string header,
BindingBase binding,
string prop,
IDataGridColumnFilter filter,
string filter_template,
double width,
Color? foreground = null,
Color? background = null,
HorizontalAlignment h_alignment = HorizontalAlignment.Right,
string tooltip = null,
BindingBase tooltip_binding = null) {
var col = new HyperlinkElementColumn<Columns>(
col_key, filter, filter_template) {
// it's necessary to have this id populated for grid settings
id = bts.shell.MarketViewWindowFactory.col_id(col_key),
Header = header,
sort_member_path = prop,
font_color_override = foreground,
background_color_override = background,
TextBinding = binding,
Width = DefaultColumnWidths.Price,
horizontal_alignment = h_alignment
};
col.view_generated += on_view_generated;
columns.Add(col);
return col;
//
void on_view_generated(HyperlinkCell cell_view) {
cell_view.SetValue(ColumnKeyProperty, col_key);
cell_view.SetValue(ColumnProperty, col);
cell_view.SetBinding(GridProperty, new Binding { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DIYGrid), 1) });
if (tooltip != null) {
cell_view.ToolTip = tooltip;
} else if (tooltip_binding != null) {
cell_view.SetBinding(FrameworkElement.ToolTipProperty, tooltip_binding);
}
if (use_mouse_state_transitions)
init_hyperlink_hover_animation(cell_view, col);
}
}
BindingBase price_binding(string prop) {
var mb = new MultiBinding { Converter = price_converter };
@@ -108,72 +277,95 @@ namespace mbts.spark {
mb.Bindings.Add(new Binding($"{nameof(Row.security)}.{nameof(SecurityImmutable.ContractSpec)}") { Mode = BindingMode.OneWay });
return mb;
}
//
// make font-color immutable for Side, Size, (the font-color rule is hard-coded: MarketData.bid for Buy orders, MarketData.ask for Sells)
var col_settings_flags = new Dictionary<string, GridColumnSettingsFeatureFlags> {
{ Columns.Bid.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor },
{ Columns.Ask.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor},
{ Columns.WorkingAsks.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor },
{ Columns.WorkingBids.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }
};
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, col_settings_flags, row_vms);
// this will initialize the immutable properties that correspond to the col_settings_flags above (e.g. Bid/Ask font_color_override, etc.)
grid_settings_manager.load_from(settings_manager.current_settings());
//
_resources.AddRange(
state.OnDispatcher().Subscribe(s => {
using (rows_view.DeferRefresh()) {
if (s.grid != null) {
// this means we are not using defaults,
// Note: settings_manager in turn notifies grid_settings_manager of this update
_settings_manager.instance_settings = s.grid;
void init_hyperlink_hover_animation(HyperlinkCell cell_view, ColumnBase col) {
// background timeline
var hover_bg_timeline = VisualStateHelper.create_color_animation(
cell_view,
new PropertyPath("(0).(1)", HyperlinkCell.BackgroundProperty, SolidColorBrush.ColorProperty),
new Binding(nameof(ColumnBase.background_color)) {
Source = col,
Converter = new ColorBlendConverter { Overlay = Colors.Black.with(0x1A) }
});
// background pressed timeline
var pressed_bg_timeline = VisualStateHelper.create_color_animation(
cell_view,
new PropertyPath("(0).(1)", HyperlinkCell.BackgroundProperty, SolidColorBrush.ColorProperty),
new Binding(nameof(ColumnBase.background_color)) {
Source = col,
Converter = new ColorBlendConverter { Overlay = Colors.Black.with(0x25) }
});
// border timeline
var bd_brush = new SolidColorBrush();
BindingOperations.SetBinding(bd_brush, SolidColorBrush.ColorProperty, new Binding(nameof(ColumnBase.background_color)) { Source = col });
cell_view.BorderBrush = bd_brush;
cell_view.BorderThickness = new Thickness(1);
var hover_bd_timeline = VisualStateHelper.create_color_animation(
cell_view, new PropertyPath("(0).(1)", HyperlinkCell.BorderBrushProperty, SolidColorBrush.ColorProperty), Colors.Black.with(0x60));
// mouse states
VisualStateManager.GetVisualStateGroups(cell_view).Add(
VisualStateHelper.create_group("MouseStates",
new VisualState { Name = "Normal" },
VisualStateHelper.create_state("MouseOver", hover_bg_timeline, hover_bd_timeline),
VisualStateHelper.create_state("Pressed", pressed_bg_timeline, hover_bd_timeline)));
// state groups manager
StateManager.SetGroups(cell_view, new StateGroupsManager {
StateDefinitions = new StateDefinitionCollection {
new MouseStateDefinition { MouseSource = cell_view }
}
if (s.filters is FilterStates filters) {
security_filter.load_state(filters.security);
bid_price_filter.load_state(filters.bid_price);
bid_merged_size_filter.load_state(filters.bid_merged_size);
bid_direct_size_filter.load_state(filters.bid_direct_size);
bid_implied_size_filter.load_state(filters.bid_implied_size);
ask_price_filter.load_state(filters.ask_price);
ask_merged_size_filter.load_state(filters.ask_merged_size);
ask_direct_size_filter.load_state(filters.ask_direct_size);
ask_implied_size_filter.load_state(filters.ask_implied_size);
last_price_filter.load_state(filters.last_price);
last_price_from_spread_filter.load_state(filters.last_price_from_spread);
last_size_filter.load_state(filters.last_size);
day_change_filter.load_state(filters.day_change);
settlement_filter.load_state(filters.settlement);
session_low_filter.load_state(filters.session_low);
session_high_filter.load_state(filters.session_high);
position_filter.load_state(filters.position);
raw_day_position_filter.load_state(filters.raw_day_position);
working_bid_volume_filter.load_state(filters.working_bid_volume);
working_ask_volume_filter.load_state(filters.working_ask_volume);
volume_filter.load_state(filters.volume);
security_key_filter.load_state(filters.security_key);
}
rows_view.set_sort_descriptions(s.sorting_criterion, columns);
}
}), DataGridFilterHelper.hook_filters(rows_view, security_filter,
bid_price_filter, bid_merged_size_filter, bid_direct_size_filter, bid_implied_size_filter,
ask_price_filter, ask_merged_size_filter, ask_direct_size_filter, ask_implied_size_filter,
last_price_filter, last_price_from_spread_filter, last_size_filter, day_change_filter, settlement_filter,
session_low_filter, session_high_filter, position_filter, raw_day_position_filter, working_bid_volume_filter, working_ask_volume_filter, volume_filter, security_key_filter),
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager),
WorkLoop.Start("MarketView", _refresh)
);
});
}
}
public event PropertyChange<Row> current_row_changed { add => _current_row.Changed += value; remove => _current_row.Changed -= value; }
event Action _filter_changed;
public ICollectionView rows_view => _rows_view;
public ObservableCollection<Row> rows => _rows;
public ObservableCollection<Row> selected_rows { get; } = new ObservableCollection<Row>();
public IReadOnlyObservableCollection<SecurityImmutable> selected_securities { get; }
public IReadOnlyObservableCollection<SecurityMD> selected_security_mds { get; }
public Color selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
public Row current_row { get => _current_row.Value; set => _current_row.set(value); }
public SecurityImmutable? current_security => current_row?.security;
public SecurityMD? current_security_md => current_row?.security_md;
public SimpleGridSettingsManager<Row> grid_settings_manager { get; }
internal IBus bus => _bus;
internal OrdersModel orders_model => _orders_model;
public GridSettings get_grid_save_state() {
if (_settings_manager.instance_settings == null)
return null;
else
return grid_settings_manager.create();
}
public IDataGridColumnPersistentFilter<Row, string> security_filter { get; }
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<Row>(r => r.bid_price, nameof(Row.bid_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_merged_size, nameof(Row.bid_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_direct_size, nameof(Row.bid_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_implied_size, nameof(Row.bid_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<Row>(r => r.ask_price, nameof(Row.ask_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_merged_size, nameof(Row.ask_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_direct_size, nameof(Row.ask_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_implied_size, nameof(Row.ask_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<Row>(r => r.last_price, nameof(Row.last_price));
public IDataGridColumnPersistentFilter<Row, string> last_price_from_spread_filter { get; } = new RegexFilterVM<Row>(r => r.last_price_from_spread.ToString(), nameof(Row.last_price_from_spread));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> last_size_filter { get; } = new UIntMinMax<Row>(r => r.last_size, nameof(Row.last_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> day_change_filter { get; } = new DoubleMinMax<Row>(r => r.day_change, nameof(Row.day_change));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> settlement_filter { get; } = new DoubleMinMax<Row>(r => r.settlement, nameof(Row.settlement));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_low_filter { get; } = new DoubleMinMax<Row>(r => r.session_low, nameof(Row.session_low));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_high_filter { get; } = new DoubleMinMax<Row>(r => r.session_high, nameof(Row.session_high));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> day_position_filter { get; } = new IntMinMax<Row>(r => unchecked((int)r.day_position), nameof(Row.day_position));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> raw_day_position_filter { get; } = new DoubleMinMax<Row>(r => r.raw_day_position, nameof(Row.raw_day_position));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_bid_volume_filter { get; } = new IntMinMax<Row>(r => r.working_bid_volume, nameof(Row.working_bid_volume));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_ask_volume_filter { get; } = new IntMinMax<Row>(r => r.working_ask_volume, nameof(Row.working_ask_volume));
public IDataGridColumnPersistentFilter<Row, ULongMinMaxState> volume_filter { get; } = new ULongMinMax<Row>(r => r.volume, nameof(Row.volume));
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MarketDescription : "", nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ClearedAlias : "", nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ProductName : "", nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
IEnumerable<IDataGridColumnFilter<Row>> _all_filters =>
_filter_getters.Select(g => g(this));
public static IComparer<Row> get_column_comparer(Columns column) {
switch (column) {
@@ -181,8 +373,9 @@ namespace mbts.spark {
case Columns.SecurityKey: return wrap(r => r.security.Id);
case Columns.Volume: return wrap(r => r.volume);
case Columns.Last: return wrap(r => r.last_price);
case Columns.LastSpd: return wrap(r => r.last_price_from_spread);
case Columns.LastSize: return wrap(r => r.last_size);
case Columns.Position: return wrap(r => r.position);
case Columns.Position: return wrap(r => r.day_position);
case Columns.WorkingBids: return wrap(r => r.working_bid_volume);
case Columns.BidImpliedSize: return wrap(r => r.bid_implied_size);
case Columns.BidDirectSize: return wrap(r => r.bid_direct_size);
@@ -209,37 +402,60 @@ namespace mbts.spark {
Comparer<T>.Default.wrap(selector);
}
#region Filters
public IDataGridColumnPersistentFilter<Row, string> security_filter { get; }
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<Row>(r => r.bid_price, nameof(Row.bid_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_merged_size, nameof(Row.bid_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_direct_size, nameof(Row.bid_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> bid_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.bid_implied_size, nameof(Row.bid_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<Row>(r => r.ask_price, nameof(Row.ask_price));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_merged_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_merged_size, nameof(Row.ask_merged_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_direct_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_direct_size, nameof(Row.ask_direct_size));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> ask_implied_size_filter { get; } = new UIntMinMax<Row>(r => r.ask_implied_size, nameof(Row.ask_implied_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<Row>(r => r.last_price, nameof(Row.last_price));
public IDataGridColumnPersistentFilter<Row, string> last_price_from_spread_filter { get; } = new RegexFilterVM<Row>(r => r.last_price_from_spread.ToString(), nameof(Row.last_price_from_spread));
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> last_size_filter { get; } = new UIntMinMax<Row>(r => r.last_size, nameof(Row.last_size));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> day_change_filter { get; } = new DoubleMinMax<Row>(r => r.day_change, nameof(Row.day_change));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> settlement_filter { get; } = new DoubleMinMax<Row>(r => r.settlement, nameof(Row.settlement));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_low_filter { get; } = new DoubleMinMax<Row>(r => r.session_low, nameof(Row.session_low));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> session_high_filter { get; } = new DoubleMinMax<Row>(r => r.session_high, nameof(Row.session_high));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> position_filter { get; } = new IntMinMax<Row>(r => r.position, nameof(Row.position));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> raw_day_position_filter { get; } = new DoubleMinMax<Row>(r => r.raw_day_position, nameof(Row.raw_day_position));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_bid_volume_filter { get; } = new IntMinMax<Row>(r => r.working_bid_volume, nameof(Row.working_bid_volume));
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> working_ask_volume_filter { get; } = new IntMinMax<Row>(r => r.working_ask_volume, nameof(Row.working_ask_volume));
public IDataGridColumnPersistentFilter<Row, ULongMinMaxState> volume_filter { get; } = new ULongMinMax<Row>(r => r.volume, nameof(Row.volume));
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MarketDescription : "", nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ClearedAlias : "", nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ProductName : "", nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
public IComparer<Row> comparer {
get => _comparer;
set => _rows_view.CustomSort = (_comparer = value)?.to_non_generic();
}
public (FillsVM vm, string context, string title, Color fg) fills_params(IEnumerable<SecurityImmutable> securities, Columns column) {
var (title_desc, fg) = _fills_title(column);
var state = StartupData.DefaultAuxiliaryFills;
if (state.filters == null)
state.filters = new bts.shell.fills.FillsFilterStates();
switch (column) {
case Columns.RawDayPosition:
state.filters.fill_kind = new[] {
Fill.Types.FillKind.Standard
};
break;
case Columns.Position:
state.filters.fill_kind = new[] {
Fill.Types.FillKind.Standard,
Fill.Types.FillKind.Sod
};
break;
}
state.grid = GridSettingsHelper.hide_cols(state.grid, new[] { ui.spark.FillsView.Columns.Security });
var sec_keys = securities.Select(SecurityDataHelper.key_fn);
if (securities.All(s => s.ContractCase == Security.ContractOneofCase.Synthetic)) {
state.root_securities = sec_keys.ToArray();
} else {
state.filters.security = SecurityCriteria.from_keys(sec_keys);
}
var vm = new FillsVM(
_bus, _orders_model, Observable.Return(state), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
return (vm, $"{string.Join(_security_delimiter, securities)}", title_desc, fg);
}
public (OrderBookVM vm, string context, string title, Color fg) orderbook_params(IEnumerable<SecurityImmutable> securities, Columns column) {
var (title_desc, fg) = _orderbook_title(column);
var filters = new bts.shell.orderbook.FilterStates {
security_criteria = SecurityCriteria.from_keys(securities.Select(SecurityDataHelper.key_fn)),
order_types = securities.All(s => s.ContractCase == Security.ContractOneofCase.Synthetic) ?
new[] { new AlgoID(CustomOrderDefinition.CustomOneofCase.Spreader) } :
AlgoID.exchange_algo_keys,
statuses = new[] { OrderStatus.Working }
};
switch (column) {
case Columns.WorkingBids: filters.sides = new[] { Side.Buy }; break;
case Columns.WorkingAsks: filters.sides = new[] { Side.Sell }; break;
}
var vm = _mk_orderbook_vm_common(filters, new[] {
OrderBookGridView.ColumnKeys.Security,
OrderBookGridView.ColumnKeys.Side
});
return (vm, $"{string.Join(_security_delimiter, securities)}", title_desc, fg);
}
public FilterStates get_filter_states() {
return new FilterStates {
@@ -259,7 +475,7 @@ namespace mbts.spark {
settlement = settlement_filter.get_state(),
session_low = session_low_filter.get_state(),
session_high = session_high_filter.get_state(),
position = position_filter.get_state(),
day_position = day_position_filter.get_state(),
raw_day_position = raw_day_position_filter.get_state(),
working_bid_volume = working_bid_volume_filter.get_state(),
working_ask_volume = working_ask_volume_filter.get_state(),
@@ -273,14 +489,109 @@ namespace mbts.spark {
mic_code = mic_code_filter.get_state(),
};
}
#endregion
protected override Row row_factory(SecurityMD smd) => new Row(_bus, smd, _orders_model);
protected override void on_row_removed(Row row) => row.Dispose();
protected override IEnumerable<SortingCriteria> get_default_sorting() { yield return new SortingCriteria(nameof(Row.security), true); }
public void load_filters(FilterStates filters) {
if (filters != null) {
using (rows_view.DeferRefresh()) {
security_filter.load_state(filters.security);
bid_price_filter.load_state(filters.bid_price);
bid_merged_size_filter.load_state(filters.bid_merged_size);
bid_direct_size_filter.load_state(filters.bid_direct_size);
bid_implied_size_filter.load_state(filters.bid_implied_size);
ask_price_filter.load_state(filters.ask_price);
ask_merged_size_filter.load_state(filters.ask_merged_size);
ask_direct_size_filter.load_state(filters.ask_direct_size);
ask_implied_size_filter.load_state(filters.ask_implied_size);
last_price_filter.load_state(filters.last_price);
last_price_from_spread_filter.load_state(filters.last_price_from_spread);
last_size_filter.load_state(filters.last_size);
day_change_filter.load_state(filters.day_change);
settlement_filter.load_state(filters.settlement);
session_low_filter.load_state(filters.session_low);
session_high_filter.load_state(filters.session_high);
day_position_filter.load_state(filters.day_position);
raw_day_position_filter.load_state(filters.raw_day_position);
working_bid_volume_filter.load_state(filters.working_bid_volume);
working_ask_volume_filter.load_state(filters.working_ask_volume);
volume_filter.load_state(filters.volume);
security_key_filter.load_state(filters.security_key);
}
}
}
static (string desc, Color fg) _fills_title(Columns key) {
switch (key) {
case Columns.Position: return ("Day Fills", Colors.Black);
case Columns.RawDayPosition: return ("Raw Day Fills", Colors.Black);
default: return ("Fills", Colors.Black);
}
}
static (string desc, Color fg) _orderbook_title(Columns key) {
switch (key) {
case Columns.WorkingBids: return ("Working Buy Orders", BTSColors.MarketData.bid);
case Columns.WorkingAsks: return ("Working Sell Orders", BTSColors.MarketData.ask);
default: return ("Working Orders", Colors.Black);
}
}
static GridSettings _hide_cols(GridSettings grid, IEnumerable<OrderBookGridView.ColumnKeys> columns) =>
grid.with(_hide_cols(grid.columns, columns));
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<OrderBookGridView.ColumnKeys> columns) {
// hide columns who's data is implied by the grouping
var col_hash = new HashSet<string>(columns.Select(k => k.ToString()));
foreach (var c in cols)
yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
}
static string _order_ticket_tooltip(Columns key) {
var bid_or_ask = Regex.Match(key.ToString(), "(Bid|Ask)");
return bid_or_ask.Success ? $"Order Ticket @ {bid_or_ask.Value}" : "Order Ticket";
}
OrderBookVM _mk_orderbook_vm_common(bts.shell.orderbook.FilterStates filters, IEnumerable<OrderBookGridView.ColumnKeys> hide_cols, IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source = null) {
var state = new bts.shell.orderbook.OrderBookStartupData(filters, bts.shell.orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_cols);
var settings_man = new WindowSettingsManager<bts.shell.orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
var obvm = orders_source == null ?
new OrderBookVM(_bus, null, _orders_model, settings_man, true) :
new OrderBookVM(_bus, null, _orders_model, orders_source, settings_man, true);
foreach (var c in OrderBookVM.OrderBookColumnCollection.editable_columns)
obvm.columns.Insert(0, c);
obvm.load_from(state);
return obvm;
}
void _refresh() {
foreach (var r in _row_vms) r.refresh();
foreach (var r in _rows) {
r.refresh();
r.is_visible = _filter == null || _filter(r);
}
}
Row _row_factory(SecurityMD smd) => new Row(_bus, smd, _orders_model);
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
selected_rows.RemoveSorted(e.RemovedItems.OfType<Row>(), _row_key_comparer);
selected_rows.InsertSorted(e.AddedItems.OfType<Row>(), _row_key_comparer);
}
void _on_securities(TableUpdate<SecurityMD, uint> table) {
var sw = new Stopwatch();
sw.Start();
foreach (var smd in table.removed) {
int i = CollectionHelper.binary_search(_wrapped_rows, smd, _sec_md_key_comparer);
Log.assert(i >= 0);
_wrapped_rows.RemoveAt(i);
}
foreach (var smd in table.added) {
int i = CollectionHelper.binary_search(_wrapped_rows, smd, _sec_md_key_comparer);
Log.assert(i < 0);
_wrapped_rows.Insert(~i, smd);
}
sw.Stop();
Log.Ui.info($"MarketView update took {sw.ElapsedMilliseconds:0.00}ms.");
}
}
}
@@ -3,8 +3,12 @@
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:bv="clr-namespace:bts.view"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
u:WindowStyle.UnsafeWindow="{Binding Path='(u:WindowStyle.UnsafeWindow)', RelativeSource={RelativeSource AncestorType=bv:SparkWindowView}}"
FocusManager.IsFocusScope="True"
Background="{StaticResource CommonBackground}"
>
@@ -13,8 +17,56 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/FilterTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style TargetType="c:HyperlinkCell">
<Setter Property="Background" Value="{Binding Path='(c:DIYGrid.Column).background_brush', RelativeSource={RelativeSource Self}}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Command" Value="{x:Static usp:MarketViewView.CellClickCommand}"/>
<Setter Property="CommandParameter">
<Setter.Value>
<MultiBinding Converter="{x:Static usp:MarketViewView.CellConverter}">
<Binding Path=""/>
<Binding Path="(c:DIYGrid.ColumnKey)" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=usp:MarketViewView}}"/>
<Style.Triggers>
<Trigger Property="IsMouseOverClickArea" Value="True">
<Setter Property="Background">
<Setter.Value>
<MultiBinding>
<MultiBinding.Converter>
<cv:MultiValueConverterPipe>
<cv:ColorBlendMultiConverter/>
<cv:ColorToBrushConverter/>
</cv:MultiValueConverterPipe>
</MultiBinding.Converter>
<Binding Path="(c:DIYGrid.Grid).SelectionHighlight" RelativeSource="{RelativeSource Self}"/>
<Binding Path="(c:DIYGrid.Column).background_color" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<Binding Path="SelectionHighlight" RelativeSource="{RelativeSource AncestorType=c:DIYGrid}">
<Binding.Converter>
<cv:ValueConverterPipe>
<cv:ColorWithConverter A="255"/>
<cv:ColorToBrushConverter/>
</cv:ValueConverterPipe>
</Binding.Converter>
</Binding>
</Setter.Value>
</Setter>
<Setter Property="BorderThickness" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static usp:MarketViewView.CellClickCommand}" Executed="{x:Static usp:MarketViewView.CellClickExecuted}"/>
</UserControl.CommandBindings>
<AdornerDecorator>
<Grid>
<Grid.RowDefinitions>
@@ -22,13 +74,14 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<c:DIYGrid
x:Name="_grid"
Grid.Row="0"
Columns="{Binding columns}"
CurrentRow="{Binding current_row}"
Rows="{Binding rows_view}"
SelectionHighlight="{Binding grid_settings_manager.selection_highlight, Mode=OneWay}"
>
x:Name="_grid"
Grid.Row="0"
Columns="{Binding columns}"
CurrentRow="{Binding current_row}"
Rows="{Binding rows_view}"
SelectionHighlight="{Binding selection_highlight, Mode=OneWay}"
SyncColumnSortDirectionWithSortDescriptions="False"
>
<c:DIYGrid.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
<KeyBinding Key="Return" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
@@ -1,14 +1,38 @@
using mbts.spark;
using bts.api;
using mbts.spark;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace mbts.ui.spark {
/// <summary>
/// Interaction logic for MarketSheetsView.xaml
/// </summary>
partial class MarketViewView : UserControl {
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
MarketViewVM _vm;
DIYGridPopupManager _popup_man_backing;
public MarketViewView() {
InitializeComponent();
this.ConnectDataContext<MarketViewVM>(vm => _vm = vm);
AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(
_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
}
public enum Columns {
None,
Security,
Volume,
Last,
@@ -39,14 +63,116 @@ namespace mbts.ui.spark {
MicCode
}
public MarketViewView() {
InitializeComponent();
AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(
_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
public static ExecutedRoutedEventHandler
CellClickExecuted => CommandImpl.cell_click;
public static IMultiValueConverter CellConverter =>
ValueConverter.create<MarketViewVM.Row, Columns, MarketViewCell>((r, c) => new MarketViewCell(r, c));
IBus _bus => _vm.bus;
OrdersModel _model => _vm.orders_model;
DIYGridPopupManager _popup_man =>
_popup_man_backing ?? (_popup_man_backing = DIYGridPopupManager.create(this));
public void show_working_orders(IReadOnlyList<SecurityImmutable> securities) =>
_show_mini_orderbook(securities, Columns.None);
public void show_mini_fills(IReadOnlyList<SecurityImmutable> securities) =>
_show_mini_fills(securities, Columns.None);
public void show_order_ticket(SecurityMD security_md) =>
_show_order_ticket(security_md);
void _on_cell_click(MarketViewVM.Row row, Columns col) {
switch (col) {
case Columns.WorkingAsks:
case Columns.WorkingBids:
_show_mini_orderbook(row.security, col);
break;
case Columns.Bid:
_show_order_ticket(row.security_md, row.bid_price);
break;
case Columns.BidMergedSize: _show_order_ticket(row.security_md, row.bid_price, row.bid_merged_size); break;
case Columns.BidDirectSize: _show_order_ticket(row.security_md, row.bid_price, row.bid_direct_size); break;
case Columns.BidImpliedSize: _show_order_ticket(row.security_md, row.bid_price, row.bid_implied_size); break;
case Columns.Ask:
_show_order_ticket(row.security_md, row.ask_price);
break;
case Columns.AskMergedSize: _show_order_ticket(row.security_md, row.ask_price, row.ask_merged_size); break;
case Columns.AskDirectSize: _show_order_ticket(row.security_md, row.ask_price, row.ask_direct_size); break;
case Columns.AskImpliedSize: _show_order_ticket(row.security_md, row.ask_price, row.ask_implied_size); break;
case Columns.Last:
_show_order_ticket(row.security_md, row.last_price);
break;
case Columns.LastSize:
_show_order_ticket(row.security_md, row.last_price, row.last_size);
break;
case Columns.Position:
case Columns.RawDayPosition:
_show_mini_fills(row.security, col);
break;
}
}
void _show_mini_orderbook(SecurityImmutable security, Columns col) =>
_show_mini_orderbook(new[] { security }, col);
void _show_mini_orderbook(IReadOnlyList<SecurityImmutable> securities, Columns col) {
var (vm, context, title, fg) = _vm.orderbook_params(securities, col);
_show_popup(vm, new AuxiliaryDataHeader {
context = context,
title = title,
title_foreground = fg
});
}
void _show_mini_fills(SecurityImmutable security, Columns col) =>
_show_mini_fills(new[] { security }, col);
void _show_mini_fills(IEnumerable<SecurityImmutable> securities, Columns col) {
var (vm, context, title, fg) = _vm.fills_params(securities, col);
_show_popup(vm, new AuxiliaryDataHeader {
context = context,
title = title,
title_foreground = fg
});
}
void _show_order_ticket(SecurityMD security_md, double price = double.NaN, uint size = 0) {
var resources = new OrderTicketResources();
resources.MergedDictionaries.Add(new AuxiliaryDataResources());
var ticket_vm = new NewOrderTicketVM(
_model, security_md, _model.factory.order(security_md.security), price, size, Observable.Return(mbts.spark.LadderSettings.Default.calculator_buttons));
_show_popup(ticket_vm, new OrderTicketHeader { bbo = ticket_vm.bbo }, resources);
}
void _show_popup(object content, object header) {
var resources = new AuxiliaryDataResources();
resources.MergedDictionaries.Add(new ScrollBarResources());
_show_popup(content, header, resources);
}
void _show_popup(object content, object header, ResourceDictionary resources) {
var view = new HeaderedContentControl {
DataContext = content,
Content = content,
Header = header,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Resources = resources ?? new AuxiliaryDataResources()
};
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
_popup_man.show(view);
}
static class CommandImpl {
public static void cell_click(object sender, ExecutedRoutedEventArgs e) {
if (sender is MarketViewView mview && e.Parameter is MarketViewCell cell)
mview._on_cell_click(cell.row, cell.column);
e.Handled = true;
}
}
}
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row, MarketViewView.Columns> { }
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row> { }
}
@@ -1,18 +1,21 @@
using bts;
using bts.api;
using bts.api.core;
using bts.collections;
using bts.shell;
using bts.shell.fills;
using bts.spark;
using bts.utils;
using mbts.persist;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@@ -21,9 +24,18 @@ using System.Windows;
namespace mbts.spark {
class MarketViewWindow : SparkWindow<MarketViewWindowFactory.StartupData>, ISharedWindow, ISlowMeasureContentSize {
const string _window_type = "MarketView", _window_type_display_name = "Market View";
// make font-color immutable for Side, Size, (the font-color rule is hard-coded: MarketData.bid for Buy orders, MarketData.ask for Sells)
static readonly IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> _col_settings_flags = new Dictionary<string, GridColumnSettingsFeatureFlags> {
{ MarketViewView.Columns.Bid.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor },
{ MarketViewView.Columns.Ask.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableBackgroundColor},
{ MarketViewView.Columns.WorkingAsks.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor },
{ MarketViewView.Columns.WorkingBids.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }
};
readonly OrdersModel _orders_model;
readonly AllowOnce _init_guard = new AllowOnce("MarketView");
readonly IWindowSettingsManager<GridSettings> _settings_manager;
List<SortingCriteria> _current_sorting_criterion;
MarketViewVM _vm;
MarketViewView _view;
MarketViewWindowFactory.StartupData _state;
@@ -32,6 +44,7 @@ namespace mbts.spark {
SecurityCriteria
_selected_security_criteria_backing,
_sorted_selected_security_criteria_backing;
SimpleGridSettingsManager<MarketViewVM.Row> _grid_settings_manager;
public MarketViewWindow(MarketViewWindowFactory factory, IBus bus, OrdersModel orders_model, MarketViewWindowFactory.StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
: base("Market View", bus, factory, SparkCommands.ShowMarketView.icon) {
@@ -42,13 +55,17 @@ namespace mbts.spark {
thread_id = config.Configuration.get<bts.config.MultiThreadedWindows>().market_view ? "MarketView" : null;
}
event Action<MarketViewWindowFactory.StartupData> _state_changed;
event Action<SecurityCriteria> _security_criteria_changed;
public override string window_type => _window_type;
public override UIElement Content => _view;
public override IEnumerable<SparkCommand> Commands => _commands;
public override MarketViewWindowFactory.StartupData SaveState =>
_state = new MarketViewWindowFactory.StartupData(_state.security_criteria, _vm.get_filter_states(), _vm.get_grid_save_state(), _vm.rows_view.get_sorting_criteria(_vm.columns));
_state = new MarketViewWindowFactory.StartupData(
_state.security_criteria,
_vm.get_filter_states(),
_settings_manager.instance_settings,
_current_sorting_criterion);
public override double MinWidth => MarketViewWindowFactory.StartupData.DefaultGridSettings.columns.Where(c => c.is_visible).Sum(gcs => gcs.width)
+ SPARK_MENU_COLLAPSED_WIDTH
@@ -68,43 +85,47 @@ namespace mbts.spark {
return _selected_security_criteria_backing;
}
}
SecurityCriteria _sorted_selected_security_criteria {
get {
if (_sorted_selected_security_criteria_backing == null) {
if (_vm != null) {
var sorting_criteria = _vm.rows_view.get_sorting_criteria(_vm.columns);
var comp_builder = new ComparerBuilder<MarketViewVM.Row>();
foreach (var sc in sorting_criteria) {
if (_vm.columns.TryGetFirst(col => string.Equals(col.id, sc.col_name, StringComparison.CurrentCultureIgnoreCase), out var match)) {
if (match is ui.controls.DIYGrid.IKeyedColumn<MarketViewView.Columns> kcol) {
var col_comp = MarketViewVM.get_column_comparer(kcol.key);
if (col_comp != null) {
comp_builder.add(sc.is_ascending ? col_comp : col_comp.reverse());
} else {
Log.Ui.warn($"{_window_type} is unable to find the row comparer for the '{sc.col_name}' column.");
}
} else {
Log.Ui.warn($"{_window_type} is unable find a column with id '{sc.col_name}.'");
}
}
}
var comp = comp_builder.to_comparer();
_sorted_selected_security_criteria_backing = new SecurityCriteria(_vm.selected_rows.Ordered(comp).Select(r => r.security));
}
}
return _sorted_selected_security_criteria_backing;
}
}
SecurityCriteria _sorted_selected_security_criteria => _sorted_selected_security_criteria_backing ?? (
_sorted_selected_security_criteria_backing = _vm != null ?
new SecurityCriteria(_vm.selected_rows.Ordered(_vm.comparer).Select(r => r.security)) :
null);
bool _has_selected_rows() => _vm != null && _vm.selected_rows.Count > 0;
public void thread_init() {
_init_guard.check_access();
Log.notnull(_state);
var state_src = Observable.FromEvent<Action<MarketViewWindowFactory.StartupData>, MarketViewWindowFactory.StartupData>(cb => _state_changed += cb, cb => _state_changed -= cb).StartWith(_state);
_vm = new MarketViewVM(_bus, _orders_model, state_src, _settings_manager);
var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
var security_md_map_src = Observable.Create<IReadOnlyDictionary<uint, SecurityMD>>(o => {
IDisposable sub = null;
_security_criteria_changed += on_security_criteria_changed;
on_security_criteria_changed(_state.security_criteria);
return Disposable.Create(dispose);
void on_security_criteria_changed(SecurityCriteria security_criteria) {
sub?.Dispose();
sub = _state.security_criteria.subscribe_md_map(_bus).Subscribe(o.OnNext);
}
void dispose() =>
Disposable.dispose(ref sub);
});
_vm = new MarketViewVM(
_bus,
_orders_model,
security_md_map_src
.BufferLatest(TimeSpan.FromSeconds(1))
.Table(SecurityMD.key_fn)
.Where(t => t.has_changes())
.OnDispatcher());
var sort_man = new ColumnSortManager(_vm.columns, MarketViewView.Columns.Security.ToString());
_current_sorting_criterion = _state.sorting_criterion.ToList();
sort_man.load(_state.sorting_criterion);
_vm.load_filters(_state.filters);
_vm.comparer = sort_man.build_comparer<MarketViewView.Columns, MarketViewVM.Row>(MarketViewVM.get_column_comparer);
_view = new MarketViewView { DataContext = _vm };
_view._grid.Sorting += on_grid_sorting;
_grid_settings_manager = new SimpleGridSettingsManager<MarketViewVM.Row>(_vm.columns, _col_settings_flags, _vm.rows);
_grid_settings_manager.load_from(_settings_manager.current_settings());
/// The 'CanExecute' plumbing for the cmds below is designed to avoid potentially costly .ToList() operations on the selected_rows/securities.
/// Some of the cmds post ITags with data that is, or has, a SecurityCriteria member; the SecurityCriteria constructor creates a new List of the specified securities.
/// For these commands, the objective is to evaluate 'CanExecute' without instantiating the SecurityCriteria (or more importantly, without creating a new List).
@@ -126,6 +147,13 @@ namespace mbts.spark {
});
var show_fills_actual_cmd = SparkCommands.ShowFills();
var show_fills_cmd_wrapper = new SparkCommandCommand(show_fills_actual_cmd.title, show_fills_actual_cmd.icon, show_fills_cmd, SparkCommands.KeyGestures.ShowFills);
var show_mini_fills_cmd = new DelegateCommand(() => _view.show_mini_fills(_vm.selected_securities), () => _vm.selected_securities.Count > 0);
var show_ticket_cmd = new DelegateCommand(() => {
var smd = _vm.current_security_md;
if (smd.HasValue)
_view.show_order_ticket(smd.Value);
}, () => _vm.current_security_md.HasValue);
var show_ticket_actual_cmd = SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket);
// show synthetic create
var show_synth_create_cmd = new DelegateCommand(() => {
var sc = _sorted_selected_security_criteria;
@@ -134,15 +162,29 @@ namespace mbts.spark {
}, () => _vm.selected_rows.Count > 0 && _vm.selected_rows.Count <= 24);
var show_synth_create_actual_cmd = SparkCommands.ShowSyntheticCreate();
var show_synth_create_cmd_wrapper = new SparkCommandCommand(show_synth_create_actual_cmd.title, show_synth_create_actual_cmd.icon, show_synth_create_cmd, SparkCommands.KeyGestures.ShowSyntheticCreate);
var show_working_orders_cmd = new DelegateCommand(() => _view.show_working_orders(_vm.selected_securities), () => _vm.selected_securities.Count > 0);
var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
// commands
var current_sec_md_src = Observable.Create<SecurityMD?>(o => {
notify(null, null);
_vm.current_row_changed += notify;
return Disposable.Create(() => _vm.current_row_changed -= notify);
void notify(MarketViewVM.Row oldval, MarketViewVM.Row newval) {
if (newval != null)
o.OnNext(newval.security_md);
else o.OnNext(null);
}
});
_commands = new List<SparkCommand> {
SparkCommands.ShowLadders(SparkCommands.KeyGestures.ShowLadder).complete(_vm.selected_security_mds_observable(), smds => smds.Count > 0),
SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket).complete(_vm.current_security_md, md => md.HasValue, ConvertUtils.ValueOrDefault),
SparkCommands.ShowLadders(SparkCommands.KeyGestures.ShowLadder).complete(_vm.selected_rows.Observe().Select(_ => _vm.selected_rows.WrapSelectorRO(r => r.security_md)), smds => smds.Count > 0),
SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket).complete(current_sec_md_src, md => md.HasValue, ConvertUtils.ValueOrDefault),
show_fills_cmd_wrapper,
show_tas_cmd_wrapper,
show_synth_create_cmd_wrapper,
// Note: the commands commented out below open the mini popup versions of their respective windows
// TODO: determine if/how to integrate the mini popups into the toolbar.
//new SparkCommandCommand("Working Orders", Icons.Orders, show_working_orders_cmd),
//new SparkCommandCommand("Fills", show_fills_actual_cmd.icon, show_mini_fills_cmd),
//new SparkCommandCommand(show_ticket_actual_cmd.title, show_ticket_actual_cmd.icon, show_ticket_cmd),
SparkCommands.change_securities_command(() => (update_securities_cmd, _state.security_criteria)),
// 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(() => {
@@ -150,7 +192,7 @@ namespace mbts.spark {
_settings_manager,
us => us.market_view_grid,
UserSettings.FileExtensions.GRID,
s => _vm.grid_settings_manager.create_vm(
s => _grid_settings_manager.create_vm(
// we need to get the most up to date GridSettings when launching b/c users may
// have changed some settings on the grid directly, like column widths.
s.with(_vm.columns.Select(c => c.create()))
@@ -163,26 +205,40 @@ namespace mbts.spark {
}))
};
//
_vm.selection_changed += on_selection_changed;
_subscriptions = new CompositeDisposable(_bus.subscribe(update_securities_cmd.tag).OnDispatcher().Subscribe(c =>
_state_changed?.Invoke(_state = new MarketViewWindowFactory.StartupData(c, _vm.get_filter_states(), _vm.grid_settings_manager.create())))) {
_vm.selected_rows.CollectionChanged += on_selection_changed;
_subscriptions = new CompositeDisposable(
_bus.subscribe(update_securities_cmd.tag).OnDispatcher().Subscribe(c => {
_state = new MarketViewWindowFactory.StartupData(c, _state.filters, _state.grid, _state.sorting_criterion.ToList());
_security_criteria_changed?.Invoke(c);
}),
_settings_manager.subscribe_current().OnDispatcher().Subscribe(new CompositeObserver<GridSettings>(_grid_settings_manager) {
s => _vm.selection_highlight = s.selection_highlight
})) {
() => {
_vm.selection_changed -= on_selection_changed;
_vm.selected_rows.CollectionChanged -= on_selection_changed;
_view._grid.Sorting -= on_grid_sorting;
}
};
void on_selection_changed((IEnumerable<MarketViewVM.Row> removed, IEnumerable<MarketViewVM.Row> added) change) {
void on_selection_changed(object sender, NotifyCollectionChangedEventArgs e) {
// invalidate selected security criteria
_selected_security_criteria_backing = null;
_sorted_selected_security_criteria_backing = null;
show_tas_cmd.RaiseCanExecuteChanged();
show_fills_cmd.RaiseCanExecuteChanged();
show_synth_create_cmd.RaiseCanExecuteChanged();
show_working_orders_cmd.RaiseCanExecuteChanged();
show_mini_fills_cmd.RaiseCanExecuteChanged();
show_ticket_cmd.RaiseCanExecuteChanged();
}
void on_grid_sorting(object sender, ui.controls.DIYGridSortingEventArgs e) =>
void on_grid_sorting(object sender, ui.controls.DIYGridSortingEventArgs e) {
_sorted_selected_security_criteria_backing = null;
sort_man.on_column_click(e.Column, KeyboardExtensions.IsShiftDown());
_vm.comparer = sort_man.build_comparer<MarketViewView.Columns, MarketViewVM.Row>(MarketViewVM.get_column_comparer);
_current_sorting_criterion = sort_man.create().ToList();
e.Handled = true;
}
}
public override void Dispose() {
@@ -1,6 +1,7 @@
using bts.api;
using bts.proto;
using bts.settings;
using bts.utils;
using mbts.persist;
using mbts.spark;
using mbts.spark.filters;
@@ -88,7 +89,22 @@ namespace bts.shell {
}
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion, enable_background_color: true);
public static GridSettings DefaultGridSettings =>
GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion, enable_background_color: true);
public static fills.FillsSettings DefaultAuxiliaryFills {
get {
var grid = fills.FillsSettings.MiniGridSettings;
//if (grid.columns.TryGetFirst(c => c.id == nameof(mbts.ui.spark.FillsView.Columns.Side), out var side_col, out int side_col_pos)) {
// var cols = grid.columns.ToList();
// cols[side_col_pos] = side_col.with(is_visible: false);
// grid = grid.with(cols);
//}
var rv = fills.FillsSettings.Default();
rv.grid = grid;
return rv;
}
}
public IReadOnlyList<SortingCriteria> sorting_criterion => _sorting_criteria ?? (_sorting_criteria = DefaultColumns.default_sorting_criterion.ToList());
@@ -196,7 +212,7 @@ namespace bts.shell {
[ProtoMember(11)]
public DoubleMinMaxState session_high;
[ProtoMember(12)]
public IntMinMaxState position;
public IntMinMaxState day_position;
[ProtoMember(27)]
public DoubleMinMaxState raw_day_position;
[ProtoMember(13)]
@@ -229,6 +245,47 @@ namespace bts.shell {
public string last_price_from_spread;
[ProtoMember(28)]
public string mic_code;
public Func<MarketViewVM.Row, bool> build_predicate() {
var pb = new ColumnPredicateBuilder<MarketViewVM.Row>();
// security
pb.add_regex(security, r => r.security.ToString());
pb.add_regex(security_key, r => r.security_key.ToString());
// bid
pb.add_numeric(bid_price, r => r.bid_price);
pb.add_numeric(bid_merged_size, r => r.bid_merged_size);
pb.add_numeric(bid_direct_size, r => r.bid_direct_size);
pb.add_numeric(bid_implied_size, r => r.bid_implied_size);
pb.add_numeric(working_bid_volume, r => r.working_bid_volume);
// ask
pb.add_numeric(ask_price, r => r.ask_price);
pb.add_numeric(ask_implied_size, r => r.ask_implied_size);
pb.add_numeric(ask_direct_size, r => r.ask_direct_size);
pb.add_numeric(ask_merged_size, r => r.ask_merged_size);
pb.add_numeric(working_ask_volume, r => r.working_ask_volume);
// last
pb.add_numeric(last_price, r => r.last_price);
pb.add_regex(last_price_from_spread, r => r.last_price_from_spread.ToString());
pb.add_numeric(last_size, r => r.last_size);
// position
// TODO: convert day_position to DoubleMinMaxState
pb.add_numeric(day_position, r => unchecked((int)r.day_position));
pb.add_numeric(raw_day_position, r => r.raw_day_position);
// market
pb.add_numeric(day_change, r => r.day_change);
pb.add_numeric(settlement, r => r.settlement);
pb.add_numeric(session_low, r => r.session_low);
pb.add_numeric(session_high, r => r.session_high);
pb.add_numeric(volume, r => r.volume);
// ice
pb.add_regex(market_description, r => r.market_description);
pb.add_regex(cleared_alias, r => r.cleared_alias);
pb.add_regex(product_description, r => r.product_description);
pb.add_regex(hub_alias, r => r.hub_alias);
pb.add_regex(strip_name, r => r.strip_name);
pb.add_regex(mic_code, r => r.mic_code);
return pb.to_predicate();
}
}
// migration rules
@@ -271,5 +328,11 @@ namespace bts.shell {
return false;
}
}
[Export(typeof(IMarketViewGridSettingsMigrationRule))]
class EnsureSelectionHighlightTranslucency : IMarketViewGridSettingsMigrationRule {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
}
}
}
@@ -13,5 +13,5 @@ namespace mbts.ui.spark {
InitializeComponent();
}
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesVM.Row, TimeAndSalesView.Columns> { }
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesVM.Row> { }
}
@@ -20,5 +20,5 @@ namespace mbts.ui.spark {
InitializeComponent();
}
class SyntheticManagementRowView : DIYGrid.HighPerformanceRow<SyntheticManagementDialogVM.Row, SyntheticManagementView.Columns> { }
class SyntheticManagementRowView : DIYGrid.HighPerformanceRow<SyntheticManagementDialogVM.Row> { }
}
@@ -357,19 +357,13 @@ namespace mbts.spark {
if (leg_tuples.Count == 2) {
_is_setting_price_mults = true;
_price_mult_model.override_value(leg_tuples[1], -1);
} else if (leg_tuples.Count == 3) {
_is_setting_price_mults = true;
_price_mult_model.override_value(leg_tuples[1], -2);
}
}
}
void _deduce_quantities() {
if (leg_tuples.Count == 2) {
_is_setting_quantities = true;
_quantity_model.override_value(leg_tuples[1], -1);
} else if (leg_tuples.Count == 3) {
_is_setting_quantities = true;
_quantity_model.override_value(leg_tuples[1], -2);
}
}
@@ -47,13 +47,13 @@ namespace mbts.spark {
add_col(Columns.Name, DefaultColumnHeaders.Name, r => r.name, nameof(Row.name), name_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Security);
add_col(Columns.Description, DefaultColumnHeaders.Description, r => r.description, nameof(Row.description), description_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Description);
add_col(Columns.n_legs, DefaultColumnHeaders.N_Legs, r => r.n_legs.ToString(), nameof(Row.n_legs), n_legs_filter, DataGridFilterKnownTemplates.IntMinMax, DefaultColumnWidths.N_Legs);
columns.Add(new HyperlinkElementColumn<Row, Columns>(
columns.Add(new HyperlinkElementColumn<Columns>(
Columns.n_working_orders,
new Binding(nameof(Row.n_working_orders)),
new Binding(nameof(Row.show_order_book)),
n_working_orders_filter,
DataGridFilterKnownTemplates.UIntMinMax) {
id = col_id(Columns.n_working_orders),
CommandBinding = new Binding(nameof(Row.show_order_book)),
TextBinding = new Binding(nameof(Row.n_working_orders)),
Header = DefaultColumnHeaders.N_Working_Orders,
Width = DefaultColumnWidths.N_Working_Orders
});
@@ -214,10 +214,13 @@ namespace mbts.spark {
((IKeyedColumn<Columns>)column).key;
static IObservable<TableUpdate<FillTuple, string>> _get_observable(IBus bus, OrdersModel model, FillsSettings state) {
if (!state.orders.is_null_or_empty()) {
var orders_hash = new HashSet<uint>(state.orders);
return model.on_fill_tuples_with_root.Filter(f => orders_hash.Contains(f.fill.OrderKey));
} else return model.on_fill_tuples_with_root;
var orders_hash = state.orders.is_null_or_empty() ? null : new HashSet<uint>(state.orders);
var root_securities_hash = state.root_securities.is_null_or_empty() ? null : new HashSet<uint>(state.root_securities);
return orders_hash == null && root_securities_hash == null ?
model.on_fill_tuples_with_root :
model.on_fill_tuples_with_root.Filter(f =>
(orders_hash == null || orders_hash.Contains(f.fill.OrderKey)) &&
(root_securities_hash == null || root_securities_hash.Contains(f.root_order_security?.security?.Id ?? 0)));
}
static IObservable<TableUpdate<FillTuple, string>> _get_observable(IBus bus, OrdersModel model, IObservable<FillsSettings> state) =>
@@ -31,5 +31,5 @@ namespace mbts.ui.spark {
InitializeComponent();
}
class FillsRowView : DIYGrid.HighPerformanceRow<FillsVM.Row, FillsView.Columns> { }
class FillsRowView : DIYGrid.HighPerformanceRow<FillsVM.Row> { }
}
@@ -72,5 +72,11 @@ namespace bts.shell {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
settings.add_missing_columns(FillsSettings.DefaultGridSettings.columns, out new_settings);
}
[Export(typeof(IFillsGridSettingsMigrationRule))]
class EnsureSelectionHighlightTranslucency : IFillsGridSettingsMigrationRule {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
GridSettingsMigrationRules.SelectionHighlightIsTranslucent.apply(settings, out new_settings);
}
}
}
@@ -38,6 +38,8 @@ namespace bts.shell.fills {
public static FillsSettings Default() =>
new FillsSettings(null, null, new FillsFilterStates(), null);
public uint[] root_securities { get; set; }
static class DefaultColumns {
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
@@ -249,6 +249,7 @@ namespace mbts.spark {
const double _default_order_entry_panel_width = 110.0;
const double _default_zoom_factor = 1.0;
const bool _default_show_warn_on_cross = true;
const bool _default_enforce_max_clip = false;
public static readonly IEqualityComparer<LadderSettings> profile_equality_comparer =
EqualityComparer.Create<LadderSettings>(profile_equals);
// this class is 100% mutable (there are literally 0 readonly members),
@@ -282,6 +283,7 @@ namespace mbts.spark {
use_defaults = true;
favorite_algos = new LadderFavoriteAlgosSettings(null, 2, null);
show_warn_on_cross = _default_show_warn_on_cross;
enforce_max_clip = _default_enforce_max_clip;
}
public static LadderSettings Default {
@@ -347,6 +349,8 @@ namespace mbts.spark {
zoom_factor = _default_zoom_factor,
net_change = NetChangeTerms.None,
mini_ob = new LadderMiniOrderBookSettings(bts.shell.orderbook.OrderBookStartupData.default_mini_grid.general),
max_clip = 0,
price_display_format = PriceDisplayFormat.Default,
};
rv.row_height = LadderRender.measure_row_height(rv);
return rv;
@@ -365,6 +369,13 @@ namespace mbts.spark {
Ticks
}
public enum PriceDisplayFormat {
[Description("Default")]
Default,
[Description("Integer")]
Integer
}
[ProtoMember(1)]
public double row_height { get; set; }
[ProtoMember(2)]
@@ -499,6 +510,15 @@ namespace mbts.spark {
[ProtoMember(57)]
public double left_panel_scroll_position { get; set; }
[ProtoMember(58), DefaultValue(_default_enforce_max_clip)]
public bool enforce_max_clip { get; set; }
[ProtoMember(59)]
public uint max_clip { get; set; }
[ProtoMember(60)]
public PriceDisplayFormat price_display_format { get; set; }
public void sanitize() =>
sanitize(this);
@@ -600,7 +620,10 @@ namespace mbts.spark {
a.favorite_algos == b.favorite_algos &&
a.net_change == b.net_change &&
a.show_warn_on_cross == b.show_warn_on_cross &&
a.mini_ob == b.mini_ob;
a.mini_ob == b.mini_ob &&
a.enforce_max_clip == b.enforce_max_clip &&
a.max_clip == b.max_clip &&
a.price_display_format == b.price_display_format;
public bool Equals(LadderSettings other) =>
profile_equals(this, other) &&
@@ -29,7 +29,7 @@ using TextUtils = mbts.ui.utility.text.TextUtils;
namespace mbts.spark {
class LadderSettingsVM : DisposableViewModelBase, IWindowSettingsVM<LadderSettings>, IValueFactory<LadderSettings>, IObservable<LadderSettings>, ITemplateSelectorKey {
readonly Property<LadderSettings.BadgeStyle> _badge_style;
readonly Property<uint> _alternate_order_size;
readonly Property<uint> _alternate_order_size, _max_clip;
readonly DelegateProperty<double> _row_height;
readonly UserProperty<AccountTuple?> _default_account;
readonly UserProperty<AlgoDefinitionVM> _default_algo;
@@ -41,8 +41,9 @@ namespace mbts.spark {
AlgoID? _desired_algo;
TimeInForce _default_time_in_force;
LadderSettings.NetChangeTerms _price_format;
LadderSettings.PriceDisplayFormat _price_display_format;
double _order_entry_panel_width, _zoom_factor;
bool _auto_clear, _show_warn_on_cross;
bool _auto_clear, _show_warn_on_cross, _enforce_max_clip;
LadderSettings _current;
LadderColumnSettingsVM
_orders = new OrdersColumnSettingsVM(),
@@ -91,6 +92,7 @@ namespace mbts.spark {
_alternate_order_size = backing<uint>(nameof(alternate_order_size));
_default_account = user_backing<AccountTuple?>(nameof(default_account));
_default_algo = user_backing<AlgoDefinitionVM>(nameof(default_algo));
_max_clip = backing<uint>(nameof(max_clip));
// be sure to initialize backings before this measure_row_height call
_row_height = backing(nameof(row_height), () => LadderRender.measure_row_height(get_result()));
_orders.font_size_changed += on_font_size_changed;
@@ -166,7 +168,9 @@ namespace mbts.spark {
public SimpleGridSettingsVM mini_ob { get; } = new SimpleGridSettingsVM(OrderBookGridSettingsManager.hard_column_flags);
// price format
public LadderSettings.NetChangeTerms price_format { get => _price_format; set => Set(ref _price_format, value); }
public LadderSettings.PriceDisplayFormat price_display_format { get => _price_display_format; set => Set(ref _price_display_format, value); }
public IEnumerable<LadderSettings.NetChangeTerms> price_format_options => EnumHelper.get_values<LadderSettings.NetChangeTerms>();
public IEnumerable<LadderSettings.PriceDisplayFormat> price_display_format_options => EnumHelper.get_values<LadderSettings.PriceDisplayFormat>();
// time in force
public TimeInForce default_time_in_force { get => _default_time_in_force; set => Set(ref _default_time_in_force, value); }
public IEnumerable<TimeInForce> time_in_force_options => EnumHelper.get_values_except(TimeInForce.Undefined);
@@ -182,6 +186,8 @@ namespace mbts.spark {
public double zoom_factor { get => _zoom_factor; set => Set(ref _zoom_factor, value); }
// safety checks
public bool show_warn_on_cross { get => _show_warn_on_cross; set => Set(ref _show_warn_on_cross, value); }
public bool enforce_max_clip { get => _enforce_max_clip; set => Set(ref _enforce_max_clip, value); }
public uint max_clip { get => _max_clip.Value; set => _max_clip.Value = value; }
// favorites
public FavoriteAlgosVM favorite_algos_vm { get; }
@@ -354,8 +360,11 @@ namespace mbts.spark {
_cancel_sells_visibility.show = settings.cancel_sells_visible;
_cancel_all_visibility.show = settings.cancel_all_visible;
price_format = settings.net_change;
price_display_format = settings.price_display_format;
// safety checks
show_warn_on_cross = settings.show_warn_on_cross;
enforce_max_clip = settings.enforce_max_clip;
max_clip = settings.max_clip;
}
}
@@ -428,8 +437,11 @@ namespace mbts.spark {
clone.cancel_all_visible = _cancel_all_visibility.show;
clone.zoom_slider_visible = _zoom_slider_visbility.show;
clone.net_change = price_format;
clone.price_display_format = price_display_format;
// safety checks
clone.show_warn_on_cross = show_warn_on_cross;
clone.enforce_max_clip = enforce_max_clip;
clone.max_clip = max_clip;
return clone;
}
@@ -397,6 +397,8 @@ namespace mbts.spark {
public uint volume_column_width { get => _volume_column_width.Value; set => _volume_column_width.set(value); }
public uint alternate_order_size_from_settings => current_settings.alternate_order_size;
public bool show_warn_on_cross_from_settings => current_settings.show_warn_on_cross;
public bool enforce_max_clip_from_settings => current_settings.enforce_max_clip;
public uint max_clip_from_settings => current_settings.max_clip;
internal LadderModel model => _model;
internal IObservable<LadderSettings> settings { get; }
@@ -447,6 +449,9 @@ namespace mbts.spark {
public void key_down(KeyEventArgs e) {
if (order_properties_vm == null)
return;
if (KeyboardExtensions.IsDown(Key.Space)) {
_bus?.post(SparkTags.RequestRecenterLadder, null);
}
// the ctrl modifier requirement helps avoid keyboard-input collisions w/ algo-templates popup
if (!KeyboardExtensions.IsCtrlDown()) return;
var new_algo = order_properties_vm.algo_definitions.FirstOrDefault(o => o.algo_root.key_match(e));
@@ -470,6 +475,10 @@ namespace mbts.spark {
size_calculator.flash_error();
} else {
uint size = size_from_alternate ? current_settings.alternate_order_size : (uint)Math.Abs(size_calculator.value ?? 0);
// enforce max clip
if (enforce_max_clip_from_settings && size > max_clip_from_settings) {
size = max_clip_from_settings;
}
return send_order_async(args.side.Value, size, args.price);
}
}
@@ -12,7 +12,6 @@ using mbts.ui.spark.algo;
using mbts.ui.utility.text;
using mbts.utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@@ -256,9 +255,8 @@ namespace mbts.spark {
DependencyProperty.Register("Orders", typeof(ObservableCollection<LadderOrderVM>), typeof(LadderGridHost), new PropertyMetadata(null, orders_changed));
static void orders_changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
LadderGridHost lgh = d as LadderGridHost;
WPFHelper.manage_collection_change(e, lgh.orders_collection_changed, src => lgh._ordershost.clear_orders(),
src => lgh.orders_collection_changed(lgh, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)src)));
if (d is LadderGridHost lgh)
WPFHelper.manage_collection_change(e, lgh.orders_collection_changed);
}
void orders_collection_changed(object sender, NotifyCollectionChangedEventArgs e) {
@@ -350,6 +348,17 @@ namespace mbts.spark {
}
} else if (_vm != null) {
uint size = e.ChangedButton == MouseButton.Right ? _vm.alternate_order_size_from_settings : _vm.size_calculator.value ?? 0;
// does the size violate the local max clip
if (_vm.enforce_max_clip_from_settings && size > _vm.max_clip_from_settings) {
// note this size is only used for the cross market message, not the actual order command
size = _vm.max_clip_from_settings;
// only update the calculator value when it's a standard left-click order
if (e.ChangedButton == MouseButton.Left) {
_vm.size_calculator.value = _vm.max_clip_from_settings;
}
}
// if warning enabled && (buy above opposite inside market || ask below opposite inside market)
// when 1-sided market, _get_top_price will return NaN, and so the price comparison is false
// meaning show no warning == correct
@@ -588,7 +597,8 @@ namespace mbts.spark {
_settings.row_height,
_price_list,
column.settings,
security.ContractSpec);
security.ContractSpec,
_settings.price_display_format);
}
break;
case ColumnType.Ask: render_market_col(_asks); break;
@@ -911,7 +921,7 @@ namespace mbts.spark {
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);
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);
_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
@@ -40,6 +40,8 @@ namespace mbts.spark {
// 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);
const string tick_separator_period = ".";
const string tick_separator_dash = "-";
// 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.
@@ -52,8 +54,11 @@ 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) =>
glyph_context.get_glyph_run(text, settings.font_size, settings.width, row_height, HorizontalAlignment.Center, 0, VerticalAlignment.Center, row * row_height, true);
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);
public static double[] gen_prices(double mid, int row_count, ContractSpecificationImmutable spec) {
// require odd number
@@ -79,10 +84,10 @@ namespace mbts.spark {
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) {
public static GlyphRun[] format_prices(double[] prices, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
var result = new GlyphRun[prices.Length];
Parallel.For(0, prices.Length, i =>
result[i] = format_text(spec.format(prices[i]), i, row_height, settings, glyph_context));
result[i] = format_text(spec.format(prices[i]), i, row_height, settings, glyph_context, price_display_format));
return result;
}
@@ -104,9 +109,9 @@ namespace mbts.spark {
}
public static double[] render_prices(DrawingVisual price, GlyphContext glyph_context, double mid, int row_count, double row_height,
LadderColumnSettings settings, ContractSpecificationImmutable spec) {
LadderColumnSettings settings, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
var dprices = gen_prices(mid, row_count, spec);
render_prices(price, glyph_context, row_count, row_height, dprices, settings, spec);
render_prices(price, glyph_context, row_count, row_height, dprices, settings, spec, price_display_format);
return dprices;
}
@@ -122,10 +127,10 @@ namespace mbts.spark {
}
}
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, double[] dprices, LadderColumnSettings settings, ContractSpecificationImmutable spec) {
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, double[] dprices, LadderColumnSettings settings, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
var foreground = settings.foreground.rgb_brush();
//var background = new SolidColorBrush(settings.background.rgb_color());
var fprices = format_prices(dprices, row_height, settings, glyph_context, spec);
var fprices = format_prices(dprices, row_height, settings, glyph_context, spec, price_display_format);
using (var dc = price.RenderOpen())
using (_draw_with_clip(dc, settings, row_height, row_count)) {
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, row_count * row_height));
@@ -178,6 +178,14 @@
<Setter Property="VerticalAlignment" Value="Bottom"/>
</Style>
<Style x:Key="EnableDisableFeatureText" TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</Style.Triggers>
</Style>
<u:KeyedTemplateSelector x:Key="LadderColumnTemplateSelector">
<u:KeyedTemplateSelector.Default>
<DataTemplate>
@@ -630,7 +638,11 @@
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<GroupBox Header="Size Display" DataContext="{Binding size_display}" Padding="4">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<GroupBox Header="Size Display" DataContext="{Binding size_display}" Padding="4" Grid.Row="0" Grid.RowSpan="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -710,6 +722,7 @@
</GroupBox>
<GroupBox
Grid.Column="2"
Grid.Row="0"
VerticalAlignment="Top"
Header="Price Display"
Visibility="{Binding is_synthetic, Converter={StaticResource NetChangeGroupBoxIsVisibleVisibilityConverter}}"
@@ -717,6 +730,15 @@
>
<c:RadioButtonsControl ItemsSource="{Binding price_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_format}"/>
</GroupBox>
<GroupBox
Grid.Row="1"
Grid.Column="2"
VerticalAlignment="Stretch"
Header="Price Display Format"
Padding="4"
>
<c:RadioButtonsControl ItemsSource="{Binding price_display_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_display_format}"/>
</GroupBox>
</Grid>
</TabItem>
<!-- Size Presets Tab-->
@@ -991,21 +1013,45 @@
<Grid HorizontalAlignment="Left" Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox Content="Warn on Market Cross" FlowDirection="RightToLeft" IsChecked="{Binding show_warn_on_cross}">
<!-- market cross -->
<CheckBox Name="warn" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding show_warn_on_cross}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight"/>
</Style>
</CheckBox.Resources>
</CheckBox>
<TextBlock Grid.Column="1"
Style="{StaticResource EnableDisableFeatureText}"
IsEnabled="{Binding ElementName=warn, Path=IsChecked}"
Text="Warn on Market Cross" />
<!-- max order qty -->
<CheckBox Name="enforce_max_clip" Grid.Row="1" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding enforce_max_clip}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight"/>
</Style>
</CheckBox.Resources>
</CheckBox>
<TextBlock Grid.Row="1" Grid.Column="1"
Style="{StaticResource EnableDisableFeatureText}"
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
MinWidth="{Binding ElementName=warn, Path=ActualWidth}"
Text="Max Order Quantity"/>
<c:TickUpDownUInt Grid.Row="1" Grid.Column="2"
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
Value="{Binding max_clip}" />
</Grid>
</TabItem>
<TabItem Header="Mini Order Book">
@@ -22,7 +22,7 @@ namespace mbts.spark {
DataContext = vm;
}
class MiniOrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM, OrderBookGridView.ColumnKeys> {
class MiniOrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
public MiniOrderBookRowView() : base() {
FocusManager.SetIsFocusScope(this, true);
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
@@ -650,12 +650,13 @@ namespace mbts.spark {
ListSortDirection? _cached_sort_direction;
public OrderBookLastMessageColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter)
: base(Columns.LastMessage, new Binding(nameof(OrderBookRowVM.last_msg)), filter, DataGridFilterKnownTemplates.Regex) {
: base(Columns.LastMessage, filter, DataGridFilterKnownTemplates.Regex) {
id = key.ToString();
this.filter = filter;
Header = "Last Message";
Width = DefaultColumnWidths.LastMessage;
tooltip_binding = new Binding(nameof(OrderBookRowVM.last_msg));
TextBinding = new Binding(nameof(OrderBookRowVM.last_msg));
ToolTipBinding = new Binding(nameof(OrderBookRowVM.last_msg));
}
public new IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter { get; }
@@ -9,7 +9,6 @@
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:uspcv="clr-namespace:mbts.ui.spark.converters"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
InactiveSelectionOpacity="0.3"
>
<c:DIYGrid.Resources>
<ResourceDictionary>
@@ -99,7 +99,7 @@ namespace mbts.spark {
}
}
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM, OrderBookGridView.ColumnKeys> {
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
public OrderBookRowView() : base() { }
}
}
@@ -49,8 +49,9 @@ namespace bts.shell.pnl {
PNLHeaderRowDisplay _header_row_display = PNLHeaderRowDisplay.Total;
PNLRowCollectionView _rows_view;
IReadOnlyList<(ColumnKeys key, ListSortDirection direction)> _sorting_hierarchy;
Brush _selection_highlight;
Color _grid_background_color = Colors.Transparent;
Color
_grid_background_color = Colors.Transparent,
_selection_highlight;
bool _is_paused;
// default appearances
ImmutableDictionary<ColumnKeys, PNLCellAppearance>
@@ -175,7 +176,7 @@ namespace bts.shell.pnl {
}
}
public Brush selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public Color selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
public Color grid_background_color { get => _grid_background_color; set => Set(ref _grid_background_color, value); }
public IValueConverter group_background_converter => _group_background_converter;
@@ -291,7 +292,7 @@ namespace bts.shell.pnl {
};
public void load_from(PNLSettings settings) {
selection_highlight = settings.grid.selection_highlight.to_brush();
selection_highlight = settings.grid.selection_highlight;
grid_background_color = settings.grid.background;
calculation_type = settings.calculation_type;
if (settings.filters != null) {
@@ -381,7 +382,7 @@ namespace bts.shell.pnl {
_append_key_filters(state.filters, key);
_append_row_filters(state.filters, row);
// hide implicit cols
state.grid = _hide_cols(state.grid, anchors);
state.grid = GridSettingsHelper.hide_cols(state.grid, anchors);
return new FillsVM(
_bus, _orders_model, Observable.Return(state), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
}
@@ -391,7 +392,7 @@ namespace bts.shell.pnl {
_append_key_filters(state.filters, key);
_append_group_filters(state.filters, group, anchors);
// hide implicit cols
state.grid = _hide_cols(state.grid, anchors.WrapSubRange(0, group.level));
state.grid = GridSettingsHelper.hide_cols(state.grid, anchors.WrapSubRange(0, group.level));
return new FillsVM(
_bus, _orders_model, Observable.Return(state), _visible_fills_source(), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
}
@@ -401,7 +402,7 @@ namespace bts.shell.pnl {
state.grid = orderbook.OrderBookStartupData.DefaultFillsGrid.general;
state.filters = new fills.FillsFilterStates();
// hide implicit cols
state.grid = _hide_cols(orderbook.OrderBookStartupData.DefaultFillsGrid.general, anchors);
state.grid = GridSettingsHelper.hide_cols(orderbook.OrderBookStartupData.DefaultFillsGrid.general, anchors);
_append_key_filters(state.filters, key);
var src = _orders_model.on_fill_tuples;
switch (header) {
@@ -532,16 +533,6 @@ namespace bts.shell.pnl {
new PNLColumnComparer<T>(selector);
}
static GridSettings _hide_cols(GridSettings grid, IEnumerable<ColumnKeys> columns) =>
grid.with(_hide_cols(grid.columns, columns));
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<ColumnKeys> columns) {
// hide columns who's data is implied by the grouping
var col_hash = new HashSet<string>(columns.Select(k => k.ToString()));
foreach (var c in cols)
yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
}
#region Fills Popup Utils
static void _append_key_filters(fills.FillsFilterStates f, ColumnKeys key) {
switch (key) {
@@ -680,7 +671,7 @@ namespace bts.shell.pnl {
OrderBookVM _mk_orderbook_vm_common(orderbook.FilterStates filters, IEnumerable<ColumnKeys> hide_counterparts, IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source = null) {
var state = new orderbook.OrderBookStartupData(filters, orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_counterparts);
state.orderbook_grid.general = GridSettingsHelper.hide_cols(state.orderbook_grid.general, hide_counterparts);
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
var obvm = orders_source == null ?
new OrderBookVM(_bus, null, _orders_model, settings_man, true) :
@@ -12,6 +12,7 @@
xmlns:sys="clr-namespace:System;assembly=mscorlib"
FocusManager.IsFocusScope="True"
Background="{StaticResource CommonBackground}"
Tag="{Binding ElementName=_grid}"
>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static pnl:PNLView.CellClickCommand}" Executed="_cell_click_executed"/>
@@ -29,6 +30,10 @@
<Color x:Key="GroupRowForegroundColor">#FF000000</Color>
<!--Converters-->
<cv:GridLengthConverter x:Key="GridLengthPixelConverter" UnitType="Pixel"/>
<cv:ValueConverterPipe x:Key="GroupRowHeightConverter">
<cv:RangeBlockConverter Minimum="{x:Static pnl:PNLView.MinGroupRowHeight}"/>
<cv:GridLengthConverter UnitType="Pixel"/>
</cv:ValueConverterPipe>
<cv:MultiValueConverterPipe x:Key="GroupRowBackgroundConverter">
<cv:PartialConverter/>
<cv:ColorToBrushConverter/>
@@ -258,7 +263,7 @@
</Setter>
</Style>
<Style x:Key="RowTextCell" TargetType="c:TextCell">
<Style x:Key="TextCellBase" TargetType="c:TextCell">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding background}"/>
@@ -269,10 +274,14 @@
<SolidColorBrush Color="{Binding foreground}"/>
</Setter.Value>
</Setter>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="FontWeight" Value="{Binding font_weight}"/>
<Setter Property="FontSize" Value="{Binding font_size}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Text" Value="{Binding text}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<Style x:Key="RowTextCell" TargetType="c:TextCell" BasedOn="{StaticResource TextCellBase}">
<Setter Property="FontSize" Value="{Binding font_size}"/>
<Setter Property="c:DIYGrid.ObservedCachedColumnParams">
<Setter.Value>
<x:Array Type="{x:Type c:CachedColumnParam}">
@@ -284,20 +293,7 @@
</Setter>
</Style>
<Style x:Key="GroupRowTextCell" TargetType="c:TextCell">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding background}"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding foreground}"/>
</Setter.Value>
</Setter>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="FontWeight" Value="{Binding font_weight}"/>
<Setter Property="Text" Value="{Binding text}"/>
<Style x:Key="GroupRowTextCell" TargetType="c:TextCell" BasedOn="{StaticResource TextCellBase}">
<Setter Property="c:DIYGrid.ObservedCachedColumnParams">
<Setter.Value>
<x:Array Type="{x:Type c:CachedColumnParam}">
@@ -309,21 +305,8 @@
</Setter>
</Style>
<Style x:Key="HeaderRowTextCell" TargetType="c:TextCell">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding background}"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding foreground}"/>
</Setter.Value>
</Setter>
<Style x:Key="HeaderRowTextCell" TargetType="c:TextCell" BasedOn="{StaticResource TextCellBase}">
<Setter Property="Margin" Value="0,0,0,1"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="FontWeight" Value="{Binding font_weight}"/>
<Setter Property="Text" Value="{Binding text}"/>
<Setter Property="c:DIYGrid.ObservedCachedColumnParams">
<Setter.Value>
<x:Array Type="{x:Type c:CachedColumnParam}">
@@ -335,7 +318,23 @@
</Setter>
</Style>
<Style x:Key="HyperlinkCell" TargetType="c:HyperlinkCell">
<Style x:Key="HyperlinkCellBase" TargetType="c:HyperlinkCell">
<Setter Property="c:DIYGrid.Grid" Value="{Binding RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"/>
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding background}"/>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{Binding border}"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding foreground}"/>
</Setter.Value>
</Setter>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
@@ -349,41 +348,46 @@
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="c:DIYGrid.ObservedCachedColumnParams">
<Setter.Value>
<x:Array Type="{x:Type c:CachedColumnParam}">
<c:CachedColumnParam SourcePropertyName="font_family" TargetProperty="c:HyperlinkCell.FontFamily"/>
<c:CachedColumnParam SourcePropertyName="font_size" TargetProperty="c:HyperlinkCell.FontSize"/>
<c:CachedColumnParam SourcePropertyName="font_brush" TargetProperty="c:HyperlinkCell.Foreground"/>
<c:CachedColumnParam SourcePropertyName="horizontal_alignment" TargetProperty="c:HyperlinkCell.HorizontalContentAlignment"/>
</x:Array>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="RowHyperlinkCell" TargetType="c:HyperlinkCell">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding background}"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding foreground}"/>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{Binding border}"/>
</Setter.Value>
</Setter>
<Setter Property="Padding" Value="{x:Static c:DIYGrid.AnimatableCellPadding}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="FontWeight" Value="{Binding font_weight}"/>
<Setter Property="FontSize" Value="{Binding font_size}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Text" Value="{Binding text}"/>
<Setter Property="Command" Value="{x:Static pnl:PNLView.CellClickCommand}"/>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=pnl:PNLView}}"/>
<Style.Triggers>
<Trigger Property="IsMouseOverClickArea" Value="True">
<Setter Property="Background">
<Setter.Value>
<MultiBinding>
<MultiBinding.Converter>
<cv:MultiValueConverterPipe>
<cv:ColorBlendMultiConverter/>
<cv:ColorToBrushConverter/>
</cv:MultiValueConverterPipe>
</MultiBinding.Converter>
<Binding Path="(c:DIYGrid.Grid).SelectionHighlight" RelativeSource="{RelativeSource Self}"/>
<Binding Path="background"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<Binding Path="(c:DIYGrid.Grid).SelectionHighlight" RelativeSource="{RelativeSource Self}">
<Binding.Converter>
<cv:ValueConverterPipe>
<cv:ColorWithConverter A="255"/>
<cv:ColorToBrushConverter/>
</cv:ValueConverterPipe>
</Binding.Converter>
</Binding>
</Setter.Value>
</Setter>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="RowHyperlinkCell" TargetType="c:HyperlinkCell" BasedOn="{StaticResource HyperlinkCellBase}">
<Setter Property="Padding" Value="{x:Static c:DIYGrid.AnimatableCellPadding}"/>
<Setter Property="CommandParameter">
<Setter.Value>
<MultiBinding Converter="{x:Static pnl:PNLView.CellParamsConverter}">
@@ -403,24 +407,17 @@
</Setter>
</Style>
<Style x:Key="HeaderRowHyperlinkCellBase" TargetType="c:HyperlinkCell">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding background}"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding foreground}"/>
</Setter.Value>
</Setter>
<Style x:Key="HeaderRowHyperlinkCellBase" TargetType="c:HyperlinkCell" BasedOn="{StaticResource HyperlinkCellBase}">
<!--
Note: the DIYGrid.Grid property serves as a proxy for the DIYGrid, because the header rows, unlike the regular rows/groups,
are not visual children of the DIYGrid, and cannot access the grid with a RelativeSource.
Such a proxy allows the hyperlink cell base style triggers to display a hover brush that is a blend of the grid's SelectionHighlight color and the column's background.
Caution: the fact that Element is used in this Setter.Value binding makes it precarious (if anyone renames _grid, this binding could silently break).
-->
<Setter Property="c:DIYGrid.Grid" Value="{Binding ElementName=_grid}"/>
<Setter Property="Margin" Value="0,0,0,1"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="FontWeight" Value="{Binding font_weight}"/>
<Setter Property="FontSize" Value="{Binding font_size}"/>
<Setter Property="Text" Value="{Binding text}"/>
<Setter Property="Command" Value="{x:Static pnl:PNLView.CellClickCommand}"/>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=pnl:PNLView}}"/>
<Setter Property="c:DIYGrid.ObservedCachedColumnParams">
<Setter.Value>
<x:Array Type="{x:Type c:CachedColumnParam}">
@@ -454,23 +451,7 @@
</Setter>
</Style>
<Style x:Key="GroupRowHyperlinkCell" TargetType="c:HyperlinkCell">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding background}"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding foreground}"/>
</Setter.Value>
</Setter>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="FontWeight" Value="{Binding font_weight}"/>
<Setter Property="FontSize" Value="{Binding font_size}"/>
<Setter Property="Text" Value="{Binding text}"/>
<Setter Property="Command" Value="{x:Static pnl:PNLView.CellClickCommand}"/>
<Setter Property="CommandTarget" Value="{Binding RelativeSource={RelativeSource AncestorType=pnl:PNLView}}"/>
<Style x:Key="GroupRowHyperlinkCell" TargetType="c:HyperlinkCell" BasedOn="{StaticResource HyperlinkCellBase}">
<Setter Property="CommandParameter">
<Setter.Value>
<MultiBinding Converter="{x:Static pnl:PNLView.CellParamsConverter}">
@@ -506,7 +487,7 @@
<Grid
c:DIYGrid.SyncWithColumnGeometrySource="{Binding ElementName=_grid}"
c:DIYGrid.SyncWithColumnGeometry="True"
Height="{Binding RowHeight, ElementName=_grid}"
Height="{Binding RowHeight, ElementName=_grid, Converter={x:Static pnl:PNLView.HeaderRowHeightConverter}}"
>
<Grid.Visibility>
<Binding Path="header_row_display">
@@ -559,7 +540,7 @@
<Grid
c:DIYGrid.SyncWithColumnGeometrySource="{Binding ElementName=_grid}"
c:DIYGrid.SyncWithColumnGeometry="True"
Height="{Binding RowHeight, ElementName=_grid}"
Height="{Binding RowHeight, ElementName=_grid, Converter={x:Static pnl:PNLView.HeaderRowHeightConverter}}"
>
<Grid.Visibility>
<Binding Path="header_row_display">
@@ -609,7 +590,6 @@
c:DIYGrid.CachedColumnParamsContainer="{Binding RelativeSource={RelativeSource AncestorType=pnl:PNLView}}"
VirtualizingPanel.ScrollUnit="Pixel"
ScrollViewer.CanContentScroll="True"
RowHeight="24"
Rows="{Binding rows_view}"
SelectionHighlight="{Binding selection_highlight}"
SupportGroupItems="True"
@@ -1053,7 +1033,7 @@
<!--Row Template-->
<c:DIYGrid.RowTemplate>
<DataTemplate>
<Grid c:DIYGrid.SyncWithColumnGeometry="True" usp:GridAnimation.RowAnimation="{Binding animations}">
<Grid c:DIYGrid.SyncWithColumnGeometry="True" usp:GridAnimation.RowAnimation="{Binding animations}" Height="{Binding RowHeight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}}">
<Grid.Background>
<SolidColorBrush Color="{Binding DataContext.grid_background_color, RelativeSource={RelativeSource AncestorType=pnl:PNLView}}"/>
</Grid.Background>
@@ -1147,7 +1127,7 @@
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="{Binding RowHeight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}, Converter={StaticResource GridLengthPixelConverter}}"/>
<RowDefinition Height="{Binding RowHeight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}, Converter={StaticResource GroupRowHeightConverter}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.ColumnSpan="99" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="Gray" StrokeThickness="1"/>
@@ -1217,4 +1197,4 @@
<Rectangle Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="1" Fill="Gray"/>
</Grid>
</AdornerDecorator>
</UserControl>
</UserControl>
@@ -4,8 +4,8 @@ using mbts.collections;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.controls.adorners;
using mbts.ui.converters;
using mbts.ui.dpi;
using mbts.ui.extensions;
using mbts.ui.spark;
using mbts.ui.utility;
@@ -14,9 +14,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
@@ -31,6 +29,11 @@ namespace bts.shell.pnl {
}
partial class PNLView : UserControl {
public static readonly double
MinGroupRowHeight = 24.0.RoundLayoutValue(),
MinHeaderRowHeight = 24.0.RoundLayoutValue();
DIYGridPopupManager _popup_man_backing;
public enum Columns {
Account = 1,
// Security
@@ -85,6 +88,7 @@ namespace bts.shell.pnl {
public static readonly IValueConverter
IncrementalGroupLevelConverter = ValueConverter.create<int, int>(level => level + 1),
GroupLevelMarginConverter = ValueConverter.create<int, Thickness>(level => new Thickness(24 * level, 0, 0, 0)),
HeaderRowHeightConverter = new RangeBlockConverter<double> { Minimum = MinHeaderRowHeight },
ExpandedVisualStateConverter = ValueConverter.create<bool, string>(is_expanded => is_expanded ? "Expanded" : "Collapsed");
public static readonly IMultiValueConverter
@@ -163,17 +167,22 @@ namespace bts.shell.pnl {
void _grid_loaded(object sender, RoutedEventArgs e) {
// this is a hack to make room for the 'totals row' between the column headers and the regular rows.
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter rp)) {
rp.SetBinding(MarginProperty, new Binding(nameof(PNLVM.header_row_display)) {
Converter = ValueConverter.create<PNLHeaderRowDisplay, Thickness>(hrd => {
double top = 0;
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
top += 24;
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
top += 24;
return new Thickness(0, top, 0, 0);
}),
Source = DataContext
});
rp.SetBinding(MarginProperty,
BindingHelper.create_multi(
ValueConverter.create<PNLHeaderRowDisplay, double, Thickness>((hrd, row_height) => {
double top = 0;
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
top += row_height;
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
top += row_height;
return new Thickness(0, top, 0, 0);
}),
new Binding(nameof(PNLVM.header_row_display)),
new Binding {
Path = new PropertyPath(DIYGrid.RowHeightProperty),
Converter = HeaderRowHeightConverter,
Source = _grid
}));
}
}
#endregion
@@ -240,6 +249,15 @@ namespace bts.shell.pnl {
}
#endregion
DIYGridPopupManager _popup_man {
get {
if (_popup_man_backing == null) {
_popup_man_backing = DIYGridPopupManager.create(this);
}
return _popup_man_backing;
}
}
void _cell_click(PNLCell cell) {
Log.Ui.info($"PNLCell Click: {cell}.");
try {
@@ -357,41 +375,20 @@ namespace bts.shell.pnl {
}
}
async void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
// vm
dynamic header = new ExpandoObject();
header.context = context_desc;
header.title = title_desc;
header.title_foreground = fg;
header.keep_open = false;
// view
var view = new PNLAuxiliaryPopupView {
void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
var view = new HeaderedContentControl {
Content = content,
Header = header,
Header = new AuxiliaryDataHeader {
context = context_desc,
title = title_desc,
title_foreground = fg
},
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
VerticalAlignment = VerticalAlignment.Top,
Resources = new AuxiliaryDataResources()
};
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// popup adorner
// tho unlikely, better not assume target cell is non-null
var target_cell = _grid.ChildAtPoint<HyperlinkCell>(Mouse.GetPosition(_grid));
var popup_adorner = new PopupAdorner(_grid, target_cell ?? (FrameworkElement)_grid, view, HorizontalPosition.Middle, VerticalPosition.Bottom, 0, 0) {
OpenRelativeToMouse = target_cell == null,
EnableDragMove = true,
ClipToBounds = true
};
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding("keep_open") { Source = header });
popup_adorner.PopupElement.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
popup_adorner.PopupElement.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => {
Log.assert(popup_adorner != null);
popup_adorner.detach();
popup_adorner = null;
}));
popup_adorner.attach();
popup_adorner.IsOpen = true;
FocusManager.SetIsFocusScope(popup_adorner.PopupElement, true);
popup_adorner.AdornedElement.Focus();
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
_popup_man.show(view);
}
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
+1
View File
@@ -41,6 +41,7 @@
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/DoubleMinMaxFilterControl.xaml"/>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/DiscreteValuesFilterControl.xaml"/>
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/TimeFilterControl.xaml"/>
<ResourceDictionary Source="/view/Controls/ActionMenu/ActionMenu.xaml"/>
<ResourceDictionary Source="/Modules/Spreader/ExecutionStrategyEditor.xaml"/>
<ResourceDictionary Source="/view/Controls/Order/AlgoContentTemplateSelector.xaml"/>
<ResourceDictionary Source="/view/Controls/Order/AlgoPickerNodeView.xaml"/>
@@ -70,5 +70,21 @@ namespace mbts.ui {
}, () => Disposable.dispose(ref can_execute_sub));
return cmd;
}
public static ICommand create<T, TResult>(Action<TResult> execute, Func<T, bool> get_can_execute, Func<T, TResult> convert, out IObserver<T> observer) {
TResult param = default;
bool can_execute = false;
var cmd = new DelegateCommand(() => execute(param), () => can_execute);
IDisposable can_execute_sub = null;
observer = new AnonymousObserver<T>(val => {
var could_execute = can_execute;
can_execute = get_can_execute(val);
if (can_execute)
param = convert(val);
if (can_execute != could_execute)
cmd.RaiseCanExecuteChanged();
}, () => Disposable.dispose(ref can_execute_sub));
return cmd;
}
}
}
@@ -34,6 +34,9 @@ namespace mbts {
public static bool IsNaNOrInfinity(double d) => double.IsNaN(d) || double.IsInfinity(d);
public static bool IsNaNOrInfinityOr(double d, params double[] values) => IsNaNOrInfinity(d) || values.Contains(d);
public static bool IsOneOf(double d, params double[] values) => values.Contains(d);
// TODO: replace refs to IsNaNOrInfinity with IsReal (it's a shorter name, and just as expressive)
public static bool IsReal(this double d) => !double.IsNaN(d) && !double.IsInfinity(d);
public static bool IsRealNonZero(this double d) => d != 0 && !double.IsNaN(d) && !double.IsInfinity(d);
public static double CoerceNaN(double value) => CoerceNaN(value, 0.0);
public static double CoerceNaN(double value, double if_nan) => double.IsNaN(value) ? if_nan : value;
public static double CoerceNaNOrInfinity(double value) => CoerceNaNOrInfinity(value, 0.0);
@@ -131,6 +131,11 @@ namespace mbts.ui.native
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out Win32Point pt);
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
public static readonly IntPtr HWND_TOP = new IntPtr(0);
public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwnd_insert_after, int x, int y, int cx, int cy, SWP flags);
@@ -255,7 +260,20 @@ namespace mbts.ui.native
{
SWP_NOSIZE = 1,
SWP_NOMOVE = 2,
SWP_FRAMECHANGED = 0x20
SWP_NOZORDER = 0x0004,
SWP_NOREDRAW = 0x0008,
SWP_NOACTIVATE = 0x0010,
// The frame changed: send WM_NCCALCSIZE
SWP_FRAMECHANGED = 0x0020,
SWP_SHOWWINDOW = 0x0040,
SWP_HIDEWINDOW = 0x0080,
SWP_NOCOPYBITS = 0x0100,
// Don’t do owner Z ordering
SWP_NOOWNERZORDER = 0x0200,
// Don’t send WM_WINDOWPOSCHANGING
SWP_NOSENDCHANGING = 0x0400,
TOPMOST_FLAGS = SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSENDCHANGING
}
public enum SPI : uint
@@ -138,6 +138,7 @@ namespace mbts.ui.native
NCMBUTTONDOWN = 0x00A7,
NCMBUTTONUP = 0x00A8,
NCMOUSEMOVE = 0x00A0,
NCMOUSELEAVE = 0x02A2,
NCPAINT = 0x0085,
NCRBUTTONDBLCLK = 0x00A6,
NCRBUTTONDOWN = 0x00A4,
@@ -15,6 +15,7 @@ namespace bts.settings {
// determine the various windows to which such rules apply.
interface IFillsGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface ITimeAndSalesGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
@@ -26,7 +26,8 @@ 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)),
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs));
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs)),
_time_and_sales_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.time_and_sales_grid, (us, gs) => us.with(time_and_sales: gs));
static readonly IImmutableProperty<UserSettings, PNLSettings>
_pnl_propi = ImmutableProperty.create<UserSettings, PNLSettings>(us => us.pnl, (us, pnl) => us.with(pnl: pnl));
@@ -38,6 +39,7 @@ namespace mbts.persist {
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
readonly IEnumerable<ITimeAndSalesGridSettingsMigrationRule> _time_and_sales_migration_rules;
readonly IUserPersist _persist;
UserSettings _default_settings;
@@ -49,7 +51,8 @@ namespace mbts.persist {
[ImportMany]IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
[ImportMany]IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
[ImportMany]IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules,
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules) {
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules,
[ImportMany]IEnumerable<ITimeAndSalesGridSettingsMigrationRule> time_and_sales_migration_rules) {
//
_persist = persist;
_fills_migration_rules = fills_migration_rules ?? Enumerable.Empty<IFillsGridSettingsMigrationRule>();
@@ -57,6 +60,7 @@ namespace mbts.persist {
_pandl_migration_rules = pandl_migration_rules ?? Enumerable.Empty<IPAndLGridSettingsMigrationRule>();
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
_time_and_sales_migration_rules = time_and_sales_migration_rules ?? Enumerable.Empty<ITimeAndSalesGridSettingsMigrationRule>();
init();
async void init() {
UserSettings s;
@@ -68,7 +72,8 @@ namespace mbts.persist {
_mview_migration_rules.merge().wrap(_mview_propi),
_pandl_migration_rules.merge().wrap(_pandl_propi),
_pnl_migration_rules.merge().wrap(_pnl_propi),
_synth_migration_rules.merge().wrap(_synth_propi)
_synth_migration_rules.merge().wrap(_synth_propi),
_time_and_sales_migration_rules.merge().wrap(_time_and_sales_propi)
};
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
// TODO: move ladder sanitization logic into ISettingsMigrationRule
@@ -148,6 +153,12 @@ namespace mbts.persist {
_synth_migration_rules,
_synth_propi);
public IWindowSettingsManager<GridSettings> create_time_and_sales_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_time_and_sales_migration_rules,
_time_and_sales_propi);
public async Task save_default(UserSettings value) {
// notify observers before persisting so the changes are reflected immediately in the ui
lock (_default_settings_lock) {
+2 -2
View File
@@ -102,14 +102,14 @@ namespace mbts.utils
string identifier = ((felem.Name != null) && (felem.Name.Length > 0)) ?
felem.Name :
felem.GetType().ToString();
return "$FrameworkElement [{identifier}]";
return $"FrameworkElement [{identifier}]";
} else {
// Maybe a FrameworkContentElement has focus
if (elem is FrameworkContentElement fcelem) {
string identifier = ((fcelem.Name != null) && (fcelem.Name.Length > 0)) ?
fcelem.Name :
fcelem.GetType().ToString();
return "$FrameworkContentElement [{identifier}]";
return $"FrameworkContentElement [{identifier}]";
}
return $"Element of type [{elem.GetType().ToString()}] has focus";
}
+15 -1
View File
@@ -5,9 +5,23 @@
xmlns:ui="clr-namespace:mbts.ui"
>
<!--Control Base Colors-->
<Color x:Key="Control.SuperLightGrayColor">#F6F6F6</Color>
<Color x:Key="Control.LightGrayColor">#E5E5E5</Color>
<Color x:Key="Control.LightMediumGrayColor">#CFCFCF</Color>
<Color x:Key="Control.MediumGrayColor">#B3B3B3</Color>
<Color x:Key="Control.MediumDarkGrayColor">#ACACAC</Color>
<Color x:Key="Control.AlphaLightGrayColor">#10000000</Color>
<Color x:Key="Control.AlphaLightMediumGrayColor">#24000000</Color>
<Color x:Key="Control.AlphaMediumGrayColor">#3A000000</Color>
<Color x:Key="Control.AlphaMediumGrayColor2">#FF0000</Color>
<Color x:Key="Control.DarkMediumGrayColor">#FF959595</Color>
<Color x:Key="Control.DarkGrayColor">#707070</Color>
<Color x:Key="Text.Prompt.Foreground.Color">#FF999999</Color>
<SolidColorBrush x:Key="Control.AlphaLightGrayBrush" Color="{StaticResource Control.AlphaLightGrayColor}"/>
<SolidColorBrush x:Key="Control.AlphaLightMediumGrayBrush" Color="{StaticResource Control.AlphaLightMediumGrayColor}"/>
<SolidColorBrush x:Key="Control.LightGrayBrush" Color="{StaticResource Control.LightGrayColor}"/>
<SolidColorBrush x:Key="Control.LightMediumGrayBrush" Color="{StaticResource Control.LightMediumGrayColor}"/>
<SolidColorBrush x:Key="Control.MediumGrayBrush" Color="{StaticResource Control.MediumGrayColor}"/>
<SolidColorBrush x:Key="CommonBackground" Color="{ui:BTSColor common_background}" />
<SolidColorBrush x:Key="ControlBackground" Color="{x:Static mbts:BTSColors.common_background}" />
@@ -130,4 +144,4 @@
<SolidColorBrush x:Key="ListBoxItem.Selected.Inactive.Border" Color="{StaticResource ListBoxItem.Selected.Inactive.Border.Color}"/>
<SolidColorBrush x:Key="ListBoxItem.MouseOver.Background" Color="{StaticResource ListBoxItem.MouseOver.Background.Color}"/>
<SolidColorBrush x:Key="ListBoxItem.MouseOver.Border" Color="{StaticResource ListBoxItem.MouseOver.Border.Color}"/>
</ResourceDictionary>
</ResourceDictionary>
Loaded 100 of 214 files, more files were not shown because too many files have changed in this diff. Show more