Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d1b03d1e2 | ||
|
|
9a677cf980 | ||
|
|
08bdd9c8c7 | ||
|
|
886b606093 | ||
|
|
b70510b614 | ||
|
|
6db9a227b1 | ||
|
|
a819eb921d | ||
|
|
af4317a7f9 | ||
|
|
5edd7d5467 | ||
|
|
60d2cb5bc1 | ||
|
|
f26e3d27c4 | ||
|
|
31dab6ca09 | ||
|
|
1498bf17bb | ||
|
|
1cafe23bd0 | ||
|
|
25290df798 | ||
|
|
20ca48e365 | ||
|
|
78dfb03b8e | ||
|
|
a1f71153c8 | ||
|
|
b1d260d98c | ||
|
|
a81aafefe0 | ||
|
|
454330e360 | ||
|
|
995159b365 | ||
|
|
fd1a33e9d2 | ||
|
|
e9615bc3aa | ||
|
|
b1b1597686 | ||
|
|
bc2eb65c0a | ||
|
|
7baa7ca8a1 | ||
|
|
9bbc73b48c | ||
|
|
b07a379ee1 | ||
|
|
af2c6174e8 | ||
|
|
6e5444709d | ||
|
|
831aa43f0e | ||
|
|
50a8592e5e | ||
|
|
cb7b6ef5cd | ||
|
|
b7c72d1607 | ||
|
|
cb2747528b | ||
|
|
52ad4ad2c9 | ||
|
|
93c81089f6 | ||
|
|
d7d23a7a93 | ||
|
|
5525e35d1a | ||
|
|
cdd7856ba0 | ||
|
|
7a6de46b17 | ||
|
|
1fcccc674a | ||
|
|
c0e6b5556d | ||
|
|
8aa2a7e44e | ||
|
|
0303d999ed | ||
|
|
914a817ddb | ||
|
|
4873e523a9 | ||
|
|
672ef518e2 | ||
|
|
d22cb150f6 | ||
|
|
8605b4f11f | ||
|
|
20eb661fb0 | ||
|
|
fad1ca2b6e | ||
|
|
747a687fe5 | ||
|
|
e34c63382c | ||
|
|
4b5c744ba5 | ||
|
|
cb0dfa50b3 | ||
|
|
2429e6019c | ||
|
|
e0bb48b0c7 | ||
|
|
a38b0dbc79 | ||
|
|
73065ab48a | ||
|
|
bd07c96504 | ||
|
|
dc4033875a | ||
|
|
130794c538 | ||
|
|
b1743f0b1c | ||
|
|
33492c9131 | ||
|
|
d434778a82 | ||
|
|
4c6cc9b3e0 | ||
|
|
6aaeddb7e9 | ||
|
|
1685a13994 | ||
|
|
54633dd6db | ||
|
|
510f163e5b | ||
|
|
ee4779da73 | ||
|
|
28e98259f6 | ||
|
|
8e6a17cd0c | ||
|
|
cbde36e9e0 | ||
|
|
6fc20924bd | ||
|
|
381b8064fc | ||
|
|
fc1a3ff7dc | ||
|
|
5fd3d13cfe | ||
|
|
1b38822459 | ||
|
|
8b7ecc430b | ||
|
|
d9d7a08fd6 | ||
|
|
2383c00bb9 | ||
|
|
dcce738d53 | ||
|
|
0daa3e0685 | ||
|
|
82bc402216 | ||
|
|
ff70e9694b | ||
|
|
e06daa349c | ||
|
|
525d0eda30 | ||
|
|
e9e85b44c9 | ||
|
|
13aa693da1 | ||
|
|
f8afa7fc57 | ||
|
|
2f6a66a2ce | ||
|
|
b4674a9120 | ||
|
|
d76eb1153b | ||
|
|
0934c0014d | ||
|
|
19da3e1390 | ||
|
|
2654b2e446 | ||
|
|
61d0ae5257 | ||
|
|
ebea6a8f50 | ||
|
|
f0b3baaf3f | ||
|
|
956837d3f3 | ||
|
|
be45938b51 | ||
|
|
663f9ecc52 | ||
|
|
a6edbbe7b5 | ||
|
|
51276d161c | ||
|
|
3fa699b4c9 | ||
|
|
c9396bd6af | ||
|
|
b80f96fa5e | ||
|
|
b7dea6817c | ||
|
|
f4dc550f1a | ||
|
|
bc978ed065 | ||
|
|
202c5faffd | ||
|
|
a565dcbab7 | ||
|
|
65d55a7618 | ||
|
|
57c3ec34c2 | ||
|
|
4e85218d02 | ||
|
|
f6784f1196 | ||
|
|
7f05a2c2af | ||
|
|
c3d2101dcd | ||
|
|
8855ea44b0 | ||
|
|
51f70ad5ed | ||
|
|
5aa3f1a684 | ||
|
|
5cd3a710bd | ||
|
|
6d8b4636ab | ||
|
|
64a75c4b48 | ||
|
|
143b6ce5d8 | ||
|
|
aa5c0a5613 | ||
|
|
d5fc0c5a56 | ||
|
|
25445d58d0 | ||
|
|
0b313a8aa8 | ||
|
|
54f954fd4b | ||
|
|
980f38ba03 | ||
|
|
9eada2724b | ||
|
|
b5f53ee918 | ||
|
|
7c4daa0f4b | ||
|
|
ab430e4afb | ||
|
|
29f6ea6d48 | ||
|
|
e46372d08e | ||
|
|
1d2faeba3e | ||
|
|
23cffc0630 | ||
|
|
90b597fc70 | ||
|
|
5ea2a8d6b6 | ||
|
|
6e64f7c6df | ||
|
|
8a210f39b6 | ||
|
|
2aacd141ad | ||
|
|
efc175f654 | ||
|
|
6074afd999 | ||
|
|
219dff4055 | ||
|
|
9a3b07b622 | ||
|
|
5c01e6a99a | ||
|
|
2688281a10 | ||
|
|
c6cd5fcf93 | ||
|
|
177b96b453 | ||
|
|
292f66a774 | ||
|
|
7ccd9134c4 | ||
|
|
54d82e1773 | ||
|
|
9483962e7b | ||
|
|
f94c5bc542 | ||
|
|
ce985f4b44 | ||
|
|
032f2253a7 | ||
|
|
4b4eb66ef0 | ||
|
|
8c837f9f64 | ||
|
|
a71914d566 | ||
|
|
bd00183f7a | ||
|
|
2674e526be | ||
|
|
4ea2c08cc3 | ||
|
|
99432ad7ac | ||
|
|
8c210bff34 | ||
|
|
e566a94380 | ||
|
|
85f97ae552 | ||
|
|
7ca0d10eb0 | ||
|
|
07da9fd296 | ||
|
|
436dbd217c | ||
|
|
0937381a3f | ||
|
|
d42f6b6fff | ||
|
|
03940b291b | ||
|
|
2a72b03afc | ||
|
|
98caeb5410 | ||
|
|
73cb56a661 | ||
|
|
88932081b7 | ||
|
|
ca8aed1f2b | ||
|
|
a5f27d0d35 | ||
|
|
ce7ff0d14f | ||
|
|
d08571e862 | ||
|
|
b62d9751cf | ||
|
|
b32d63fcda | ||
|
|
26b5fe5699 | ||
|
|
2fccceb91d | ||
|
|
fa49a0de07 | ||
|
|
bd28a0fa54 | ||
|
|
14ebe5ef51 | ||
|
|
a9b4049307 | ||
|
|
54d9d5e2f3 | ||
|
|
234d499731 | ||
|
|
324b9c1cc7 | ||
|
|
6aad329e09 | ||
|
|
0641cf45d9 | ||
|
|
1fe0efe5c3 | ||
|
|
93938b6ef2 | ||
|
|
818806a4e1 | ||
|
|
261bf3e475 | ||
|
|
dbe2194ed3 | ||
|
|
45e10ca377 | ||
|
|
c3299232f1 | ||
|
|
4de77ce339 | ||
|
|
96f71290a4 | ||
|
|
e2b80942df | ||
|
|
51fb068469 | ||
|
|
81b36edfe3 | ||
|
|
90976ffeba | ||
|
|
43aad8b53f | ||
|
|
5434647ecb | ||
|
|
7c4aa11c69 | ||
|
|
176c1f8d92 | ||
|
|
8b0c9e7722 | ||
|
|
a5c2e8b287 | ||
|
|
24e766ba59 | ||
|
|
ddd81ea93a | ||
|
|
5481cce34c | ||
|
|
58d39c4833 | ||
|
|
367d9be724 | ||
|
|
99f2ab7c8a | ||
|
|
8b091b39eb | ||
|
|
9d1f0e5538 | ||
|
|
02ade1e0c9 | ||
|
|
296749bae3 | ||
|
|
49ec9885eb | ||
|
|
04264e70b9 | ||
|
|
eaadc74242 | ||
|
|
6760ca1898 | ||
|
|
5f2d3c7c26 | ||
|
|
326532ed5c | ||
|
|
5cfb4a65e9 | ||
|
|
f955fef680 | ||
|
|
2214988442 | ||
|
|
e80ab05969 | ||
|
|
92bdbd460a | ||
|
|
a357d978ca | ||
|
|
dfb673bd63 | ||
|
|
24f366491b | ||
|
|
55134bf172 | ||
|
|
b6b33f14ef | ||
|
|
12d532a17f | ||
|
|
09679ff494 | ||
|
|
781e7c98a0 | ||
|
|
4189b05665 | ||
|
|
ff5b04f8ee | ||
|
|
4180b9a8ec | ||
|
|
5ba244c982 | ||
|
|
4029a6a58a | ||
|
|
c03d05285b | ||
|
|
78323d09e8 | ||
|
|
f2621da423 | ||
|
|
cf2bc03dd1 | ||
|
|
88e17743ef | ||
|
|
950073703f | ||
|
|
504d09eb6c | ||
|
|
c2ea5aa36f | ||
|
|
c367488844 | ||
|
|
fb19dd8ce5 | ||
|
|
c0a2c6a24a | ||
|
|
7cbd5adccc | ||
|
|
3b8a4f89cc | ||
|
|
04f165b1a3 | ||
|
|
893aa7a370 | ||
|
|
245b5d0f99 | ||
|
|
d5225aa75b | ||
|
|
48e6e1e139 | ||
|
|
1ebd0be57c | ||
|
|
28779d333c | ||
|
|
c9387a92be | ||
|
|
a4694462ed | ||
|
|
f404b0e936 | ||
|
|
2df1e94fb3 | ||
|
|
acd0d7c343 | ||
|
|
1e8a7f09dd | ||
|
|
b65d50a9c7 | ||
|
|
909b4b6cfe | ||
|
|
2fbc238ecc | ||
|
|
7c73024919 | ||
|
|
0d85aa1644 | ||
|
|
df16acc4af | ||
|
|
7ad0c23704 | ||
|
|
e85e0322e3 | ||
|
|
1a6a31c857 | ||
|
|
65b09c6802 | ||
|
|
148dae32d5 | ||
|
|
9c116fa3d6 | ||
|
|
2eba014203 | ||
|
|
37d022251e | ||
|
|
04cc1da4d9 | ||
|
|
56bba90fdb | ||
|
|
a95841b0e9 | ||
|
|
9c6be9e87c | ||
|
|
7c830786ef | ||
|
|
9eb9047249 | ||
|
|
f7847b7767 | ||
|
|
ae720e4966 | ||
|
|
3b77bc99b6 | ||
|
|
b69a0c245e | ||
|
|
14bcef4c0e | ||
|
|
a23fe74cdc | ||
|
|
7b48f71dec | ||
|
|
c334c1f636 | ||
|
|
8572ff273f | ||
|
|
e6ddc71c75 | ||
|
|
81a0f9193d |
No files matched your search
@@ -34,3 +34,4 @@ trace_data/
|
||||
/doc/user/public/*
|
||||
.ionide/*
|
||||
/.ignore
|
||||
/build
|
||||
@@ -5,7 +5,9 @@
|
||||
"includePath": [
|
||||
"/usr/include",
|
||||
"/usr/local/include",
|
||||
"${workspaceRoot}"
|
||||
"${workspaceRoot}",
|
||||
"${workspaceFolder}/src",
|
||||
"${workspaceRoot}/externals/linux/include"
|
||||
],
|
||||
"defines": [],
|
||||
"intelliSenseMode": "clang-x64",
|
||||
@@ -13,13 +15,16 @@
|
||||
"path": [
|
||||
"/usr/include",
|
||||
"/usr/local/include",
|
||||
"${workspaceRoot}"
|
||||
"${workspaceRoot}",
|
||||
"${workspaceFolder}/src",
|
||||
"${workspaceRoot}/externals/linux/include"
|
||||
],
|
||||
"limitSymbolsToIncludedHeaders": true,
|
||||
"databaseFilename": ""
|
||||
},
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17"
|
||||
"cppStandard": "c++17",
|
||||
"compileCommands": "${workspaceFolder}/build/ninja_debug/compile_commands.json"
|
||||
},
|
||||
{
|
||||
"name": "Windows",
|
||||
|
||||
@@ -14,24 +14,21 @@
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "server_build",
|
||||
"label": "server_build",
|
||||
"type": "shell",
|
||||
"command": ".\\tools\\buildsparkd.cmd",
|
||||
"problemMatcher": {
|
||||
"base": "$gcc",
|
||||
"problemMatcher": {
|
||||
"base": "$gcc",
|
||||
"owner": "cpp",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceFolder}"
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"pattern": {
|
||||
"regexp": "(.?)(home)(.?)(bts)(.?)(spark)(.?)(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)",
|
||||
"regexp": "(.?)(home)(.?)(bts)(.?)(spark)(.?)(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)",
|
||||
"file": 8,
|
||||
"line": 9,
|
||||
"column": 10,
|
||||
@@ -49,6 +46,18 @@
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "linux build (Debug)",
|
||||
"type": "shell",
|
||||
"command": "cd build/ninja_debug; time ninja",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
public
|
||||
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
+++
|
||||
title = "BTS Spark API"
|
||||
+++
|
||||
|
||||
This is index
|
||||
@@ -0,0 +1,7 @@
|
||||
+++
|
||||
title="Client API"
|
||||
redirect_to = "client/getting-started/overview/"
|
||||
sort_by="weight"
|
||||
+++
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title="Getting Started"
|
||||
sort_by="weight"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
+++
|
||||
title = "Install Prerequisites"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
# C#/.NET
|
||||
|
||||
We target [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) so you can use .NET Framework 4.6.1+ or .NET Core 2.0+. Simply add a reference to the [Spark Nuget package](https://www.nuget.org/packages/spark_sdk/) to get started.
|
||||
|
||||
```
|
||||
dotnet new console -lang c#
|
||||
dotnet add package spark_sdk
|
||||
dotnet build
|
||||
```
|
||||
|
||||
# C++
|
||||
|
||||
The easiest way to get started with C++ is to use the Spark Server SDK to develop your application. Follow the instructions to [download and install the Spark Server SDK](/server/getting-started/install) and open the client_sdk sample project. If you want to run your C++ Client SDK application on Windows [contact us](mailto://api@bluetradesys.com) to get the required headers and binaries.
|
||||
|
||||
# JavaScript
|
||||
|
||||
We use JavaScript internally with the Spark Client API over a websocket connection but the documentation and packaging isn't ready yet. If this is something you're interested in get in touch with us at [api@bluetradesys.com](mailto:api@bluetradesys.com).
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
+++
|
||||
title = "Overview"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
The Spark Client SDK is a high level command and control API which can be used to build scripts or integrate Spark into your existing ecosystem. The API is divided into several independent **"services"** responsible for different aspects of the system. A Service can define both [remote procedure call (RPC)](/client/getting-started/services#rpc) and incremental [streaming](/client/getting-started/services#streams) data.
|
||||
|
||||
The [Spark Desktop](https://www.bluetradingsystems.com/bts-spark/) is built on top of this API so you can be assured that it is complete and well tested.
|
||||
|
||||
Spark uses [Google Protocol Buffers](https://developers.google.com/protocol-buffers/) as an interface definition language similar to [GRPC](https://grpc.io) but backed by our custom high-performance TCP protocol. We currently support C#/.NET and C++ <!--and Javascript--> but the architecture is designed for easy extension so contact us about your specific business needs.
|
||||
|
||||
## Orders
|
||||
|
||||
Use the [OrderService](/client/services/orderservice) to send and manage orders including native exchange orders, spreader orders, and custom Algo orders created using the [Spark Server SDK](https://api.bluetradesys.com/server/).
|
||||
|
||||
## Spreader
|
||||
|
||||
Use the [SyntheticSecurityService](/client/services/syntheticsecurityservice) to create and manage synthetic spreads. Once created these spreads show up in the security and market data feeds just like exchange listed spreads.
|
||||
|
||||
## Position
|
||||
|
||||
Use the [PositionService](/client/services/positionservice) to get your position and P&L reports or add fills manually to keep the Spark system up to date with an external system.
|
||||
|
||||
## Administration
|
||||
|
||||
Use the [AdminService](/client/services/adminservice) to manage users and control limits.
|
||||
|
||||
## Market Data
|
||||
|
||||
For .NET subscribe to market data though the high level MarketDataClient interface. From C++ or JavaScript use the MarketDataRelay service directly.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
+++
|
||||
title = "Services"
|
||||
weight = 20
|
||||
+++
|
||||
|
||||
The Spark Client SDK uses [Google Protocol Buffers](https://developers.google.com/protocol-buffers/) to define the interface for each service in a language independent manner. Services have both functions which return a single result and streams of data which return values continuously while subscribed. This page describes the mapping from the Protocol Buffer definition to the concrete definition in each supported language.
|
||||
|
||||
# ServiceContext
|
||||
|
||||
To connect your client to the Spark Server you need to get or create a `ServiceContext` instance. A `ServiceContext` represents a connection to the Spark Server and a background thread for handling messages on that connection. Generally there should be a single `ServiceContext` per process. All of the methods on the `ServiceContext` are safe thread safe.
|
||||
|
||||
Construct a ServiceContext by calling the default constructor. The ServiceContext will find the Spark Server by loading the server configuration over an HTTP connection. To determine the URL it will search the following locations and use the first one that is present.
|
||||
|
||||
* A command line argument `-configserver <url>`
|
||||
* An environment variable `BTSCONFIG`
|
||||
* `%APPDATA%/bts/conf/spark.conf` (Windows)
|
||||
* /sparkdata/conf/spark.conf (Linux)
|
||||
* $HOME/sparkdata/conf/spark.conf (Linux)
|
||||
|
||||
If your application is running on a system with the BTS Client installed this will probably work without any further configuration. If you plan to run your client application on a separate machine we recommend setting the `BTSCONFIG` environment variable.
|
||||
|
||||
<div class="info">
|
||||
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
|
||||
If you are using the Spark Server SDK to run a local copy of the Spark server set <p><strong>BTSCONFIG = http://localhost:9000/config/</strong></p>
|
||||
</div>
|
||||
|
||||
To get a proxy for a given service call ServiceContext.locate<T>("qualifier") where T is the type of the service and "qualifier" is an optional qualifier used to disambiguate between 2 instances of the same service. The locate call cannot fail but it may return a disconnected proxy. When you make a call on the proxy you should check the call result to see if the method call succeeded.
|
||||
|
||||
# RPC
|
||||
|
||||
Here is the OrderService reduced to a single method definition. A RPC method always takes exactly 1 argument and returns 1 value. In this example the `create_order` call takes a `Create` instance and returns a `Response`.
|
||||
|
||||
```C
|
||||
service OrderService {
|
||||
rpc create_order(Create) returns (Response);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The `Create` and `Response` types are also defined as protocol buffer messages. Each field can either be a primitive type another message or a list or map.
|
||||
|
||||
```C
|
||||
message Create {
|
||||
OrderDefinition new_order = 1;
|
||||
double price = 2;
|
||||
uint32 quantity = 3;
|
||||
}
|
||||
|
||||
message Response {
|
||||
uint32 order_key = 1;
|
||||
OrderError error_code = 2;
|
||||
string msg = 3;
|
||||
}
|
||||
```
|
||||
|
||||
Each RPC call generates both synchronous (blocking) and asynchronous variants. In C# the asynchronous calls have the `_async` suffix and return a `Task`. In C++ the call is overloaded to either return a value synchronously or accept a callback function to be called on the ServiceContext thread when the call completes. <!-- In Javascript only async methods returning a Promise are generated because it never makes sense to make a blocking call in the browser. -->
|
||||
|
||||
To call the resulting service locate a proxy using the [ServiceContext](#servicecontext) and then call `cancel_order` or `cancel_order_async`. If you are calling the Spark API from a GUI application you should always prefer the `_async` version of the calls.
|
||||
|
||||
|
||||
### C# example
|
||||
```c#
|
||||
var ctx = new ServiceContext();
|
||||
var orders = ctx.locate<OrderService>();
|
||||
var response =
|
||||
await orders.create_order_async(
|
||||
new Create { price = 2.25, quantity = 42 });
|
||||
if (response.success) {
|
||||
Console.WriteLine($"success: {response.value}");
|
||||
}
|
||||
else {
|
||||
Console.WriteLine($"Error: {response.error_message}");
|
||||
}
|
||||
```
|
||||
### C++ example
|
||||
```cpp
|
||||
auto ctx = ServiceContext{};
|
||||
auto orders = ctx.locate<OrderService>();
|
||||
Create c;
|
||||
c.set_price(2.25);
|
||||
c.set_quantity(42);
|
||||
// Instead of providing an _async method C++ uses overloading.
|
||||
// To make a syncronous call simply omit the callback
|
||||
orders.create_order(c, [=](const Result<Response>& response) {
|
||||
if(response.success()) {
|
||||
std::cout << "success.";
|
||||
}
|
||||
else {
|
||||
std::cout << "error: " << response.error_message();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
# Streams
|
||||
|
||||
Streams allow you to subscribe to live updating data. Each stream represents a changing collection of protocol buffer objects. Similar to a database row each item has an `id` which uniquely identifies it within the collection. Updates from the Spark server can add, remove or replace entries in the collection. Since only the changed items are sent over the wire the stream can remain responsive even when it represents a large collection.
|
||||
|
||||
Streams are implemented using the language bindings for [ReativeX](http://reactivex.io/) which provide utility functions to drill down to the specific data your application needs. In C# we provide both mutable and immutable versions of the generated data types (C++ users can rely on `const`).
|
||||
|
||||
When defined in a proto file streams use the `stream` keyword on the result type.
|
||||
```C
|
||||
service OrderService {
|
||||
...
|
||||
rpc fills (FillsSub) returns (stream Fill);
|
||||
}
|
||||
```
|
||||
|
||||
### C# example
|
||||
```c#
|
||||
var sc = new ServiceContext();
|
||||
var orders = sc.locate<OrderService>();
|
||||
var fills_observable = orders.subscribei_fills();
|
||||
var unsub = fills_observable.Subscribe(update => {
|
||||
Console.WriteLine(
|
||||
"Added {0} items, Removed {1} items, Updated {2} items. Total items {3}",
|
||||
update.added.Count(),
|
||||
update.removed.Count(),
|
||||
update.changes.Count(),
|
||||
update.all.Count());
|
||||
});
|
||||
|
||||
Thread.Sleep(TimeSpan.FromMinutes(5));
|
||||
unsub.Dispose();
|
||||
```
|
||||
### C++ Example
|
||||
```cpp
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title="Samples"
|
||||
sort_by="weight"
|
||||
weight = 30
|
||||
+++
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
+++
|
||||
title = "AdminService"
|
||||
+++
|
||||
@@ -0,0 +1,3 @@
|
||||
+++
|
||||
title = "OrderService"
|
||||
+++
|
||||
@@ -0,0 +1,3 @@
|
||||
+++
|
||||
title = "PositionService"
|
||||
+++
|
||||
@@ -0,0 +1,3 @@
|
||||
+++
|
||||
title = "SyntheticService"
|
||||
+++
|
||||
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title="Services"
|
||||
#sort_by="weight"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
+++
|
||||
title = "Market Data"
|
||||
+++
|
||||
@@ -0,0 +1,24 @@
|
||||
+++
|
||||
title="Algo API"
|
||||
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,6 @@
|
||||
+++
|
||||
title="Getting Started"
|
||||
sort_by="weight"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
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 2048m --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 2048m --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.
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
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:api@bluetradesys.com">api@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
|
||||
|
||||
``` -->
|
||||
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
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.
|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 12 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" />
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 93 KiB |
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title="Reference"
|
||||
sort_by="weight"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
+++
|
||||
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.
|
||||
|
||||
## I don't see the header files in the .zip file. Where are they?
|
||||
|
||||
The SDK headers are baked into the docker image along with the build tools. Once you've loaded the image with `docker load < spark_sdk.img` you can run this command to copy them out to your local file system `docker run --rm -it -v $PWD:/out spark_sdk /bin/tar -cvf /out/headers.tar /sdk/include` This will copy the headers to the current directory in headers.tar. We still recommend doing the actual build inside the docker environment but having a copy of the headers can be useful for setting up your development environment.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
After Width: | Height: | Size: 46 KiB |
@@ -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)
|
||||
-->
|
||||
|
||||
|
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;
|
||||
```
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title="Samples"
|
||||
sort_by="weight"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
@@ -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 ¶ms = 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 ¶ms = 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
|
||||
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -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 |
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<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">
|
||||
<script> dataLayer = [{ 'visitorType': 'geek' }]; </script>
|
||||
</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 content--reversed">
|
||||
{% block content %}
|
||||
<div class="hero">
|
||||
<h1>Spark SDK</h1>
|
||||
<p class="hero__tagline">
|
||||
Spark consists of a client UI
|
||||
(<a 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">
|
||||
<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="{{ get_url(path="@/client/_index.md") }}" class="button">Get started</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
</div>
|
||||
<footer>
|
||||
© Blue Trading Systems
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
if (location.hostname == "api.bluetradesys.com") {
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-56FHG8R');
|
||||
}
|
||||
</script>
|
||||
<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>
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "index.html" %}
|
||||
{# this has access to the "page" variable but documentation.html does not #}
|
||||
|
||||
{% block title %}{{ page.title }} | {{ super() }} {% endblock title %}
|
||||
|
||||
{% block next_link %}
|
||||
{% endblock next_link %}
|
||||
|
||||
{% block content %}
|
||||
{% set section = get_section(path=page.ancestors[1]) %}
|
||||
{#
|
||||
{#<div>{{ section.title }} </div>#}
|
||||
<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">
|
||||
<h1>{{page.title}}</h1>
|
||||
{{page.content | safe}}
|
||||
|
||||
{# next/pre links #}
|
||||
<p>
|
||||
{% if page.lighter %}
|
||||
<a href="{{ page.lighter.permalink }}">
|
||||
<i class="fa fa-chevron-left fa-fw" aria-hidden="true"></i>
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if page.heavier %}
|
||||
<a href="{{ page.heavier.permalink }}">
|
||||
Next
|
||||
<i class="fa fa-chevron-right fa-fw" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="info">
|
||||
<i class="fa fa-lightbulb-o fa-fw" aria-hidden="true"></i>
|
||||
{{body}}
|
||||
</div>
|
||||
@@ -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) }}" />
|
||||
@@ -4,6 +4,48 @@ weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.15.3
|
||||
### Bug Fixes
|
||||
- Fixed a bug where some CME currency spreads exhibited incorrect tick values
|
||||
## 2.15.2
|
||||
### User Visible Changes
|
||||
- Excel RTD plugin can now receive ICE market data
|
||||
## 2.15.1
|
||||
### Bug Fixes
|
||||
- Fixed a FIX log parsing issue that resulted in exchange gateway logon problems
|
||||
## 2.15.0
|
||||
### User Visible Changes
|
||||
- Added ability to hide Ladder status messages via Ladder Settings -> Left Panel tab -> Show/Hide groupbox -> Status Message
|
||||
- Synthetic cloning now allows for any parameter to be changed, including tick size and leg contracts
|
||||
- Added ability to import synthetics from a CSV file; see our manual for further details on this process
|
||||
- Reduced Iceberg reloading latency
|
||||
### Bug Fixes
|
||||
- Fixed a bug where highlighted fills could lose their highlight status
|
||||
- Fixed a bug where the (Mini) OrderBook PIQ could be displaying an old value
|
||||
|
||||
## 2.14.3
|
||||
### Bug Fixes
|
||||
- Fixed a bug regarding ICE logon messaging
|
||||
|
||||
## 2.14.2
|
||||
### Bug Fixes
|
||||
- Fixed a bug regarding ICE messaging
|
||||
|
||||
## 2.14.1
|
||||
### Bug Fixes
|
||||
- Fixed a bug regarding ICE logon authentication
|
||||
|
||||
## 2.14.0
|
||||
### User Visible Changes
|
||||
- All ladders can now be recentered by (shift or alt) + double clicking over the Price column in a ladder
|
||||
- Added a Max Order Quantity setting to the Safety Checks tab on the Ladder settings. This setting allows for a user-defined maximum order quantity.
|
||||
- Added a Price Display Format setting to the Market Price and Size Display tab on the Ladder settings. This setting allows for price levels in the ladder to be displayed without a decimal separator.
|
||||
- Synthetic instrument creation with 3 legs no longer defaults to butterfly quantites
|
||||
### Bug Fixes
|
||||
- Fixed an issue where market transition were not observed by IAlgoOrder
|
||||
- Fixed a bug in error reporting
|
||||
- Fixed a bug in the PnL window where specifying a certain grouping order would lead to a UI failure
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,43 +4,112 @@ using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
namespace bts {
|
||||
[Export, Export(typeof(IShellDaemon))]
|
||||
class DeleteSyntheticDaemon : IShellDaemon, IDisposable {
|
||||
// request_id is noted in the series of log messages that occur during a particular delete op.
|
||||
static int _request_id = 1;
|
||||
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly ErrorReporter _error_reporter;
|
||||
IReadOnlyDictionary<uint, ExecutionStrategyImmutable> _strategy_map =
|
||||
DictionaryHelper.empty<uint, ExecutionStrategyImmutable>();
|
||||
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public DeleteSyntheticDaemon(IBus bus, OrdersModel orders_model, ErrorReporter error_reporter) {
|
||||
_orders_model = orders_model;
|
||||
_error_reporter = error_reporter;
|
||||
_dispose = bus.subscribe(SparkTags.SyntheticDeleteTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(_delete_synthetic);
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SyntheticDeleteTag).Subscribe(_delete_synthetic),
|
||||
orders_model.spreader.strategy.Subscribe(map => _strategy_map = map));
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
|
||||
async void _delete_synthetic(uint id) {
|
||||
var (orders, orders_success) = await _orders_model.on_orders.FirstAsyncTask(SparkUtils.DefaultTimeOut);
|
||||
async void _delete_synthetic(IImmutableSet<uint> keys) {
|
||||
var log = new LoggerContext(Log.Ui, $"Synthetic Delete Req {unchecked(Interlocked.Increment(ref _request_id))} - ", null);
|
||||
if (keys.is_null_or_empty()) {
|
||||
log.warn("received empty delete request (no securities specified).");
|
||||
return;
|
||||
}
|
||||
log.info($"received request to delete the following {keys.Count} securities: {string.Join(", ", keys)}");
|
||||
var (orders, orders_success) = await _orders_model.security_order_groups.FirstAsyncTask(SparkUtils.DefaultTimeOut);
|
||||
if (!orders_success) {
|
||||
_error_reporter.report_error($"Unable to delete synthetic security ({id}): unable to determine whether or not security ({id}) has working orders (deletion is not permitted while there are working orders).");
|
||||
Log.Ui.warn($"Ignoring request to delete synthetic security ({id}) because the orders lookup timed out (must confirm no working orders).");
|
||||
} else if (orders.state.Values.Any(o => o.order.Def.SecurityKey == id && o.order.is_non_terminal())) {
|
||||
Log.Ui.warn($"Ignoring request to delete synthetic security ({id}) because non-terminal-orders were detected.");
|
||||
var msg = "Unable to delete synthetic securities: " +
|
||||
$"timed out waiting for orders table (must verify that a synthetic security does not have any working orders before it can be deleted).";
|
||||
_error_reporter.report_error(msg);
|
||||
log.warn(msg);
|
||||
} else {
|
||||
ExecutionStrategyImmutable? strategy = await _orders_model.spreader.lookup_strategy_async(id);
|
||||
if (strategy.HasValue) {
|
||||
ExecutionStrategyImmutable deleted_strategy = strategy.Value.with(deleted: true);
|
||||
await _orders_model.spreader.save_strategy(id, deleted_strategy);
|
||||
var working_orders = new List<uint>();
|
||||
var missing_strategies = new List<uint>();
|
||||
var items = new List<(uint key, ExecutionStrategyImmutable strategy)>();
|
||||
var temp_strats = _strategy_map;
|
||||
foreach (var k in keys) {
|
||||
if (orders.state.TryGetValue(k, out var sec_group) && sec_group.table.state.Values.Any(tup => tup.order.is_active())) {
|
||||
working_orders.Add(k);
|
||||
} else if (!temp_strats.TryGetValue(k, out var strat)) {
|
||||
missing_strategies.Add(k);
|
||||
} else {
|
||||
items.Add((k, strat.with(deleted: true)));
|
||||
}
|
||||
}
|
||||
var lines = new List<string>();
|
||||
if (working_orders.Count > 0) {
|
||||
lines.Add($"skipping {working_orders.Count} securities because they have working orders.");
|
||||
}
|
||||
if (missing_strategies.Count > 0) {
|
||||
lines.Add($"skipping {missing_strategies.Count} securities because their execution strategies were not discovered.");
|
||||
}
|
||||
if (lines.Count > 0)
|
||||
log.warn(string.Join("; ", lines));
|
||||
var errors = await _orders_model.spreader.save_strategies(items);
|
||||
int error_count = errors.Count(e => !string.IsNullOrEmpty(e));
|
||||
log.info($"{items.Count - error_count}/{items.Count} attempted deletes were successful (skipped {working_orders.Count + missing_strategies.Count}).");
|
||||
// display results to user
|
||||
const string dialog_title = "Synthetic Deletion";
|
||||
string display_msg;
|
||||
MessageBoxImage icon = MessageBoxImage.None;
|
||||
if (keys.Count > 1) {
|
||||
if (error_count == 0 && working_orders.Count == 0 && missing_strategies.Count == 0) {
|
||||
display_msg = $"Synthetic securities have been deleted.";
|
||||
icon = MessageBoxImage.Information;
|
||||
} else {
|
||||
display_msg = $"{keys.Count - error_count - working_orders.Count - missing_strategies.Count}/{keys.Count} securities were successfully deleted.";
|
||||
icon = MessageBoxImage.Error;
|
||||
}
|
||||
} else {
|
||||
if (error_count == 0 && working_orders.Count == 0 && missing_strategies.Count == 0) {
|
||||
display_msg = "Synthetic security has been deleted.";
|
||||
icon = MessageBoxImage.Information;
|
||||
} else {
|
||||
string reason;
|
||||
if (error_count > 0) {
|
||||
reason = errors[0];
|
||||
} else if (missing_strategies.Count > 0) {
|
||||
reason = "did not find execution strategy.";
|
||||
} else if (working_orders.Count > 0) {
|
||||
reason = "working orders detected.";
|
||||
} else {
|
||||
// unexpected code path
|
||||
reason = "unknown error.";
|
||||
}
|
||||
icon = MessageBoxImage.Error;
|
||||
display_msg = $"Unable to delete synthetic security: " + reason;
|
||||
}
|
||||
}
|
||||
BTSMessageBox.Show(display_msg, dialog_title, BTSMessageBoxButtons.OK, icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.support;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
@@ -164,5 +167,45 @@ namespace bts.spark {
|
||||
void _report_error(string msg) =>
|
||||
error_reporter.report_error($"Error {desc}: {msg}", AlgoStudioWindow.NAME, MessageBoxImage.Warning);
|
||||
}
|
||||
|
||||
public static async Task<(TResult result, string error_msg)[]> safe_execute_batch<T, TResult>(
|
||||
this ErrorReporter error_reporter,
|
||||
IEnumerable<Task<T>> execute,
|
||||
Func<T, (TResult result, string error_msg)> result_selector,
|
||||
string desc,
|
||||
string title) {
|
||||
//
|
||||
try {
|
||||
var results = await Task.WhenAll(execute as Task<T>[] ?? ArrayHelper.clone_optimized(execute));
|
||||
var result_values = new (TResult, string)[results.Length];
|
||||
StringBuilder error_bd = null;
|
||||
for (int i = 0; i < results.Length; i++) {
|
||||
var r = results[i];
|
||||
var (result, error) = result_selector(r);
|
||||
if (!string.IsNullOrEmpty(error)) {
|
||||
if (error_bd == null) {
|
||||
error_bd = new StringBuilder();
|
||||
} else {
|
||||
error_bd.AppendLine();
|
||||
}
|
||||
error_bd.Append(error);
|
||||
}
|
||||
result_values[i] = (result, error);
|
||||
}
|
||||
if (error_bd != null) {
|
||||
var error_msg = error_bd.ToString();
|
||||
if (!string.IsNullOrEmpty(error_msg))
|
||||
_report_error(error_msg);
|
||||
}
|
||||
return result_values;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_report_error(ex.ToString());
|
||||
}
|
||||
return null;
|
||||
//
|
||||
void _report_error(string msg) =>
|
||||
error_reporter.report_error($"Error {desc}: {msg}", title, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ namespace bts {
|
||||
_relay.set_token(_token.Value);
|
||||
_dispose.Add(
|
||||
_ss.Value.subscribei_metadata()
|
||||
.Select(upd => upd.all.ToDictionary(i => i.SecurityKey, i => i.Strategy))
|
||||
.Select(upd => upd.all.Where(i => i.HasStrategy).ToDictionary(i => i.SecurityKey, i => i.Strategy))
|
||||
.Subscribe(t => {
|
||||
var deleted_synthetic_keys = new HashSet<uint>(t.Where(kv => kv.Value.Deleted).Select(kv => kv.Key));
|
||||
// add back 'undeleted' synthetics
|
||||
@@ -64,4 +64,4 @@ namespace bts {
|
||||
Task.Run(run);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.spark;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.spark;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
@@ -19,20 +21,26 @@ namespace bts.shell {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _model;
|
||||
readonly ErrorReporter _error_reporter;
|
||||
readonly bool _close_on_send;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _md_map;
|
||||
TableUpdate<(SecurityImmutable? sec, OrderRecordImmutable order), uint> _orders;
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public OrderTicketDaemon(IBus bus, OrdersModel model, ErrorReporter error_reporter) {
|
||||
public OrderTicketDaemon(IBus bus, OrdersModel model, ErrorReporter error_reporter)
|
||||
: this(bus, model, error_reporter, Configuration.get<bts.config.OrderTicketOptions>().close_on_send) { }
|
||||
|
||||
public OrderTicketDaemon(IBus bus, OrdersModel model, ErrorReporter error_reporter, bool close_on_send) {
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
_error_reporter = error_reporter;
|
||||
_close_on_send = close_on_send;
|
||||
_dispose = Disposable.Create(
|
||||
model.on_orders.OnDispatcher().Subscribe(o => _orders = o),
|
||||
bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(m =>_md_map = m),
|
||||
bus.subscribe(SparkTags.ShowOrderTicketFromSecurityTag).OnDispatcher().Subscribe(_show_order_ticket),
|
||||
bus.subscribe(SparkTags.ShowOrderTicketTag).OnDispatcher().Subscribe(_show_order_ticket),
|
||||
bus.subscribe(SparkTags.ShowOrderTicketAutoLookupTag).Subscribe(_show_order_tickets),
|
||||
bus.subscribe(SparkTags.ShowModifyOrderTag).OnDispatcher().Subscribe(_show_modify_order)
|
||||
);
|
||||
}
|
||||
@@ -54,17 +62,28 @@ namespace bts.shell {
|
||||
void _show_order_ticket(SecurityMD smd) =>
|
||||
_show_order_window(new OrderTicket {
|
||||
DataContext = new NewOrderTicketVM(_model, smd,
|
||||
Observable.Return(LadderSettings.Default))
|
||||
Observable.Return(LadderSettings.Default)) {
|
||||
close_on_send = _close_on_send
|
||||
}
|
||||
});
|
||||
|
||||
void _show_order_ticket((OrderDefinitionImmutable def, double price, uint size) ops) {
|
||||
if (_md_map.TryGetValue(ops.def.SecurityKey, out SecurityMD smd))
|
||||
_show_order_window(new OrderTicket {
|
||||
DataContext = new NewOrderTicketVM(_model, smd, ops.def, ops.price, ops.size,
|
||||
Observable.Return(LadderSettings.Default.calculator_buttons))
|
||||
Observable.Return(LadderSettings.Default.calculator_buttons)) {
|
||||
close_on_send = _close_on_send
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _show_order_tickets(IImmutableSet<uint> securities) {
|
||||
foreach (var id in securities.empty_if_null()) {
|
||||
if (_md_map.TryGetValue(id, out var smd))
|
||||
_show_order_ticket(smd);
|
||||
}
|
||||
}
|
||||
|
||||
void _show_modify_order(uint order_key) {
|
||||
if (_orders.state.TryGetValue(order_key, out var ori) && ori.sec.HasValue
|
||||
&& (ori.order.HasDef && ori.order.Def.HasCustomDef && !(ori.order.Def.CustomDef.HasStop || ori.order.Def.CustomDef.HasStopLimit))) {
|
||||
@@ -78,4 +97,4 @@ namespace bts.shell {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts;
|
||||
using mbts.ui.dpi;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -25,6 +26,7 @@ namespace mbts.ui {
|
||||
AttachAlgosKey = nameof(AttachAlgosKey),
|
||||
CheckMarkGeometryKey = nameof(CheckMarkGeometryKey),
|
||||
CopyWFGeometryKey = nameof(CopyWFGeometryKey),
|
||||
CopyBoxesGeometryKey = nameof(CopyBoxesGeometryKey),
|
||||
DataEditGeometryKey = nameof(DataEditGeometryKey),
|
||||
DetachAlgosKey = nameof(DetachAlgosKey),
|
||||
DiskGeometryKey = nameof(DiskGeometryKey),
|
||||
@@ -44,6 +46,7 @@ namespace mbts.ui {
|
||||
KeyGeometryKey = nameof(KeyGeometryKey),
|
||||
LetterAKey = nameof(LetterAKey),
|
||||
LetterDKey = nameof(LetterDKey),
|
||||
MenuOrientationGeometryKey = nameof(MenuOrientationGeometryKey),
|
||||
OrdersGeometryKey = nameof(OrdersGeometryKey),
|
||||
PauseGeometryKey = nameof(PauseGeometryKey),
|
||||
PaymentGeometryKey = nameof(PaymentGeometryKey),
|
||||
@@ -55,10 +58,14 @@ namespace mbts.ui {
|
||||
StarGeometryKey = nameof(StarGeometryKey),
|
||||
ToolsGeometryKey = nameof(ToolsGeometryKey),
|
||||
TrashGeometryKey = nameof(TrashGeometryKey),
|
||||
UndoCircularGeometryKey = nameof(UndoCircularGeometryKey);
|
||||
UndoCircularGeometryKey = nameof(UndoCircularGeometryKey),
|
||||
WarningExclamationGeometryKey = nameof(WarningExclamationGeometryKey),
|
||||
WarningGeometryKey = nameof(WarningGeometryKey);
|
||||
|
||||
public static Geometry PlayGeometry => Geometry.Parse(get_geometric_path(PlayGeometryKey).FirstOrDefault() ?? string.Empty);
|
||||
|
||||
static readonly Pen _copy_boxes_pen = new Pen(Brushes.Black, HighDpiExtensions.RoundLayoutValue(5)).frozen();
|
||||
|
||||
static readonly string base_uri = "pack://application:,,,/Images/Icons/";
|
||||
private static ResourceDictionary _resources = (ResourceDictionary)Application.LoadComponent(new Uri("/Images/Vector/Icons.xaml", UriKind.RelativeOrAbsolute));
|
||||
|
||||
@@ -153,6 +160,7 @@ namespace mbts.ui {
|
||||
public static ImageSource CheckMark => create_icon(CheckMarkGeometryKey);
|
||||
public static ImageSource Clock { get { return load_resource_and_freeze("Clock"); } }
|
||||
public static ImageSource CopyWF => create_icon(CopyWFGeometryKey);
|
||||
public static ImageSource CopyBoxes => create_icon(CopyBoxesGeometryKey, null, _copy_boxes_pen);
|
||||
public static ImageSource DataEdit => create_icon(DataEditGeometryKey);
|
||||
public static ImageSource Defrag { get { return load_resource_and_freeze("Defrag"); } }
|
||||
public static ImageSource DetachAlgos => create_icon_and_rotate(DetachAlgosKey, Brushes.Red);
|
||||
@@ -176,6 +184,7 @@ namespace mbts.ui {
|
||||
public static ImageSource Import => load_resource_and_freeze("Import");
|
||||
public static ImageSource Key => create_icon(KeyGeometryKey);
|
||||
public static ImageSource Knight { get { return load_resource_and_freeze("Knight"); } }
|
||||
public static ImageSource MenuOrientation => create_icon(MenuOrientationGeometryKey);
|
||||
public static ImageSource Pause => create_icon(PauseGeometryKey);
|
||||
public static ImageSource Payment => create_icon(PaymentGeometryKey);
|
||||
public static ImageSource Pencil => create_icon(PencilGeometryKey);
|
||||
@@ -190,6 +199,8 @@ namespace mbts.ui {
|
||||
public static ImageSource Tetris { get { return load_resource_and_freeze("Tetris"); } }
|
||||
public static ImageSource Tools => create_icon(ToolsGeometryKey);
|
||||
public static ImageSource Trash => create_icon(TrashGeometryKey);
|
||||
public static ImageSource Warning => create_icon(WarningGeometryKey, Brushes.IndianRed);
|
||||
public static ImageSource WarningExclamation => create_icon(WarningExclamationGeometryKey, Brushes.IndianRed);
|
||||
public static ImageSource UndoCircular => create_icon(UndoCircularGeometryKey);
|
||||
public static ImageSource ViewSettings => load_resource_and_freeze("ViewSettings");
|
||||
public static ImageSource Wrench { get { return load_resource_and_freeze("Wrench"); } }
|
||||
|
||||
@@ -46,6 +46,18 @@
|
||||
</sys:String>
|
||||
</x:Array>
|
||||
|
||||
<x:Array x:Key="{x:Static mbts:Icons.CopyBoxesGeometryKey}" Type="{x:Type sys:String}">
|
||||
<sys:String>
|
||||
M-55,5 L-55,55 L-5,55
|
||||
</sys:String>
|
||||
<sys:String>
|
||||
M-40,-10 L-40,40 L10,40 L10,-10 L-40,-10
|
||||
</sys:String>
|
||||
<sys:String>
|
||||
M-10,10 L40,10 L40,-40 L-10,-40 L-10,10
|
||||
</sys:String>
|
||||
</x:Array>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.CopyWFGeometryKey}">
|
||||
M14.733902,12.084991L9.751783,17.388992 12.982842,17.436005C13.569886,17.462997 14.5229,17.350998 14.876848,17.005005 14.954852,16.929993 15.031879,16.824005 15.02486,16.593002 14.986895,15.457993 14.838883,13.438004 14.733902,12.084991z M22.507753,11.134453C22.480678,11.134323,22.467085,11.135494,22.469961,11.136993L20.64694,11.145004 16.291904,11.192993C16.388891,12.405991 16.596901,15.100998 16.647928,16.539001 16.668924,17.177994 16.448888,17.742004 16.007904,18.168991 15.210897,18.947998 13.893864,19.065002 13.179865,19.065002L9.2818066,19.005005 9.3258129,25.522003 9.3728109,25.528 9.3278271,25.882996 9.3317944,26.395996 9.3107983,26.397995C9.3167797,26.789993,9.3378369,27.552994,9.3958212,29.065994L9.4068075,29.393005C9.4207855,29.819992 9.5648295,29.994995 9.6707884,30.089005 9.9807898,30.365997 10.539817,30.390991 10.717799,30.369003L10.717799,30.375C10.88284,30.354996 11.280855,30.356003 12.823844,30.365997 13.867863,30.371002 15.206869,30.376999 16.594886,30.376999 20.757965,30.376999 23.270998,30.354996 23.862007,30.311005 24.454971,30.268005 24.661029,29.154999 24.690021,28.729996 24.690021,28.729996 24.677997,27.876999 24.661029,26.582993 24.612016,22.671005 24.51198,14.697998 24.472002,12.802002 24.461013,12.302994 24.316969,11.929993 24.034008,11.662003 23.547475,11.200003 22.697284,11.13536 22.507753,11.134453z M8.8867823,2.8410034L2.5417312,10.296997 6.4037791,10.356995C6.8917614,10.390991 8.3258005,10.341003 8.9567896,9.7299957 9.1738335,9.5189972 9.2667919,9.2659912 9.2558046,8.9089966 9.2067934,7.3889923 9.0258213,4.7480011 8.8867823,2.8410034z M10.424826,1.6239929C10.551842,3.302002 10.816799,6.951004 10.876797,8.8560028 10.903836,9.6750031 10.636804,10.360992 10.083818,10.897995 9.0667769,11.882004 7.3527657,11.987991 6.6627546,11.987991 6.4657918,11.987991 6.3367616,11.979004 6.3177796,11.977005L1.6477377,11.905991C1.6387044,14.278992 1.6236895,20.283997 1.6496909,22.358994 1.651705,22.569992 1.6437093,22.780991 1.6366904,22.992004 1.6117265,23.593994 1.5926833,24.112991 1.8537338,24.386002 2.0716933,24.612991 2.6497034,24.886002 4.1457549,24.886002 6.0127832,24.886002 7.0867711,24.876999 7.6967641,24.867004L7.6457993,17.259995 14.853899,9.5849915 16.681924,9.5650024 16.681924,9.5429993 19.454908,9.5289917 19.406934,2.6210022C19.323925,2.3549957,18.960945,1.6239929,17.387928,1.6239929z M9.1738335,0L17.387928,0C19.94692,0,20.874971,1.5299988,21.022983,2.4329987L21.077976,9.5209961 22.446951,9.5139923C22.43395,9.5130005 24.031017,9.4250031 25.148035,10.481995 25.753999,11.054001 26.071996,11.822998 26.092992,12.766998 26.142006,15.065994 26.281044,26.268997 26.307047,28.412003L26.311014,28.764999C26.279029,29.817993 25.750032,31.802002 23.979013,31.931 23.342958,31.977005 20.887972,32 16.683878,32 13.798893,32 11.107818,31.988998 10.808803,31.987991 10.789821,31.990997 9.5147806,32.112 8.6008288,31.309006 8.0858074,30.854996 7.8107797,30.229004 7.7838017,29.444992L7.7728153,29.126999C7.721789,27.759995 7.6978022,26.970993 7.6898061,26.490005 7.0547882,26.5 5.9867817,26.509003 4.1457549,26.509003 2.4377257,26.509003 1.3387128,26.192001 0.68367504,25.509003 -0.055286743,24.742004 -0.017322397,23.742004 0.012707477,22.938004 0.020702986,22.744003 0.028698692,22.559998 0.026684528,22.378998 -0.00029337304,20.240997 0.016674615,13.856995 0.026684528,11.212997L0.026684528,10.867004 0.14167619,10.610992 8.7147823,0.54100037 8.7088009,0.45899963 8.8018194,0.43800354z
|
||||
</sys:String>
|
||||
@@ -280,6 +292,10 @@
|
||||
M22.399981,2.4000006C22.099982,2.4000006 21.899982,2.5 21.599982,2.6999998 21.199983,3.1000004 21.199983,3.8000002 21.599982,4.2000003L26.199979,8.9000001C26.599978,9.3000002 27.299977,9.3000002 27.699977,8.9000001 28.099977,8.5000002 28.099977,7.8000002 27.699977,7.4000006L23.099981,2.6999998C22.899981,2.5,22.699981,2.4000006,22.399981,2.4000006z M21.499982,0C22.299982,0,23.199981,0.30000019,23.79998,0.90000057L29.299976,6.4000001C30.599975,7.7000003,30.599975,9.7000003,29.299976,11L22.599981,17.7C21.299983,19,19.299984,19,17.999985,17.7L17.299985,17 12.699989,21.6 10.399991,20.900001 10.899991,23.400001 8.4999928,22.8 8.9999924,25.5 6.2999945,25.1 6.7999945,27.6 4.2999964,27 4.7999954,29.6 2.4999971,32 0,26.6 13.499989,13.1 16.999986,16.6 12.699989,12.3C11.399991,11,11.399991,9.0000002,12.699989,7.7000003L19.399984,1C19.899983,0.30000019,20.699983,0,21.499982,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.MenuOrientationGeometryKey}">
|
||||
M14.997001,19.999038L15.000001,23.499012C15.000001,24.327005,14.327008,25,13.500016,25L9.9970522,25 9.9970522,23.000015 13.000022,23.000015 12.997022,20.001038z M9.000001,19.000006L9.000001,30.000006 30.000001,30.000006 30.000001,19.000006z M2,2L2,23 7.000001,23 7.000001,18.500006C7.000001,17.673006,7.6730013,17.000006,8.500001,17.000006L13.000001,17.000006 13.000001,2z M1.5,0L13.500001,0C14.327001,0,15.000001,0.67300034,15.000001,1.5L15.000001,17.000006 26.956518,17.000006 23.162404,13.207182C22.772376,12.816143 22.772376,12.184205 23.162404,11.793225 23.357907,11.597736 23.613928,11.499992 23.869827,11.499992 24.125725,11.499992 24.381502,11.597736 24.576517,11.793225L25.889371,13.105454 25.866285,12.914201C25.554945,10.547287 24.699391,8.7684691 23.308905,7.6089654 21.974919,6.4954615 20.356436,6.1229606 19.066825,6.0297103L19.059648,6.0292783 20.545799,7.5160289C20.936786,7.9070184 20.936786,8.5390015 20.545799,8.929991 20.350807,9.1249857 20.094815,9.2229831 19.838825,9.2229831 19.582834,9.2229831 19.326843,9.1249857 19.131849,8.929991L15.185988,4.9830971 18.981854,1.1871986C19.371841,0.79620934 20.004819,0.79620934 20.395805,1.1871986 20.785791,1.5771885 20.785791,2.2101717 20.395805,2.601161L18.976535,4.020443 18.999199,4.0214248C20.641604,4.1119318 22.785724,4.567862 24.590892,6.0739603 26.493747,7.660778 27.606404,10.054969 27.91277,13.209869L27.913735,13.221022 29.341699,11.793225C29.536714,11.597736 29.792459,11.499992 30.048328,11.499992 30.304196,11.499992 30.560187,11.597736 30.75569,11.793225 31.146694,12.184205 31.146694,12.816143 30.75569,13.207182L26.962552,17.000006 30.500001,17.000006C31.327001,17.000006,32.000001,17.673006,32.000001,18.500006L32.000001,30.500006C32.000001,31.327005,31.327001,32.000006,30.500001,32.000006L8.500001,32.000006C7.6730013,32.000006,7.000001,31.327005,7.000001,30.500006L7.000001,25 1.5,25C0.67300034,25,0,24.327001,0,23.5L0,1.5C0,0.67300034,0.67300034,0,1.5,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.OrdersGeometryKey}">
|
||||
M50.14375,33.389126C50.416863,33.394688,50.653496,33.587773,50.705998,33.864617L50.015995,45.114388C49.810295,46.291462,49.130394,45.097489,49.130394,45.097489L47.65659,42.640538C42.747776,49.635197 34.633152,54.221001 25.433924,54.221001 14.884193,54.221001 5.7437552,48.204125 1.2449617,39.419205 0.80087067,38.552021 0.21358696,37.705738 0.052086769,36.736958 -0.49735315,33.490925 3.4507786,32.901036 4.8049124,34.432404 5.3674143,35.067692 6.034036,36.16407 6.5079177,37.026051 10.106778,43.799416 17.2321,48.413819 25.433924,48.413819 32.656544,48.413819 39.026364,44.826695 42.903977,39.351506L40.229367,38.854115C40.229367,38.854115,38.677464,38.647118,39.742466,37.996033L50.024895,33.398526C50.064972,33.391365,50.104733,33.388333,50.14375,33.389126z M23.030291,9.0310004L27.889918,9.0310004 27.889918,13.028299C30.539786,13.205498,32.324932,13.958096,33.464161,14.528294L33.964176,14.775793 32.417435,20.038878 31.706417,19.68068C30.579987,19.11958 28.839241,18.244583 26.149073,18.244583 24.006916,18.244583 22.723183,19.122181 22.723183,20.590876 22.723183,21.894272 23.943314,22.804369 27.551308,24.187165 32.706444,26.124659 34.911,28.702853 34.911,32.817341 34.911,36.892929 32.090527,40.05172 27.673611,40.989117L27.673611,45.540001 22.767485,45.540001 22.767485,41.330414C20.252919,41.155815,17.695551,40.424118,16.101811,39.404721L15.683999,39.137619 17.221739,33.731438 17.969059,34.180634C19.088988,34.851235 21.402748,35.97873 24.340325,35.97873 26.734886,35.97873 28.276728,34.921635 28.276728,33.291336 28.276728,31.986641 27.547409,30.853846 23.986016,29.57255 20.083815,28.144253 16.095511,26.04406 16.095511,21.239174 16.095511,17.280985 18.737379,14.291395 23.030291,13.329098z M25.268765,0C35.819447,2.7347414E-07 44.961449,6.0155495 49.458798,14.804573 49.902695,15.669173 50.490094,16.516972 50.651287,17.48427 51.198281,20.734265 47.255726,21.318962 45.901341,19.789065 45.336346,19.153567 44.669552,18.051968 44.19566,17.19517 40.5957,10.421881 33.473275,5.8072398 25.268765,5.8072393 18.047545,5.8072398 11.678013,9.3918336 7.8003562,14.867073L10.474627,15.367072C10.474627,15.367072,12.026909,15.574172,10.962921,16.223872L0.67933281,20.826762C0.36037083,20.880062,0.057029126,20.669165,0.00099972012,20.354063L0.69093342,9.1118043C0.89552094,7.9296158,1.5738537,9.124924,1.5738539,9.124924L3.0452772,11.583379C7.9566038,4.584462,16.073666,2.7347414E-07,25.268765,0z
|
||||
</sys:String>
|
||||
@@ -363,6 +379,18 @@
|
||||
c-84.15,0-153-68.85-153-153h-51c0,112.2,91.8,204,204,204s204-91.8,204-204S360.825,89.25,248.625,89.25z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.WarningExclamationGeometryKey}">
|
||||
M15.998999,17.988002C16.551,17.988002 16.999,18.437009 16.999,18.990018 16.999,19.543027 16.551,19.992034 15.998999,19.992034 15.446999,19.992034 14.998999,19.543027 14.998999,18.990018 14.998999,18.437009 15.446999,17.988002 15.998999,17.988002z
|
||||
M15.998999,8.0630169C16.552,8.0630169,16.999,8.5110159,16.999,9.0650149L16.999,16.167001C16.999,16.721 16.552,17.168998 15.998999,17.168998 15.446,17.168998 14.998999,16.721 14.998999,16.167001L14.998999,9.0650149C14.998999,8.5110159,15.446,8.0630169,15.998999,8.0630169z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.WarningGeometryKey}">
|
||||
M15.998999,17.988002C16.551,17.988002 16.999,18.437009 16.999,18.990018 16.999,19.543027 16.551,19.992034 15.998999,19.992034 15.446999,19.992034 14.998999,19.543027 14.998999,18.990018 14.998999,18.437009 15.446999,17.988002 15.998999,17.988002z
|
||||
M15.998999,8.0630169C16.552,8.0630169,16.999,8.5110159,16.999,9.0650149L16.999,16.167001C16.999,16.721 16.552,17.168998 15.998999,17.168998 15.446,17.168998 14.998999,16.721 14.998999,16.167001L14.998999,9.0650149C14.998999,8.5110159,15.446,8.0630169,15.998999,8.0630169z
|
||||
M15.999277,2.3337629L2.3830507,22.996989 29.615442,22.996989z
|
||||
M15.999513,0C16.50052,-3.8678991E-07,17.001274,0.21826346,17.288297,0.65479128L31.74151,22.585975C32.057495,23.064982 32.084472,23.677966 31.812494,24.184989 31.541494,24.687009 31.02049,24.999999 30.450475,24.999999L1.5480773,24.999999C0.97903845,24.999999 0.45705819,24.687009 0.18703459,24.184989 -0.085979476,23.677966 -0.057963934,23.064982 0.25704309,22.585975L14.709219,0.65479128C14.997247,0.21826346,15.498506,-3.8678991E-07,15.999513,0z
|
||||
</sys:String>
|
||||
|
||||
<!--
|
||||
'y' for equilateral triangle of side length x:
|
||||
(x/2)^2 + Y^2 = x^2
|
||||
|
||||
@@ -48,5 +48,5 @@ namespace mbts.ui.spark {
|
||||
}
|
||||
}
|
||||
|
||||
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row, MarketViewView.Columns> { }
|
||||
class MarketViewRowView : DIYGrid.HighPerformanceRow<MarketViewVM.Row> { }
|
||||
}
|
||||
@@ -4,10 +4,10 @@ using bts.shell;
|
||||
using bts.shell.fills;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using bts.view;
|
||||
using mbts.persist;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
@@ -32,6 +32,7 @@ namespace mbts.spark {
|
||||
SecurityCriteria
|
||||
_selected_security_criteria_backing,
|
||||
_sorted_selected_security_criteria_backing;
|
||||
SparkMenuDisplayVM _menu_display_vm;
|
||||
|
||||
public MarketViewWindow(MarketViewWindowFactory factory, IBus bus, OrdersModel orders_model, MarketViewWindowFactory.StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
|
||||
: base("Market View", bus, factory, SparkCommands.ShowMarketView.icon) {
|
||||
@@ -48,7 +49,9 @@ namespace mbts.spark {
|
||||
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(), _vm.get_grid_save_state(), _vm.rows_view.get_sorting_criteria(_vm.columns)) {
|
||||
menu_display = _menu_display_vm.safe_create()
|
||||
};
|
||||
|
||||
public override double MinWidth => MarketViewWindowFactory.StartupData.DefaultGridSettings.columns.Where(c => c.is_visible).Sum(gcs => gcs.width)
|
||||
+ SPARK_MENU_COLLAPSED_WIDTH
|
||||
@@ -101,6 +104,8 @@ namespace mbts.spark {
|
||||
public void thread_init() {
|
||||
_init_guard.check_access();
|
||||
Log.notnull(_state);
|
||||
_menu_display_vm = new SparkMenuDisplayVM();
|
||||
_menu_display_vm.load_from(_state.menu_display);
|
||||
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);
|
||||
_view = new MarketViewView { DataContext = _vm };
|
||||
@@ -160,7 +165,8 @@ namespace mbts.spark {
|
||||
var dialog = new ShellSettingsDialog { DataContext = vm };
|
||||
vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog);
|
||||
return dialog;
|
||||
}))
|
||||
})),
|
||||
_menu_display_vm.spark_cmd
|
||||
};
|
||||
//
|
||||
_vm.selection_changed += on_selection_changed;
|
||||
@@ -190,5 +196,8 @@ namespace mbts.spark {
|
||||
_vm.Dispose();
|
||||
Commands.OfType<IDisposable>().DisposeAll();
|
||||
}
|
||||
|
||||
protected override void OnParentViewCreated(SparkWindowView parent_view) =>
|
||||
_menu_display_vm.safe_bind_to_view(parent_view);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts.api;
|
||||
using bts.proto;
|
||||
using bts.settings;
|
||||
using bts.view;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
@@ -53,6 +54,8 @@ namespace bts.shell {
|
||||
public readonly FilterStates filters;
|
||||
[ProtoMember(4)]
|
||||
public GridSettings grid;
|
||||
[ProtoMember(5)]
|
||||
public SparkMenuDisplaySettings menu_display;
|
||||
|
||||
public StartupData() { }
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
|
||||
namespace bts.shell.time_and_sales {
|
||||
[ProtoContract]
|
||||
class FilterStates {
|
||||
[ProtoMember(1)]
|
||||
public TimeFilterTuple time;
|
||||
[ProtoMember(2)]
|
||||
public string security;
|
||||
[ProtoMember(3)]
|
||||
public DoubleMinMaxState price;
|
||||
[ProtoMember(4)]
|
||||
public UIntMinMaxState size;
|
||||
// [ProtoMember(5)]
|
||||
// ProtoMember 5 cannot be used further
|
||||
[ProtoMember(6)]
|
||||
public string security_key;
|
||||
[ProtoMember(7)]
|
||||
public co2.AggressorSide[] aggressor_sides;
|
||||
}
|
||||
|
||||
static class TimeAndSalesFilterStatesExtensions {
|
||||
public static Func<TimeAndSalesRowVM, bool> build_predicate(this FilterStates fs) {
|
||||
var pb = new ColumnPredicateBuilder<TimeAndSalesRowVM>();
|
||||
pb.add_regex(fs.security, r => r.security.ToString());
|
||||
pb.add_time_filter(fs.time, r => r.time);
|
||||
pb.add_price(fs.price, r => r.price, r => r.security);
|
||||
pb.add_numeric(fs.size, r => r.size);
|
||||
pb.add_discrete_values(fs.aggressor_sides, r => r.aggressor_side);
|
||||
return pb.to_predicate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
using Columns = bts.shell.time_and_sales.TimeAndSalesView.Columns;
|
||||
|
||||
namespace bts.shell.time_and_sales {
|
||||
[ProtoContract]
|
||||
class TimeAndSalesSettings {
|
||||
[ProtoMember(1)]
|
||||
public readonly SecurityCriteria security_criteria;
|
||||
[ProtoMember(2)]
|
||||
List<SortingCriteria> _sorting_criterion;
|
||||
[ProtoMember(3)]
|
||||
public FilterStates filters;
|
||||
[ProtoMember(4)]
|
||||
public GridSettings grid;
|
||||
[ProtoMember(5)]
|
||||
public SparkMenuDisplaySettings menu_display;
|
||||
[ProtoMember(6)]
|
||||
public bool use_defaults;
|
||||
|
||||
public TimeAndSalesSettings() { }
|
||||
|
||||
public TimeAndSalesSettings(SecurityCriteria security_criteria) :
|
||||
this(security_criteria, new FilterStates(), null, null) { }
|
||||
|
||||
public TimeAndSalesSettings(SecurityCriteria security_criteria, FilterStates filters, GridSettings grid = default, List<SortingCriteria> sorting_criterion = null) {
|
||||
this.security_criteria = security_criteria;
|
||||
this.filters = filters;
|
||||
this.grid = grid;
|
||||
_sorting_criterion = sorting_criterion;
|
||||
}
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public List<SortingCriteria> sorting_criterion => _sorting_criterion ?? (_sorting_criterion = DefaultColumns.default_sorting_criterion.ToList());
|
||||
|
||||
public static TimeAndSalesSettings Default(SecurityCriteria security_criteria) =>
|
||||
new TimeAndSalesSettings(security_criteria, new FilterStates(), null, DefaultColumns.default_sorting_criterion.ToList());
|
||||
|
||||
public TimeAndSalesSettings with(SecurityCriteria security_criteria) =>
|
||||
new TimeAndSalesSettings(security_criteria, filters, grid);
|
||||
|
||||
public static class DefaultColumns {
|
||||
public static GridColumnSettings security => new GridColumnSettings(Columns.Security.col_id(), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
|
||||
public static GridColumnSettings time => new GridColumnSettings(Columns.Time.col_id(), DefaultColumnHeaders.Time, width: DefaultColumnWidths.TimeOfDay);
|
||||
public static GridColumnSettings price => new GridColumnSettings(Columns.Price.col_id(), DefaultColumnHeaders.Price, width: DefaultColumnWidths.Price, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings size => new GridColumnSettings(Columns.Size.col_id(), DefaultColumnHeaders.Size, width: DefaultColumnWidths.Size, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings aggressor => new GridColumnSettings(Columns.Aggressor.col_id(), DefaultColumnHeaders.Aggressor, width: DefaultColumnWidths.Aggressor);
|
||||
public static GridColumnSettings security_key => new GridColumnSettings(Columns.SecurityKey.col_id(), DefaultColumnHeaders.SecurityKey, is_visible: false, width: DefaultColumnWidths.SecurityKey);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(time.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return security;
|
||||
yield return time;
|
||||
yield return price;
|
||||
yield return size;
|
||||
yield return aggressor;
|
||||
yield return security_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
using bts.api;
|
||||
using bts.co2;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
using mbts.spark;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace mbts.spark {
|
||||
namespace bts.shell.time_and_sales {
|
||||
class TimeAndSalesModel : IDisposable {
|
||||
internal readonly SecurityCriteria security_criteria;
|
||||
|
||||
static readonly TimeSpan _poll_interval = TimeSpan.FromSeconds(0.4);
|
||||
Lazy<IObservable<IReadOnlyList<TradeInfo>>> _trades;
|
||||
IDisposable _dispose;
|
||||
|
||||
public TimeAndSalesModel(IBus bus, MDConsumer consumer, SecurityCriteria security_criteria) {
|
||||
this.security_criteria = security_criteria;
|
||||
var curr_channels = new Dictionary<ChannelKey, ChannelConsumer>();
|
||||
public TimeAndSalesModel(IBus bus, MDConsumer consumer, IObservable<SecurityCriteria> security_criteria) {
|
||||
var curr_channels = new Dictionary<ChannelKey, ChannelConsumer>();
|
||||
_trades = new Lazy<IObservable<IReadOnlyList<TradeInfo>>>(() => Observable.Interval(_poll_interval).Select(_ => {
|
||||
var copy = curr_channels;
|
||||
return copy.Values.SelectMany(c => c.read_trades()).ToList();
|
||||
}));
|
||||
_dispose = security_criteria.subscribe_md_map(bus).Subscribe(
|
||||
_dispose = security_criteria.SelectManyInSeries(sc =>sc.subscribe_md_map(bus)).Subscribe(
|
||||
md_map => {
|
||||
var copy = new Dictionary<ChannelKey, ChannelConsumer>(curr_channels);
|
||||
copy.sync_with_demux(md_map, get_channel_key, get_channel_consumer, update_channel_consumer);
|
||||
@@ -95,4 +94,4 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using bts.api;
|
||||
using bts.co2;
|
||||
using bts.utils;
|
||||
using bts.utils.csv;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace bts.shell.time_and_sales {
|
||||
class TimeAndSalesRowVM : ViewModelBase, DIYGrid.IRowVM, ISecurityRow, IEquatable<TimeAndSalesRowVM> {
|
||||
// key comparer guarentees deterministic sorting
|
||||
// if the rows in the collection have unique row_keys.
|
||||
public static readonly IComparer<TimeAndSalesRowVM> key_comparer =
|
||||
new ComparerBuilder<TimeAndSalesRowVM>(new[] {
|
||||
Comparer<DateTime>.Default.wrap<DateTime, TimeAndSalesRowVM>(r => r.time),
|
||||
Comparer<int>.Default.wrap<int, TimeAndSalesRowVM>(r => r.row_key)
|
||||
})
|
||||
.to_comparer()
|
||||
.to_safe();
|
||||
|
||||
readonly TradeMessageImmutable _trade;
|
||||
|
||||
public TimeAndSalesRowVM(int row_key, SecurityImmutable security, TradeMessageImmutable trade) {
|
||||
this.row_key = row_key;
|
||||
this.security = security;
|
||||
_trade = trade;
|
||||
trade_key = trade.Key;
|
||||
}
|
||||
|
||||
public event Action invalidated;
|
||||
|
||||
public int row_key { get; }
|
||||
public uint trade_key { get; }
|
||||
public uint security_key => security.Id;
|
||||
public SecurityImmutable security { get; }
|
||||
public DateTime time => ((ulong)_trade.Time).lldatetime().ToLocalTime();
|
||||
public double price => _trade.price();
|
||||
public uint size => _trade.Size;
|
||||
public AggressorSide aggressor_side => _trade.AggressorSide;
|
||||
|
||||
public void raise_invalidated() =>
|
||||
invalidated?.Invoke();
|
||||
|
||||
public static System.Collections.Specialized.OrderedDictionary csv_ordered_properties =>
|
||||
CsvHelper.create_ordering_maintainer(
|
||||
typeof(TimeAndSalesRowVM),
|
||||
new string[] {
|
||||
nameof(security),
|
||||
nameof(time),
|
||||
nameof(price),
|
||||
nameof(size),
|
||||
nameof(aggressor_side),
|
||||
nameof(security_key),
|
||||
});
|
||||
|
||||
public bool Equals(TimeAndSalesRowVM other) =>
|
||||
other != null &&
|
||||
row_key == other.row_key;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as TimeAndSalesRowVM);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
row_key;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +1,363 @@
|
||||
using bts.api;
|
||||
using bts.co2;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using static bts.shell.TimeAndSalesWindowFactory;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
|
||||
namespace mbts.spark {
|
||||
class TimeAndSalesVM : SecurityGrid<TimeAndSalesVM.Row, TradeInfo> {
|
||||
readonly IBus _bus;
|
||||
readonly TimeAndSalesModel _model;
|
||||
IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
namespace bts.shell.time_and_sales {
|
||||
class TimeAndSalesVM : DisposableViewModelBase, IHandleGridSorting, IHandleRowSelection {
|
||||
const int _max_row_count = 1000;
|
||||
static int _id_counter;
|
||||
static IReadOnlyList<Func<TimeAndSalesVM, IDataGridColumnFilter<TimeAndSalesRowVM>>> _filter_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<TimeAndSalesVM, IDataGridColumnFilter<TimeAndSalesRowVM>>();
|
||||
|
||||
readonly ColumnSortManager _sort_manager;
|
||||
readonly RowFactory _row_factory = new RowFactory();
|
||||
readonly LoggerContext _log;
|
||||
readonly IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
readonly DeferrableActionWrapper
|
||||
_refresh_filter,
|
||||
_refresh_sort,
|
||||
_refresh_selected_rows;
|
||||
IComparer<TimeAndSalesRowVM> _comparer_backing;
|
||||
Func<TimeAndSalesRowVM, bool> _filter_backing;
|
||||
ImmutableHashSet<TimeAndSalesRowVM> _selected_rows = ImmutableHashSet<TimeAndSalesRowVM>.Empty;
|
||||
ImmutableList<TimeAndSalesRowVM> _all_rows = ImmutableList<TimeAndSalesRowVM>.Empty;
|
||||
ImmutableList<TradeInfo> _buffered_trades;
|
||||
/// Although the sorting criteria can be obtained from the <see cref="_sort_manager"/> (<see cref="ColumnSortManager.SortingCriteria"/>),
|
||||
/// a local copy is kept because the settings collector may call <see cref="create"/> from a thread
|
||||
/// other than the dispatcher thread associated with this instance as a viewmodel,
|
||||
/// and the <see cref="ColumnSortManager.SortingCriteria"/> property is not thread-safe.
|
||||
/// Therefore, <see cref="create"/> returns this local copy, and thus avoids accessing the non-thread-safe property.
|
||||
ImmutableList<SortingCriteria> _sorting_criteria = ImmutableList<SortingCriteria>.Empty;
|
||||
TimeAndSalesRowVM _current_row;
|
||||
bool _is_paused;
|
||||
int
|
||||
_load_settings_count,
|
||||
_filter_change_count,
|
||||
_sort_change_count;
|
||||
|
||||
public TimeAndSalesVM(IBus bus, IWindowSettingsManager<GridSettings> settings_manager, TimeAndSalesModel model, StartupData startup_data)
|
||||
: base(RXCollection.CreateBuffer(model.trades, 1000), Enumerable.Empty<SortingCriteria>()) {
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
public TimeAndSalesVM(IWindowSettingsManager<GridSettings> settings_manager) {
|
||||
_sort_manager = new ColumnSortManager(columns.WrapSelector(c => c as ISortableDIYGridColumn, null), TimeAndSalesView.Columns.RowKey.ToString());
|
||||
_comparer_backing = _sort_manager.BuildComparer(_get_column_comparer);
|
||||
_log = new LoggerContext(Log.Ui, $"Time And Sales VM ({Interlocked.Increment(ref _id_counter)})", null);
|
||||
_settings_manager = settings_manager;
|
||||
show_ladder = DelegateCommandParamObserver.Create(current_row_observable, r => _show_ladder_async(r).ignore(), r => r != null);
|
||||
security_filter = new RegexFilterVM<Row>(r => r.security.ToString(), nameof(Row.security));
|
||||
if (startup_data.filters is FilterStates filters) {
|
||||
// deferrable filter/sort refresh
|
||||
_refresh_filter = new DeferrableActionWrapper(_refresh_filter_impl);
|
||||
_refresh_sort = new DeferrableActionWrapper(_refresh_sort_impl);
|
||||
_refresh_selected_rows = new DeferrableActionWrapper(() => selected_rows_changed?.Invoke(_selected_rows));
|
||||
// grid settings
|
||||
grid_settings_manager = new SimpleGridSettingsManager<TimeAndSalesRowVM>(columns, new Dictionary<string, GridColumnSettingsFeatureFlags> { }, rows);
|
||||
grid_settings_manager.load_from(settings_manager.current_settings());
|
||||
// Columns
|
||||
add_column(
|
||||
TimeAndSalesView.Columns.Security,
|
||||
r => r.security.ToString(),
|
||||
DefaultColumnHeaders.Security,
|
||||
DefaultColumnWidths.Security,
|
||||
security_filter,
|
||||
DataGridFilterKnownTemplates.Regex);
|
||||
add_column(
|
||||
TimeAndSalesView.Columns.Time,
|
||||
r => r.time.ToString("h:mm:ss tt"),
|
||||
DefaultColumnHeaders.Time,
|
||||
DefaultColumnWidths.Time,
|
||||
time_filter,
|
||||
DataGridFilterKnownTemplates.TimeFilter);
|
||||
add_column(
|
||||
TimeAndSalesView.Columns.Price,
|
||||
r => r.security.spec_or_default().format(r.price),
|
||||
DefaultColumnHeaders.Price,
|
||||
DefaultColumnWidths.Price,
|
||||
price_filter,
|
||||
DataGridFilterKnownTemplates.DoubleMinMax,
|
||||
System.Windows.HorizontalAlignment.Right);
|
||||
add_column(
|
||||
TimeAndSalesView.Columns.Size,
|
||||
r => r.size.ToString(),
|
||||
DefaultColumnHeaders.Size,
|
||||
DefaultColumnWidths.Size,
|
||||
size_filter,
|
||||
DataGridFilterKnownTemplates.UIntMinMax,
|
||||
System.Windows.HorizontalAlignment.Right);
|
||||
add_column(
|
||||
TimeAndSalesView.Columns.Aggressor,
|
||||
r => r.aggressor_side.ToString(),
|
||||
DefaultColumnHeaders.Aggressor,
|
||||
DefaultColumnWidths.Aggressor,
|
||||
aggressor_filter,
|
||||
DataGridFilterKnownTemplates.EnumValues);
|
||||
add_column(
|
||||
TimeAndSalesView.Columns.SecurityKey,
|
||||
r => r.security_key.ToString(),
|
||||
DefaultColumnHeaders.SecurityKey,
|
||||
DefaultColumnWidths.SecurityKey,
|
||||
security_key_filter,
|
||||
DataGridFilterKnownTemplates.Regex);
|
||||
|
||||
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager);
|
||||
// commands
|
||||
show_ladder_cmd = new DelegateCommand(() => {
|
||||
if (current_row != null)
|
||||
request_show_ladder?.Invoke(current_row);
|
||||
});
|
||||
// init IDisposable resources
|
||||
var resources = new List<IDisposable>();
|
||||
resources.AddRange(_filters.Select(f => {
|
||||
f.filter_changed += _refresh_filter.execute;
|
||||
return Disposable.Create(() => f.filter_changed -= _refresh_filter.execute);
|
||||
}));
|
||||
_sort_manager.active_sorting_changed += _refresh_sort.execute;
|
||||
resources.Add(Disposable.Create(() => _sort_manager.active_sorting_changed -= _refresh_sort.execute));
|
||||
_dispose = Disposable.Create(resources.ToArray());
|
||||
// local methods
|
||||
void add_column(
|
||||
TimeAndSalesView.Columns col_key,
|
||||
Func<TimeAndSalesRowVM, string> get_cell_text,
|
||||
string header,
|
||||
double width,
|
||||
IDataGridColumnFilter filter,
|
||||
string filter_template,
|
||||
System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
|
||||
columns.Add(new RenderTextColumn<TimeAndSalesRowVM, TimeAndSalesView.Columns>(
|
||||
col_key, get_cell_text, filter, filter_template, _get_aggressor_brush) {
|
||||
id = col_key.col_id(), Header = header, Width = width, horizontal_alignment = h_alignment
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public event Action<ImmutableHashSet<TimeAndSalesRowVM>> selected_rows_changed;
|
||||
public event Action<TimeAndSalesRowVM> current_row_changed, request_show_ladder;
|
||||
|
||||
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
|
||||
public TimeAndSalesRowVM current_row {
|
||||
get => _current_row;
|
||||
set {
|
||||
if (Set(ref _current_row, value))
|
||||
current_row_changed?.Invoke(value);
|
||||
}
|
||||
}
|
||||
public ObservableCollection<TimeAndSalesRowVM> rows { get; } = new ObservableCollection<TimeAndSalesRowVM>();
|
||||
public ImmutableHashSet<TimeAndSalesRowVM> selected_rows => _selected_rows;
|
||||
public int total_row_count => _all_rows.Count;
|
||||
public SimpleGridSettingsManager<TimeAndSalesRowVM> grid_settings_manager { get; }
|
||||
public bool is_paused {
|
||||
get => _is_paused;
|
||||
set {
|
||||
if (_is_paused != value) {
|
||||
_is_paused = value;
|
||||
if (value) {
|
||||
_buffered_trades = ImmutableList<TradeInfo>.Empty;
|
||||
} else {
|
||||
var resume = _buffered_trades;
|
||||
_buffered_trades = null;
|
||||
on_new_trades(resume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// filters
|
||||
public IDataGridColumnPersistentFilter<TimeAndSalesRowVM, TimeFilterTuple> time_filter { get; } = new TimeFilterVM<TimeAndSalesRowVM>(r => r.time);
|
||||
public IDataGridColumnPersistentFilter<TimeAndSalesRowVM, string> security_filter { get; } = new RegexFilterVM<TimeAndSalesRowVM>(r => r.security.ToString(), nameof(TimeAndSalesRowVM.security));
|
||||
public IDataGridColumnPersistentFilter<TimeAndSalesRowVM, DoubleMinMaxState> price_filter { get; } = new DoubleMinMax<TimeAndSalesRowVM>(r => r.price);
|
||||
public IDataGridColumnPersistentFilter<TimeAndSalesRowVM, UIntMinMaxState> size_filter { get; } = new UIntMinMax<TimeAndSalesRowVM>(r => r.size, nameof(TimeAndSalesRowVM.size));
|
||||
public IDataGridColumnPersistentFilter<TimeAndSalesRowVM, AggressorSide[]> aggressor_filter { get; } = DiscreteValuesFilterVM.Create<TimeAndSalesRowVM, AggressorSide>(EnumHelper.get_values<AggressorSide>().ToArray(), r => r.aggressor_side);
|
||||
public IDataGridColumnPersistentFilter<TimeAndSalesRowVM, string> security_key_filter { get; } = new RegexFilterVM<TimeAndSalesRowVM>(r => r.security_key.ToString());
|
||||
// commands
|
||||
public ICommand show_ladder_cmd { get; }
|
||||
|
||||
IComparer<TimeAndSalesRowVM> _comparer {
|
||||
get => _comparer_backing;
|
||||
set {
|
||||
if (value == null) {
|
||||
_log.warn("Comparer should not be set to null, will use the default comparer instead (based on trade time, then row key).");
|
||||
_comparer_backing = new ComparerBuilder<TimeAndSalesRowVM>(new[] {
|
||||
Comparer<DateTime>.Default.wrap<DateTime, TimeAndSalesRowVM>(r => r.time),
|
||||
Comparer<int>.Default.wrap<int, TimeAndSalesRowVM>(r => r.row_key)
|
||||
})
|
||||
.to_comparer()
|
||||
.to_safe();
|
||||
} else {
|
||||
_comparer_backing = value;
|
||||
}
|
||||
var sorted_rows = rows.ToArray();
|
||||
var snapshot = _selected_rows;
|
||||
Array.Sort(sorted_rows, _comparer_backing);
|
||||
using (_refresh_selected_rows.defer_execution()) {
|
||||
for (int i = 0; i < sorted_rows.Length; i++) {
|
||||
if (rows[i].row_key != sorted_rows[i].row_key)
|
||||
rows[i] = sorted_rows[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Func<TimeAndSalesRowVM, bool> _filter {
|
||||
get => _filter_backing;
|
||||
set {
|
||||
var old_filter = _filter_backing;
|
||||
var new_filter = value;
|
||||
_filter_backing = new_filter;
|
||||
if (new_filter != null) {
|
||||
for (int i = 0; i < rows.Count; i++) {
|
||||
if (!new_filter(rows[i]))
|
||||
rows.RemoveAt(i);
|
||||
}
|
||||
rows.RemoveWhere(r => !new_filter(r));
|
||||
if (old_filter != null) {
|
||||
foreach (var r in _all_rows) {
|
||||
if (!old_filter(r) && new_filter(r))
|
||||
rows.InsertSorted(r, _comparer);
|
||||
}
|
||||
}
|
||||
} else if (old_filter != null) {
|
||||
foreach (var r in _all_rows) {
|
||||
if (!old_filter(r))
|
||||
rows.InsertSorted(r, _comparer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<IDataGridColumnFilter<TimeAndSalesRowVM>> _filters =>
|
||||
_filter_getters.Select(fn => fn(this));
|
||||
|
||||
public (FilterStates filters, IReadOnlyList<SortingCriteria> sorting_criteria, GridSettings grid_settings) create() =>
|
||||
(_get_filter_states(), _sorting_criteria, grid_settings_manager.create());
|
||||
|
||||
public void load(FilterStates filters, IEnumerable<SortingCriteria> sorting_criteria, GridSettings grid_settings) {
|
||||
using (_refresh_filter.defer_execution())
|
||||
using (_refresh_sort.defer_execution()) {
|
||||
_log.info($"begin load settings ({++_load_settings_count})...");
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
grid_settings_manager.load_from(grid_settings);
|
||||
_sort_manager.SortingCriteria = sorting_criteria;
|
||||
// load filters
|
||||
time_filter.load_state(filters.time);
|
||||
security_filter.load_state(filters.security);
|
||||
price_filter.load_state(filters.price);
|
||||
size_filter.load_state(filters.size);
|
||||
aggressor_filter.load_state(filters.aggressor_sides);
|
||||
security_key_filter.load_state(filters.security_key);
|
||||
//
|
||||
timer.Stop();
|
||||
_log.info($"load settings ({_load_settings_count}) took {timer.ElapsedMilliseconds:0.00}ms.");
|
||||
}
|
||||
// Columns
|
||||
add_column(TimeAndSalesView.Columns.Security, DefaultColumnHeaders.Security, DefaultColumnWidths.Security, nameof(Row.security), security_filter, DataGridFilterKnownTemplates.Regex);
|
||||
add_column(TimeAndSalesView.Columns.Time, DefaultColumnHeaders.Time, DefaultColumnWidths.Time, nameof(Row.time), time_filter, DataGridFilterKnownTemplates.TimeFilter);
|
||||
add_column(TimeAndSalesView.Columns.Price, DefaultColumnHeaders.Price, DefaultColumnWidths.Price, nameof(Row.price), price_filter, DataGridFilterKnownTemplates.DoubleMinMax, System.Windows.HorizontalAlignment.Right);
|
||||
add_column(TimeAndSalesView.Columns.Size, DefaultColumnHeaders.Size, DefaultColumnWidths.Size, nameof(Row.size), size_filter, DataGridFilterKnownTemplates.UIntMinMax, System.Windows.HorizontalAlignment.Right);
|
||||
add_column(TimeAndSalesView.Columns.Aggressor, DefaultColumnHeaders.Aggressor, DefaultColumnWidths.Aggressor, nameof(Row.aggressor_side), aggressor_filter, DataGridFilterKnownTemplates.EnumValues);
|
||||
add_column(TimeAndSalesView.Columns.SecurityKey, DefaultColumnHeaders.SecurityKey, DefaultColumnWidths.SecurityKey, nameof(Row.security_key), security_key_filter, DataGridFilterKnownTemplates.Regex);
|
||||
void add_column(TimeAndSalesView.Columns col_key, string header, double width, string sort_member_path, IDataGridColumnFilter filter, string filter_template, System.Windows.HorizontalAlignment h_alignment = System.Windows.HorizontalAlignment.Left) =>
|
||||
columns.Add(new RenderTextColumn<Row, TimeAndSalesView.Columns>(col_key, r => r[col_key], filter, filter_template, r => _get_aggressor_brush(r))
|
||||
{ id = col_id(col_key), Header = header, Width = width, sort_member_path = sort_member_path, horizontal_alignment = h_alignment }
|
||||
);
|
||||
|
||||
// be sure to load GridSettings AFTER the columns collection has been populated, the grid settings manager looks columns by id to apply the appropriate settings,
|
||||
// if the collection is not populated, the column settings will not be applied
|
||||
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, new Dictionary<string, GridColumnSettingsFeatureFlags> { }, row_vms);
|
||||
grid_settings_manager.load_from(settings_manager.current_settings());
|
||||
_resources.AddRange(
|
||||
show_ladder,
|
||||
DataGridFilterHelper.hook_filters(
|
||||
rows_view, time_filter, security_filter, price_filter, size_filter, aggressor_filter, security_key_filter),
|
||||
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager)
|
||||
);
|
||||
rows_view.set_sort_descriptions(startup_data.sorting_criterion, columns);
|
||||
}
|
||||
|
||||
public ObservableCollection<DIYGrid.ColumnBase> columns { get; } = new ObservableCollection<DIYGrid.ColumnBase>();
|
||||
public DelegateCommandParamObserver<Row> show_ladder { get; }
|
||||
public SimpleGridSettingsManager<Row> grid_settings_manager { get; }
|
||||
|
||||
public GridSettings get_grid_save_state() {
|
||||
if (_settings_manager.instance_settings == null)
|
||||
return null;
|
||||
else
|
||||
return grid_settings_manager.create();
|
||||
public void on_new_trades(IReadOnlyList<TradeInfo> trades) {
|
||||
/// The total row count is limited by <see cref="_max_row_count"/>;
|
||||
/// trades are kept in a circular buffer (FIFO).
|
||||
/// If paused, just need to add trades to <see cref="_buffered_trades"/>.
|
||||
if (is_paused) {
|
||||
if (_buffered_trades == null)
|
||||
_buffered_trades = ImmutableList<TradeInfo>.Empty;
|
||||
if (trades.Count >= _max_row_count) {
|
||||
_buffered_trades = _buffered_trades
|
||||
.Clear()
|
||||
.AddRange(ReadOnlyListSubRangeWrapper.Create(trades, trades.Count - _max_row_count, _max_row_count));
|
||||
} else {
|
||||
int overflow_count = _buffered_trades.Count + trades.Count - _max_row_count;
|
||||
if (overflow_count > 0) {
|
||||
var rows_to_remove = _buffered_trades.WrapSubRange(0, overflow_count);
|
||||
_buffered_trades = _buffered_trades.RemoveRange(0, overflow_count);
|
||||
}
|
||||
_buffered_trades = _buffered_trades.AddRange(trades);
|
||||
}
|
||||
return;
|
||||
}
|
||||
/// If not paused, update in the normal fasion:
|
||||
/// add to <see cref="_all_rows"/> and <see cref="rows"/>
|
||||
if (trades.Count >= _max_row_count) {
|
||||
trades = ReadOnlyListSubRangeWrapper.Create(trades, trades.Count - _max_row_count, _max_row_count);
|
||||
_all_rows = _all_rows.Clear();
|
||||
rows.Clear();
|
||||
} else {
|
||||
int overflow_count = _all_rows.Count + trades.Count - _max_row_count;
|
||||
if (overflow_count > 0) {
|
||||
var rows_to_remove = _all_rows.WrapSubRange(0, overflow_count);
|
||||
_all_rows = _all_rows.RemoveRange(0, overflow_count);
|
||||
rows.RemoveSorted(rows_to_remove, _comparer);
|
||||
}
|
||||
}
|
||||
var all_rows_bd = _all_rows.ToBuilder();
|
||||
foreach (var t in trades) {
|
||||
var r = _row_factory.create(t.security, t.trade);
|
||||
all_rows_bd.Add(r);
|
||||
if (_filter_backing == null || _filter_backing(r))
|
||||
rows.InsertSorted(r, _comparer);
|
||||
}
|
||||
_all_rows = all_rows_bd.ToImmutable();
|
||||
RaisePropertyChanged(nameof(total_row_count));
|
||||
}
|
||||
|
||||
public StartupData create() =>
|
||||
new StartupData(_model.security_criteria, _get_filter_states(), get_grid_save_state(), rows_view.get_sorting_criteria(columns));
|
||||
public void on_sorting(object sender, DIYGridSortingEventArgs e) {
|
||||
try {
|
||||
if (e.Column is ISortableDIYGridColumn sc) {
|
||||
_log.info($"handling sort event for column {sc.id}.");
|
||||
_sort_manager.OnColumnClick(sc, KeyboardExtensions.IsShiftDown());
|
||||
var sorted_rows = rows.ToArray();
|
||||
var snapshot = _selected_rows;
|
||||
_refresh_sort_impl();
|
||||
} else {
|
||||
_log.warn($"ignoring sort event because column '{e.Column?.id}' does not implement {nameof(ISortableDIYGridColumn)}.");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
_log.error($"error handling sort event: {ex}.");
|
||||
}
|
||||
}
|
||||
|
||||
private Brush _get_aggressor_brush(Row r) {
|
||||
public void row_selection_changed(SelectionChangedEventArgs e) {
|
||||
var bd = _selected_rows.ToBuilder();
|
||||
if (e.AddedItems != null)
|
||||
bd.AddRange(e.AddedItems.OfType<TimeAndSalesRowVM>());
|
||||
if (e.RemovedItems != null)
|
||||
bd.removeRange(e.RemovedItems.OfType<TimeAndSalesRowVM>());
|
||||
_selected_rows = bd.ToImmutable();
|
||||
_refresh_selected_rows.execute();
|
||||
}
|
||||
|
||||
static IComparer<TimeAndSalesRowVM> _get_column_comparer(string column_name) =>
|
||||
_get_column_comparer(EnumHelper.parse<TimeAndSalesView.Columns>(column_name));
|
||||
|
||||
static IComparer<TimeAndSalesRowVM> _get_column_comparer(TimeAndSalesView.Columns column) {
|
||||
switch (column) {
|
||||
case TimeAndSalesView.Columns.Aggressor: return wrap(r => r.aggressor_side);
|
||||
case TimeAndSalesView.Columns.Time: return wrap(r => r.time);
|
||||
case TimeAndSalesView.Columns.Security: return wrap(r => r.security);
|
||||
case TimeAndSalesView.Columns.Price: return wrap(r => r.price);
|
||||
case TimeAndSalesView.Columns.Size: return wrap(r => r.size);
|
||||
case TimeAndSalesView.Columns.SecurityKey: return wrap(r => r.security_key);
|
||||
case TimeAndSalesView.Columns.RowKey: return wrap(r => r.row_key);
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(column, nameof(column));
|
||||
}
|
||||
IComparer<TimeAndSalesRowVM> wrap<T>(Func<TimeAndSalesRowVM, T> selector) =>
|
||||
Comparer<T>.Default.wrap(selector);
|
||||
}
|
||||
|
||||
Brush _get_aggressor_brush(TimeAndSalesRowVM r) {
|
||||
BTSColor c;
|
||||
Brush b;
|
||||
if (r.aggressor_side == AggressorSide.Buy) c = BTSColors.MarketData.bid;
|
||||
@@ -90,14 +367,6 @@ namespace mbts.spark {
|
||||
return b;
|
||||
}
|
||||
|
||||
#region Filters
|
||||
public IDataGridColumnPersistentFilter<Row, TimeFilterTuple> time_filter { get; } = new TimeFilterVM<Row>(r => r.time, nameof(Row.time));
|
||||
public IDataGridColumnPersistentFilter<Row, string> security_filter { get; }
|
||||
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> price_filter { get; } = new DoubleMinMax<Row>(r => r.price, nameof(Row.price));
|
||||
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> size_filter { get; } = new UIntMinMax<Row>(r => r.size, nameof(Row.size));
|
||||
public IDataGridColumnPersistentFilter<Row, AggressorSide[]> aggressor_filter { get; } = DiscreteValuesFilterVM.Create<Row, AggressorSide>(EnumHelper.get_values<AggressorSide>().ToArray(), r => r.aggressor_side, nameof(Row.aggressor_side));
|
||||
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
|
||||
|
||||
FilterStates _get_filter_states() =>
|
||||
new FilterStates {
|
||||
time = time_filter.get_state(),
|
||||
@@ -107,73 +376,35 @@ namespace mbts.spark {
|
||||
security_key = security_key_filter.get_state(),
|
||||
aggressor_sides = aggressor_filter.get_state()
|
||||
};
|
||||
#endregion
|
||||
|
||||
protected override IEnumerable<SortingCriteria> get_default_sorting() {
|
||||
yield return new SortingCriteria(nameof(Row.time), false);
|
||||
void _refresh_sort_impl() {
|
||||
_log.info($"begin change sort ({++_sort_change_count})...");
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
_comparer = _sort_manager.BuildComparer(_get_column_comparer, TimeAndSalesRowVM.key_comparer);
|
||||
_sorting_criteria = ImmutableList.CreateRange(_sort_manager.SortingCriteria);
|
||||
timer.Stop();
|
||||
_log.info($"sort change ({_sort_change_count}) took {timer.ElapsedMilliseconds:0.00}ms.");
|
||||
}
|
||||
|
||||
protected override Row row_factory(TradeInfo row_model) =>
|
||||
new Row(this, row_model.security, row_model.trade);
|
||||
|
||||
|
||||
async Task _show_ladder_async(Row row) {
|
||||
var smd_lookup = await SparkUtils.get_security_md_async(_bus, row.security.Id);
|
||||
if (smd_lookup.success) _bus.post(SparkTags.ShowLadderTag, smd_lookup.result);
|
||||
void _refresh_filter_impl() {
|
||||
_log.info($"begin change filter ({++_filter_change_count})...");
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
_filter = _get_filter_states().build_predicate();
|
||||
timer.Stop();
|
||||
_log.info($"filter change ({_filter_change_count}) took {timer.ElapsedMilliseconds:0.00}ms.");
|
||||
}
|
||||
|
||||
public class Row : SecurityRow, DIYGrid.IRowVM {
|
||||
readonly uint _trade_key;
|
||||
class RowFactory {
|
||||
// the row key must be unique to each row in the collection
|
||||
int _row_id_counter;
|
||||
|
||||
readonly string
|
||||
_security_text,
|
||||
_time_text,
|
||||
_price_text,
|
||||
_size_text,
|
||||
_aggressor_text,
|
||||
_trade_key_text,
|
||||
_security_key_text;
|
||||
public TimeAndSalesRowVM create(TradeInfo trade_info) =>
|
||||
new TimeAndSalesRowVM(_row_id_counter++, trade_info.security, trade_info.trade);
|
||||
|
||||
public Row(TimeAndSalesVM parent, SecurityImmutable security, TradeMessageImmutable trade) : base(security) {
|
||||
_trade_key = trade.Key;
|
||||
time = ((ulong)trade.Time).lldatetime().ToLocalTime();
|
||||
price = trade.price();
|
||||
size = trade.Size;
|
||||
aggressor_side = trade.AggressorSide;
|
||||
show_ladder = new DelegateCommand(() => parent._show_ladder_async(this).ignore());
|
||||
_security_text = security.ToString();
|
||||
_time_text = time.ToString("M/d h:mm:ss tt");
|
||||
_price_text = security.ContractSpec.format(price);
|
||||
_size_text = size.ToString();
|
||||
_aggressor_text = aggressor_side.ToString();
|
||||
_trade_key_text = _trade_key.ToString();
|
||||
_security_key_text = security_key.ToString();
|
||||
}
|
||||
|
||||
public event Action invalidated;
|
||||
|
||||
public uint trade_key => _trade_key;
|
||||
public DateTime time { get; }
|
||||
public double price { get; }
|
||||
public uint size { get; }
|
||||
public AggressorSide aggressor_side { get; }
|
||||
public ICommand show_ladder { get; }
|
||||
|
||||
public string this[TimeAndSalesView.Columns col] {
|
||||
get {
|
||||
switch (col) {
|
||||
case TimeAndSalesView.Columns.Security: return _security_text;
|
||||
case TimeAndSalesView.Columns.Time: return _time_text;
|
||||
case TimeAndSalesView.Columns.Price: return _price_text;
|
||||
case TimeAndSalesView.Columns.Size: return _size_text;
|
||||
case TimeAndSalesView.Columns.Aggressor: return _aggressor_text;
|
||||
case TimeAndSalesView.Columns.SecurityKey: return _security_key_text;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void raise_invalidated() => invalidated?.Invoke();
|
||||
public TimeAndSalesRowVM create(SecurityImmutable security, TradeMessageImmutable trade) =>
|
||||
new TimeAndSalesRowVM(_row_id_counter++, security, trade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<UserControl
|
||||
x:Class="mbts.ui.spark.TimeAndSalesView"
|
||||
x:Class="bts.shell.time_and_sales.TimeAndSalesView"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:tas="clr-namespace:bts.shell.time_and_sales"
|
||||
FocusManager.IsFocusScope="True"
|
||||
Background="{StaticResource CommonBackground}"
|
||||
>
|
||||
@@ -26,13 +26,14 @@
|
||||
x:Name="_grid"
|
||||
Grid.Row="0"
|
||||
Columns="{Binding columns}"
|
||||
Rows="{Binding rows_view}"
|
||||
Rows="{Binding rows}"
|
||||
CurrentRow="{Binding current_row}"
|
||||
SelectionHighlight="{Binding grid_settings_manager.selection_highlight, Mode=OneWay}"
|
||||
>
|
||||
>
|
||||
<c:DIYGrid.InputBindings>
|
||||
<MouseBinding MouseAction="LeftDoubleClick" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
|
||||
<KeyBinding Key="Return" Command="{x:Static c:DIYGrid.DefaultRowCommand}"/>
|
||||
<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding show_ladder_cmd}"/>
|
||||
<KeyBinding Key="Return" Command="{Binding show_ladder_cmd}"/>
|
||||
<KeyBinding Key="A" Modifiers="Ctrl" Command="{Binding select_all_cmd}"/>
|
||||
</c:DIYGrid.InputBindings>
|
||||
<c:DIYGrid.CommandBindings>
|
||||
<CommandBinding
|
||||
@@ -43,13 +44,18 @@
|
||||
</c:DIYGrid.CommandBindings>
|
||||
<c:DIYGrid.RowTemplate>
|
||||
<DataTemplate>
|
||||
<usp:TimeAndSalesRowView/>
|
||||
<tas:TimeAndSalesRowView/>
|
||||
</DataTemplate>
|
||||
</c:DIYGrid.RowTemplate>
|
||||
</c:DIYGrid>
|
||||
<StatusBar Grid.Row="1" DockPanel.Dock="Bottom">
|
||||
<StatusBarItem HorizontalAlignment="Right">
|
||||
<TextBlock Text=" "/>
|
||||
<TextBlock>
|
||||
<Run Text="Trades:"/>
|
||||
<Run Text="{Binding rows.Count, Mode=OneWay}" />
|
||||
<Run Text="of"/>
|
||||
<Run Text="{Binding total_row_count, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</Grid>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.controls;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
namespace bts.shell.time_and_sales {
|
||||
/// <summary>
|
||||
/// Interaction logic for TimeAndSalesView.xaml
|
||||
/// </summary>
|
||||
partial class TimeAndSalesView : UserControl {
|
||||
public enum Columns { Time, Security, Price, Size, OrderType, SecurityKey, Aggressor }
|
||||
public enum Columns { Time, Security, Price, Size, SecurityKey, Aggressor, RowKey }
|
||||
|
||||
public TimeAndSalesView() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesVM.Row, TimeAndSalesView.Columns> { }
|
||||
}
|
||||
class TimeAndSalesRowView : DIYGrid.HighPerformanceRow<TimeAndSalesRowVM> { }
|
||||
|
||||
static class TimeAndSalesViewColumnsExtensions {
|
||||
public static string col_id(this TimeAndSalesView.Columns column) =>
|
||||
column.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,125 +1,218 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.shell;
|
||||
using bts.api;
|
||||
using bts.shell.fills;
|
||||
using bts.spark;
|
||||
using bts.view;
|
||||
using bts.utils.csv;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
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.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using static bts.shell.TimeAndSalesWindowFactory;
|
||||
|
||||
namespace mbts.spark {
|
||||
class TimeAndSalesWindow : SparkWindow<StartupData>, ISharedWindow {
|
||||
namespace bts.shell.time_and_sales {
|
||||
class TimeAndSalesWindow : SparkWindow<TimeAndSalesSettings>, ISharedWindow {
|
||||
const string _window_type = "TimeAndSales", _window_type_display_name = "Time And Sales";
|
||||
readonly MDConsumer _consumer;
|
||||
readonly TimeAndSalesWindowFactory _factory;
|
||||
readonly IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
SingleValuePump<TimeAndSalesVM> _vm;
|
||||
readonly bool _can_pause_updates;
|
||||
TimeAndSalesVM _vm;
|
||||
TimeAndSalesModel _model;
|
||||
StartupData _state;
|
||||
SecurityCriteria _security_criteria_backing;
|
||||
TimeAndSalesSettings _state;
|
||||
TimeAndSalesView _view;
|
||||
SparkCommand[] _commands;
|
||||
SparkMenuDisplayVM _menu_display_vm;
|
||||
IDisposable _subscriptions;
|
||||
|
||||
public TimeAndSalesWindow(TimeAndSalesWindowFactory factory, IBus bus, MDConsumer consumer, StartupData state, ImageSource icon, IWindowSettingsManager<GridSettings> settings_manager)
|
||||
public TimeAndSalesWindow(TimeAndSalesWindowFactory factory, IBus bus, MDConsumer consumer, TimeAndSalesSettings state, ImageSource icon, IWindowSettingsManager<GridSettings> settings_manager, bool can_pause_updates)
|
||||
: base("Time And Sales", bus, factory, SparkCommands.ShowTimeAndSales.icon) {
|
||||
Log.assert(state != null);
|
||||
_consumer = consumer;
|
||||
_factory = factory;
|
||||
_state = state;
|
||||
_state = state ?? TimeAndSalesSettings.Default(new SecurityCriteria());
|
||||
if (_state.security_criteria == null)
|
||||
_state = _state.with(new SecurityCriteria());
|
||||
_security_criteria_backing = _state.security_criteria;
|
||||
_settings_manager = settings_manager;
|
||||
_can_pause_updates = can_pause_updates;
|
||||
thread_id = mbts.config.Configuration.get<bts.config.MultiThreadedWindows>().time_and_sales ? _window_type : null;
|
||||
Icon.Freeze();
|
||||
}
|
||||
|
||||
event Action<SecurityCriteria> _security_criteria_changed;
|
||||
|
||||
public override string window_type => _window_type;
|
||||
public override IEnumerable<SparkCommand> Commands => _commands;
|
||||
public override UIElement Content => _view;
|
||||
public override double MinWidth => StartupData.DefaultGridSettings.columns.Sum(gcs => gcs.width)
|
||||
public override double MinWidth => TimeAndSalesSettings.DefaultGridSettings.columns.Sum(gcs => gcs.width)
|
||||
+ SPARK_MENU_COLLAPSED_WIDTH
|
||||
+ LAUNCHER_BUTTON_BORDER_WIDTH
|
||||
+ EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH;
|
||||
public string thread_id { get; }
|
||||
public override StartupData SaveState => _state = _vm.value.create();
|
||||
public override TimeAndSalesSettings SaveState {
|
||||
get {
|
||||
_ensure_state(true);
|
||||
return _state;
|
||||
}
|
||||
}
|
||||
|
||||
SecurityCriteria _security_criteria {
|
||||
get => _security_criteria_backing;
|
||||
set {
|
||||
_security_criteria_backing = value;
|
||||
_state = null;
|
||||
_security_criteria_changed?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose() =>
|
||||
_dispose_resources();
|
||||
|
||||
public void thread_init() {
|
||||
_dispose_resources();
|
||||
_vm = new SingleValuePump<TimeAndSalesVM>();
|
||||
var connectable_builder = new ConnectableSeriesBuilder<TimeAndSalesVM>(_vm.observable);
|
||||
_ensure_state();
|
||||
if (!_state.use_defaults)
|
||||
_settings_manager.instance_settings = _state.grid;
|
||||
_menu_display_vm = new SparkMenuDisplayVM();
|
||||
_menu_display_vm.load_from(_state.menu_display);
|
||||
_vm = new TimeAndSalesVM(_settings_manager);
|
||||
var selected_rows_src = Observable.Create<ImmutableHashSet<TimeAndSalesRowVM>>(o => {
|
||||
_vm.selected_rows_changed += o.OnNext;
|
||||
return Disposable.Create(() => _vm.selected_rows_changed -= o.OnNext);
|
||||
});
|
||||
var current_row_src = Observable.Create<TimeAndSalesRowVM>(o => {
|
||||
if (_vm.current_row != null)
|
||||
o.OnNext(_vm.current_row);
|
||||
_vm.current_row_changed += o.OnNext;
|
||||
return Disposable.Create(() => _vm.current_row_changed -= o.OnNext);
|
||||
});
|
||||
var current_sec_key_src = current_row_src.Select(r => {
|
||||
var result = ImmutableHashSet<uint>.Empty;
|
||||
if (r != null)
|
||||
result = result.Add(r.security_key);
|
||||
return result;
|
||||
});
|
||||
var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
|
||||
var current_smd_source = connectable_builder.create(vm => vm.current_security_md_observable(_bus)).OnDispatcher();
|
||||
var selected_secs_source = connectable_builder.create(vm => vm.selected_securities_observable()).OnDispatcher();
|
||||
var security_criteria_changed = new Action<SecurityCriteria>(sc => { });
|
||||
SparkCommand toggle_is_paused_cmd = null;
|
||||
toggle_is_paused_cmd = new SparkCommandCommand(get_is_paused_cmd_title(), Icons.Pause, new DelegateCommand(() => {
|
||||
_vm.is_paused = !_vm.is_paused;
|
||||
toggle_is_paused_cmd.title = get_is_paused_cmd_title();
|
||||
})) {
|
||||
show_in_toolbar = _can_pause_updates,
|
||||
show_in_context_menu = _can_pause_updates
|
||||
};
|
||||
_commands = new SparkCommand[] {
|
||||
SparkCommands.ShowLadder(SparkCommands.KeyGestures.ShowLadder).complete(current_smd_source, md => md.HasValue, ConvertUtils.ValueOrDefault),
|
||||
SparkCommands.ShowOrderTicketFromSecurity(SparkCommands.KeyGestures.ShowOrderTicket).complete(current_smd_source, md => md.HasValue, ConvertUtils.ValueOrDefault),
|
||||
SparkCommands.ShowFills(SparkCommands.KeyGestures.ShowFills).complete(selected_secs_source, always_true, get_fills_startup_data),
|
||||
SparkCommands.ShowMarketView.complete(selected_secs_source, always_true, secs => secs.Count > 0 ? new SecurityCriteria(secs) : _state.security_criteria),
|
||||
SparkCommands.change_securities_command(() => (update_securities_cmd, _state.security_criteria)),
|
||||
// csv export
|
||||
new SparkCommandCommand("Export to csv", Icons.Export, FileDialogCommand.export_csv(() => _vm.rows.ToCsv(TimeAndSalesRowVM.csv_ordered_properties), "TaS")),
|
||||
// ladder
|
||||
SparkCommands.ShowLadderAutoLookup(SparkCommands.KeyGestures.ShowLadder)
|
||||
.complete(current_sec_key_src, sec_keys => sec_keys.Count > 0),
|
||||
// ticket
|
||||
SparkCommands.ShowOrderTicketAutoLookup(SparkCommands.KeyGestures.ShowOrderTicket)
|
||||
.complete(current_sec_key_src, sec_keys => sec_keys.Count > 0),
|
||||
// fills
|
||||
SparkCommands.ShowFills(SparkCommands.KeyGestures.ShowFills)
|
||||
.complete(selected_rows_src, always_true, get_fills_startup_data),
|
||||
// mview
|
||||
SparkCommands.ShowMarketView
|
||||
.complete(selected_rows_src, always_true, rows => rows.Count > 0 ? new SecurityCriteria(rows.Select(r => r.security)) : _security_criteria),
|
||||
// pause
|
||||
toggle_is_paused_cmd,
|
||||
// change securities
|
||||
SparkCommands.change_securities_command(() => (update_securities_cmd, _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(() => {
|
||||
var vm = new ShellSettingsDialogVM<GridSettings, SimpleGridSettingsVM>(
|
||||
_settings_manager,
|
||||
s => s.time_and_sales_grid,
|
||||
UserSettings.FileExtensions.GRID,
|
||||
s => _vm.value.grid_settings_manager.create_vm(
|
||||
s => _vm.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.value.columns.Select(c => c.create()))
|
||||
s.with(_vm.columns.Select(c => c.create()))
|
||||
)
|
||||
);
|
||||
vm.SubscribeDisposed(literal_title_source.OnDispatcher().Subscribe(t => vm.title = t));
|
||||
var dialog = new ShellSettingsDialog { DataContext = vm };
|
||||
vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog);
|
||||
return dialog;
|
||||
}))
|
||||
})),
|
||||
_menu_display_vm.spark_cmd
|
||||
};
|
||||
_subscriptions = Disposable.Create(
|
||||
_bus.subscribe(update_securities_cmd.tag).OnDispatcher().Subscribe(update_criteria),
|
||||
connectable_builder.connect_all());
|
||||
_initialize();
|
||||
|
||||
_vm.load(_state.filters, _state.sorting_criterion, _state.grid);
|
||||
_vm.request_show_ladder += show_ladder;
|
||||
var security_criteria_src = _bus.subscribe(update_securities_cmd.tag);
|
||||
var subs = new List<IDisposable> {
|
||||
security_criteria_src.Subscribe(sc => _security_criteria = sc),
|
||||
Disposable.Create(() => _vm.request_show_ladder -= show_ladder)
|
||||
};
|
||||
_model = new TimeAndSalesModel(_bus, _consumer, Observable.Create<SecurityCriteria>(o => {
|
||||
o.OnNext(_security_criteria);
|
||||
return security_criteria_src.Subscribe(o);
|
||||
}));
|
||||
subs.Add(_model.trades.OnDispatcher().Subscribe(_vm.on_new_trades));
|
||||
_subscriptions = Disposable.Create(subs.ToArray());
|
||||
if (_view == null)
|
||||
_view = new TimeAndSalesView();
|
||||
_view.DataContext = _vm;
|
||||
// local methods
|
||||
bool always_true<T>(T _) => true;
|
||||
FillsSettings get_fills_startup_data(IReadOnlyList<SecurityImmutable> secs) =>
|
||||
|
||||
FillsSettings get_fills_startup_data(ImmutableHashSet<TimeAndSalesRowVM> rows) =>
|
||||
new FillsSettings(new FillsFilterStates {
|
||||
security = secs.Count > 0 ? new SecurityCriteria(secs) : _state.security_criteria
|
||||
security = rows.Count > 0 ? new SecurityCriteria(rows.Select(r => r.security)) : _security_criteria
|
||||
});
|
||||
void update_criteria(SecurityCriteria c) {
|
||||
_state = _vm.value.create().with(c);
|
||||
_initialize();
|
||||
|
||||
string get_is_paused_cmd_title() =>
|
||||
$"{(_vm.is_paused ? "Resume" : "Pause")} Updates";
|
||||
|
||||
void show_ladder(TimeAndSalesRowVM row) {
|
||||
if (row != null)
|
||||
_bus.post(SparkTags.ShowLadderAutoLookupTag, ImmutableHashSet.Create(new[] { row.security_key }));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnParentViewCreated(SparkWindowView parent_view) =>
|
||||
_menu_display_vm.safe_bind_to_view(parent_view);
|
||||
|
||||
void _dispose_resources() {
|
||||
Disposable.dispose(ref _subscriptions);
|
||||
Disposable.dispose(ref _vm);
|
||||
Disposable.dispose(ref _model);
|
||||
if (_commands != null) {
|
||||
_commands.OfType<IDisposable>().DisposeAll();
|
||||
_commands = null;
|
||||
}
|
||||
Disposable.dispose(ref _model);
|
||||
Disposable.dispose(ref _vm);
|
||||
}
|
||||
|
||||
void _initialize() {
|
||||
Disposable.dispose(ref _model);
|
||||
_model = new TimeAndSalesModel(_bus, _consumer, _state.security_criteria);
|
||||
// the previous _vm.value is automatically disposed when _vm.value is set - no need to dispose it here
|
||||
_vm.value = new TimeAndSalesVM(_bus, _settings_manager, _model, _state);
|
||||
if (_view == null)
|
||||
_view = new TimeAndSalesView();
|
||||
_view.DataContext = _vm.value;
|
||||
void _ensure_state(bool force_recreate = false) {
|
||||
if (_state == null || force_recreate) {
|
||||
if (_vm == null) {
|
||||
_state = TimeAndSalesSettings.Default(_security_criteria);
|
||||
} else {
|
||||
var (filters, sorting, grid) = _vm.create();
|
||||
_state = new TimeAndSalesSettings(_security_criteria, filters, grid, sorting.ToList()) {
|
||||
use_defaults = _settings_manager.instance_settings == null
|
||||
};
|
||||
}
|
||||
// should be safe to assume _menu_display_vm and _vm will either both be null, or both be non null,
|
||||
// but redundant null checks don't hurt, and just may prevent a crash.
|
||||
if (_menu_display_vm != null)
|
||||
_state.menu_display = _menu_display_vm.safe_create();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
using bts.api;
|
||||
using bts.spark;
|
||||
using bts.shell.time_and_sales;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
using static mbts.ui.spark.TimeAndSalesView;
|
||||
using Columns = bts.shell.time_and_sales.TimeAndSalesView.Columns;
|
||||
using mbts.config;
|
||||
|
||||
namespace bts.shell {
|
||||
class TimeAndSalesWindowFactory : ShellWindowFactory<TimeAndSalesWindowFactory.StartupData> {
|
||||
class TimeAndSalesWindowFactory : ShellWindowFactory<TimeAndSalesSettings> {
|
||||
readonly IBus _bus;
|
||||
readonly MDConsumer _consumer;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly TimeAndSalesConfig _time_and_sales_conf;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
|
||||
|
||||
public TimeAndSalesWindowFactory(IShellModule module, IBus bus, MDConsumer consumer, UserSettingsHub settings_hub) : base(module, "TimeAndSales") {
|
||||
_bus = bus;
|
||||
_consumer = consumer;
|
||||
_settings_hub = settings_hub;
|
||||
|
||||
_time_and_sales_conf = new TimeAndSalesConfig(Configuration.get<bts.config.TimeAndSalesOptions>());
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
bus.subscribe(SparkTags.ShowTimeAndSalesTag).OnDispatcher().Subscribe(show)
|
||||
@@ -36,91 +35,21 @@ namespace bts.shell {
|
||||
|
||||
public static string col_id(Columns col) => col.ToString();
|
||||
|
||||
public override Task<IShellWindow> load(StartupData state) =>
|
||||
public override Task<IShellWindow> load(TimeAndSalesSettings state) =>
|
||||
Task.FromResult<IShellWindow>(new TimeAndSalesWindow(this, _bus, _consumer, state, SparkCommands.PickTimeAndSales.icon,
|
||||
new WindowSettingsManager<GridSettings>(_settings_hub, state.grid, s => s.time_and_sales_grid, (s, g) => s.with(time_and_sales:g))));
|
||||
new WindowSettingsManager<GridSettings>(_settings_hub, state.grid, s => s.time_and_sales_grid, (s, g) => s.with(time_and_sales:g)),
|
||||
_time_and_sales_conf.can_pause_updates));
|
||||
|
||||
public async void show(SecurityCriteria criteria) {
|
||||
var w = await load(new StartupData(criteria));
|
||||
var w = await load(new TimeAndSalesSettings(criteria));
|
||||
_bus.post(SparkTags.ShowWindow, w);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class StartupData {
|
||||
[ProtoMember(1)]
|
||||
public readonly SecurityCriteria security_criteria;
|
||||
[ProtoMember(2)]
|
||||
List<SortingCriteria> _sorting_criterion;
|
||||
[ProtoMember(3)]
|
||||
public FilterStates filters;
|
||||
[ProtoMember(4)]
|
||||
public GridSettings grid;
|
||||
readonly struct TimeAndSalesConfig {
|
||||
public readonly bool can_pause_updates;
|
||||
|
||||
public StartupData() { }
|
||||
|
||||
public StartupData(SecurityCriteria security_criteria) :
|
||||
this(security_criteria, new FilterStates(), null, null) { }
|
||||
|
||||
public StartupData(SecurityCriteria security_criteria, FilterStates filters, GridSettings grid = default, List<SortingCriteria> sorting_criterion = null) {
|
||||
this.security_criteria = security_criteria;
|
||||
this.filters = filters;
|
||||
this.grid = grid;
|
||||
_sorting_criterion = sorting_criterion;
|
||||
}
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public List<SortingCriteria> sorting_criterion => _sorting_criterion ?? (_sorting_criterion = DefaultColumns.default_sorting_criterion.ToList());
|
||||
|
||||
public static StartupData Default(SecurityCriteria security_criteria) =>
|
||||
new StartupData(security_criteria, new FilterStates(), null, DefaultColumns.default_sorting_criterion.ToList());
|
||||
|
||||
public StartupData with(SecurityCriteria security_criteria) =>
|
||||
new StartupData(security_criteria, filters, grid);
|
||||
|
||||
public static class DefaultColumns {
|
||||
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
|
||||
public static GridColumnSettings time => new GridColumnSettings(col_id(Columns.Time), DefaultColumnHeaders.Time, width: DefaultColumnWidths.Time);
|
||||
public static GridColumnSettings price => new GridColumnSettings(col_id(Columns.Price), DefaultColumnHeaders.Price, width: DefaultColumnWidths.Price, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings size => new GridColumnSettings(col_id(Columns.Size), DefaultColumnHeaders.Size, width: DefaultColumnWidths.Size, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings aggressor => new GridColumnSettings(col_id(Columns.Aggressor), DefaultColumnHeaders.Aggressor, width: DefaultColumnWidths.Aggressor);
|
||||
public static GridColumnSettings security_key => new GridColumnSettings(col_id(Columns.SecurityKey), DefaultColumnHeaders.SecurityKey, is_visible:false, width: DefaultColumnWidths.SecurityKey);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(time.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return security;
|
||||
yield return time;
|
||||
yield return price;
|
||||
yield return size;
|
||||
yield return aggressor;
|
||||
yield return security_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class FilterStates {
|
||||
[ProtoMember(1)]
|
||||
public TimeFilterTuple time;
|
||||
[ProtoMember(2)]
|
||||
public string security;
|
||||
[ProtoMember(3)]
|
||||
public DoubleMinMaxState price;
|
||||
[ProtoMember(4)]
|
||||
public UIntMinMaxState size;
|
||||
// [ProtoMember(5)]
|
||||
// ProtoMember 5 cannot be used further
|
||||
[ProtoMember(6)]
|
||||
public string security_key;
|
||||
[ProtoMember(7)]
|
||||
public co2.AggressorSide[] aggressor_sides;
|
||||
public TimeAndSalesConfig(bts.config.TimeAndSalesOptions options) =>
|
||||
can_pause_updates = options.can_pause_updates;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@
|
||||
<DataTemplate>
|
||||
<ComboBox SelectedItem="{Binding value}" ItemsSource="{u:EnumBindingSource {u:Nested sp:ConditionalQuoting.Types.PriceType}}" ItemTemplate="{StaticResource EnumDescriptionTemplate}"/>
|
||||
</DataTemplate>
|
||||
</usp:DataGridParameterCellTemplateConverter.ConditionalPriceTypeTemplate>
|
||||
</usp:DataGridParameterCellTemplateConverter.ConditionalPriceTypeTemplate>
|
||||
<usp:DataGridParameterCellTemplateConverter.QuantityMeasureTemplate>
|
||||
<DataTemplate>
|
||||
<Grid IsEnabled="{Binding enabled}">
|
||||
@@ -333,13 +333,61 @@
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ContractPickerButton" TargetType="Button">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="ToolTip" Value="Change Security"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="MouseStates">
|
||||
<VisualState Name="Normal"/>
|
||||
<VisualState Name="MouseOver">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="Bd" Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="#24FFFFFF"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState Name="Pressed">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="Bd" Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="#40FFFFFF"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Border.Background>
|
||||
<SolidColorBrush/>
|
||||
</Border.Background>
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<DataGridTextColumn x:Key="{x:Static usp:ExecutionStrategyEditor.AddColumnPlaceholderKey}" x:Shared="False" IsReadOnly="True">
|
||||
<DataGridTextColumn.Header>
|
||||
<c:PopupControl
|
||||
ShowExpanderArrow="False" PopupPlacement="Right" Header="Add Leg" Padding="6,0"
|
||||
ShowExpanderArrow="False"
|
||||
PopupPlacement="Right"
|
||||
Header="Add Leg"
|
||||
Padding="6,0"
|
||||
Background="{StaticResource Button.Static.BackgroundBrush}"
|
||||
IsOpen="{Binding IsNewLegPopupOpen, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}, Mode=TwoWay}"
|
||||
IsEnabled="{Binding CanUserAddNewLeg, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}}"
|
||||
VerticalAlignment="Center"
|
||||
>
|
||||
<msp:ContractPickerView
|
||||
CloseOnOK="False" CloseOnCancel="False"
|
||||
@@ -348,7 +396,7 @@
|
||||
</c:PopupControl>
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.RemovableColumnHeaderTemplateKey}">
|
||||
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.RemovableColumnHeaderTemplateKey}" DataType="{x:Type usp:LegSpreaderParamsTuple}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -356,13 +404,40 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Command="{Binding RemoveLegCommand, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}}"
|
||||
CommandParameter="{Binding}" Style="{StaticResource RoundCloseButton}"
|
||||
CommandParameter="{Binding}"
|
||||
Style="{StaticResource RoundCloseButton}"
|
||||
ToolTip="Remove Leg"
|
||||
/>
|
||||
<TextBlock Grid.Column="1" Margin="4,0,0,0" Text="{Binding Converter={StaticResource SecurityTextConverter}}"/>
|
||||
<c:ContractPickerPopup
|
||||
Grid.Column="1"
|
||||
BorderThickness="0"
|
||||
ButtonStyle="{StaticResource ContractPickerButton}"
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding security}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{Binding ContractPickerModel.all_securities, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}, Mode=OneWay}"
|
||||
SelectedSecurity="{Binding security}"
|
||||
ShowDialogSkirt="True"
|
||||
>
|
||||
<c:ContractPickerPopup.Background>
|
||||
<SolidColorBrush/>
|
||||
</c:ContractPickerPopup.Background>
|
||||
<c:ContractPickerPopup.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Foreground="{x:Static u:BTSColors.header_text_brush}" Text="{Binding}"/>
|
||||
</DataTemplate>
|
||||
</c:ContractPickerPopup.HeaderTemplate>
|
||||
</c:ContractPickerPopup>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.NonRemovableColumnHeaderTemplateKey}">
|
||||
<TextBlock Text="{Binding Converter={StaticResource SecurityTextConverter}}"/>
|
||||
<DataTemplate x:Key="ReadOnlyColumnHeaderTemplate" DataType="{x:Type usp:LegSpreaderParamsTuple}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding security}"
|
||||
/>
|
||||
</DataTemplate>
|
||||
<ControlTemplate x:Key="ExecutionStrategyEditorTemplate" TargetType="usp:ExecutionStrategyEditor">
|
||||
<Border
|
||||
@@ -560,6 +635,14 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Padding" Value="5,0"/>
|
||||
<Setter Property="MinHeight" Value="26"/>
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
<DataGrid.GroupStyle>
|
||||
<GroupStyle ContainerStyle="{StaticResource GroupItemStyle}"/>
|
||||
</DataGrid.GroupStyle>
|
||||
|
||||
@@ -227,8 +227,8 @@ namespace mbts.ui.spark {
|
||||
}
|
||||
|
||||
DataGridColumn _column_factory(LegSpreaderParamsTuple l) {
|
||||
var col = new DataGridColumnInternal(l.leg.Security, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter);
|
||||
col.HeaderTemplate = (DataTemplate)FindResource(IsNew ? RemovableColumnHeaderTemplateKey : NonRemovableColumnHeaderTemplateKey);
|
||||
var col = new DataGridColumnInternal(l, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter);
|
||||
col.HeaderTemplate = TryFindResource(IsNew ? RemovableColumnHeaderTemplateKey : NonRemovableColumnHeaderTemplateKey) as DataTemplate;
|
||||
DataGridEx.SetRightBorder(col, new DataGridExVerticalLine(false, true, Colors.DarkGray, 1));
|
||||
return col;
|
||||
}
|
||||
@@ -237,10 +237,12 @@ namespace mbts.ui.spark {
|
||||
|
||||
private class DataGridColumnInternal : DataGridDynamicColumn {
|
||||
readonly IValueConverter _cell_template_converter, _cell_editing_template_converter;
|
||||
readonly LegSpreaderParamsTuple _leg_tup;
|
||||
|
||||
public DataGridColumnInternal(Security security, IValueConverter cell_template_converter, IValueConverter cell_editing_template_converter) {
|
||||
Header = security;
|
||||
SetValue(DataGridEx.CellKeyProperty, security);
|
||||
public DataGridColumnInternal(LegSpreaderParamsTuple leg_tup, IValueConverter cell_template_converter, IValueConverter cell_editing_template_converter) {
|
||||
_leg_tup = leg_tup;
|
||||
Header = leg_tup;
|
||||
SetValue(DataGridEx.CellKeyProperty, leg_tup);
|
||||
_cell_template_converter = cell_template_converter;
|
||||
_cell_editing_template_converter = cell_editing_template_converter;
|
||||
}
|
||||
@@ -248,8 +250,8 @@ namespace mbts.ui.spark {
|
||||
// I decided this was an easy and simple enough solution to justify breaking the mvvm paradigm
|
||||
// The alternative - within strict bounds of the paradigm - is to change the DataGridParameterCellTemplateConverter into an IMultiValueConverter, bound to data_type AND read_only
|
||||
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) {
|
||||
if (dataItem is SyntheticEditorVM.RowVM row_vm) {
|
||||
if (row_vm.read_only || row_vm.get_cell(Header) is SyntheticEditorVM.ICell cell_vm && !cell_vm.enabled)
|
||||
if (dataItem is RowVM row_vm) {
|
||||
if (row_vm.read_only || row_vm.get_cell(_leg_tup) is ICell cell_vm && !cell_vm.enabled)
|
||||
return base.GenerateElement(cell, dataItem);
|
||||
}
|
||||
return base.GenerateEditingElement(cell, dataItem);
|
||||
@@ -352,4 +354,4 @@ namespace mbts.ui.spark {
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<UserControl
|
||||
x:Class="bts.shell.spreader.ContractLegTypeView"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:spr="clr-namespace:bts.shell.spreader"
|
||||
>
|
||||
<ItemsControl ItemsSource="{Binding Controller.flags, RelativeSource={RelativeSource AncestorType=spr:ContractLegTypeView}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Rows="1"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
Margin="2"
|
||||
IsChecked="{Binding is_selected}"
|
||||
Content="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeShortDescConverter}}"
|
||||
ToolTip="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeLongDescConverter}}"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,104 @@
|
||||
using bts.collections;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace bts.shell.spreader {
|
||||
static class EnumFlagItemsVM {
|
||||
public static EnumFlagItemsVM<T> create<T>(IEnumerable<T> flags) where T : struct =>
|
||||
new EnumFlagItemsVM<T>(flags);
|
||||
}
|
||||
|
||||
class EnumFlagItemsVM<T> : ViewModelBase where T : struct {
|
||||
readonly ObservableCollection<SelectableItemVM<T>> _flags = new ObservableCollection<SelectableItemVM<T>>();
|
||||
bool _is_busy;
|
||||
T _result;
|
||||
|
||||
public EnumFlagItemsVM()
|
||||
: this(EnumHelper.get_values<T>()) { }
|
||||
|
||||
public EnumFlagItemsVM(IEnumerable<T> flags) {
|
||||
this.flags = ObservableAutoWrapper.create_read_only_wrapper(_flags);
|
||||
_flags.AddRange(flags.Select(f => new SelectableItemVM<T>(f)));
|
||||
foreach (var vm in _flags) {
|
||||
vm.is_selected_changed += on_is_selected_changed;
|
||||
void on_is_selected_changed(bool value) {
|
||||
if (!_is_busy) {
|
||||
_is_busy = true;
|
||||
result = value ?
|
||||
EnumHelper.add_flags(result, vm.value) :
|
||||
EnumHelper.remove_flags(result, vm.value);
|
||||
_is_busy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
|
||||
public IReadOnlyObservableCollection<SelectableItemVM<T>> flags { get; }
|
||||
|
||||
public T result {
|
||||
get => _result;
|
||||
set {
|
||||
if (!EqualityComparer<T>.Default.Equals(_result, value)) {
|
||||
_result = value;
|
||||
if (!_is_busy) {
|
||||
_is_busy = true;
|
||||
for (int i = 0; i < flags.Count; i++)
|
||||
flags[i].is_selected = EnumHelper.has_flags(value, flags[i].value);
|
||||
_is_busy = false;
|
||||
}
|
||||
result_changed?.Invoke();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Interaction logic for ContractLegTypeView.xaml
|
||||
/// </summary>
|
||||
partial class ContractLegTypeView : UserControl {
|
||||
public static IValueConverter LegTypeShortDescConverter =>
|
||||
ValueConverter.create<ContractLegType, string>(SyntheticSecurityParser.display_name);
|
||||
|
||||
public static IValueConverter LegTypeLongDescConverter =>
|
||||
ValueConverter.create<ContractLegType, string>(lt =>
|
||||
string.Join(" & ", EnumHelper.split_flags(lt).Select(f => EnumHelper.get_display_name(f))));
|
||||
|
||||
public ContractLegTypeView() {
|
||||
InitializeComponent();
|
||||
Controller.result = Value;
|
||||
Controller.result_changed += _on_controller_result_changed;
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ValueProperty =
|
||||
WPFHelper.create_dp<ContractLegType, ContractLegTypeView>(
|
||||
nameof(Value),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => {
|
||||
o.Controller.result_changed -= o._on_controller_result_changed;
|
||||
o.Controller.result = v;
|
||||
o.Controller.result_changed += o._on_controller_result_changed;
|
||||
});
|
||||
|
||||
public ContractLegType Value {
|
||||
get => (ContractLegType)GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
public EnumFlagItemsVM<ContractLegType> Controller { get; } = EnumFlagItemsVM.create(EnumHelper.get_values_except(ContractLegType.None));
|
||||
|
||||
void _on_controller_result_changed() =>
|
||||
SetCurrentValue(ValueProperty, Controller.result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<Window
|
||||
x:Class="bts.shell.spreader.SyntheticImportConflictsDialog"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:spr="clr-namespace:bts.shell.spreader"
|
||||
u:WindowStyle.AllowResize="True"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
u:WindowStyle.TitleContent="Import Synthetics Conflicts"
|
||||
Style="{StaticResource BTSDialogTitleTemplate}"
|
||||
>
|
||||
<Window.MaxHeight>
|
||||
<Binding Path="SizeToContent" RelativeSource="{RelativeSource Self}">
|
||||
<Binding.Converter>
|
||||
<cv:WindowMaxHeightConverter SizeToContentThreshold="{u:DPIDouble 700}"/>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</Window.MaxHeight>
|
||||
<u:WindowStyle.TitleContentTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<Image Height="12" Margin="5,2" Source="{x:Static u:Icons.Import}"/>
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:WindowStyle.TitleContentTemplate>
|
||||
<Window.Content>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<spr:SyntheticImportConflictsView Margin="12,10,12,8"/>
|
||||
<c:DialogSkirt Grid.Row="1">
|
||||
<c:DialogSkirt.RightItems>
|
||||
<x:Array Type="{x:Type Button}">
|
||||
<Button Command="{Binding ok_cmd}">Continue</Button>
|
||||
<Button Command="{Binding cancel_cmd}" IsCancel="True">Cancel</Button>
|
||||
</x:Array>
|
||||
</c:DialogSkirt.RightItems>
|
||||
</c:DialogSkirt>
|
||||
</Grid>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace bts.shell.spreader {
|
||||
/// <summary>
|
||||
/// Interaction logic for SyntheticImportConflictsDialog.xaml
|
||||
/// </summary>
|
||||
partial class SyntheticImportConflictsDialog : Window {
|
||||
public SyntheticImportConflictsDialog() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using bts.api;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.user.settings;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.spreader {
|
||||
readonly struct SyntheticComponent {
|
||||
public readonly SyntheticContractComponent type;
|
||||
public readonly int leg_index;
|
||||
|
||||
public SyntheticComponent(SyntheticContractComponent type, int leg_index) {
|
||||
this.type = type;
|
||||
this.leg_index = leg_index;
|
||||
}
|
||||
}
|
||||
|
||||
class SyntheticImportConflictsVM : ImportConflictsVM<(SyntheticImportRowVM synth, ContractLegVM leg), SyntheticImportRowVM, SyntheticContractComponent>, IRequestViewClose {
|
||||
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
|
||||
|
||||
public SyntheticImportConflictsVM(int num_synthetics)
|
||||
: base(num_synthetics, tup => tup.synth) { }
|
||||
|
||||
// misc items sources
|
||||
public IReadOnlyDictionary<uint, SecurityMD> security_map {
|
||||
get => _security_map;
|
||||
set {
|
||||
if (!ReferenceEquals(_security_map, value)) {
|
||||
_security_map = value;
|
||||
_resolve_securities();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _resolve_securities() {
|
||||
if (_security_map == null)
|
||||
return;
|
||||
var reverse_lookup = SyntheticSecurityParser.reverse_lookup(MappedDictionary.create(_security_map, v => v.security.ToString()));
|
||||
foreach (var r in conflicts) {
|
||||
if (r.component == SyntheticContractComponent.Security && r.has_error) {
|
||||
var id = reverse_lookup(r.text);
|
||||
if (id != 0 && _security_map.TryGetValue(id, out var smd))
|
||||
r.value = smd.security;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyntheticImportConflictRowVM : ImportConflictRowVM<(SyntheticImportRowVM synth, ContractLegVM leg), SyntheticContractComponent>, ITemplateSelectorKey {
|
||||
public SyntheticImportConflictRowVM(
|
||||
SyntheticContractComponent component,
|
||||
string text,
|
||||
IReadOnlyCollection<(SyntheticImportRowVM synth, ContractLegVM leg)> affected_parts,
|
||||
object error_value,
|
||||
object value)
|
||||
: base(component, text, affected_parts, error_value, value, affected_parts.DistinctOn(tup => tup.synth).Count()) { }
|
||||
|
||||
public override void on_conflicts_resolved() {
|
||||
if (has_error)
|
||||
return;
|
||||
foreach (var (synth, leg) in affected_parts) {
|
||||
switch (component) {
|
||||
case SyntheticContractComponent.Name:
|
||||
if (value is string str)
|
||||
synth.name = str;
|
||||
break;
|
||||
case SyntheticContractComponent.TickSize:
|
||||
if (value is double ts && ts.IsNonZero()) {
|
||||
synth.tick_factors = TickUtils.get_regional_ticks(new ContractSpecificationImmutable(new ContractSpecification {
|
||||
LowTick = ts,
|
||||
HighTick = ts
|
||||
})).low_tick;
|
||||
} else if (value is TickFactors tf && tf.is_valid())
|
||||
synth.tick_factors = tf;
|
||||
break;
|
||||
case SyntheticContractComponent.Security:
|
||||
if (value is SecurityImmutable sec && sec.is_exchange_security())
|
||||
leg.security = sec;
|
||||
break;
|
||||
case SyntheticContractComponent.PriceMultiplier:
|
||||
if (value is double pm && pm.IsNonZero())
|
||||
leg.price_multiplier = pm;
|
||||
break;
|
||||
case SyntheticContractComponent.Quantity:
|
||||
if (value is double q && q.IsNonZero())
|
||||
leg.quantity = q;
|
||||
break;
|
||||
case SyntheticContractComponent.LegType:
|
||||
if (value is ContractLegType lt)
|
||||
leg.leg_type = lt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<UserControl
|
||||
x:Class="bts.shell.spreader.SyntheticImportConflictsView"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:spr="clr-namespace:bts.shell.spreader"
|
||||
MinWidth="400"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="VerticallyCenteredText" TargetType="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<!--Value Cell Template Selector-->
|
||||
<u:KeyedTemplateSelector x:Key="ValueCellTemplateSelector">
|
||||
<u:KeyedTemplateSelector.Default>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding value}" VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
</u:KeyedTemplateSelector.Default>
|
||||
<u:TemplateSelectorKeyValue Key="PriceMultiplier">
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding value, Converter={StaticResource DoubleTextConverter}}" VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="Quantity">
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding value, Converter={StaticResource DoubleTextConverter}}" VerticalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="Security">
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding value, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="LegType">
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeShortDescConverter}}">
|
||||
<TextBlock.ToolTip>
|
||||
<TextBlock>
|
||||
<Run Text="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeLongDescConverter}}"/>
|
||||
<LineBreak/>
|
||||
<Run Text="Q = Quote"/>
|
||||
<LineBreak/>
|
||||
<Run Text="H = Hedge"/>
|
||||
<LineBreak/>
|
||||
<Run Text="N = None"/>
|
||||
</TextBlock>
|
||||
</TextBlock.ToolTip>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</u:KeyedTemplateSelector>
|
||||
<!--Value Cell Editing Template Selector-->
|
||||
<u:KeyedTemplateSelector x:Key="ValueCellEditingTemplateSelector">
|
||||
<u:TemplateSelectorKeyValue Key="Security">
|
||||
<DataTemplate>
|
||||
<c:ContractPickerPopup
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding value, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{Binding DataContext.security_map, RelativeSource={RelativeSource AncestorType=DataGrid}, Mode=OneWay}"
|
||||
SelectedSecurity="{Binding value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ShowDialogSkirt="True"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="Name">
|
||||
<DataTemplate>
|
||||
<TextBox u:Behavior.FocusOnLoaded="True" Text="{Binding value}"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="TickSize">
|
||||
<DataTemplate>
|
||||
<c:TickUpDownDouble Value="{Binding value, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="PriceMultiplier">
|
||||
<DataTemplate>
|
||||
<c:TickUpDownDouble Value="{Binding value}" DisplayConverter="{StaticResource DoubleTextConverter}"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="Quantity">
|
||||
<DataTemplate>
|
||||
<c:TickUpDownDouble Value="{Binding value}" DisplayConverter="{StaticResource DoubleTextConverter}"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="LegType">
|
||||
<DataTemplate>
|
||||
<spr:ContractLegTypeView Value="{Binding value}"/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</u:KeyedTemplateSelector>
|
||||
</UserControl.Resources>
|
||||
<!--Content-->
|
||||
<Grid x:Name="content_root">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image VerticalAlignment="Center" Margin="0,0,8,0">
|
||||
<Image.Source>
|
||||
<Binding Source="{x:Static MessageBoxImage.Exclamation}">
|
||||
<Binding.Converter>
|
||||
<cv:SystemIconConverter/>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
<Canvas x:Name="text_canvas" Grid.Column="1" HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Column="1" TextWrapping="Wrap" Width="{Binding ActualWidth, ElementName=text_canvas}">
|
||||
<Run Text="{Binding num_successfully_parsed_rows, Mode=OneWay}"/>
|
||||
<Run Text="/"/>
|
||||
<Run Text="{Binding num_rows, Mode=OneWay}"/>
|
||||
<Run Text=" synthetics were successfully converted."/>
|
||||
<Run Text="Resolve conflicts below by selecting a value to assign each affected synthetic,"/>
|
||||
<Run Text="or modify individual synthetics in the main window."/>
|
||||
<LineBreak/>
|
||||
<LineBreak/>
|
||||
<Run Text="Click 'Continue' to proceed to the main window."/>
|
||||
</TextBlock>
|
||||
<DataGrid
|
||||
x:Name="data_grid"
|
||||
u:DataGridBehavior.SingleClickEdit="True"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserSortColumns="True"
|
||||
HeadersVisibility="All"
|
||||
ItemsSource="{Binding conflicts_view}"
|
||||
Margin="0,8,0,0"
|
||||
MinRowHeight="17"
|
||||
RowHeaderWidth="22"
|
||||
SelectionMode="Single"
|
||||
>
|
||||
<DataGrid.RowValidationErrorTemplate>
|
||||
<ControlTemplate>
|
||||
<Grid Background="Green">
|
||||
<Border x:Name="exclamation" HorizontalAlignment="Center">
|
||||
<Image Height="12" VerticalAlignment="Center" Source="{x:Static u:Icons.WarningExclamation}"/>
|
||||
</Border>
|
||||
<ContentControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ToolTip="{Binding (Validation.Errors), RelativeSource={RelativeSource AncestorType=DataGridRow}}">
|
||||
<ContentControl.Template>
|
||||
<ControlTemplate TargetType="ContentControl">
|
||||
<Grid Background="Transparent">
|
||||
<Grid.ToolTip>
|
||||
<ToolTip Content="{Binding ToolTip, RelativeSource={RelativeSource TemplatedParent}}">
|
||||
<ToolTip.ContentTemplate>
|
||||
<DataTemplate>
|
||||
<ItemsControl ItemsSource="{Binding}" DisplayMemberPath="ErrorContent"/>
|
||||
</DataTemplate>
|
||||
</ToolTip.ContentTemplate>
|
||||
</ToolTip>
|
||||
</Grid.ToolTip>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</ContentControl.Template>
|
||||
</ContentControl>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</DataGrid.RowValidationErrorTemplate>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell" BasedOn="{StaticResource DataGridCellBaseStyle}">
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="ValidationErrorTemplate" Value="{Binding RowValidationErrorTemplate, RelativeSource={RelativeSource AncestorType=DataGrid}}"/>
|
||||
<Setter Property="IsSelected" Value="{Binding is_selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource SelectedRowBackground}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Component" Binding="{Binding component, Converter={StaticResource EnumDescriptionConverter}}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Text" Binding="{Binding text}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Affected Synthetics" Binding="{Binding num_affected_rows}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
|
||||
<DataGridTemplateColumn Header="Value" CellTemplateSelector="{StaticResource ValueCellTemplateSelector}" CellEditingTemplateSelector="{StaticResource ValueCellEditingTemplateSelector}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace bts.shell.spreader {
|
||||
enum SyntheticContractComponent {
|
||||
Name,
|
||||
TickSize,
|
||||
Security,
|
||||
PriceMultiplier,
|
||||
Quantity,
|
||||
LegType
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for SyntheticImportConflictsView.xaml
|
||||
/// </summary>
|
||||
partial class SyntheticImportConflictsView : UserControl {
|
||||
public SyntheticImportConflictsView() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Window
|
||||
x:Class="bts.shell.spreader.SyntheticImportDialog"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:spr="clr-namespace:bts.shell.spreader"
|
||||
u:WindowStyle.AllowResize="True"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
u:WindowStyle.TitleContent="Import Synthetics"
|
||||
Style="{StaticResource BTSDialogTitleTemplate}"
|
||||
Content="{Binding}"
|
||||
>
|
||||
<Window.MaxHeight>
|
||||
<Binding Path="SizeToContent" RelativeSource="{RelativeSource Self}">
|
||||
<Binding.Converter>
|
||||
<cv:WindowMaxHeightConverter SizeToContentThreshold="{u:DPIDouble 700}"/>
|
||||
</Binding.Converter>
|
||||
</Binding>
|
||||
</Window.MaxHeight>
|
||||
<u:WindowStyle.TitleContentTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<Image Height="12" Margin="5,2" Source="{x:Static u:Icons.Import}"/>
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:WindowStyle.TitleContentTemplate>
|
||||
<Window.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type spr:SyntheticImportVM}">
|
||||
<AdornerDecorator>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<spr:SyntheticImportView/>
|
||||
<c:DialogSkirtOKCancel Grid.Row="1" OKCommand="{Binding ok_cmd}" CancelCommand="{Binding cancel_cmd}"/>
|
||||
</Grid>
|
||||
</AdornerDecorator>
|
||||
</DataTemplate>
|
||||
</Window.ContentTemplate>
|
||||
</Window>
|
||||