Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8766089f0 | ||
|
|
18d3c268de | ||
|
|
fdf255a7a3 | ||
|
|
995be6cfd0 | ||
|
|
96c7cc1528 | ||
|
|
01113e645a | ||
|
|
17c57878e1 | ||
|
|
4357fbc410 | ||
|
|
7d216978a3 | ||
|
|
fc3d84c02a | ||
|
|
f6fd9155b1 | ||
|
|
7723f1e004 | ||
|
|
041a7988c1 | ||
|
|
daa9800af2 | ||
|
|
dcb10a3484 | ||
|
|
70e826b212 | ||
|
|
af737564aa | ||
|
|
582f97d1ce | ||
|
|
a83c1eb633 | ||
|
|
1f893c73ad | ||
|
|
ef755ea6fb | ||
|
|
ebfc1bc485 | ||
|
|
111767522f | ||
|
|
429e16996a | ||
|
|
1ed85af40e | ||
|
|
7465a38022 | ||
|
|
8afa03cc62 | ||
|
|
fe54d5b973 | ||
|
|
d573688064 | ||
|
|
14c601034a | ||
|
|
4955c1a792 | ||
|
|
2990710b88 | ||
|
|
280ae81860 | ||
|
|
5e713d9008 | ||
|
|
763c2fa41a | ||
|
|
9d170e32f1 | ||
|
|
b4410f7412 | ||
|
|
b702ce9479 | ||
|
|
abc7b1e2e9 | ||
|
|
f71ff0e306 | ||
|
|
f3ac92c434 | ||
|
|
a8694d6fb2 | ||
|
|
52e6536018 | ||
|
|
42274edbc4 | ||
|
|
6d438b4764 | ||
|
|
3688ec0739 | ||
|
|
ce8d27bb71 | ||
|
|
ee66ddf9a6 | ||
|
|
bea0e5ced7 | ||
|
|
b2d1d11826 | ||
|
|
d075f2ad0c | ||
|
|
409bbdc0f9 | ||
|
|
d4fd3900be | ||
|
|
e45736a692 | ||
|
|
bd8336e22b | ||
|
|
f3dedfc4e3 | ||
|
|
326bcb4544 | ||
|
|
884cdef97b | ||
|
|
5e3467e3b6 | ||
|
|
979c6e078a | ||
|
|
69f38daad7 | ||
|
|
a64d428a89 | ||
|
|
78c89c6677 | ||
|
|
8d0d678c01 | ||
|
|
8b9df305b4 | ||
|
|
02ee5cc6d3 | ||
|
|
207c93c972 | ||
|
|
14012a16da | ||
|
|
dc264e1063 | ||
|
|
2e883808b6 | ||
|
|
184a900eea | ||
|
|
3bf086c73a | ||
|
|
9838d123c7 | ||
|
|
9d5b80c990 | ||
|
|
040fc3af96 | ||
|
|
cc07e28aca | ||
|
|
d2aec0c7f2 | ||
|
|
a71953b871 | ||
|
|
70e962000f | ||
|
|
f413938943 | ||
|
|
a7250b4734 | ||
|
|
060b88e4c9 | ||
|
|
ae200b374b | ||
|
|
64b94c58e7 | ||
|
|
8e907d7395 | ||
|
|
c567283d3f | ||
|
|
6b84dc433a | ||
|
|
a833944ec5 | ||
|
|
97d16115e8 | ||
|
|
2e3957a7ac | ||
|
|
b866157bff | ||
|
|
8e9c7383b6 | ||
|
|
930f3b23b8 | ||
|
|
d3fb52246c | ||
|
|
e173fe5515 | ||
|
|
dbe02e44fd | ||
|
|
7453540140 | ||
|
|
9830dcbe31 | ||
|
|
69bc734b6a | ||
|
|
2e69966d44 | ||
|
|
c2070b8e52 | ||
|
|
64e2408b59 | ||
|
|
17a8d47452 | ||
|
|
b4eabf4eb8 | ||
|
|
f12c1d5e74 | ||
|
|
714dfd5038 | ||
|
|
6dfe9b3892 | ||
|
|
15e557a209 | ||
|
|
8075bafb86 | ||
|
|
e2f4a86fcf | ||
|
|
95ae7b780e | ||
|
|
0644d301b2 | ||
|
|
7e68dc4ff7 | ||
|
|
879963b828 | ||
|
|
5320e16088 | ||
|
|
6116e24df0 | ||
|
|
6ba1de8c5c | ||
|
|
0d66054e8e | ||
|
|
daf39324fd | ||
|
|
ab9d3cede3 | ||
|
|
7429c42de2 | ||
|
|
9147b4605a | ||
|
|
6fc8a6c7fe | ||
|
|
b537c0c5bd | ||
|
|
400ae269e3 | ||
|
|
30cd749d7c | ||
|
|
efbf45167c | ||
|
|
913b81b8b6 | ||
|
|
606d37744f | ||
|
|
8a4d3d4c97 | ||
|
|
7cbf0dc683 | ||
|
|
e05ce79abf | ||
|
|
2ea95e7938 | ||
|
|
641141810e | ||
|
|
65c4c23b22 | ||
|
|
54dde449ce | ||
|
|
c206d38982 | ||
|
|
72a1bddbd2 | ||
|
|
8ee63d7b8e | ||
|
|
73cd8d3f26 | ||
|
|
86a87c501c | ||
|
|
4705368097 | ||
|
|
c3ac2957d2 | ||
|
|
950509e5d6 | ||
|
|
9002ca6183 | ||
|
|
b36715a6a0 | ||
|
|
2305c3af32 | ||
|
|
034aaa5b98 | ||
|
|
cbbd678112 | ||
|
|
d62a2ea9d4 | ||
|
|
6259f8b03f | ||
|
|
3866e63a70 | ||
|
|
877e0192a1 | ||
|
|
6d3b7305df | ||
|
|
aa975ea1c5 | ||
|
|
49445c6871 | ||
|
|
ef7c3c70da | ||
|
|
404a193068 | ||
|
|
42a59c9c92 | ||
|
|
809bd16e99 | ||
|
|
04446df8cc | ||
|
|
6a1a2d5f0a | ||
|
|
d998d3ccbb | ||
|
|
62892f1fb9 | ||
|
|
8e75c80e02 | ||
|
|
b90f6cb45c | ||
|
|
1cc5a77d7f | ||
|
|
fb14a9c3af | ||
|
|
1a2a82eda3 | ||
|
|
71147d038c | ||
|
|
e82db39f8c | ||
|
|
c3292d2f60 | ||
|
|
e749a24a77 | ||
|
|
b706c36edb | ||
|
|
7823972bfd | ||
|
|
f958ee19ad | ||
|
|
dffb9c5320 | ||
|
|
63cbd74744 | ||
|
|
fc78475037 | ||
|
|
3ef42a453a | ||
|
|
469acd23a5 | ||
|
|
d2a0efebd3 | ||
|
|
5cde9d55a7 | ||
|
|
ddd08cb036 | ||
|
|
5bb064197f | ||
|
|
2417c6b57f | ||
|
|
a1addee1b3 | ||
|
|
2e1bd765f6 | ||
|
|
d108ff3151 | ||
|
|
0f7cae6de7 | ||
|
|
2be4eb1c5c | ||
|
|
1ff84f26d2 | ||
|
|
175204efdf | ||
|
|
dbd8a7d742 | ||
|
|
8894aea1ad | ||
|
|
e45123fd76 | ||
|
|
4924a873a2 | ||
|
|
f0ba6b72fc | ||
|
|
5e6a0799d2 | ||
|
|
bea84c03f7 | ||
|
|
869faa5c6c | ||
|
|
43e82fb8c4 | ||
|
|
141c28ed78 | ||
|
|
4d9bce82ab | ||
|
|
8428423ff9 | ||
|
|
fa18cf5aba | ||
|
|
de11e00d89 | ||
|
|
8feab27332 | ||
|
|
8c26ffd5d3 | ||
|
|
dad7d030bc | ||
|
|
7e7af8f606 | ||
|
|
d0647fb8d9 | ||
|
|
bb2564fb0a | ||
|
|
6b0e8b6600 | ||
|
|
9e768b7a24 | ||
|
|
a3774c8d01 | ||
|
|
59a88f76a8 | ||
|
|
24d8929e88 | ||
|
|
dcaae15e28 | ||
|
|
a94d753fdc | ||
|
|
bbd95db97b | ||
|
|
a048874734 | ||
|
|
6b24745ae8 | ||
|
|
a981965fd9 | ||
|
|
c4910f011f | ||
|
|
0c2593cb20 | ||
|
|
37b4657023 | ||
|
|
a608b5b99d | ||
|
|
b1fb6ed06a | ||
|
|
c88db07474 | ||
|
|
dbea159fa9 | ||
|
|
b33b671342 | ||
|
|
70570164c7 | ||
|
|
9338328b1f | ||
|
|
70db5ceb10 | ||
|
|
e07d602cf5 | ||
|
|
077505509a | ||
|
|
fd85f0507c | ||
|
|
24f0b48f9d | ||
|
|
30ed5b296e | ||
|
|
b7a620d30e | ||
|
|
dcee2930f5 | ||
|
|
1b2aeee069 | ||
|
|
c4393637e7 | ||
|
|
04dadc46d5 | ||
|
|
b64cd3b031 | ||
|
|
5e1c4143db | ||
|
|
ba4b6aa1ff | ||
|
|
ad8e803a8c | ||
|
|
601eec3ddb | ||
|
|
38edcb27f1 | ||
|
|
e1b638a4b6 | ||
|
|
b80fe1e748 | ||
|
|
0419d084f4 | ||
|
|
f26faa5676 | ||
|
|
09d4d1f907 | ||
|
|
bdbf7e53e8 | ||
|
|
9c5ddba7f7 | ||
|
|
91dab2c841 | ||
|
|
8cfca8e40e | ||
|
|
9cb619d374 | ||
|
|
fba8233c63 | ||
|
|
631158ebbc | ||
|
|
5f12e2f1b2 | ||
|
|
807f8b5876 | ||
|
|
e6a9c7e433 | ||
|
|
096787d710 | ||
|
|
0468fe4f4d | ||
|
|
69b75c5f5e | ||
|
|
5626614e2d | ||
|
|
67d0a0351a | ||
|
|
97925a82d4 | ||
|
|
0bdd2dfca0 | ||
|
|
a0c572ef2e | ||
|
|
b5c24880a1 | ||
|
|
31c8a50200 | ||
|
|
0219dd2013 | ||
|
|
91cb215b09 | ||
|
|
4446ef0f57 | ||
|
|
41d662b3ba | ||
|
|
05d0eee8c7 | ||
|
|
93abf315a3 | ||
|
|
0d706efb10 | ||
|
|
73fe142904 | ||
|
|
8668c54784 | ||
|
|
f29e04751d | ||
|
|
f9f64c23c1 | ||
|
|
d3e9f15e99 | ||
|
|
112a5703e0 | ||
|
|
6dba376e8b | ||
|
|
99b380a9f3 | ||
|
|
175061a81e | ||
|
|
3daad4f7e7 | ||
|
|
8bcf2fde87 | ||
|
|
a5d20157f0 | ||
|
|
d3deac46d4 | ||
|
|
288e8a364a | ||
|
|
61c6d0513c | ||
|
|
b0f2b8e906 | ||
|
|
c194e76e46 | ||
|
|
e7f8cfaec1 | ||
|
|
0aa4f80752 | ||
|
|
b8eecfd853 | ||
|
|
2960ff0996 | ||
|
|
a071b62560 | ||
|
|
95004adbad | ||
|
|
f28334f551 | ||
|
|
718a12200b | ||
|
|
d6dab59254 | ||
|
|
9e6bdf74f6 | ||
|
|
030a8d5614 | ||
|
|
39946baf94 | ||
|
|
19be7d7740 | ||
|
|
37856b9768 | ||
|
|
dd2067802f | ||
|
|
de6c53a765 | ||
|
|
b2a20b47ff | ||
|
|
be16bc2a27 | ||
|
|
71c66f127d | ||
|
|
4987fc9fc1 | ||
|
|
16a4c315a2 | ||
|
|
71619c2e15 | ||
|
|
5392b706df | ||
|
|
cbda28c8b7 | ||
|
|
3956455e81 | ||
|
|
955b9d1fec | ||
|
|
d652646e93 | ||
|
|
76eb541878 | ||
|
|
86199755f5 | ||
|
|
d17c56b799 | ||
|
|
e3795f9498 | ||
|
|
8da2569a27 | ||
|
|
d68156f214 | ||
|
|
a598188c14 | ||
|
|
3648f18a09 | ||
|
|
895bf3be6a | ||
|
|
860132469f | ||
|
|
e1568c1e67 | ||
|
|
f4ee9434cc | ||
|
|
3bbef9f328 | ||
|
|
736e051225 | ||
|
|
e007958afd | ||
|
|
1682513af1 | ||
|
|
d3043e9a19 | ||
|
|
cc7e66aa61 | ||
|
|
c6b494db16 | ||
|
|
cea153df5a | ||
|
|
e8f797739c | ||
|
|
9903b340a6 | ||
|
|
fc5e4039b2 | ||
|
|
d6c1285ada | ||
|
|
f07b272cff | ||
|
|
9d74a9d114 | ||
|
|
e8f9bb8226 | ||
|
|
033c1ec5de | ||
|
|
7fd2c075a1 | ||
|
|
125773b840 | ||
|
|
4cb743004b | ||
|
|
9dbc9df2a4 | ||
|
|
7c1016d940 | ||
|
|
a5c0c95e4e | ||
|
|
43198f9602 | ||
|
|
8b1a195121 | ||
|
|
308c5e5579 | ||
|
|
1cde234be4 | ||
|
|
bba6da76fb | ||
|
|
0f68a7f56b | ||
|
|
a4963f8bd9 | ||
|
|
58a762a18a | ||
|
|
90c13d526a | ||
|
|
743d8e0e81 | ||
|
|
eb7a79488f | ||
|
|
3dd7b9638b | ||
|
|
771d4976e9 | ||
|
|
f0a08c6f83 | ||
|
|
c7097faba2 | ||
|
|
e9a341a013 | ||
|
|
15839ba75d | ||
|
|
cd0b24c16a | ||
|
|
67b5aa7f15 | ||
|
|
70bcf3ff0d | ||
|
|
5dfca66665 | ||
|
|
5e1fd41935 | ||
|
|
ef00559f1e | ||
|
|
7151619645 | ||
|
|
684c4037b6 | ||
|
|
97a50faf16 | ||
|
|
ca2a6a1e60 | ||
|
|
61a112c221 | ||
|
|
8e079a970f | ||
|
|
a4bc22fc3b | ||
|
|
0f81ce1716 | ||
|
|
97deb63deb | ||
|
|
a3b91a4363 | ||
|
|
2551a0f11e | ||
|
|
2f5b06b628 | ||
|
|
575195c67a | ||
|
|
9c20fe892e | ||
|
|
4b626a160d | ||
|
|
bfef852de7 | ||
|
|
dbb859cce9 | ||
|
|
abb451349c | ||
|
|
e77ecaa48c | ||
|
|
1895a75bc7 | ||
|
|
0da184ab73 | ||
|
|
f9357d0860 | ||
|
|
f7bd209efa | ||
|
|
f794ffdace | ||
|
|
504323d5b7 | ||
|
|
9450a952d4 | ||
|
|
62394d0e71 | ||
|
|
8d9697d06d | ||
|
|
d4744d300b | ||
|
|
9b7275e713 | ||
|
|
51ed3a4c2c | ||
|
|
d9e967610e | ||
|
|
2cfbd655cb | ||
|
|
3ed5e5275e | ||
|
|
d997aeca1e | ||
|
|
3b371f3588 | ||
|
|
256b296521 | ||
|
|
38668f8de1 | ||
|
|
e22649c19d | ||
|
|
25f749bb0c | ||
|
|
6e4ed3f328 | ||
|
|
4a292c8030 | ||
|
|
29fbfd4479 | ||
|
|
500e5d508a | ||
|
|
396ac023c4 | ||
|
|
4b3cdf9ca4 | ||
|
|
2fc4c8bd60 | ||
|
|
7e0d79ac68 | ||
|
|
42b73006a8 | ||
|
|
2fc85d6810 | ||
|
|
385387b739 | ||
|
|
ea3eacbbae | ||
|
|
9f3941d1a1 | ||
|
|
9c8bb7efeb | ||
|
|
a171abb741 | ||
|
|
52825e0e59 | ||
|
|
efc490cc0d | ||
|
|
559ab69940 | ||
|
|
e75be340e2 | ||
|
|
a5b7332fa4 | ||
|
|
08c54f75eb | ||
|
|
e332edb36b | ||
|
|
70b5d9adc6 | ||
|
|
23ee9508c2 | ||
|
|
1dc87de975 | ||
|
|
49ba2e72a9 | ||
|
|
47dc0d579b | ||
|
|
ef6662d4f8 | ||
|
|
b9aaaa7d37 | ||
|
|
ec43039b94 | ||
|
|
4939b85819 | ||
|
|
fab7596d06 | ||
|
|
81d67f640b | ||
|
|
fe8f217587 |
No files matched your search
@@ -28,3 +28,5 @@ src/server/sparkd/orderman/perf_analysis/trace_data/
|
||||
*.fsx.lock
|
||||
src/lib/orderman/perf_analysis/logs/latency_data/
|
||||
trace_data/
|
||||
/src/lib/orderman/perf_analysis/last_pretty_log.csv
|
||||
/src/lib/orderman/perf_analysis/log.csv
|
||||
Vendored
+4
-5
@@ -36,8 +36,8 @@
|
||||
"name": "SDK Launch",
|
||||
"type": "clr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/build/Debug/bin/net471/spark_sdk.exe",
|
||||
"args": ["md","GLBX","Future","ZN", "ZNU8"],
|
||||
"program": "${workspaceRoot}/build/Debug/bin/net471/spark_sdk.exe",
|
||||
"args": ["md","GLBX","Future","CL", "CLZ9", "--user=bts", "--pass="],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"stopAtEntry": false,
|
||||
"externalConsole": true,
|
||||
@@ -48,7 +48,7 @@
|
||||
"type": "clr",
|
||||
"request": "launch",
|
||||
"program": "C:/Program Files/Microsoft Office/Office14/EXCEL.EXE",
|
||||
"args": ["${workspaceRoot}/build/Debug/bin/net471/spark_excel-addin64.xll"],
|
||||
"args": ["${workspaceRoot}/build/Debug/bin/net471/spark_excel-addin64-packed.xll"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"stopAtEntry": false,
|
||||
"externalConsole": true,
|
||||
@@ -209,8 +209,7 @@
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "/home/bts/spark/build/ninja_debug/src/server/sparkd/sparkd",
|
||||
// "args": ["test","Co2RelayTests"],
|
||||
"args": ["md", "run", "10"],
|
||||
"args": ["test","AuthToken"],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "/home/bts/spark/build/ninja_debug/src/server/sparkd/",
|
||||
"environment": [],
|
||||
|
||||
+1
-1
@@ -41,6 +41,7 @@ else ()
|
||||
-Wno-missing-braces -Wno-ignored-qualifiers \
|
||||
--std=c++17 -fdiagnostics-color")
|
||||
set (CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -DNDEBUG -O3")
|
||||
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wno-undef")
|
||||
add_subdirectory(src/server/sparkd)
|
||||
add_subdirectory(src/lib)
|
||||
add_subdirectory(src/web)
|
||||
@@ -51,7 +52,6 @@ else ()
|
||||
add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/src/server/sparkd/sparkd ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/src/server/sparkd/defaults ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/web ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/data_files ${CMAKE_BINARY_DIR}/server
|
||||
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/src/lib/libspark.so ${CMAKE_BINARY_DIR}/server
|
||||
|
||||
@@ -25,3 +25,47 @@
|
||||
***** FillStore
|
||||
***** SecuritySrc
|
||||
* Benchmarks
|
||||
* OrderMan
|
||||
** OrderServices
|
||||
*** callbacks
|
||||
**** standard
|
||||
- update
|
||||
- cancel
|
||||
- crack
|
||||
- activate
|
||||
- book
|
||||
- settlement
|
||||
- child order state
|
||||
- timer
|
||||
**** low level
|
||||
- exe reports
|
||||
- cancel rejects
|
||||
- gateway status
|
||||
*** services
|
||||
**** standard
|
||||
- secdb
|
||||
- get time
|
||||
- schedule actions
|
||||
- order builder
|
||||
- activate
|
||||
- update
|
||||
- cancel
|
||||
- crack
|
||||
**** low level
|
||||
- governor
|
||||
- send fix
|
||||
- get time str
|
||||
- limiter
|
||||
** Internalize plan
|
||||
- change order initiated update/cancel/crack/activate/report_state to
|
||||
use direct method calls
|
||||
- need to watch out for reentrancy
|
||||
- need to also report state externally
|
||||
- also want to separate active from non active orders in map to speed
|
||||
up look ups
|
||||
- want to use direct references in orders to avoid lookups
|
||||
- want to change how time is captured, the trace is too big to copy
|
||||
around
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# The BTS Development Process #
|
||||
|
||||
## The Big Picture ##
|
||||
|
||||
Our development work is organized in projects. A project is an effort
|
||||
to specifiy, develop and validate a related set of additions to our
|
||||
Spark platform. A project can be assigned to one more developers and a
|
||||
developer might concurrently be assigned to multiple
|
||||
projects. Projects typically start out with specification work,
|
||||
proceed to implementation and finish with validation efforts. At any
|
||||
given time there will be multiple projects being worked on
|
||||
concurrently. A project is concluded when the features it implements
|
||||
are merged into the product and deployed to customers.
|
||||
|
||||
## The Details ##
|
||||
Typically project ideas come from sales interactions, opposition
|
||||
research and customer feedback, alltough on occasion development or
|
||||
operational requirements will also suggest potentially valuable
|
||||
ideas. Whatever the source of a project idea the final decision of
|
||||
which projects to pursue is always done by management. Once a project
|
||||
idea has been selected for implementation the corresponding project
|
||||
will go through several phases: inception, specification, development
|
||||
and merge. We detail each of these phases below.
|
||||
|
||||
* Inception. In this phase we create the infrastructure for the
|
||||
project. Typically this means selecting a team, creating a project
|
||||
case in Jira (that is a case assigned to the team@bluetradesys.com
|
||||
Jira user) and possibly creating a new branch for the project. This
|
||||
phase is mostly mechanical and the responsibility of Pedro.
|
||||
|
||||
* Specification. The specification phase turns a usually vague and
|
||||
ill-considered idea project idea into a concrete, implementable
|
||||
specification that attempts to maximize the value/cost ratio of the
|
||||
project. It is hard to exaggerate the impact of a this phase on the
|
||||
rest of the project. *Nothing* has more impact on our ability to
|
||||
quickly deliver value to customers than good specification
|
||||
work. Specification typically take the form of shared google docs,
|
||||
using a mix of text and UI mockups to capture both user requirements
|
||||
and suggested implementation strategies. Taha is responsible for this
|
||||
phase.
|
||||
|
||||
* Development. Once a specification is complete, a team of developers
|
||||
will use a project specific branch to implement the
|
||||
specification. Periodically, typically once a week, progress on the
|
||||
project is assessed by Pedro, possibly with the help of others. This
|
||||
assessment takes the form of code reviews, demos, or just one-on-one
|
||||
conversations. To ensure development does not go off on sub-optimal
|
||||
tangents, developers are expected to continuously merge master into
|
||||
the project branch as well as encouraged to create Pull Requests (PRs)
|
||||
early in the project implementation, so that their work is kept
|
||||
visible and integrated with that of the rest of the group. During the
|
||||
implementation phase developers should reach out to Pedro for UI
|
||||
reviews, architecture and design concerns as well as specification
|
||||
trade off concerns.
|
||||
|
||||
* Verification. Once code is complete think carefully about the best way
|
||||
to verify that it is good before it goes to production. We don't have a
|
||||
dedicated QA cycle to flush out bugs so it's important that a change be
|
||||
fully tested before it's marked "Ready to Merge." For any non-trivial
|
||||
change consider enlisting a 'test buddy' to help review the code and
|
||||
think of test cases that you may not have considered. Stability from
|
||||
release to release is especially critical right now as we develop a
|
||||
reputation in the futures world.
|
||||
- Code Reviews
|
||||
- Unit tests
|
||||
- System tests
|
||||
- Manual tests
|
||||
|
||||
* Merge. Once a project is considered complete by developers, a PR
|
||||
that includes Pedro should be created (unless, as mentioned above, an
|
||||
early PR was already created in the development phase). Whether the PR
|
||||
already existed or not, its description should be changed to start
|
||||
with the "Ready To Merge" statement. This marks the project as ready
|
||||
to be merged into master. Once a week, typically on Mondays, Pedro
|
||||
will go through every PR marked as "Ready To Merge", build it, check
|
||||
it out and if everything looks good merge it to master. At the end of
|
||||
the day the resulting master will be tagged with a new version
|
||||
number. This version will be go through some additional testing by
|
||||
Brian and will then be deployed by Karl and Pete to customers
|
||||
throughout the rest of the week.
|
||||
|
||||
## Tools ##
|
||||
* Pre-Merge checklist
|
||||
* Code Review tutorial
|
||||
@@ -1,5 +1,9 @@
|
||||
#Help
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/yGQfr_puT3w" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
##Report a Problem
|
||||
If there is any issue at all, you can click here
|
||||
and it’ll bring up a “Report a Problem” window, which you can type
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#Module Launcher
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/OxVqQBKFIDw" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- The module launcher is the first item that will come up after
|
||||
logging in to Spark.
|
||||
- This is your central control for all modules
|
||||
@@ -9,7 +13,7 @@
|
||||
|
||||

|
||||
|
||||
- If you right click anywhere on the launcher you can modify
|
||||
- If you right-click anywhere on the launcher you can modify
|
||||
various display settings including:
|
||||
- Vertical layout, instead of horizontal
|
||||
- Showing the text of the individual modules
|
||||
@@ -25,5 +29,5 @@
|
||||
Some common features within BTS Spark
|
||||
|
||||
- All Windows are tabbed so they can be combined or separated to organize your workspace
|
||||
- Some Spark windows have a menu on the left hand side where you can launch related modules or change settings. You can see the menu item names either by hovering your mouse over the icon, or clicking on the right edge of the menu (a blue bar will appear).
|
||||
- Some Spark windows have a menu on the left-hand side where you can launch related modules or change settings. You can see the menu item names either by hovering your mouse over the icon or clicking on the right edge of the menu (a blue bar will appear).
|
||||
- Some table columns have filtering capabilities, where if you hover over the column header, it will display a filter icon, which enables you to filter the table based on the information in that column.
|
||||
+16
-8
@@ -9,6 +9,10 @@
|
||||
|
||||
##Market View
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Y54S1uscT_g" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- This provides a grid of outright, spread, and synthetic products
|
||||
showing real-time market data all in one place
|
||||
- You can create multiple market view grids and customize the
|
||||
@@ -29,7 +33,7 @@
|
||||
and all securities will be displayed. This will show up in the
|
||||
selected securities window as “<symbol> future at
|
||||
<exchange>”
|
||||
- Synthetic’s can be displayed by clicking on exchange “BTS”, then
|
||||
- Synthetics can be displayed by clicking on exchange “BTS”, then
|
||||
synthetic, symbol, and finally under the securities window.
|
||||
- Below is an example of an outright, spread, all futures, and
|
||||
synthetic chosen:
|
||||
@@ -98,10 +102,10 @@
|
||||
either the Bid or Ask price and volume.
|
||||
- Highlighting a single security and clicking on the order ticket
|
||||
 icon
|
||||
on the left hand browser window of the market view
|
||||
on the left-hand browser window of the market view
|
||||
|
||||
- Launch a ladder for 1 security
|
||||
- You can double click on any security in the market view, besides
|
||||
- You can double-click on any security in the market view, besides
|
||||
the Bid Qty, Bid Price, Ask Price, or Ask Qty columns to launch a
|
||||
ladder for that Security.
|
||||
|
||||
@@ -110,7 +114,7 @@
|
||||
- If you hold down the Ctrl key and select multiple securities, you
|
||||
can then click on the ladder icon
|
||||
 on
|
||||
the left hand side to bring up the selected ladders all at
|
||||
the left-hand side to bring up the selected ladders all at
|
||||
once.
|
||||
|
||||
- Launching a Fills window for 1 or many securities
|
||||
@@ -118,7 +122,7 @@
|
||||
- This is done by either highlighting 1 or many securities and then
|
||||
clicking on the Fills icon
|
||||
 on
|
||||
the left hand side to bring up a fill window for the selected
|
||||
the left-hand side to bring up a fill window for the selected
|
||||
securities.
|
||||
|
||||
- Please note - If there haven’t been any fills for the day in that
|
||||
@@ -129,14 +133,14 @@
|
||||
- This is accomplished by highlighting 1 or many securities and then
|
||||
clicking on the time and sales icon
|
||||
 on
|
||||
the left hand side
|
||||
the left-hand side
|
||||
|
||||
- Launching the synthetic window to create a synthetic product
|
||||
|
||||
- This is accomplished by highlighting either 1 or 2 securities and
|
||||
clicking on the Synthetic icon
|
||||
 on
|
||||
the lower left hand side
|
||||
the lower left-hand side
|
||||
|
||||
- Please note - If only 1 security is chosen, a create synthetic
|
||||
window will appear, and you will be required to add another leg from
|
||||
@@ -144,7 +148,11 @@
|
||||
|
||||
##Time and Sales
|
||||
|
||||
- Provides a display of real time fills as they happen for 1 or many
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/nEkSGacjt0s" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- Provides a display of real-time fills as they happen for 1 or many
|
||||
securities
|
||||
- You can create multiple time and sale windows and customize the
|
||||
security selection to fit your needs
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#My Windows
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/pjm1JJ9Q3jM" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
This is the housing of the windows that you have manually named and
|
||||
creates a place where you can quickly and easily launch windows
|
||||
which you’ve customized.
|
||||
@@ -18,9 +22,9 @@ section of the module launcher.
|
||||
|
||||

|
||||
|
||||
Right clicking on a tab that has already had its default title modified
|
||||
Right-clicking on a tab that has already had its default title modified
|
||||
will show the following which will allow you to rename the modified
|
||||
title or remove it from the my windows, which will then restore the
|
||||
title or remove it from the My Windows, which will then restore the
|
||||
default standard title.
|
||||
|
||||

|
||||
@@ -1,6 +1,10 @@
|
||||
|
||||
#Settings
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/yGQfr_puT3w" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
##Workspaces
|
||||
|
||||
This is where your workspaces can be saved or switched
|
||||
|
||||
+34
-14
@@ -1,14 +1,35 @@
|
||||
|
||||
#Spreader
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/XEZpuPAgMC0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/EVFK2yNQFS4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/d5ywlHen6yo" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Mmw8PNJA0Dw" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/QK-bBSH6Rc0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- Launching the spreader from the module launcher will bring up the
|
||||
synthetic management browser which appears like below:
|
||||
|
||||

|
||||
|
||||
- From this window it’ll show any synthetics that you have already
|
||||
- From this window, it will show any synthetics that you have already
|
||||
created as well as various other options of both managing the
|
||||
synthetics and other tasks all from the left hand side pane within
|
||||
synthetics and other tasks all from the left-hand side pane within
|
||||
the window, including:
|
||||
|
||||
- Adding a new synthetic 
|
||||
@@ -30,7 +51,7 @@
|
||||
- From the main module launcher, you can go to spreader, then
|
||||
synthetics, then click add
|
||||
- Highlighting one or more securities in a market view and clicking
|
||||
on create synthetic
|
||||
on Create Synthetic
|
||||
- Cloning an existing security
|
||||
|
||||
- An empty synthetic browser will look like below without any legs
|
||||
@@ -43,10 +64,10 @@
|
||||
- Upon adding legs, you can then go through each of the parameters to
|
||||
customize how you would like the spread to be worked in the
|
||||
market
|
||||
- We’ll start at the top of create synthetic window and work our way
|
||||
- We will start at the top of the Create Synthetic window and work our way
|
||||
down:
|
||||
|
||||
- At the very top of the create synthetic window you will have to
|
||||
- At the very top of the Create Synthetic window you will have to
|
||||
name your synthetic product. By default, it will auto-populate
|
||||
based on the symbols you have chosen:
|
||||
|
||||
@@ -73,16 +94,15 @@
|
||||
exchange
|
||||
- Furthermore, if you are ever unsure of what a parameter’s function
|
||||
is and how it is controlled you can hover over the parameter’s title
|
||||
and a “?” will be shown. Clicking on this will expand built in
|
||||
and a “?” will be shown. Clicking on this will expand built-in
|
||||
details of how that parameter functions. Below is an example of the
|
||||
throttle details displayed:
|
||||
|
||||

|
||||
|
||||
- Main parameters - these are the top 4 rows in the create synthetic
|
||||
- Main parameters - these are the top 4 rows in the Create Synthetic
|
||||
window which all synthetics will be required to have set.
|
||||
Everything below these are custom to a traders needs, however the
|
||||
top 4 are the main foundational aspects used for the calculation of
|
||||
Everything below these can be customized to a trader's needs, however, the top 4 are the main foundational aspects used for the calculation of
|
||||
the synthetic:
|
||||
|
||||

|
||||
@@ -95,7 +115,7 @@
|
||||
be priced, by default, it’ll be based on the same as the quantity of
|
||||
the synthetic produced, but you can change it to modify the balance
|
||||
of the price, giving more weighting to one leg over another.
|
||||
- Enable quoting - This parameter is in the form of a check box,
|
||||
- Enable quoting - This parameter is in the form of a checkbox,
|
||||
where if the box is checked the leg will be quoted versus not
|
||||
checked, it will not be shown in the market. If it is not actively
|
||||
quoted in the market, the leg will still be used for calculation of
|
||||
@@ -107,7 +127,7 @@
|
||||
If your Hedge Ratio is 3 on the NQ leg, then you’ll work the ES
|
||||
order based on where you could fill 6 (2 (Qty of NQ) \* 3 (Hedge
|
||||
Ratio)).
|
||||
- The next 6 parameters are all completely custom to a traders needs
|
||||
- The next 6 parameters are all completely custom to a trader's needs
|
||||
and can further define how a synthetic is worked. As previously
|
||||
mentioned, if you hover over any of these parameters a “?” will be
|
||||
displayed and click on such will display further details on how this
|
||||
@@ -118,7 +138,7 @@
|
||||
- Quote
|
||||
|
||||
- Throttle - This can be enabled or disabled based on if the box is
|
||||
checked per leg. It’s main function is to reduce re-quoting updates
|
||||
checked per leg. Its main function is to reduce re-quoting updates
|
||||
by defining a region on either side of the working quote order,
|
||||
inside which quote updates will not be sent:
|
||||
|
||||
@@ -148,8 +168,8 @@
|
||||
|
||||

|
||||
|
||||
- Time Based Cross - Basically the exact same as ticks away cross,
|
||||
however this is based on time (in ms) on when to automatically cross
|
||||
- Time Based Cross - Basically the same as Ticks Away Cross,
|
||||
however, this is based on time (in ms) on when to automatically cross
|
||||
(fill) the market if they get too far away from the opposite inside
|
||||
market. If a legged hedge order’s price gets X amount of “ticks”
|
||||
away from the opposite inside market, it is re-priced to cross the
|
||||
|
||||
+47
-33
@@ -9,7 +9,11 @@
|
||||
|
||||
##Order Book
|
||||
|
||||
- This provides list of all orders a user has placed, by default it
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/ygMFkjBBGeU" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- This provides a list of all orders a user has placed, by default it
|
||||
will display only working orders
|
||||
- You can create multiple order books filtered however you would
|
||||
like
|
||||
@@ -44,13 +48,13 @@
|
||||
- Anytime
|
||||
- Today
|
||||
- Since Open
|
||||
- A specific user defined period of time
|
||||
- A specific user-defined period of time
|
||||
|
||||

|
||||
|
||||
- The left hand box within an order book provides users with further
|
||||
- The left-hand box within an order book provides users with further
|
||||
functionality to quickly perform various tasks with live orders as
|
||||
well as launch other modules. It’s appears like the below:
|
||||
well as launch other modules. It appears like the below:
|
||||
|
||||

|
||||
|
||||
@@ -62,7 +66,7 @@
|
||||
- Cancel
|
||||
selected - clicking on this will cancel the selected orders you have
|
||||
highlighted in the order book. You can choose more than one by
|
||||
holding hold the Ctrl key when click on each row of orders
|
||||
holding hold the Ctrl key when clicking on each row of orders
|
||||
- Cancel
|
||||
buys - clicking on this will cancel all the buy orders you have in
|
||||
the order book
|
||||
@@ -75,8 +79,8 @@
|
||||
|
||||
##Fills
|
||||
|
||||
- This provides list of all fills a user has had for a given time
|
||||
period, by default it will open up with “since open” time frame
|
||||
- This provides a list of all fills a user has had for a given time
|
||||
period, by default it will open up with “since open” timeframe
|
||||
selected showing you fills on the day thus far
|
||||
- You can create multiple fill windows, filtered however you
|
||||
like
|
||||
@@ -85,8 +89,12 @@
|
||||
|
||||
##P&L
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/xj6cEQ6By34" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- This provides a running ledger of a users account(s) profit and
|
||||
loss broken down per symbol
|
||||
loss, broken down per symbol
|
||||
- You can create multiple P&L windows, filtered however you would
|
||||
like
|
||||
- All P&L windows are brought up as tabs and can be combined with all
|
||||
@@ -101,6 +109,10 @@
|
||||
|
||||
##Ladder
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Q0qsyc1nvYo" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- The ladder was designed to be the most logically produced ladder on
|
||||
the market, engineered and designed by professional traders for
|
||||
professional traders
|
||||
@@ -112,8 +124,8 @@
|
||||
|
||||

|
||||
|
||||
- The ladder consist of 2 major parts, the Ladder itself and the
|
||||
information and control section on the left hand side
|
||||
- The ladder consists of 2 major parts, the Ladder itself and the
|
||||
information and control section on the left-hand side
|
||||
|
||||
- The information section on the left contains the following: 
|
||||
- Day - showing the current price of the security on the day
|
||||
@@ -126,17 +138,17 @@
|
||||
|
||||
|
||||
- The control section right underneath the information section on the
|
||||
left hand side offers the following:
|
||||
left-hand side offers the following:
|
||||
|
||||

|
||||
|
||||
- Account in which the orders will flow into when trading this
|
||||
- The account in which the orders will flow into when trading this
|
||||
security
|
||||
- Length of time the order will remain in market, either DAY or
|
||||
- Length of time the order will remain in the market, either DAY or
|
||||
GTC
|
||||
- Order type, by default orders will go in as Limit, however all of
|
||||
- Order type, by default, orders will go in as Limit, however all of
|
||||
the following are offered out of the box. The top 5 are all fairly
|
||||
standard order types, however the bottom 5 are all specialized
|
||||
standard order types, however, the bottom 5 are all specialized
|
||||
algos. They are only offered by BTS and require specialized
|
||||
parameters. Please see below for each order type:
|
||||
|
||||
@@ -147,8 +159,8 @@
|
||||
|
||||
- Limit
|
||||
|
||||
- This is a buy or sell order designed to take place on the exact
|
||||
price level you have entered the order at.
|
||||
- This is a buy or sell order designed to take place on the
|
||||
price level where you entered the order.
|
||||
|
||||
- Stop
|
||||
|
||||
@@ -218,8 +230,7 @@
|
||||
market flips and has at least threshold size as defined.
|
||||
- “Flip” refers to the event where the best offer becomes the best
|
||||
bid or the market trades away from you, i.e. the line moves up in
|
||||
the GUI. This is all true in the reverse way as well if
|
||||
selling.
|
||||
the GUI. The reverse is true if selling.
|
||||
|
||||

|
||||
|
||||
@@ -241,7 +252,7 @@
|
||||
|
||||
- You can manually type in a quantity here as well
|
||||
- Furthermore, if you have a value in the volume bar, you can
|
||||
increase this size by right clicking and the value will increase
|
||||
increase this size by right-clicking and the value will increase
|
||||
incrementally by 1. The same is true if you would like to decrease
|
||||
this value by left click and the value will decrease.
|
||||
- By having the auto clear box checked, it will bring the value back
|
||||
@@ -252,12 +263,12 @@
|
||||
- The bottom section of the control area of a ladder has 4 aspects
|
||||
to it
|
||||
|
||||
- Cancel Buys - which will cancel all buys currently working on the
|
||||
- Cancel Buys, which will cancel all buys currently working on the
|
||||
ladder
|
||||
- Cancel Sells - which will cancel all sells currently working on the
|
||||
- Cancel Sells, which will cancel all sells currently working on the
|
||||
ladder
|
||||
- Cancel All - which will cancel all orders on the ladder
|
||||
- Settings - this will launch the ladder settings window which
|
||||
- Cancel All, which will cancel all orders on the ladder
|
||||
- Settings, which will launch the ladder settings window which
|
||||
appears like below
|
||||
|
||||

|
||||
@@ -269,27 +280,26 @@
|
||||
|
||||
- There are 5 main columns
|
||||
|
||||
- Orders - which display current orders working in the market
|
||||
- Bid - which displays the current bid volumes at each price
|
||||
- Orders, which display current orders working in the market
|
||||
- Bid, which displays the current bid volumes at each price
|
||||
level
|
||||
- Price - which displays the price levels of the symbol shown
|
||||
- Ask - which displays the current ask volumes at each price
|
||||
- Price, which displays the price levels of the symbol shown
|
||||
- Ask, which displays the current ask volumes at each price
|
||||
level
|
||||
- Last Trade - which displays where the price level and volume of
|
||||
- Last Trade, which displays the price level and volume
|
||||
where the last trade occurred
|
||||
|
||||
- Functionality of the ladder
|
||||
|
||||
- To enter an order directly by clicking in the ladder, left click on
|
||||
either the bid or ask volume with a volume greater than 0 on the
|
||||
predefined value box on the left hand section
|
||||
predefined value box on the left-hand section
|
||||
- A badge will be displayed within the orders column of the value you
|
||||
have chosen and the quantity filled as displayed below
|
||||
|
||||

|
||||
|
||||
- To change the price level you wish to work the order, you can right
|
||||
click and hold on the badge to move it up or down the ladder for the
|
||||
- To change the price level you wish to work the order, you can right-click and hold on the badge to move it up or down the ladder for the
|
||||
desired price
|
||||
- To cancel the order, left click on the badge
|
||||
- Furthermore, you can click down on the scroll wheel of your mouse
|
||||
@@ -298,6 +308,10 @@
|
||||
|
||||
## Order Ticket
|
||||
|
||||
<div>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/bdC5pZ6T35I" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
- The order ticket was designed to provide another means other than
|
||||
the ladder to enter orders into the market
|
||||
- You can bring up an order ticket directly from the module launcher,
|
||||
@@ -307,7 +321,7 @@
|
||||
|
||||

|
||||
|
||||
- On the top left you have the price for the order as well as the
|
||||
- On the top left, you have the price for the order as well as the
|
||||
volume
|
||||
- Beneath are the predefined volumes as well as a clear button to
|
||||
clear out the volume
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
|
||||
Click on the headings on the the left to expand their contents. If you need specific information quickly use the Search button in the top right of this page.
|
||||
Click on the headings on the left to expand their contents. If you need specific information quickly use the Search button in the top right of this page.
|
||||
|
||||
+102
-17
@@ -1,12 +1,97 @@
|
||||
# Release Notes
|
||||
|
||||
## 1.4.10
|
||||
### Bug Fixes
|
||||
- Removed duplicate entries in performance log
|
||||
|
||||
## 1.4.9
|
||||
### SDK
|
||||
- Added ticker events to the order API
|
||||
### Performance Improvements
|
||||
- Significantly improved hedging speed
|
||||
### Bug Fixes
|
||||
- Fixed a visual oddity in the order book UI when using High DPI displays
|
||||
|
||||
## 1.4.8
|
||||
### Marketdata
|
||||
- Additional trade details for CME ticker
|
||||
|
||||
## 1.4.7
|
||||
### Bug Fixes
|
||||
- Corrected FIX tag 1028, ManualOrderIndicator, when orders are triggered by algos
|
||||
|
||||
## 1.4.6
|
||||
### Ladder Usability Improvements
|
||||
- The ladder can now be zoomed in/out according to a user's preference
|
||||
- The zoom slider can be enabled from Ladder Form -> Settings button -> Left Pane tab -> check Zoom Slider checkbox
|
||||
- The zoom functionality can also be performed via hotkey and mouse wheel combinations
|
||||
- Zoom In = CTRL + Mouse wheel up
|
||||
- Zoom Out = CTRL + Mouse wheel down
|
||||
- Reset zoom = CTRL + Mouse wheel click or double click on the zoom slider
|
||||
### Exchange simulator
|
||||
- Improved the exchange simulator to track real market data more closely
|
||||
### Bug Fixes
|
||||
- Fixed a UI crash in some cases when attempting to sort the order book by time
|
||||
|
||||
## 1.4.5
|
||||
- Fixed entitlement validation for ICS spreads
|
||||
- Tuned performance logging
|
||||
|
||||
## 1.4.4
|
||||
|
||||
### Performance Improvements
|
||||
- Reduced performance logging activity
|
||||
|
||||
## 1.4.3
|
||||
|
||||
### Ladder Usability Improvements
|
||||
- Controls found in Left Pane of the ladder can now be hidden via the checkboxes found under Ladder Form -> Settings button -> Left Pane tab
|
||||
- The Left Pane is now scrollable with the mouse wheel when there isn't enough vertical space to accomodate the entire form
|
||||
- The Left Pane can now grown and shrunk horizontally via the splitter between the left pane and the main ladder area
|
||||
### Performance Improvements
|
||||
- Requoting and hedging latency has been reduced by up to 30%
|
||||
### Software Development Kit (SDK) Improvements
|
||||
- The API now enforces authentication
|
||||
### Bug Fixes
|
||||
- The Market View form now honors filters when new securities are added
|
||||
|
||||
## 1.4.2
|
||||
|
||||
### User Interface Improvements
|
||||
- When creating a synthetic from the Market View window, securities are now added to the Create Synthetic dialog in the same sort order as they appear in grid
|
||||
### Bug Fixes
|
||||
- Shell no longer crashes when removing a security from the synthetics window
|
||||
- It is now possible to create a new synthetic using the same name as a previously deleted synthetic
|
||||
### SDK Improvements
|
||||
- Improved Client and Server SDK clarity and documentation
|
||||
### Performance Improvements
|
||||
- Synthetic calculation speed was improved by 400%.
|
||||
### Risk / Entitlement System
|
||||
- Risk Admin UI
|
||||
- Spark users now have separate Trading, RiskAdmin, and/or MDAdmin permissions
|
||||
- Spark users can now only add other users or accounts if permissioned to do so.
|
||||
- Market Data Entitlement
|
||||
- Automated market data reporting
|
||||
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Tabs
|
||||
- Tabs now shrink and expand to make better use of horizontal space
|
||||
### Bug Fixes
|
||||
- Fixed a bug that could cause excessive CPU usage when a connection broke
|
||||
- Fixed a layout bug in hedging/requoting performance reports
|
||||
- Fixed account name mismatching bug for old fills
|
||||
### Absolute Position limits
|
||||
- Positions can now be netted or absolute for purposes of limit enforcing
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Speed Enhancements
|
||||
- Improved requoting and hedging performance
|
||||
- Improved order book form performance
|
||||
### Server Side SDK
|
||||
- Added beta version of the server side SDK
|
||||
- Added a beta version of the server side SDK
|
||||
### Front End
|
||||
- Added status bars to all UI grids
|
||||
### Reporting
|
||||
@@ -18,7 +103,7 @@
|
||||
- Further speed enhancements
|
||||
### Conditional (new quoting parameter)
|
||||
- Reduces re-quoting updates by only quoting if N ticks away from the opposite inside market.
|
||||
- Quotes are cancelled when it gets further away.
|
||||
- Quotes are canceled when it gets further away.
|
||||
- "Price Type" dictates if this logic applies to the quoting market or to the synthetic spread market.
|
||||
|
||||
## 1.3.7
|
||||
@@ -42,7 +127,7 @@
|
||||
### Synthetic creation option to choose Merged or Direct
|
||||
- The ability to choose Merged or Direct per leg when creating a synthetic
|
||||
### Volume at price
|
||||
- Within a ladder you can now see a volume at price graph
|
||||
- Within a ladder, you can now see a volume at price graph
|
||||
- This is configurable from a ladder's settings as well
|
||||
### Customized settings in all windows
|
||||
- All windows now contain a settings page where grid settings (font color, size, etc) are applied
|
||||
@@ -60,7 +145,7 @@
|
||||
|
||||
### Order Book
|
||||
- Many visual facets of the Order Book grid can now be customized
|
||||
- The Order Book settings page can be access from the gears icon in the Order Book toolbar or from the right-click menu
|
||||
- The Order Book settings page can be accessed from the gears icon in the Order Book toolbar or from the right-click menu
|
||||
- The Expand/Collapse All button has been separated into two different buttons on the toolbar
|
||||
|
||||
### Sound Alerts
|
||||
@@ -96,13 +181,13 @@
|
||||
### Synthetics
|
||||
- Hedge Ratio parameter has been renamed to Lean Qty Factor
|
||||
- Spreader now includes a Hidden mode parameter
|
||||
- A quoting leg can be hidden, ie an order will not sent out until the market crosses the computed quoting price. When that happens, the quote is sent out as an IOC order.
|
||||
- A quoting leg can be hidden, ie an order will not be sent out until the market crosses the computed quoting price. When that happens, the quote is sent out as an IOC order.
|
||||
|
||||
### Algos
|
||||
- Trigger orders now can contain a Trigger Exit parameter based on time or price; once the exit condition has been met the Trigger order will cancel
|
||||
- IOC/FAK order type added
|
||||
- Cover order type added
|
||||
- Cancelling a synthetic order no longer cancels any working hedge orders
|
||||
- Canceling a synthetic order no longer cancels any working hedge orders
|
||||
|
||||
### General
|
||||
- A manual page has been added to the launcher Help menu
|
||||
@@ -110,18 +195,18 @@
|
||||
- Various cash per point values updated
|
||||
- Time-based grid filters now have tick up/down buttons
|
||||
- Numerous performance improvements and stability enhancements
|
||||
- Automated position and orderbook maintenance during daily recycle
|
||||
- Individual fills be compacted into start of day position records
|
||||
- Automated position and Order Book maintenance during daily recycle
|
||||
- Individual fills will be compacted into the Start of Day position records
|
||||
- Orders from the day before the recycle will be deleted
|
||||
|
||||
|
||||
##1.1
|
||||
|
||||
###1.1.22
|
||||
- Orderbook: Orders in Error status can now be recreated
|
||||
- Orderbook: Orders containing child orders will now be collapsed by default.
|
||||
- Orderbook: Expand / Collapse All button added to the right-click menu and window action pane, denoted by ▲ icon
|
||||
- Orderbook: Symbol column added to allow for filtering based on group symbol
|
||||
- Order Book: Orders in Error status can now be recreated
|
||||
- Order Book: Orders containing child orders will now be collapsed by default.
|
||||
- Order Book: Expand / Collapse All button added to the right-click menu and window action pane, denoted by ▲ icon
|
||||
- Order Book: Symbol column added to allow for filtering based on group symbol
|
||||
- Time filters now have a quick Set Last Hour hyperlink which will set the Period Begin filter to (Now - 1 hour)
|
||||
|
||||
###1.1.20
|
||||
@@ -140,15 +225,15 @@
|
||||
- Fixed occasional crash when filtering fills
|
||||
- Fixed several cash per point multipliers
|
||||
- Colored time of sales row with aggressor side
|
||||
- Iceberg orders now cancel (rathern than error) when a child is canceled
|
||||
- Iceberg orders now cancel (rather than an error) when a child is canceled
|
||||
|
||||
###1.1.0
|
||||
- Added per user self match protection, location and self match protection FIX tag settings
|
||||
- Implemented a new "Trigger Order" algo
|
||||
- Corrected tick formatting for a number of products
|
||||
- Added the hability to recreate canceled orders
|
||||
- Added leg fills to facilitate futured based PnL
|
||||
- PnL report now has PnL columns calculated based on your start of day, current day, and total position
|
||||
- Added the ability to recreate canceled orders
|
||||
- Added leg fills to facilitate futures based PnL
|
||||
- PnL report now has PnL columns calculated based on your Start of Day, current day, and total position
|
||||
- Start, Day and Total PnL columns are painted with a red or green background color based on if the pnl value is negative or positive respectively
|
||||
- Corrected cash per point values, used in pnl calculations, for a number of products
|
||||
- PnL report will now remember any customizations you make to filtering and/or sorting criteria
|
||||
- PnL report will now remember any customizations you make to filtering and/or sorting criteria
|
||||
+1
-1
Submodule docker/linux updated: 8ee9692597...f6d8d59860.
+1
-1
Submodule externals updated: 5f21fd13e7...5570689de2.
@@ -1,3 +1,10 @@
|
||||
1.4.2
|
||||
- Be sure the system has a bts user before deployment. bts is the superuser, he has all permissions and can see all market data
|
||||
- After deployment re-permission all existing users for trading, riskadmin, and/or md admin permissions as appropriate
|
||||
- Also note which users can add other users and accounts and permission them appropriately (Can Add?)
|
||||
- Permission users for appropriate market data entitlements; this has monetary consequences so ask Kevin if you aren't sure.
|
||||
- MIC reporting needs to be added to audit Jenkins job (https://bluetradesys.atlassian.net/browse/SPARK-1900)
|
||||
|
||||
1.3.5
|
||||
- Change in market data wire format. Old shell will get only direct market data instead of merged which may not match what the servers is driving from.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using bts.api.core;
|
||||
|
||||
namespace mbts.spark {
|
||||
[Export]
|
||||
@@ -36,7 +37,7 @@ namespace mbts.spark {
|
||||
IEnumerable<BTSAccountImmutable> active_accounts =
|
||||
from admin_data in update.all
|
||||
from bts_account in admin_data.Accounts
|
||||
where bts_account.IsPresent
|
||||
where bts_account.IsPresent && _wrapper_subject.Value.can_view_account(bts_account.Name)
|
||||
join account_id in (
|
||||
from bts_user in active_users
|
||||
from account_id in bts_user.UserData.AccountPermissions.AccountPermissions.Keys
|
||||
|
||||
@@ -17,31 +17,22 @@ using bts.api.user;
|
||||
|
||||
namespace bts {
|
||||
[Export(typeof(IInitializable))]
|
||||
class MDSource : IObserver<List<SecurityAndHandle>>, IInitializable, IDisposable {
|
||||
class MDDaemon : bts.md.MDSource, IObserver<List<SecurityAndHandle>>, IInitializable, IDisposable {
|
||||
readonly Lazy<IBus> _bus;
|
||||
readonly Lazy<AuthToken> _token;
|
||||
readonly Lazy<AuthTokenImmutable> _token;
|
||||
readonly Lazy<SyntheticSecurityService> _ss;
|
||||
readonly MDConsumer _consumer = new MDConsumer();
|
||||
readonly Co2RelayControl _relay;
|
||||
readonly Dictionary<SecurityKey, SecurityMD> _deleted_synthetics = new Dictionary<SecurityKey, SecurityMD>();
|
||||
long _version = 0;
|
||||
readonly ConcurrentDictionary<SecurityKey, SecurityMD> _exchange_sec_map = new ConcurrentDictionary<SecurityKey, SecurityMD>();
|
||||
List<IDisposable> _dispose = new List<IDisposable>();
|
||||
|
||||
static uint get_naggle() => mbts.config.Configuration.get<bts.md.RelayOptions>().naggle_ms;
|
||||
|
||||
[ImportingConstructor]
|
||||
public MDSource(
|
||||
Lazy<IBus> bus,
|
||||
Lazy<AuthToken> token,
|
||||
Lazy<SyntheticSecurityService> ss) {
|
||||
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss) : base(get_naggle(), true) {
|
||||
_bus = bus;
|
||||
_token = token;
|
||||
_ss = ss;
|
||||
_relay = new Co2RelayControl(_consumer);
|
||||
_relay.preload_securities((co2.MDRequest.Types.Flags)0xff).ignore();
|
||||
_dispose.Add(_consumer.on_security.Subscribe(this));
|
||||
_ss = ss;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
override public void initialize() {
|
||||
_relay.set_token(_token.Value);
|
||||
_dispose.Add(
|
||||
_ss.Value.subscribei_metadata()
|
||||
@@ -49,7 +40,7 @@ namespace bts {
|
||||
.Subscribe(t => {
|
||||
var deleted_synthetic_keys = new HashSet<uint>(t.Where(kv => kv.Value.Deleted).Select(kv => kv.Key));
|
||||
// add back 'undeleted' synthetics
|
||||
foreach (var kvp in _deleted_synthetics) {
|
||||
foreach (var kvp in _deleted_synthetics.ToList()) {
|
||||
if (!deleted_synthetic_keys.Contains(kvp.Key)) {
|
||||
_exchange_sec_map.TryAdd(kvp.Key, kvp.Value);
|
||||
_deleted_synthetics.Remove(kvp.Key);
|
||||
@@ -66,6 +57,9 @@ namespace bts {
|
||||
// Post full dictionary of syntheics here
|
||||
bus.post(SparkTags.SyntheticstrategiesTag, t);
|
||||
}));
|
||||
_dispose.Add( this.on_securities.Subscribe( (md) => {
|
||||
bus.post(SparkTags.SecuritiesTag, md);
|
||||
}));
|
||||
Task.Run(run);
|
||||
}
|
||||
|
||||
@@ -74,37 +68,5 @@ namespace bts {
|
||||
|
||||
public int priority => 55;
|
||||
IBus bus => _bus.Value;
|
||||
|
||||
public void OnCompleted() { }
|
||||
public void OnError(Exception error) => Log.Md.error(error.ToString());
|
||||
public void OnNext(List<SecurityAndHandle> values) {
|
||||
if (values.Count > 0) {
|
||||
foreach (var v in values) {
|
||||
var smd = new SecurityMD(v.security, v.handle);
|
||||
_exchange_sec_map.AddOrUpdate(smd.security.Id, smd, (_, __) => smd);
|
||||
}
|
||||
_version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Disposable.dispose(_dispose);
|
||||
async Task run() {
|
||||
long run_version = 0;
|
||||
while (true) {
|
||||
var exch = _exchange_sec_map;
|
||||
long current_version = _version;
|
||||
if (current_version > run_version) {
|
||||
run_version = current_version;
|
||||
try {
|
||||
Log.Md.info($"MDSource publishing {exch.Count} securities");
|
||||
bus.post(SparkTags.SecuritiesTag, exch);
|
||||
} catch (Exception e) {
|
||||
Log.Md.error($"Error in publish_securities: {e}");
|
||||
}
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,7 @@ using System.ComponentModel.Composition;
|
||||
namespace Spark {
|
||||
public class ShellServices {
|
||||
[Export]
|
||||
public static ServiceContext context { get; } = new ServiceContext();
|
||||
|
||||
[Export]
|
||||
public UserService user_service => context.locate<UserService>();
|
||||
public ServiceContext context { get; } = new ServiceContext();
|
||||
|
||||
[Export("settings")]
|
||||
public PersistenceService settings_service => context.locate<PersistenceService>("settings");
|
||||
|
||||
@@ -31,7 +31,9 @@ namespace mbts.ui {
|
||||
}
|
||||
|
||||
private static ImageSource load_resource_and_freeze(string resource_name) {
|
||||
return ((DrawingImage)_resources[resource_name]).frozen();
|
||||
lock (_resources) {
|
||||
return ((DrawingImage)_resources[resource_name]).frozen();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
@@ -152,7 +154,10 @@ namespace mbts.ui {
|
||||
}
|
||||
|
||||
static IEnumerable<string> _get_geometric_path(string geometric_path_key) {
|
||||
object obj = _resources[geometric_path_key];
|
||||
object obj;
|
||||
lock (_resources) {
|
||||
obj = _resources[geometric_path_key];
|
||||
}
|
||||
if (obj is string path) {
|
||||
if (string.IsNullOrEmpty(path)) {
|
||||
Log.Api.error($"Resource lookup of '{geometric_path_key}' did not return a valid string.", nameof(geometric_path_key));
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace mbts.spark {
|
||||
readonly CellProperty<string> _working_bids_text, _working_asks_text;
|
||||
readonly MyOrderSummarizer _order_summarizer = new MyOrderSummarizer();
|
||||
readonly MDDisplayHelper.SizeFormatter _size_formatter = MDDisplayHelper.get_formatter(MarketSizeDisplay.get_default());
|
||||
uint _bid_direct_size, _bid_merged_size, _ask_direct_size, _ask_merged_size;
|
||||
Property<uint> _bid_direct_size, _bid_merged_size, _ask_direct_size, _ask_merged_size;
|
||||
string _bid_merged_size_text, _ask_merged_size_text;
|
||||
ulong _bbo_ref, _last_ref;
|
||||
|
||||
@@ -45,28 +45,33 @@ namespace mbts.spark {
|
||||
_security_text = smd.security.ToString();
|
||||
_security_key_text = security_key.ToString();
|
||||
string format_price(double price) => security.ContractSpec.format(price);
|
||||
_position = new CellProperty<int>(0);
|
||||
_position = new CellProperty<int>(this, nameof(position), 0);
|
||||
_bid_price = backing(nameof(bid_price), double.NaN);
|
||||
_ask_price = backing(nameof(ask_price), double.NaN);
|
||||
_last_price = backing(nameof(last_price), double.NaN);
|
||||
_last_size = new CellProperty<uint>(0);
|
||||
_last_size = new CellProperty<uint>(this, nameof(last_size), 0);
|
||||
_day_change = backing(nameof(day_change), double.NaN);
|
||||
_session_low = backing(nameof(session_low), double.NaN);
|
||||
_session_high = backing(nameof(session_high), double.NaN);
|
||||
_settlement = backing(nameof(settlement), double.NaN);
|
||||
_volume = new CellProperty<ulong>(0);
|
||||
_inside_bids = new CellProperty<bool>();
|
||||
_inside_asks = new CellProperty<bool>();
|
||||
_working_bid_volume = new CellProperty<int>();
|
||||
_working_ask_volume = new CellProperty<int>();
|
||||
_volume = new CellProperty<ulong>(this, nameof(volume), 0);
|
||||
_inside_bids = new CellProperty<bool>(this, nameof(inside_bids));
|
||||
_inside_asks = new CellProperty<bool>(this, nameof(inside_asks));
|
||||
_working_bid_volume = new CellProperty<int>(this, nameof(working_bid_volume));
|
||||
_working_ask_volume = new CellProperty<int>(this, nameof(working_ask_volume));
|
||||
//
|
||||
_bid_direct_size = backing(nameof(bid_direct_size), default(uint));
|
||||
_bid_merged_size = backing(nameof(bid_merged_size), default(uint));
|
||||
_ask_direct_size = backing(nameof(ask_direct_size), default(uint));
|
||||
_ask_merged_size = backing(nameof(ask_merged_size), default(uint));
|
||||
//
|
||||
_working_orders = new ObservableFilterCollection<OrderRecordImmutable>(_orders, o => o.is_active());
|
||||
_working_bids = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Buy);
|
||||
_working_asks = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Sell);
|
||||
_working_best_bid = new CellProperty<double>(format_price, double.NaN);
|
||||
_working_best_ask = new CellProperty<double>(format_price, double.NaN);
|
||||
_working_bids_text = new CellProperty<string>(string.Empty);
|
||||
_working_asks_text = new CellProperty<string>(string.Empty);
|
||||
_working_best_bid = new CellProperty<double>(this, nameof(working_best_bid), format_price, double.NaN);
|
||||
_working_best_ask = new CellProperty<double>(this, nameof(working_best_ask), format_price, double.NaN);
|
||||
_working_bids_text = new CellProperty<string>(this, nameof(working_bids_text), string.Empty);
|
||||
_working_asks_text = new CellProperty<string>(this, nameof(working_asks_text), string.Empty);
|
||||
//
|
||||
default_command = new DelegateCommand(_show_ladder);
|
||||
bid_price_click = new DelegateCommand(() => _show_order_ticket(MarketViewView.Columns.Bid));
|
||||
@@ -100,15 +105,15 @@ namespace mbts.spark {
|
||||
public double last_price => _last_price.Value;
|
||||
public uint last_size => _last_size.value;
|
||||
public double bid_price => _bid_price.Value;
|
||||
public uint bid_direct_size => _bid_direct_size;
|
||||
public uint bid_implied_size => _bid_merged_size - _bid_direct_size;
|
||||
public uint bid_merged_size => _bid_merged_size;
|
||||
public string bid_merged_size_text => _bid_merged_size_text ?? (_bid_merged_size_text = _size_formatter(_bid_merged_size, bid_implied_size));
|
||||
public uint bid_direct_size => _bid_direct_size.Value;
|
||||
public uint bid_implied_size => _bid_merged_size.Value - _bid_direct_size.Value;
|
||||
public uint bid_merged_size => _bid_merged_size.Value;
|
||||
public string bid_merged_size_text => _bid_merged_size_text ?? (_bid_merged_size_text = _size_formatter(_bid_merged_size.Value, bid_implied_size));
|
||||
public double ask_price => _ask_price.Value;
|
||||
public uint ask_direct_size => _ask_direct_size;
|
||||
public uint ask_implied_size => _ask_merged_size - _ask_direct_size;
|
||||
public uint ask_merged_size => _ask_merged_size;
|
||||
public string ask_merged_size_text => _ask_merged_size_text ?? (_ask_merged_size_text = _size_formatter(_ask_merged_size, ask_implied_size));
|
||||
public uint ask_direct_size => _ask_direct_size.Value;
|
||||
public uint ask_implied_size => _ask_merged_size.Value - _ask_direct_size.Value;
|
||||
public uint ask_merged_size => _ask_merged_size.Value;
|
||||
public string ask_merged_size_text => _ask_merged_size_text ?? (_ask_merged_size_text = _size_formatter(_ask_merged_size.Value, ask_implied_size));
|
||||
public ulong volume => _volume.value;
|
||||
public double settlement => _settlement.Value;
|
||||
public double day_change => _day_change.Value;
|
||||
@@ -144,9 +149,9 @@ namespace mbts.spark {
|
||||
bool dirty = false;
|
||||
if (md.md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
|
||||
_bid_price.set(b.price);
|
||||
refresh_size(ref b, ref _bid_direct_size, ref _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
|
||||
refresh_size(ref b, _bid_direct_size, _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
|
||||
_ask_price.set(a.price);
|
||||
refresh_size(ref a, ref _ask_direct_size, ref _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
|
||||
refresh_size(ref a, _ask_direct_size, _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
|
||||
_inside_bids.on_next_distinct(_bid_price.Value >= _working_best_bid.value, ref dirty);
|
||||
_inside_asks.on_next_distinct(_ask_price.Value <= _working_best_ask.value, ref dirty);
|
||||
}
|
||||
@@ -162,10 +167,10 @@ namespace mbts.spark {
|
||||
if (dirty)
|
||||
invalidated?.Invoke();
|
||||
|
||||
void refresh_size(ref NativePriceSize nps, ref uint direct, ref uint merged, ref string text, string text_prop) {
|
||||
if (nps.size != merged || nps.direct_size != direct) {
|
||||
merged = nps.size;
|
||||
direct = nps.direct_size;
|
||||
void refresh_size(ref NativePriceSize nps, Property<uint> direct, Property<uint> merged, ref string text, string text_prop) {
|
||||
if (nps.size != merged.Value || nps.direct_size != direct.Value) {
|
||||
merged.set(nps.size);
|
||||
direct.set(nps.direct_size);
|
||||
text = null;
|
||||
dirty = true;
|
||||
RaisePropertyChanged(text_prop);
|
||||
@@ -205,36 +210,28 @@ namespace mbts.spark {
|
||||
var order = _orders_model.factory.order(security);
|
||||
switch (col) {
|
||||
case MarketViewView.Columns.Bid:
|
||||
order.Side = Side.Sell;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, bid_price, 0u));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Sell), bid_price, 0u));
|
||||
break;
|
||||
case MarketViewView.Columns.BidMergedSize:
|
||||
order.Side = Side.Sell;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, bid_price, bid_merged_size));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Sell), bid_price, bid_merged_size));
|
||||
break;
|
||||
case MarketViewView.Columns.BidDirectSize:
|
||||
order.Side = Side.Sell;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, bid_price, bid_direct_size));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Sell), bid_price, bid_direct_size));
|
||||
break;
|
||||
case MarketViewView.Columns.BidImpliedSize:
|
||||
order.Side = Side.Sell;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, bid_price, bid_implied_size));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Sell), bid_price, bid_implied_size));
|
||||
break;
|
||||
case MarketViewView.Columns.Ask:
|
||||
order.Side = Side.Buy;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, ask_price, 0u));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Buy), ask_price, 0u));
|
||||
break;
|
||||
case MarketViewView.Columns.AskMergedSize:
|
||||
order.Side = Side.Buy;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, ask_price, ask_merged_size));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Buy), ask_price, ask_merged_size));
|
||||
break;
|
||||
case MarketViewView.Columns.AskDirectSize:
|
||||
order.Side = Side.Sell;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, ask_price, ask_direct_size));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Sell), ask_price, ask_direct_size));
|
||||
break;
|
||||
case MarketViewView.Columns.AskImpliedSize:
|
||||
order.Side = Side.Sell;
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, ask_price, ask_implied_size));
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order.with(side:Side.Sell), ask_price, ask_implied_size));
|
||||
break;
|
||||
case MarketViewView.Columns.Last:
|
||||
_bus.post(SparkTags.ShowOrderTicketTag, (order, last_price, 0u));
|
||||
@@ -266,7 +263,15 @@ namespace mbts.spark {
|
||||
_working_best_ask.on_next_distinct(_order_summarizer.best_working_ask, ref dirty);
|
||||
_inside_bids.on_next_distinct(_order_summarizer.best_working_bid >= bid_price, ref dirty);
|
||||
_inside_asks.on_next_distinct(_order_summarizer.best_working_ask <= ask_price, ref dirty);
|
||||
RaisePropertyChanged(nameof(working_bids_text), nameof(working_asks_text));
|
||||
|
||||
RaisePropertyChanged(
|
||||
// Implied might not be ever set, so it probably does not RaisePropertyChanged either
|
||||
// This might be part of the filter funk?
|
||||
//nameof(bid_implied_size),
|
||||
//nameof(ask_implied_size),
|
||||
nameof(working_bids_text),
|
||||
nameof(working_asks_text)
|
||||
);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace mbts.spark {
|
||||
})
|
||||
),
|
||||
SparkCommands.ShowTimeAndSales.complete(_vm.selected_securities_observable(), _ => true, secs => secs.Count > 0 ? new SecurityCriteria(secs) : _state.security_criteria),
|
||||
SparkCommands.ShowSyntheticCreate.complete(_vm.selected_securities_observable(), secs => secs.Count > 0 && secs.Count <= 24, secs => new SecurityCriteria(secs)),
|
||||
SparkCommands.ShowSyntheticCreate.complete(_vm.sorted_selected_securities_observable(), secs => secs.Count > 0 && secs.Count <= 24, secs => new SecurityCriteria(secs)),
|
||||
SparkCommands.change_securities_command(() => (update_securities_cmd, _state.security_criteria)),
|
||||
// wrap the grid settings view factory in a dialogCommand here since the vm is destroyed/recreated when the security criteria changes
|
||||
SparkCommands.window_settings_command(new DialogCommand(() => {
|
||||
|
||||
@@ -97,8 +97,11 @@ namespace mbts.spark {
|
||||
public IEnumerable<TradeInfo> read_trades() {
|
||||
var copy = _securities;
|
||||
foreach (var tm in _channel.read_trades(ref _cursor)) {
|
||||
if (copy.TryGetValue(tm.Key, out SecurityImmutable sec))
|
||||
yield return new TradeInfo(sec, tm);
|
||||
if (tm != null) {
|
||||
if (copy.TryGetValue(tm.Key, out SecurityImmutable sec)) {
|
||||
yield return new TradeInfo(sec, new TradeMessageImmutable(tm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using mbts.ui;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
@@ -43,8 +44,23 @@ namespace bts.shell {
|
||||
name + "(1)";
|
||||
}
|
||||
|
||||
async void _show_synthetic_create(SecurityCriteria security_criteria) =>
|
||||
_show_synthetic_create((await security_criteria.securities(_bus)).ToList());
|
||||
async void _show_synthetic_create(SecurityCriteria security_criteria) {
|
||||
var found_securities = await security_criteria.securities(_bus);
|
||||
var respect_clause_security_ordering_is_possible = security_criteria.clauses.Any(clause => clause.security_keys != null && clause.security_keys.Count > 0);
|
||||
if (!respect_clause_security_ordering_is_possible) {
|
||||
_show_synthetic_create(found_securities.Values.ToList());
|
||||
} else {
|
||||
List<SecurityImmutable> ordered_security_list = new List<SecurityImmutable>();
|
||||
foreach (var clause in security_criteria.clauses) {
|
||||
foreach (var sec_key in clause.security_keys) {
|
||||
if (found_securities.TryGetValue(sec_key, out SecurityImmutable found_result)) {
|
||||
ordered_security_list.Add(found_result);
|
||||
}
|
||||
}
|
||||
_show_synthetic_create(ordered_security_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _show_synthetic_create(IReadOnlyCollection<SecurityImmutable> securities) {
|
||||
if (securities.Count == 1) {
|
||||
|
||||
@@ -53,8 +53,8 @@ namespace mbts.spark {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<uint?> create_synthetic(SecurityImmutable synthetic) {
|
||||
var result = await _service.create_synthetic_async(synthetic);
|
||||
public async Task<uint?> create_synthetic(SecurityImmutable? synthetic) {
|
||||
var result = await _service.create_synthetic_async(synthetic.Value);
|
||||
return _error.handle_error(result, "Error creating synthetic: {0}", r => r.Error, e => e == CreateResult.Types.Error.Ok, r => new uint?(r.Key), () => null).result;
|
||||
}
|
||||
|
||||
|
||||
@@ -294,9 +294,9 @@ namespace mbts.spark {
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
static string _deduce_symbol(SecurityImmutable security) {
|
||||
if (!string.IsNullOrWhiteSpace(security.Symbol)) return security.Symbol;
|
||||
if (security.Synthetic.Legs.Count() > 0) return security.Synthetic.Legs.First().Security.Symbol;
|
||||
static string _deduce_symbol(SecurityImmutable? security) {
|
||||
if (!string.IsNullOrWhiteSpace(security?.Symbol)) return security?.Symbol;
|
||||
if (security?.Synthetic.Legs.Count() > 0) return security?.Synthetic.Legs.First().Security.Symbol;
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ namespace mbts.spark {
|
||||
} else return model.on_fill_tuples;
|
||||
}
|
||||
|
||||
static IConnectableObservable<IIncrementalUpdate<FillTuple, FillTupleKey>> _get_observable(IBus bus, OrdersModel model, IObservable<StartupData> state) =>
|
||||
static IConnectableObservable<IIncrementalUpdate<FillTuple, FillTupleKey>> _get_observable(IBus bus, OrdersModel model, IObservable<StartupData> state) =>
|
||||
state.SelectManyInSeries(s => _get_observable(bus, model, s));
|
||||
|
||||
public class Row : SecurityRow, IEquatable<Row>, IRowVM {
|
||||
@@ -186,7 +186,7 @@ namespace mbts.spark {
|
||||
readonly FillImmutable _fill;
|
||||
readonly OrderRecordImmutable _order;
|
||||
|
||||
public Row(FillTuple fill_tuple, OrdersModel model) : base(fill_tuple.security_md.HasValue ? fill_tuple.security_md.Value.security : SecurityDataHelper.Defaults.Security) {
|
||||
public Row(FillTuple fill_tuple, OrdersModel model) : base(fill_tuple.security_md.HasValue ? fill_tuple.security_md.Value.security : SecurityDataHelper.Defaults.SecurityImmutable) {
|
||||
_fill = fill_tuple.fill;
|
||||
_order = fill_tuple.order ?? OrderDataHelper.Defaults.OrderRecordImmutable;
|
||||
_model = model;
|
||||
|
||||
@@ -61,10 +61,12 @@ namespace bts.shell {
|
||||
var view = new Lazy<UIElement>(() => {
|
||||
var model = new LadderModel(md, _orders_model, _md_auxiliary_model);
|
||||
var settings_manager = new WindowSettingsManager<LadderSettings>(
|
||||
_settings_hub, state_settings,
|
||||
_settings_hub, state_settings == null || state_settings.use_defaults ? null : state_settings,
|
||||
us => sanitize_ladder_settings(us.ladder),
|
||||
(us, v) => us.with(sanitize_ladder_settings(v)));
|
||||
var vm = new LadderVM(_bus, model, settings_manager);
|
||||
var init = state_settings ?? settings_manager.default_settings;
|
||||
init.use_defaults |= state_settings == null;
|
||||
var vm = new LadderVM(_bus, model, settings_manager, init);
|
||||
return new LadderView(vm);
|
||||
});
|
||||
string thread = Configuration.get<bts.config.toggles.LaddersOnThreads>().enabled ? $"Ladder{id:00}" : "";
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace mbts.spark {
|
||||
class OrderBookRowCollection : ObservableCollection<OrderBookRowVM> {
|
||||
static readonly IEqualityComparer<INode<OrderBookRowVM>> _node_equality_comparer = EqualityComparer.Create<INode<OrderBookRowVM>, uint>(n => n.value.order_key);
|
||||
readonly Func<OrderRecordTuple, OrderBookRowVM> _row_factory;
|
||||
/// Critical: Any and all row mutations <see cref="OrderBookRowVM"/> must be executed through <see cref="SortedTree{T, TKey}.TryReplace(TKey, Func{T, T})"/>.
|
||||
/// Mutations can change the sort-position of a row, and <see cref="_tree"/> will be broken if its elements aren't properly sorted (it relies heavily on binary-search logic).
|
||||
SortedTree<OrderBookRowVM, uint> _tree;
|
||||
IComparer<OrderBookRowVM> _comparer;
|
||||
Func<OrderRecordTuple, bool> _filter;
|
||||
|
||||
@@ -139,13 +139,13 @@ namespace mbts.spark {
|
||||
tif => time_in_forces.Contains(tif));
|
||||
_dispose = Disposable.Create(() => _order_type.Changed -= on_selected_order_type_changed);
|
||||
// local methods
|
||||
IOrderTypeVM create_order_type_vm(CustomOrderDefinitionImmutable cd, string id, string display_name) {
|
||||
IOrderTypeVM create_order_type_vm(CustomOrderDefinitionImmutable? cd, string id, string display_name) {
|
||||
IOrderTypeVM vm = null;
|
||||
if(cd.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic)
|
||||
vm = OrderTypeVMFactory.create(cd.CustomCase, id, display_name, model, security_md_source, cd.Dynamic.DynamicOrderType);
|
||||
if(cd?.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic)
|
||||
vm = OrderTypeVMFactory.create(cd.Value.CustomCase, id, display_name, model, security_md_source, cd.Value.Dynamic.DynamicOrderType);
|
||||
else
|
||||
vm = OrderTypeVMFactory.create(cd.CustomCase, id, display_name, model, security_md_source);
|
||||
vm.load_from(cd);
|
||||
vm = OrderTypeVMFactory.create(cd.Value.CustomCase, id, display_name, model, security_md_source);
|
||||
vm.load_from(cd.Value);
|
||||
return vm;
|
||||
}
|
||||
InlineUserProperty<IOrderTypeVM> get_order_type_vm_prop(InlineOrderProperties p) {
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace mbts.spark {
|
||||
/// Warning: <see cref="_security_md_cache"/> must NOT be reassigned with <see cref="securities"/>; the latter is always the same object.
|
||||
/// This would mean that the <see cref="GlobalExtensions.difference{T}(IEnumerable{T}, IEnumerable{T}, out IEnumerable{T}, out IEnumerable{T})"/>
|
||||
/// call above would literally be calculating the difference between the keys of the same dictionary!
|
||||
foreach (var k in removed_keys) {
|
||||
foreach (var k in removed_keys.ToList()) {
|
||||
if (_security_orders.try_lookup(k, out ISet<uint> order_keys)) {
|
||||
SecurityMD old_sec_md = _security_md_cache[k];
|
||||
removed_securities.Add(k, old_sec_md);
|
||||
@@ -109,7 +109,7 @@ namespace mbts.spark {
|
||||
}
|
||||
_security_md_cache.Remove(k);
|
||||
}
|
||||
foreach (var k in added_keys) {
|
||||
foreach (var k in added_keys.ToList()) {
|
||||
SecurityMD new_sec_md = securities[k];
|
||||
if (_security_orders.try_lookup(k, out ISet<uint> order_keys)) {
|
||||
added_securities.Add(k, new_sec_md);
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace mbts.spark
|
||||
cross_threshold = new InlineUserProperty<bts.spark.QuantityMeasure>(
|
||||
new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1}, false);
|
||||
max_chase_ticks = new InlineUserProperty<uint>(2, false);
|
||||
trade_toward_behavior = new InlineUserProperty<LeanOrderDefinition.Types.OnToward>(LeanOrderDefinition.Types.OnToward.Cancel, false);
|
||||
trade_toward_behavior = new InlineUserProperty<LeanOrderDefinition.Types.OnToward>(LeanOrderDefinition.Types.OnToward.NoChange, false);
|
||||
}
|
||||
public readonly InlineUserProperty<OrderTemplateHeader> selected_child_order_type;
|
||||
public readonly InlineUserProperty<IReadOnlyCollection<OrderTemplateHeader>> child_order_types;
|
||||
@@ -47,7 +47,7 @@ namespace mbts.spark
|
||||
|
||||
public bts.spark.QuantityMeasure cross_threshold { get; set; } = new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1 };
|
||||
public uint max_chase_ticks { get; set; } = 2;
|
||||
public LeanOrderDefinition.Types.OnToward trade_toward_behavior { get; set; } = LeanOrderDefinition.Types.OnToward.Cancel;
|
||||
public LeanOrderDefinition.Types.OnToward trade_toward_behavior { get; set; } = LeanOrderDefinition.Types.OnToward.NoChange;
|
||||
|
||||
public LeanOrderVM(OrdersModel model, IObservable<SecurityMD> security_md_source, string id, string display_name)
|
||||
: this(model, security_md_source, security_md_source.Select(smd =>
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace mbts.spark {
|
||||
var so = model.factory.spreader_order();
|
||||
so.Security = s.CloneToMutable();
|
||||
so.ExecutionStrategy = strategy.CloneToMutable();
|
||||
return new InlineUserProperty<SpreaderOrderDefinitionImmutable>(so, true);
|
||||
var soi = new SpreaderOrderDefinitionImmutable(so);
|
||||
return new InlineUserProperty<SpreaderOrderDefinitionImmutable>(soi, true);
|
||||
})), id, display_name) { }
|
||||
|
||||
public SpreaderOrderVM(
|
||||
|
||||
@@ -44,8 +44,8 @@ namespace mbts.spark {
|
||||
_dynamic_helper = dynamic_helper;
|
||||
var scheduler = ThreadPoolScheduler.Instance;
|
||||
var interval = TimeSpan.FromMilliseconds(100);
|
||||
on_orders = _service.subscribei_orders().Naggle(interval, scheduler).PermissionFilter(admin_model).Table().Cache();
|
||||
on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model).Table().Cache();
|
||||
on_orders = _service.subscribei_orders().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).Table().Cache();
|
||||
on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).Table().Cache();
|
||||
working_orders =
|
||||
on_orders.Scan(
|
||||
ImmutableDictionary<uint,OrderRecordImmutable>.Empty,
|
||||
@@ -55,7 +55,7 @@ namespace mbts.spark {
|
||||
on_order_tuples = order_record_tuple_source.subscribe_incremental_update();
|
||||
on_order_tuples_table = order_record_tuple_source.subscribe_incremental_table_update();
|
||||
on_order_securities = order_record_tuple_source.subscribe_order_securities();
|
||||
on_fills = _service.subscribei_fills().Naggle(interval, scheduler).PermissionFilter(admin_model).Table().Cache();
|
||||
on_fills = _service.subscribei_fills().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).Table().Cache();
|
||||
_activity = new OrderActivitySummarizer(bus, on_orders);
|
||||
on_fill_tuples = FillTupleSource.create_observable(bus.subscribe(SparkTags.SecuritiesTag), on_orders, on_fills);
|
||||
_security_orders_demuxer = new TableUpdateDemuxer<OrderRecordImmutable, uint, uint>(o => o.Key, o => o.security_key());
|
||||
@@ -93,10 +93,13 @@ namespace mbts.spark {
|
||||
#endregion
|
||||
|
||||
#region Input
|
||||
public async Task<uint?> send_order(OrderDefinitionImmutable order, double price, uint size) {
|
||||
var result = (await send_order_get_error(order, price, size));
|
||||
_activity.on_send_order(order, price, size, result.key, result.error);
|
||||
return result.key;
|
||||
public async Task<uint?> send_order(OrderDefinitionImmutable? order, double price, uint size) {
|
||||
if (order.HasValue) {
|
||||
var result = (await send_order_get_error(order.Value, price, size));
|
||||
_activity.on_send_order(order.Value, price, size, result.key, result.error);
|
||||
return result.key;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async Task<(uint? key, string error)> send_order_get_error(OrderDefinitionImmutable order, double price, uint size) {
|
||||
|
||||
@@ -82,9 +82,4 @@
|
||||
</ItemGroup>
|
||||
<Target Name="diagnostic" AfterTargets="PreBuildEvent">
|
||||
</Target>
|
||||
<Target Name="copy_defaults" AfterTargets="PostBuildEvent">
|
||||
<!-- this feels like the wrong place to copy defaults but it works alright -->
|
||||
<!-- <Message Importance="High" Text="copy defaults :: @(ConfigDefaults)" /> -->
|
||||
<Copy SourceFiles="@(ConfigDefaults)" DestinationFolder="$(OutputPath)\defaults\" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -3,6 +3,7 @@ using bts.api.orderman;
|
||||
using bts.shell;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
||||
@@ -218,9 +218,7 @@ namespace mbts
|
||||
private Func<T, T, int> _comparer_func;
|
||||
public DelegateComparer(Func<T, T, int> comparer_func) { _comparer_func = comparer_func; }
|
||||
public int Compare(T x, T y) { return _comparer_func(x, y); }
|
||||
}
|
||||
|
||||
public static void ignore(this Task t) { }
|
||||
}
|
||||
|
||||
public static Google.Protobuf.WellKnownTypes.Timestamp ToGoogleTime(this DateTime self) {
|
||||
return Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(self);
|
||||
|
||||
@@ -22,12 +22,12 @@ namespace mbts.ui.settings
|
||||
readonly PersistenceService _persist;
|
||||
|
||||
[ImportingConstructor]
|
||||
public UISettingsFactory(UserService login, [Import("settings")] PersistenceService persist) {
|
||||
public UISettingsFactory(AuthService login, [Import("settings")] PersistenceService persist) {
|
||||
_persist = persist;
|
||||
_users = get_users(login);
|
||||
}
|
||||
|
||||
async Task<User[]> get_users(UserService login) {
|
||||
async Task<User[]> get_users(AuthService login) {
|
||||
var users = await login.get_users_async(null);
|
||||
if (users.success) return users.value.Users.ToArray();
|
||||
else return new User[0];
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace mbts.persist
|
||||
public IObservable<IEnumerable<PersistResult<T>>> subscribe<T>(string ns, string pattern) {
|
||||
var re = new Regex(pattern);
|
||||
return _persist
|
||||
.subscribe_ns(new NamespaceSubArgs() { Ns = uns(ns) })
|
||||
.subscribe_ns(uns(ns))
|
||||
.Select(update =>
|
||||
update.added
|
||||
.Where(item => re.IsMatch(item.Key))
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
using System;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace mbts.spark {
|
||||
class CellProperty<T> : ISubject<T, (T oldval, T newval)> {
|
||||
readonly Func<T, string> _format;
|
||||
readonly IRaiseNotifyPropertyChanged _owner;
|
||||
readonly string _prop_name;
|
||||
string _text;
|
||||
|
||||
public CellProperty(T initial_value = default)
|
||||
: this(t => t.ToString(), initial_value) { }
|
||||
public CellProperty(IRaiseNotifyPropertyChanged owner, string prop_name, T initial_value = default)
|
||||
: this(owner, prop_name, t => t.ToString(), initial_value) { }
|
||||
|
||||
public CellProperty(Func<T, string> format, T initial_value = default) {
|
||||
public CellProperty(IRaiseNotifyPropertyChanged owner, string prop_name, Func<T, string> format, T initial_value = default) {
|
||||
_owner = owner;
|
||||
_prop_name = prop_name;
|
||||
_format = format;
|
||||
value = initial_value;
|
||||
}
|
||||
@@ -24,6 +29,7 @@ namespace mbts.spark {
|
||||
if (!value.Equals(this.value)) {
|
||||
OnNext(value);
|
||||
dirty = true;
|
||||
_owner.raise_notify_property_changed(_prop_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
<Setter Property="ColumnHeaderStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="GridViewColumnHeader">
|
||||
<Setter Property="Background" Value="{mbts:BTSColor header}"/>
|
||||
<!-- Background - note that we're settings this property on the containing stackpanel in which the GridViewHeaderRowPresenter resides -->
|
||||
<Setter Property="Background" Value="{x:Null}"/>
|
||||
<Setter Property="Foreground" Value="{mbts:BTSColor header_text}"/>
|
||||
<Setter Property="BorderBrush" Value="{mbts:BTSColor header}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,1,0"/>
|
||||
@@ -301,7 +302,9 @@
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
x:Name="_header_scroll_viewer">
|
||||
<StackPanel>
|
||||
<!-- setting the bg color for this stackpanel is a work-around for the vertical white lines that appear on high-dpi displays between some columns -->
|
||||
<!-- in reality, there is some high dpi rounding thing going on, but that's a battle for another day -->
|
||||
<StackPanel Background="{mbts:BTSColor Name=header}">
|
||||
<!-- We don't need super headers over the fixed columns -->
|
||||
<StackPanel Orientation="Horizontal" x:Name="_super_headers" />
|
||||
<GridViewHeaderRowPresenter x:Name="_header_scroll" AllowsColumnReorder="False" ColumnHeaderContainerStyle="{TemplateBinding ColumnHeaderStyle}" UseLayoutRounding="True" />
|
||||
|
||||
@@ -34,9 +34,11 @@ namespace bts.view.controls {
|
||||
private void any_button_click(object sender, RoutedEventArgs e) {
|
||||
Close();
|
||||
}
|
||||
internal static void Show(ErrorReportService srv, ErrorReport report) {
|
||||
var window = new SupportRequestWindow(new SupportRequestVM(srv, report));
|
||||
window.Show();
|
||||
internal static void Show(ErrorReportService srv, ErrorReportImmutable? report) {
|
||||
if (report.HasValue) {
|
||||
var window = new SupportRequestWindow(new SupportRequestVM(srv, report.Value));
|
||||
window.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using bts;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.controls.adorners;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
public class UniformTabGrid : UniformGrid {
|
||||
public UniformTabGrid () {
|
||||
this.IsItemsHost = true;
|
||||
this.Rows = 1;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size constraint) {
|
||||
var child_tabs = this.Children.OfType<TabItem>();
|
||||
var child_tabs_max_width = child_tabs.Sum(t => t.MaxWidth);
|
||||
// I noticed when debugging that this can sometimes be infinty,
|
||||
// especially when a TabItem has not been set with a MaxWidth.
|
||||
if (!double.IsInfinity(child_tabs_max_width)) {
|
||||
// if the window width is greater than the total tab width, then
|
||||
// there is more than enough space for all the tabs. If this
|
||||
// is set to the default of Stretch and the window has only 1
|
||||
// tab or few tabs, the tab appears in the middle, which is not good.
|
||||
// i.e. we want tabs to added left to right, so align left
|
||||
//
|
||||
// When the window is less than the total tab width, then align
|
||||
// can be set to stretch b/c the UniformGrid will take care of
|
||||
// sizing each tab accordingly.
|
||||
this.HorizontalAlignment = (constraint.Width > child_tabs_max_width)
|
||||
? HorizontalAlignment.Left
|
||||
: HorizontalAlignment.Stretch;
|
||||
|
||||
// this bit here is necessary because when the window is expanded
|
||||
// beyond the maxwidth of all the tabs, we switch to Left and the
|
||||
// headers snap to some smaller than MaxWidth size, which is not
|
||||
// the behavior we want
|
||||
foreach (var child in child_tabs) {
|
||||
child.Width = this.HorizontalAlignment == HorizontalAlignment.Left
|
||||
? child.MaxWidth
|
||||
: double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
return base.MeasureOverride(constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
class ResponsiveTextConverter : IValueConverter {
|
||||
|
||||
public string ShortText { get; set; }
|
||||
public string LongText { get; set; }
|
||||
public double Threshold { get; set; }
|
||||
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (value is double _input) {
|
||||
if (_input < Threshold) {
|
||||
return ShortText;
|
||||
} else {
|
||||
return LongText;
|
||||
}
|
||||
}
|
||||
return LongText;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
|
||||
xmlns:ui="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
mc:Ignorable="d" AllowDrop="True"
|
||||
Style="{StaticResource BTSFloatingWindow}"
|
||||
d:DesignHeight="300" d:DesignWidth="300"
|
||||
@@ -25,18 +26,25 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<WrapPanel x:Name="HeaderPanel" Orientation="Horizontal" Margin="0,0,100,0"
|
||||
<!-- This stack panel serves as the area between the last tab and the widget control on right side of title bar (below).
|
||||
It allows the region in between (if such a region exists) to be grabbed to move the window.
|
||||
-->
|
||||
<StackPanel Grid.Row="0" Margin="0,0,100,0" Background="Transparent" ui:WindowStyle.IsNonClientArea="True">
|
||||
<c:UniformTabGrid x:Name="HeaderPanel"
|
||||
Grid.Row="0"
|
||||
Panel.ZIndex="1"
|
||||
IsItemsHost="True"
|
||||
KeyboardNavigation.TabIndex="1"
|
||||
Background="Transparent" ui:WindowStyle.IsNonClientArea="True">
|
||||
</WrapPanel>
|
||||
Background="Transparent"
|
||||
ui:WindowStyle.IsNonClientArea="True"
|
||||
>
|
||||
</c:UniformTabGrid>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="1" BorderThickness="0"
|
||||
Padding="{Binding Path=Padding, RelativeSource={RelativeSource AncestorType=Window}}" ui:WindowStyle.IsNonClientArea="False" >
|
||||
<ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent" />
|
||||
</Border>
|
||||
<Border Grid.Row="0" Width="100" HorizontalAlignment="Right" VerticalAlignment="Stretch" Panel.ZIndex="2" Background="Transparent" ui:WindowStyle.IsNonClientArea="True">
|
||||
<!-- Window widget control area-->
|
||||
<Border Grid.Row="0" MinWidth="100" HorizontalAlignment="Right" Panel.ZIndex="2" Background="Transparent" ui:WindowStyle.IsNonClientArea="True">
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top" Panel.ZIndex="2" Background="Transparent" Margin="5">
|
||||
<Button Style="{StaticResource RoundMinimizeButton}" HorizontalAlignment="Right" Command="{x:Static SystemCommands.MinimizeWindowCommand}" />
|
||||
<Button Style="{StaticResource RoundMaximizeButton}" HorizontalAlignment="Right" Command="{x:Static SystemCommands.MaximizeWindowCommand}" />
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using mbts.config;
|
||||
using mbts.ui.native;
|
||||
using mbts.utils;
|
||||
using bts.co2;
|
||||
using bts.api.user;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.persistence;
|
||||
using bts;
|
||||
using bts.api.user;
|
||||
using mbts.config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace mbts.ui.shell
|
||||
{
|
||||
namespace mbts.ui.shell {
|
||||
// The LoginContainer participates in the startup Import/Export dependency system
|
||||
// Since the login form should not depend on *anything* it will be shown before this is created
|
||||
// Once this is created it access the global LoginModel and retrieves the username
|
||||
internal class LoginContainer
|
||||
{
|
||||
[ImportingConstructor]
|
||||
public LoginContainer(UserService user, [Import("settings")] PersistenceService persist) {
|
||||
public LoginContainer(ServiceContext context) {
|
||||
var model = LoginModel.instance;
|
||||
model.provide_services(user, persist);
|
||||
model.provide_services(context);
|
||||
// await_login pumps the dispatcher but doesn't allow control to return to MEF until the username and workspace are available
|
||||
model.await_login();
|
||||
var u = model.get_user();
|
||||
UserName = u.name;
|
||||
UserId = u.id;
|
||||
Token = u.token;
|
||||
(UserName,UserId,Token) = model.get_user();
|
||||
Workspace = model.get_workspace();
|
||||
model.Dispose();
|
||||
}
|
||||
@@ -44,21 +34,19 @@ namespace mbts.ui.shell
|
||||
[Export("userid")]
|
||||
public uint UserId { get; private set; }
|
||||
[Export]
|
||||
public AuthToken Token { get; private set; }
|
||||
public AuthTokenImmutable Token { get; private set; }
|
||||
|
||||
[Export("workspace")]
|
||||
public string Workspace { get; private set; }
|
||||
}
|
||||
|
||||
public struct User {
|
||||
public User(string name, uint id, AuthToken token) {
|
||||
public User(string name, uint id) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.token = token;
|
||||
}
|
||||
public readonly string name;
|
||||
public readonly UInt32 id;
|
||||
public readonly AuthToken token;
|
||||
}
|
||||
|
||||
public class LoginModel : IDisposable
|
||||
@@ -66,11 +54,10 @@ namespace mbts.ui.shell
|
||||
// Singleton to help bootstrap the system
|
||||
private static LoginModel _instance = new LoginModel();
|
||||
public static LoginModel instance { get { return _instance; } }
|
||||
private User _user;
|
||||
private string _workspace;
|
||||
private PreloginWorkspaceEnumerator _enum_workspace;
|
||||
|
||||
UserService _logon;
|
||||
|
||||
ServiceContext _ctx;
|
||||
DispatcherFrame _frame = new DispatcherFrame();
|
||||
|
||||
private LoginModel() {
|
||||
@@ -82,13 +69,14 @@ namespace mbts.ui.shell
|
||||
public string ErrorMessage { get; private set; }
|
||||
public bool HasFatalError { get; set; }
|
||||
public User[] Users { get; private set; }
|
||||
|
||||
|
||||
|
||||
|
||||
// The final username
|
||||
public User get_user() {
|
||||
Log.assert_d(!string.IsNullOrEmpty(_user.name));
|
||||
return _user;
|
||||
public (string, uint, AuthTokenImmutable) get_user() {
|
||||
Log.assert_d(!string.IsNullOrEmpty(_ctx.user));
|
||||
return (_ctx.user, _ctx.user_id, _ctx.token.Value);
|
||||
}
|
||||
|
||||
public string get_workspace() {
|
||||
return _workspace ?? "";
|
||||
}
|
||||
@@ -112,13 +100,13 @@ namespace mbts.ui.shell
|
||||
}
|
||||
|
||||
// Wait for the hub to create the login doodad
|
||||
(var users, var persist) = await _hub_src.Task;
|
||||
_logon = users;
|
||||
_ctx = await _hub_src.Task;
|
||||
// Populate the UI
|
||||
_enum_workspace = new PreloginWorkspaceEnumerator(persist);
|
||||
var response = await _logon.get_users_async(new GetUsersRequest());
|
||||
_enum_workspace = new PreloginWorkspaceEnumerator(_ctx.locate<PersistenceService>("settings"));
|
||||
var auth = _ctx.locate<AuthService>();
|
||||
var response = await auth.get_users_async(new GetUsersRequest());
|
||||
if (response.success) {
|
||||
Users = response.value.Users.Select(x => new User(x.Login,x.Id, null)).OrderBy(u => u.name).ToArray();
|
||||
Users = response.value.Users.Select(x => new User(x.Login,x.Id)).OrderBy(u => u.name).ToArray();
|
||||
}
|
||||
else {
|
||||
ErrorMessage = "Logon: " + response.error_message;
|
||||
@@ -129,49 +117,37 @@ namespace mbts.ui.shell
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<(bool success,string info)> do_login(string username, string workspace, string password) {
|
||||
public async Task<LoginResult> do_login(string username, string workspace, string password) {
|
||||
if (Users.Select(u => u.name).Contains(username)) {
|
||||
var response = await _logon.authenticate_login_async(new AuthenticateLoginRequest() {
|
||||
Login = username,
|
||||
Password = password ??""
|
||||
});
|
||||
if (response.success) {
|
||||
if (response.value.IsValid) {
|
||||
_workspace = workspace;
|
||||
set_authenticated_user(new User(username, response.value.UserId, response.value.Token));
|
||||
return (true, "");
|
||||
}
|
||||
else {
|
||||
return (false, response.value.Reason);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return (false, response.error_message);
|
||||
var result = await _ctx.login(username, password);
|
||||
if (result.success) {
|
||||
_workspace = workspace;
|
||||
on_user_authenticated(_ctx.user, _ctx.token);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return (false, "Invalid username");
|
||||
return new LoginResult("Invalid username");
|
||||
}
|
||||
|
||||
private void set_authenticated_user(User user) {
|
||||
Log.assert(!string.IsNullOrEmpty(user.name));
|
||||
Log.assert(Users.Select(u => u.name).Contains(user.name));
|
||||
_user = user;
|
||||
Environment.SetEnvironmentVariable("BTS_USER", user.name);
|
||||
private void on_user_authenticated(string username, AuthTokenImmutable? token) {
|
||||
Log.assert(!string.IsNullOrEmpty(username));
|
||||
Log.assert(Users.Select(u => u.name).Contains(username));
|
||||
Environment.SetEnvironmentVariable("BTS_USER", username);
|
||||
_frame.Continue = false;
|
||||
}
|
||||
|
||||
public void await_login() {
|
||||
if (string.IsNullOrEmpty(_user.name)) {
|
||||
if (!_ctx.token.HasValue) {
|
||||
Dispatcher.PushFrame(_frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region provide hub
|
||||
public void provide_services(UserService u, PersistenceService p) {
|
||||
_hub_src.TrySetResult((u,p));
|
||||
internal void provide_services(ServiceContext ctx) {
|
||||
_hub_src.TrySetResult(ctx);
|
||||
}
|
||||
private TaskCompletionSource<(UserService, PersistenceService)> _hub_src = new TaskCompletionSource<(UserService, PersistenceService)>();
|
||||
private TaskCompletionSource<ServiceContext> _hub_src = new TaskCompletionSource<ServiceContext>();
|
||||
#endregion
|
||||
|
||||
#region Dispose
|
||||
|
||||
@@ -138,14 +138,15 @@ namespace mbts.ui.shell
|
||||
if (selected_workspace == "New workspace...") {
|
||||
selected_workspace = null;
|
||||
}
|
||||
(bool success, string message) = await _model.do_login(Name, selected_workspace, Password);
|
||||
if (success) { VisualState = "Loading"; }
|
||||
if (!success && !string.IsNullOrEmpty(message)) {
|
||||
WarningMessage = message;
|
||||
var result = await _model.do_login(Name, selected_workspace, Password);
|
||||
if (result.success) { VisualState = "Loading"; }
|
||||
if (!result.success && !string.IsNullOrEmpty(result.reason)) {
|
||||
WarningMessage = result.reason;
|
||||
VisualState = "BadPassword";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(message)) {
|
||||
BTSMessageBox.Show(message, "Login Message", BTSMessageBoxButtons.OK, MessageBoxImage.Information);
|
||||
else if (!string.IsNullOrEmpty(result.reason)) {
|
||||
// Login was successful but we have a message for the user
|
||||
BTSMessageBox.Show(result.reason, "Login Message", BTSMessageBoxButtons.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
private void OnCancel(object unused) {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</TabItem.RenderTransform>
|
||||
<TabItem.Template>
|
||||
<ControlTemplate TargetType="{x:Type TabItem}">
|
||||
<Border x:Name="bd" BorderBrush="Black" BorderThickness="0,0,0.5,0" Background="#d6d6d6" Width="{DynamicResource TabWidth}" Height="{DynamicResource TabHeight}" MinWidth="40" Margin="0" SnapsToDevicePixels="True" DockPanel.Dock="Left">
|
||||
<Border x:Name="bd" BorderBrush="Black" BorderThickness="0,0,0.5,0" Background="#d6d6d6" Height="{DynamicResource TabHeight}" MinWidth="40" Margin="0" SnapsToDevicePixels="True" DockPanel.Dock="Left">
|
||||
<Border.ToolTip>
|
||||
<TextBlock Text="{Binding Title}" />
|
||||
</Border.ToolTip>
|
||||
@@ -90,6 +90,7 @@
|
||||
</TabItem.Template>
|
||||
<TabItem.Style>
|
||||
<Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
|
||||
<Setter Property="MaxWidth" Value="{DynamicResource TabWidth}" />
|
||||
<Setter Property="HeaderTemplate" Value="{StaticResource _template}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding EditMode}" Value="True">
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace bts.settings {
|
||||
public class SettingsManagerTool {
|
||||
public static void run() {
|
||||
var sc = new ServiceContext();
|
||||
var users = sc.locate<UserService>();
|
||||
var users = sc.locate<AuthService>();
|
||||
var persist = sc.locate<PersistenceService>("settings");
|
||||
var user_list = users.get_users(new GetUsersRequest());
|
||||
var user_names = user_list.Users.Select(u => u.Login);
|
||||
|
||||
@@ -18,10 +18,10 @@ namespace mbts.spark {
|
||||
return self == null ? all.Values : all.Values.Where(md => ContractPickerModel.security_in_criteria(self, md.security));
|
||||
}
|
||||
|
||||
public static async Task<IEnumerable<SecurityImmutable>> securities(this SecurityCriteria self, IBus bus) {
|
||||
public static async Task<IDictionary<uint,SecurityImmutable>> securities(this SecurityCriteria self, IBus bus) {
|
||||
var md_map = await bus.subscribe(SparkTags.SecuritiesTag).FirstAsync();
|
||||
var all_secs = md_map.Values.Select(md => md.security);
|
||||
return self == null ? all_secs : all_secs.Where(s => ContractPickerModel.security_in_criteria(self, s));
|
||||
var all_secs = md_map.Values.Select(md => md.security).ToDictionary(key => key.Id);
|
||||
return self == null ? all_secs : all_secs.Values.Where(s => ContractPickerModel.security_in_criteria(self, s)).ToDictionary(key => key.Id);
|
||||
}
|
||||
|
||||
public static IObservable<IEnumerable<SecurityImmutable>> subscribe(this SecurityCriteria self, IBus bus) {
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace mbts.spark {
|
||||
return intpart + floatpart;
|
||||
}
|
||||
|
||||
public static RegionalTickFactors get_regional_ticks(ContractSpecificationImmutable spec) =>
|
||||
public static RegionalTickFactors get_regional_ticks(ContractSpecificationImmutable? spec) =>
|
||||
TickStore.get(spec);
|
||||
|
||||
public static RegionalTickFactors get_regional_ticks(SecurityImmutable security) =>
|
||||
@@ -177,8 +177,14 @@ namespace mbts.spark {
|
||||
public static RegionalTickFactors get(ContractSpecification spec) =>
|
||||
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
|
||||
|
||||
public static RegionalTickFactors get(ContractSpecificationImmutable spec) =>
|
||||
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
|
||||
public static RegionalTickFactors get(ContractSpecificationImmutable? spec) {
|
||||
if (spec.HasValue) {
|
||||
return _exchange_ticks.GetOrAdd(new SpecKey(spec.Value), _create_rtf);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static RegionalTickFactors get(SecurityImmutable security) =>
|
||||
security.HasContractSpec ? get(security.ContractSpec) : default_rtf;
|
||||
|
||||
@@ -122,6 +122,21 @@ namespace mbts.spark
|
||||
where TRow : class, ISecurityRow =>
|
||||
security_grid.selected_securities.ObserveSelf();
|
||||
|
||||
public static IObservable<IReadOnlyList<SecurityImmutable>> sorted_selected_securities_observable<TRow, TRowModel>(this SecurityGridBase<TRow, TRowModel> security_grid)
|
||||
where TRow : class, ISecurityRow =>
|
||||
security_grid.selected_rows
|
||||
.ObserveSelf()
|
||||
.Select(rows =>
|
||||
rows.Select(
|
||||
row => (security: row.security, row_index: security_grid.rows_view.indexOf(row))
|
||||
)
|
||||
.OrderBy(sec_vs_index => sec_vs_index.row_index)
|
||||
.Select(sec_vs_index => sec_vs_index.security)
|
||||
.ToReadOnlyList()
|
||||
);
|
||||
|
||||
|
||||
|
||||
public static IObservable<IReadOnlyList<SecurityMD>> selected_security_mds_observable<TRow, TRowModel>(this SecurityMDGrid<TRow, TRowModel> security_md_grid)
|
||||
where TRow : class, ISecurityMDRow =>
|
||||
security_md_grid.selected_rows.ObserveSelf(true).Select(rows => rows.Select(r => r.md).ToReadOnlyList());
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace mbts.spark {
|
||||
{
|
||||
var lgh = source as LadderGridHost;
|
||||
double width_adjustment = (double)(uint) e.NewValue - (double)(uint) e.OldValue;
|
||||
lgh._adjust_column_width(lgh.columns.First(c => c.column_type == ColumnType.Order), width_adjustment);
|
||||
lgh._adjust_column_width(lgh.columns.FirstOrDefault(c => c.column_type == ColumnType.Order), width_adjustment);
|
||||
}
|
||||
|
||||
public uint bid_column_width
|
||||
@@ -126,7 +126,7 @@ namespace mbts.spark {
|
||||
{
|
||||
var lgh = source as LadderGridHost;
|
||||
double width_adjustment = (double)(uint) e.NewValue - (double)(uint) e.OldValue;
|
||||
lgh._adjust_column_width(lgh.columns.First(c => c.column_type == ColumnType.Bid), width_adjustment);
|
||||
lgh._adjust_column_width(lgh.columns.FirstOrDefault(c => c.column_type == ColumnType.Bid), width_adjustment);
|
||||
}
|
||||
|
||||
public uint price_column_width
|
||||
@@ -142,7 +142,7 @@ namespace mbts.spark {
|
||||
{
|
||||
var lgh = source as LadderGridHost;
|
||||
double width_adjustment = (double)(uint) e.NewValue - (double)(uint) e.OldValue;
|
||||
lgh._adjust_column_width(lgh.columns.First(c => c.column_type == ColumnType.Price), width_adjustment);
|
||||
lgh._adjust_column_width(lgh.columns.FirstOrDefault(c => c.column_type == ColumnType.Price), width_adjustment);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,14 +159,14 @@ namespace mbts.spark {
|
||||
{
|
||||
var lgh = source as LadderGridHost;
|
||||
double width_adjustment = (double)(uint) e.NewValue - (double)(uint) e.OldValue;
|
||||
lgh._adjust_column_width(lgh.columns.First(c => c.column_type == ColumnType.Ask), width_adjustment);
|
||||
lgh._adjust_column_width(lgh.columns.FirstOrDefault(c => c.column_type == ColumnType.Ask), width_adjustment);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty VolumeColumnWidthProperty =
|
||||
WPFHelper.create_dp<uint, LadderGridHost>(
|
||||
nameof(VolumeColumnWidth),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, oldval, newval) => o._adjust_column_width(o.columns.First(c => c.column_type == ColumnType.Volume), (double)newval - (double)oldval),
|
||||
(o, oldval, newval) => o._adjust_column_width(o.columns.FirstOrDefault(c => c.column_type == ColumnType.Volume), (double)newval - (double)oldval),
|
||||
50u);
|
||||
|
||||
public uint VolumeColumnWidth {
|
||||
@@ -178,7 +178,7 @@ namespace mbts.spark {
|
||||
WPFHelper.create_dp<uint, LadderGridHost>(
|
||||
nameof(LastColumnWidth),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, oldval, newval) => o._adjust_column_width(o.columns.First(c => c.column_type == ColumnType.Last), (double)newval - (double)oldval),
|
||||
(o, oldval, newval) => o._adjust_column_width(o.columns.FirstOrDefault(c => c.column_type == ColumnType.Last), (double)newval - (double)oldval),
|
||||
50u);
|
||||
|
||||
public uint LastColumnWidth {
|
||||
@@ -327,6 +327,8 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
void _adjust_column_width(Column column, double adjustment) {
|
||||
if (column == null)
|
||||
return;
|
||||
int new_col_width;
|
||||
switch (column.column_type) {
|
||||
case ColumnType.Order: new_col_width = (int)orders_column_width; break;
|
||||
@@ -662,22 +664,10 @@ namespace mbts.spark {
|
||||
} else {
|
||||
init = false;
|
||||
prev_row_height = _settings.row_height;
|
||||
// This is a general issue with default-settings:
|
||||
// "Not every property in the settings object should be shared across windows."
|
||||
// Column widths are one such property (they are changed from the actual Ladder window, not the settings dialog).
|
||||
// Below is a hack which prevents overwriting current column widths; until we come up with a better solution this will suffice.
|
||||
// TODO: fix this by moving such properties from the settings proto into a separate proto (or come up with a better workaround/solution)
|
||||
copy_col_width(v => v.orders_column);
|
||||
copy_col_width(v => v.bid_column);
|
||||
copy_col_width(v => v.price_column);
|
||||
copy_col_width(v => v.ask_column);
|
||||
copy_col_width(v => v.volume_column.general);
|
||||
copy_col_width(v => v.last_column);
|
||||
void copy_col_width(Func<LadderSettings, LadderColumnSettings> select_col) =>
|
||||
select_col(s).width = select_col(_settings).width;
|
||||
}
|
||||
//
|
||||
_settings = s;
|
||||
/// The viewmodel will always have the most up-to-date settings, so ignore <see cref="s"/> and just get it from <see cref="_vm"/>.
|
||||
/// This is a workaround for the global settings-defaults issue: when defaults are updated, some properties are inadvertantly overwritten, such as column-widths, etc.
|
||||
_settings = _vm.get_save_state();
|
||||
_bid_vis_tup.glyph_context = glyph_context(s.bid_column);
|
||||
_ask_vis_tup.glyph_context = glyph_context(s.ask_column);
|
||||
_price_vis_tup.glyph_context = glyph_context(s.price_column);
|
||||
@@ -698,18 +688,19 @@ namespace mbts.spark {
|
||||
// Renders the grid lines and column backgrounds
|
||||
_visualHost.AddVisualChild(_gridvis);
|
||||
// Orders column
|
||||
orders_column_width = s.orders_column.width;
|
||||
SetCurrentValue(orders_column_widthProperty, s.orders_column.width);
|
||||
_ordershost.update_settings(_price_list, _price_comparer, s.row_height, s.orders_column);
|
||||
foreach (var col in columns) {
|
||||
if (col.visual_tup != null) {
|
||||
if (col.settings.show) {
|
||||
_visualHost.AddVisualChild(col.visual_tup.visual);
|
||||
switch (col.column_type) {
|
||||
case ColumnType.Bid: bid_column_width = col.settings.width; break;
|
||||
case ColumnType.Price: price_column_width = col.settings.width; break;
|
||||
case ColumnType.Ask: ask_column_width = col.settings.width; break;
|
||||
case ColumnType.Bid: SetCurrentValue(bid_column_widthProperty, col.settings.width); break;
|
||||
case ColumnType.Price: SetCurrentValue(price_column_widthProperty, col.settings.width); break;
|
||||
case ColumnType.Ask: SetCurrentValue(ask_column_widthProperty, col.settings.width); break;
|
||||
case ColumnType.Last: SetCurrentValue(LastColumnWidthProperty, col.settings.width); break;
|
||||
case ColumnType.Volume:
|
||||
VolumeColumnWidth = col.settings.width;
|
||||
SetCurrentValue(VolumeColumnWidthProperty, col.settings.width);
|
||||
_volume_container.mark_dirty();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Windows;
|
||||
using bts.proto;
|
||||
using mbts.ui.spark;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace mbts.spark {
|
||||
// IMPORTANT: BE SURE TO ADD ANY NEW PROPERTIES TO THE EQUALS FUNCTION!
|
||||
@@ -179,6 +180,34 @@ namespace mbts.spark {
|
||||
// Settings are liable to change in the near future, so an internal IEqualityComparer simply won't do as it risks becoming out of date.
|
||||
[ProtoContract]
|
||||
class LadderSettings : IEquatable<LadderSettings> {
|
||||
const double _default_order_entry_panel_width = 110.0;
|
||||
const double _default_zoom_factor = 1.0;
|
||||
|
||||
// Default proto constructor
|
||||
// Added this so that when clients upgrade to a version with new settings,
|
||||
// the default values are applied correctly; thx to John for this tip
|
||||
public LadderSettings() {
|
||||
day_visible = true;
|
||||
high_visible = true;
|
||||
low_visible = true;
|
||||
open_visible = true;
|
||||
settle_visible = true;
|
||||
volume_visible = true;
|
||||
account_visible = true;
|
||||
tif_visible = true;
|
||||
order_type_visible = true;
|
||||
account_position_visible = true;
|
||||
size_presets_visible = true;
|
||||
cancel_buys_visible = true;
|
||||
cancel_sells_visible = true;
|
||||
cancel_all_visible = true;
|
||||
auto_clear_visible = true;
|
||||
clear_visible = true;
|
||||
order_entry_panel_initial_width = _default_order_entry_panel_width;
|
||||
zoom_factor = _default_zoom_factor;
|
||||
use_defaults = true;
|
||||
}
|
||||
|
||||
public static LadderSettings Default {
|
||||
get {
|
||||
LadderColumnSettings
|
||||
@@ -216,7 +245,26 @@ namespace mbts.spark {
|
||||
session_high_line = new LadderMarketBreakLine { color = BTSColors.MarketData.session_high, show = true },
|
||||
session_range_fill = new LadderMarketBreakLine { color = BTSColors.MarketData.session_range_fill, show = true },
|
||||
alternate_order_size = 0,
|
||||
size_display = MarketSizeDisplay.get_default()
|
||||
size_display = MarketSizeDisplay.get_default(),
|
||||
day_visible = true,
|
||||
high_visible = true,
|
||||
low_visible = true,
|
||||
open_visible = true,
|
||||
settle_visible = true,
|
||||
volume_visible = true,
|
||||
account_visible = true,
|
||||
tif_visible = true,
|
||||
order_type_visible = true,
|
||||
account_position_visible = true,
|
||||
size_presets_visible = true,
|
||||
cancel_buys_visible = true,
|
||||
cancel_sells_visible = true,
|
||||
cancel_all_visible = true,
|
||||
auto_clear_visible = true,
|
||||
clear_visible = true,
|
||||
order_entry_panel_initial_width = _default_order_entry_panel_width,
|
||||
zoom_slider_visible = false,
|
||||
zoom_factor = _default_zoom_factor,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -278,6 +326,54 @@ namespace mbts.spark {
|
||||
[ProtoMember(26)]
|
||||
public MarketSizeDisplay size_display { get; set; }
|
||||
|
||||
// order entry panel items show/hide
|
||||
[ProtoMember(27), DefaultValue(true)]
|
||||
public bool day_visible { get; set; }
|
||||
[ProtoMember(28), DefaultValue(true)]
|
||||
public bool high_visible { get; set; }
|
||||
[ProtoMember(29), DefaultValue(true)]
|
||||
public bool low_visible { get; set; }
|
||||
[ProtoMember(30), DefaultValue(true)]
|
||||
public bool open_visible { get; set; }
|
||||
[ProtoMember(31), DefaultValue(true)]
|
||||
public bool settle_visible { get; set; }
|
||||
[ProtoMember(32), DefaultValue(true)]
|
||||
public bool volume_visible { get; set; }
|
||||
[ProtoMember(33), DefaultValue(true)]
|
||||
public bool account_visible { get; set; }
|
||||
[ProtoMember(34), DefaultValue(true)]
|
||||
public bool tif_visible { get; set; }
|
||||
[ProtoMember(35), DefaultValue(true)]
|
||||
public bool order_type_visible { get; set; }
|
||||
[ProtoMember(36), DefaultValue(true)]
|
||||
public bool account_position_visible { get; set; }
|
||||
[ProtoMember(37), DefaultValue(true)]
|
||||
public bool size_presets_visible { get; set; }
|
||||
[ProtoMember(38), DefaultValue(true)]
|
||||
public bool cancel_buys_visible { get; set; }
|
||||
[ProtoMember(39), DefaultValue(true)]
|
||||
public bool cancel_sells_visible { get; set; }
|
||||
[ProtoMember(40), DefaultValue(true)]
|
||||
public bool cancel_all_visible { get; set; }
|
||||
[ProtoMember(41), DefaultValue(true)]
|
||||
public bool auto_clear_visible { get; set; }
|
||||
[ProtoMember(42), DefaultValue(true)]
|
||||
public bool clear_visible { get; set; }
|
||||
//
|
||||
[ProtoMember(43), DefaultValue(_default_order_entry_panel_width)]
|
||||
public double order_entry_panel_initial_width { get; set; }
|
||||
|
||||
[ProtoMember(44), DefaultValue(true)]
|
||||
public bool use_defaults { get; set; }
|
||||
|
||||
// the zoom slider is the first new widget on the panel
|
||||
// since our initial general release, so its enablement
|
||||
// will be opt-in (default not visible)
|
||||
[ProtoMember(45)]
|
||||
public bool zoom_slider_visible { get; set; }
|
||||
[ProtoMember(46), DefaultValue(_default_zoom_factor)]
|
||||
public double zoom_factor { get; set; }
|
||||
|
||||
public bool Equals(LadderSettings other) =>
|
||||
other != null &&
|
||||
row_height == other.row_height &&
|
||||
@@ -302,7 +398,27 @@ namespace mbts.spark {
|
||||
session_high_line.safe_equals(other.session_high_line) &&
|
||||
session_range_fill.safe_equals(other.session_range_fill) &&
|
||||
volume_column.safe_equals(other.volume_column) &&
|
||||
size_display.safe_equals(other.size_display);
|
||||
size_display.safe_equals(other.size_display) &&
|
||||
day_visible == other.day_visible &&
|
||||
high_visible == other.high_visible &&
|
||||
low_visible == other.low_visible &&
|
||||
open_visible == other.open_visible &&
|
||||
settle_visible == other.settle_visible &&
|
||||
volume_visible == other.volume_visible &&
|
||||
account_visible == other.account_visible &&
|
||||
tif_visible == other.tif_visible &&
|
||||
order_type_visible == other.order_type_visible &&
|
||||
account_position_visible == other.account_position_visible &&
|
||||
size_presets_visible == other.size_presets_visible &&
|
||||
cancel_buys_visible == other.cancel_buys_visible &&
|
||||
cancel_sells_visible == other.cancel_sells_visible &&
|
||||
cancel_all_visible == other.cancel_all_visible &&
|
||||
auto_clear_visible == other.auto_clear_visible &&
|
||||
clear_visible == other.clear_visible &&
|
||||
order_entry_panel_initial_width == other.order_entry_panel_initial_width &&
|
||||
zoom_slider_visible == other.zoom_slider_visible &&
|
||||
zoom_factor == other.zoom_factor &&
|
||||
use_defaults == other.use_defaults;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as LadderSettings);
|
||||
|
||||
@@ -330,7 +446,46 @@ namespace mbts.spark {
|
||||
EqualityComparer.safe_get_hash_code(session_high_line),
|
||||
EqualityComparer.safe_get_hash_code(session_range_fill),
|
||||
EqualityComparer.safe_get_hash_code(volume_column),
|
||||
EqualityComparer.safe_get_hash_code(size_display));
|
||||
EqualityComparer.safe_get_hash_code(size_display),
|
||||
day_visible.GetHashCode(),
|
||||
high_visible.GetHashCode(),
|
||||
low_visible.GetHashCode(),
|
||||
open_visible.GetHashCode(),
|
||||
settle_visible.GetHashCode(),
|
||||
volume_visible.GetHashCode(),
|
||||
account_visible.GetHashCode(),
|
||||
tif_visible.GetHashCode(),
|
||||
order_type_visible.GetHashCode(),
|
||||
account_position_visible.GetHashCode(),
|
||||
size_presets_visible.GetHashCode(),
|
||||
cancel_buys_visible.GetHashCode(),
|
||||
cancel_sells_visible.GetHashCode(),
|
||||
cancel_all_visible.GetHashCode(),
|
||||
auto_clear_visible.GetHashCode(),
|
||||
clear_visible.GetHashCode(),
|
||||
order_entry_panel_initial_width.GetHashCode(),
|
||||
zoom_slider_visible.GetHashCode(),
|
||||
zoom_factor.GetHashCode(),
|
||||
use_defaults.GetHashCode());
|
||||
|
||||
public struct WindowSpecifics {
|
||||
public readonly double order_entry_panel_width, zoom_factor;
|
||||
public readonly uint orders_column_width, bid_column_width, price_column_width, ask_column_width, last_column_width, volume_column_width;
|
||||
|
||||
public WindowSpecifics(LadderSettings s)
|
||||
: this(s.order_entry_panel_initial_width, s.orders_column.width, s.bid_column.width, s.price_column.width, s.ask_column.width, s.last_column.width, s.volume_column.general.width, s.zoom_factor) { }
|
||||
|
||||
public WindowSpecifics(double order_entry_panel_width, uint orders_column_width, uint bid_column_width, uint price_column_width, uint ask_column_width, uint last_column_width, uint volume_column_width, double zoom_factor) {
|
||||
this.order_entry_panel_width = order_entry_panel_width;
|
||||
this.orders_column_width = orders_column_width;
|
||||
this.bid_column_width = bid_column_width;
|
||||
this.price_column_width = price_column_width;
|
||||
this.ask_column_width = ask_column_width;
|
||||
this.last_column_width = last_column_width;
|
||||
this.volume_column_width = volume_column_width;
|
||||
this.zoom_factor = zoom_factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class LadderSettingsExtensions {
|
||||
@@ -345,5 +500,22 @@ namespace mbts.spark {
|
||||
|
||||
public static IEnumerable<LadderColumnSettings> visible_columns(this LadderSettings settings) =>
|
||||
settings.all_columns().Where(c => c.show);
|
||||
|
||||
public static LadderSettings.WindowSpecifics get_window_specifics(this LadderSettings settings) =>
|
||||
new LadderSettings.WindowSpecifics(settings);
|
||||
|
||||
public static LadderSettings with_window_specifics(this LadderSettings source, LadderSettings.WindowSpecifics specifics) {
|
||||
// must use clone bc we are applying settings that are specific to each individual LadderWindow, and should not be shared among all ladders (even if it is default settings).
|
||||
source = SparkUtils.clone_proto(source);
|
||||
source.order_entry_panel_initial_width = specifics.order_entry_panel_width;
|
||||
source.orders_column.width = specifics.orders_column_width;
|
||||
source.bid_column.width = specifics.bid_column_width;
|
||||
source.price_column.width = specifics.price_column_width;
|
||||
source.ask_column.width = specifics.ask_column_width;
|
||||
source.last_column.width = specifics.last_column_width;
|
||||
source.volume_column.general.width = specifics.volume_column_width;
|
||||
source.zoom_factor = specifics.zoom_factor;
|
||||
return source;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ namespace mbts.spark {
|
||||
readonly Property<LadderSettings.BadgeStyle> _badge_style;
|
||||
readonly Property<uint> _alternate_order_size;
|
||||
readonly DelegateProperty<double> _row_height;
|
||||
Func<double> _get_order_entry_panel_width, _get_zoom_factor;
|
||||
LadderSettings _current;
|
||||
LadderColumnSettingsVM
|
||||
_orders = new OrdersColumnSettingsVM(),
|
||||
@@ -32,14 +33,35 @@ namespace mbts.spark {
|
||||
_session_low_mkt_line = new LadderMarketBreakLineVM("Session Low", 2, 1),
|
||||
_session_high_mkt_line = new LadderMarketBreakLineVM("Session High", 2, 1, false),
|
||||
_session_range_fill = new LadderMarketBreakLineVM("Session Range", 0, 0, false);
|
||||
OrderEntryPanelItemVisibilityVM
|
||||
_zoom_slider_visbility = new OrderEntryPanelItemVisibilityVM("Zoom Slider"),
|
||||
_day_visibility = new OrderEntryPanelItemVisibilityVM("Day Change"),
|
||||
_high_visibility = new OrderEntryPanelItemVisibilityVM("High"),
|
||||
_low_visibility = new OrderEntryPanelItemVisibilityVM("Low"),
|
||||
_open_visibility = new OrderEntryPanelItemVisibilityVM("Open"),
|
||||
_settle_visibility = new OrderEntryPanelItemVisibilityVM("Settle"),
|
||||
_volume_visibility = new OrderEntryPanelItemVisibilityVM("Volume"),
|
||||
_account_visibility = new OrderEntryPanelItemVisibilityVM("Account"),
|
||||
_tif_visibility = new OrderEntryPanelItemVisibilityVM("Time In Force"),
|
||||
_order_type_visibility = new OrderEntryPanelItemVisibilityVM("Order Type"),
|
||||
_account_position_visibility = new OrderEntryPanelItemVisibilityVM("Account Position"),
|
||||
_size_preset_visibility = new OrderEntryPanelItemVisibilityVM("Size Presets"),
|
||||
_auto_clear_visibility = new OrderEntryPanelItemVisibilityVM("Auto Clear"),
|
||||
_clear_visibility = new OrderEntryPanelItemVisibilityVM("Clear"),
|
||||
_cancel_buys_visibility = new OrderEntryPanelItemVisibilityVM("Cancel Buys"),
|
||||
_cancel_sells_visibility = new OrderEntryPanelItemVisibilityVM("Cancel Sells"),
|
||||
_cancel_all_visibility = new OrderEntryPanelItemVisibilityVM("Cancel All");
|
||||
|
||||
public LadderSettingsVM(bool is_synthetic) {
|
||||
public LadderSettingsVM(bool is_synthetic, Func<double> get_left_panel_width, Func<double> get_zoom_factor) {
|
||||
this.is_synthetic = is_synthetic;
|
||||
size_presets = new ObservableEditableCollection<IMutableValueWrapperVM<uint>>(_create_size_preset_vm);
|
||||
size_presets.CollectionChanged += (s, e) => RaisePropertyChanged(nameof(size_presets_text));
|
||||
_badge_style = backing<LadderSettings.BadgeStyle>(nameof(badge_style));
|
||||
_badge_style.Changed += _badge_style_Changed;
|
||||
_alternate_order_size = backing<uint>(nameof(alternate_order_size));
|
||||
_get_order_entry_panel_width = get_left_panel_width;
|
||||
_get_zoom_factor = get_zoom_factor;
|
||||
// be sure to initialize backings before this measure_row_height call
|
||||
_row_height = backing(nameof(row_height), () => LadderRender.measure_row_height(get_result()));
|
||||
_orders.font_size_changed += on_font_size_changed;
|
||||
_bid.font_size_changed += on_font_size_changed;
|
||||
@@ -108,6 +130,28 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<OrderEntryPanelItemVisibilityVM> order_entry_panel_items {
|
||||
get {
|
||||
yield return _zoom_slider_visbility;
|
||||
yield return _day_visibility;
|
||||
yield return _high_visibility;
|
||||
yield return _low_visibility;
|
||||
yield return _open_visibility;
|
||||
yield return _settle_visibility;
|
||||
yield return _volume_visibility;
|
||||
yield return _account_visibility;
|
||||
yield return _tif_visibility;
|
||||
yield return _order_type_visibility;
|
||||
yield return _account_position_visibility;
|
||||
yield return _size_preset_visibility;
|
||||
yield return _clear_visibility;
|
||||
yield return _auto_clear_visibility;
|
||||
yield return _cancel_buys_visibility;
|
||||
yield return _cancel_sells_visibility;
|
||||
yield return _cancel_all_visibility;
|
||||
}
|
||||
}
|
||||
|
||||
public string key => GridOrLadderSettingsSelectorKeys.Ladder;
|
||||
|
||||
public void load(LadderSettings settings) {
|
||||
@@ -128,6 +172,27 @@ namespace mbts.spark {
|
||||
_session_low_mkt_line.assign_from(_current.session_low_line);
|
||||
_session_high_mkt_line.assign_from(_current.session_high_line);
|
||||
_session_range_fill.assign_from(_current.session_range_fill);
|
||||
_zoom_slider_visbility.show = settings.zoom_slider_visible;
|
||||
_day_visibility.show = settings.day_visible;
|
||||
_high_visibility.show = settings.high_visible;
|
||||
_low_visibility.show = settings.low_visible;
|
||||
_open_visibility.show = settings.open_visible;
|
||||
_settle_visibility.show = settings.settle_visible;
|
||||
_volume_visibility.show = settings.volume_visible;
|
||||
_account_visibility.show = settings.account_visible;
|
||||
_tif_visibility.show = settings.tif_visible;
|
||||
_order_type_visibility.show = settings.order_type_visible;
|
||||
_account_position_visibility.show = settings.account_position_visible;
|
||||
_size_preset_visibility.show = settings.size_presets_visible;
|
||||
_clear_visibility.show = settings.clear_visible;
|
||||
_auto_clear_visibility.show = settings.auto_clear_visible;
|
||||
_cancel_buys_visibility.show = settings.cancel_buys_visible;
|
||||
_cancel_sells_visibility.show = settings.cancel_sells_visible;
|
||||
_cancel_all_visibility.show = settings.cancel_all_visible;
|
||||
// do not read the order entry panel width here from settings
|
||||
// because it could have changed from the UI
|
||||
// there might be a better way of doing this, like how
|
||||
// does the widths of the ladder grid column pull this off?
|
||||
}
|
||||
|
||||
public LadderSettings get_result() {
|
||||
@@ -149,6 +214,25 @@ namespace mbts.spark {
|
||||
clone.session_high_line = _session_high_mkt_line.get_settings();
|
||||
clone.session_range_fill = _session_range_fill.get_settings();
|
||||
clone.row_height = LadderRender.measure_row_height(clone);
|
||||
clone.day_visible = _day_visibility.show;
|
||||
clone.high_visible = _high_visibility.show;
|
||||
clone.low_visible = _low_visibility.show;
|
||||
clone.open_visible = _open_visibility.show;
|
||||
clone.settle_visible = _settle_visibility.show;
|
||||
clone.volume_visible = _volume_visibility.show;
|
||||
clone.account_visible = _account_visibility.show;
|
||||
clone.tif_visible = _tif_visibility.show;
|
||||
clone.order_type_visible = _order_type_visibility.show;
|
||||
clone.account_position_visible = _account_position_visibility.show;
|
||||
clone.size_presets_visible = _size_preset_visibility.show;
|
||||
clone.auto_clear_visible = _auto_clear_visibility.show;
|
||||
clone.clear_visible = _clear_visibility.show;
|
||||
clone.cancel_buys_visible = _cancel_buys_visibility.show;
|
||||
clone.cancel_sells_visible = _cancel_sells_visibility.show;
|
||||
clone.cancel_all_visible = _cancel_all_visibility.show;
|
||||
clone.order_entry_panel_initial_width = _get_order_entry_panel_width();
|
||||
clone.zoom_slider_visible = _zoom_slider_visbility.show;
|
||||
clone.zoom_factor = _get_zoom_factor();
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -276,6 +360,19 @@ namespace mbts.spark {
|
||||
};
|
||||
}
|
||||
|
||||
public class OrderEntryPanelItemVisibilityVM : ViewModelBase {
|
||||
readonly Property<bool> _show;
|
||||
|
||||
public OrderEntryPanelItemVisibilityVM(string name) {
|
||||
this.name = name;
|
||||
_show = backing(nameof(show), true);
|
||||
}
|
||||
|
||||
public string name { get; }
|
||||
public bool show { get => _show.Value; set => _show.Value = value; }
|
||||
|
||||
}
|
||||
|
||||
public class VolumeColumnVM : ViewModelBase {
|
||||
readonly DelegateProperty<bool> _any_visible;
|
||||
public VolumeColumnVM() {
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
<Setter Property="VerticalContentAlignment" Value="Top"/>
|
||||
</Style>
|
||||
</TabControl.ItemContainerStyle>
|
||||
<!-- Fonts and Colors Tab -->
|
||||
<TabItem Header="Fonts and Colors" VerticalAlignment="Stretch">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -232,6 +233,7 @@
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<!-- Market Break Lines Tab -->
|
||||
<TabItem Header="Market Break Lines" VerticalAlignment="Stretch">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -350,6 +352,7 @@
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<!-- Market Size Tab -->
|
||||
<TabItem Header="Market Size Display">
|
||||
<Grid DataContext="{Binding size_display}">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -440,6 +443,7 @@
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<!-- Volume Display Tab -->
|
||||
<TabItem Header="Volume Display" VerticalAlignment="Stretch" Visibility="{Binding is_synthetic, Converter={StaticResource FalseVisibilityConverter}}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -557,6 +561,7 @@
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<!-- Size Presets Tab-->
|
||||
<TabItem Header="Size Presets" VerticalAlignment="Stretch">
|
||||
<Grid HorizontalAlignment="Left">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -592,6 +597,63 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<!-- Order Entry Panel Item Control Visbility -->
|
||||
<TabItem Header="Left Panel" VerticalAlignment="Stretch">
|
||||
<Grid HorizontalAlignment="Left" Grid.IsSharedSizeScope="True">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ItemsControl Grid.Row="0" ItemsSource="{Binding order_entry_panel_items}" Grid.IsSharedSizeScope="True">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" Focusable="False"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="A" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Margin="0,5,5,0">
|
||||
<CheckBox Content="{Binding name}" FlowDirection="RightToLeft" IsChecked="{Binding show}">
|
||||
<CheckBox.Resources>
|
||||
<Style TargetType="{x:Type Path}">
|
||||
<Setter Property="FlowDirection" Value="LeftToRight" />
|
||||
</Style>
|
||||
</CheckBox.Resources>
|
||||
</CheckBox>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!--
|
||||
<ItemsControl ItemsSource="{Binding order_entry_panel_items}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" Focusable="False"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="52"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="{Binding name}"/>
|
||||
<CheckBox Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding show}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
-->
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -20,18 +20,24 @@ using System.Windows.Media;
|
||||
|
||||
namespace mbts.spark {
|
||||
class LadderVM : DisposableViewModelBase, IWillLetYouKnowWhenIDispose, IKeyListener {
|
||||
static readonly Brush _transparent_status_brush = BTSColors.UI.transparent;
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly LadderModel _model;
|
||||
readonly Property<double> _high, _low, _change, _open, _settlement, _position;
|
||||
readonly Property<double> _high, _low, _change, _open, _settlement, _position, _left_panel_width, _zoom_factor;
|
||||
//readonly Property<double> _window_width, _window_height;
|
||||
readonly Property<uint> _orders_column_width, _bid_column_width, _price_column_width, _ask_column_width, _last_column_width, _volume_column_width;
|
||||
readonly Property<ulong> _volume;
|
||||
readonly Property<bool> _auto_clear;
|
||||
readonly Property<bool> _auto_clear, _show_day, _show_high, _show_low, _show_open, _show_settle, _show_volume, _show_account,
|
||||
_show_tif, _show_order_type, _show_account_position, _show_size_preset, _show_auto_clear, _show_clear, _show_cancel_buys,
|
||||
_show_cancel_sells, _show_cancel_all, _show_zoom;
|
||||
readonly Property<Brush> _bid_background, _bid_foreground, _ask_background, _ask_foreground;
|
||||
IWindowSettingsManager<LadderSettings> _settings_manager;
|
||||
ulong _misc_time = 0;
|
||||
IEqualityComparer<double> _equality;
|
||||
Dictionary<double, LadderOrderVM> _slot_price_vs_orders;
|
||||
|
||||
public LadderVM(IBus bus, LadderModel model, IWindowSettingsManager<LadderSettings> settings_manager) {
|
||||
public LadderVM(IBus bus, LadderModel model, IWindowSettingsManager<LadderSettings> settings_manager, LadderSettings init) {
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
_equality = TickUtils.tick_equality_comparer(_model.security.ContractSpec);
|
||||
@@ -44,13 +50,23 @@ namespace mbts.spark {
|
||||
_change = backing(nameof(change), double.NaN);
|
||||
_open = backing(nameof(open), double.NaN);
|
||||
_settlement = backing(nameof(settlement), double.NaN);
|
||||
// init window-specific settings: these properties should not observe settings updates; they are independent from the settings dialog, so it doesn't make sense that the settings dialog should cause them to change.
|
||||
var ws = init.get_window_specifics();
|
||||
_orders_column_width = backing(nameof(orders_column_width), ws.orders_column_width);
|
||||
_bid_column_width = backing(nameof(bid_column_width), ws.bid_column_width);
|
||||
_price_column_width = backing(nameof(price_column_width), ws.price_column_width);
|
||||
_ask_column_width = backing(nameof(ask_column_width), ws.ask_column_width);
|
||||
_last_column_width = backing(nameof(last_column_width), ws.last_column_width);
|
||||
_volume_column_width = backing(nameof(volume_column_width), ws.volume_column_width);
|
||||
_left_panel_width = backing(nameof(left_panel_width), ws.order_entry_panel_width);
|
||||
_zoom_factor = backing(nameof(zoom_factor), ws.zoom_factor);
|
||||
// remember: account_positions needs to subscribe to selected_account; both the sub and account_positions need to be disposed
|
||||
var account_positions = new KeyDrivenObservableSeries<string, double>(a => _model.order_gateway.security_total_position(security.Id, a));
|
||||
_position = backing(nameof(position), account_positions.OnDispatcher());
|
||||
_volume = backing(nameof(volume), 0ul);
|
||||
_auto_clear = backing(nameof(auto_clear), false);
|
||||
_status = backing(nameof(status_message), "");
|
||||
_status_brush = backing(nameof(status_background), (Brush)BTSColors.UI.spark_menu_background);
|
||||
_status_brush = backing(nameof(status_background), _transparent_status_brush);
|
||||
// brush props
|
||||
_bid_background = brush_prop(nameof(bid_background), s => s.bid_column.background.rgb_brush());
|
||||
_bid_foreground = brush_prop(nameof(bid_foreground), s => s.bid_column.foreground.rgb_brush());
|
||||
@@ -58,7 +74,31 @@ namespace mbts.spark {
|
||||
_ask_foreground = brush_prop(nameof(ask_foreground), s => s.ask_column.foreground.rgb_brush());
|
||||
Property<Brush> brush_prop(string name, Func<LadderSettings, Brush> get_brush) =>
|
||||
backing(name, settings_manager.subscribe_current().OnDispatcher(), get_brush);
|
||||
// bool visibility props
|
||||
_show_day = bool_prop(nameof(show_day), s => s.day_visible);
|
||||
_show_high = bool_prop(nameof(show_high), s => s.high_visible);
|
||||
_show_low = bool_prop(nameof(show_low), s => s.low_visible);
|
||||
_show_open = bool_prop(nameof(show_open), s => s.open_visible);
|
||||
_show_settle = bool_prop(nameof(show_settle), s => s.settle_visible);
|
||||
_show_volume = bool_prop(nameof(show_volume), s => s.volume_visible);
|
||||
_show_account = bool_prop(nameof(show_account), s => s.account_visible);
|
||||
_show_tif = bool_prop(nameof(show_tif), s => s.tif_visible);
|
||||
_show_order_type = bool_prop(nameof(show_order_type), s => s.order_type_visible);
|
||||
_show_account_position = bool_prop(nameof(show_account_position), s => s.account_position_visible);
|
||||
_show_size_preset = bool_prop(nameof(show_size_preset), s => s.size_presets_visible);
|
||||
_show_auto_clear = bool_prop(nameof(show_auto_clear), s => s.auto_clear_visible);
|
||||
_show_clear = bool_prop(nameof(show_clear), s => s.clear_visible);
|
||||
_show_cancel_buys = bool_prop(nameof(show_cancel_buys), s => s.cancel_buys_visible);
|
||||
_show_cancel_sells = bool_prop(nameof(show_cancel_sells), s => s.cancel_sells_visible);
|
||||
_show_cancel_all = bool_prop(nameof(show_cancel_all), s => s.cancel_all_visible);
|
||||
_show_zoom = bool_prop(nameof(show_zoom), s => s.zoom_slider_visible);
|
||||
|
||||
Property<bool> bool_prop(string name, Func<LadderSettings, bool> get_bool) =>
|
||||
backing(name, settings_manager.subscribe_current().OnDispatcher(), get_bool);
|
||||
|
||||
//
|
||||
//_window_height = backing(nameof(window_height), settings_manager.subscribe_current().OnDispatcher().Select(s => s.zoom_factor), (scale) => scale * 600);
|
||||
//_window_width = backing(nameof(window_width), settings_manager.subscribe_current().OnDispatcher().Select(s => s.zoom_factor), (scale) => scale * 420);
|
||||
position_click_command = new DelegateCommand(() => size_calculator.value = DoubleHelper.safe_convert_to_uint(Math.Abs(position)));
|
||||
send_order_command = new DelegateCommand<(double price, Side? side, bool size_from_alternate)>(send_order);
|
||||
cancel_command = new AsyncCommand<LadderModel.CancelType>(_model.cancel_orders, true);
|
||||
@@ -67,11 +107,11 @@ namespace mbts.spark {
|
||||
settings_command = new DialogCommand(() => {
|
||||
var vm = new WindowSettingsDialogVM<LadderSettings, LadderSettingsVM>(
|
||||
settings_manager, s => s.ladder, UserSettings.FileExtensions.LADDER,
|
||||
s => {
|
||||
var svm = new LadderSettingsVM(security.ContractCase == Security.ContractOneofCase.Synthetic);
|
||||
s => {
|
||||
var svm = new LadderSettingsVM(security.ContractCase == Security.ContractOneofCase.Synthetic, () => left_panel_width, () => zoom_factor);
|
||||
svm.load(s);
|
||||
return svm;
|
||||
});
|
||||
}, s => s.with_window_specifics(get_save_state().get_window_specifics()));
|
||||
var dialog = new ui.spark.SimpleGridSettingsDialog {
|
||||
DataContext = vm,
|
||||
Title = $"Ladder Settings - '{security}'"
|
||||
@@ -202,7 +242,7 @@ namespace mbts.spark {
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
if (_status.Value == a.message) {
|
||||
_status.set("");
|
||||
_status_brush.set(BTSColors.UI.spark_menu_background);
|
||||
_status_brush.set(_transparent_status_brush);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,16 +273,41 @@ namespace mbts.spark {
|
||||
public Brush ask_background => _ask_background.Value;
|
||||
public Brush ask_foreground => _ask_foreground.Value;
|
||||
public DialogCommand settings_command { get; }
|
||||
public bool show_day { get => _show_day.Value; set => _show_day.set(value); }
|
||||
public bool show_high { get => _show_high.Value; set => _show_high.set(value); }
|
||||
public bool show_low { get => _show_low.Value; set => _show_low.set(value); }
|
||||
public bool show_open { get => _show_open.Value; set => _show_open.set(value); }
|
||||
public bool show_settle { get => _show_settle.Value; set => _show_settle.set(value); }
|
||||
public bool show_volume { get => _show_volume.Value; set => _show_volume.set(value); }
|
||||
public bool show_account { get => _show_account.Value; set => _show_account.set(value); }
|
||||
public bool show_tif { get => _show_tif.Value; set => _show_tif.set(value); }
|
||||
public bool show_order_type { get => _show_order_type.Value; set => _show_order_type.set(value); }
|
||||
public bool show_account_position { get => _show_account_position.Value; set => _show_account_position.set(value); }
|
||||
public bool show_size_preset { get => _show_size_preset.Value; set => _show_size_preset.set(value); }
|
||||
public bool show_auto_clear { get => _show_auto_clear.Value; set => _show_auto_clear.set(value); }
|
||||
public bool show_clear { get => _show_clear.Value; set => _show_clear.set(value); }
|
||||
public bool show_cancel_buys { get => _show_cancel_buys.Value; set => _show_cancel_buys.set(value); }
|
||||
public bool show_cancel_sells { get => _show_cancel_sells.Value; set => _show_cancel_sells.set(value); }
|
||||
public bool show_cancel_all { get => _show_cancel_all.Value; set => _show_cancel_all.set(value); }
|
||||
public bool show_zoom { get => _show_zoom.Value; set => _show_zoom.set(value); }
|
||||
public double left_panel_width { get => _left_panel_width.Value; set => _left_panel_width.set(value); }
|
||||
public double zoom_factor { get => _zoom_factor.Value; set => _zoom_factor.set(value); }
|
||||
//public double window_height { get => _window_height.Value; set => _window_height.set(value); }
|
||||
//public double window_width { get => _window_width.Value; set => _window_width.set(value); }
|
||||
public uint orders_column_width { get => _orders_column_width.Value; set => _orders_column_width.set(value); }
|
||||
public uint bid_column_width { get => _bid_column_width.Value; set => _bid_column_width.set(value); }
|
||||
public uint price_column_width { get => _price_column_width.Value; set => _price_column_width.set(value); }
|
||||
public uint ask_column_width { get => _ask_column_width.Value; set => _ask_column_width.set(value); }
|
||||
public uint last_column_width { get => _last_column_width.Value; set => _last_column_width.set(value); }
|
||||
public uint volume_column_width { get => _volume_column_width.Value; set => _volume_column_width.set(value); }
|
||||
internal LadderModel model => _model;
|
||||
internal IObservable<LadderSettings> settings => _settings_manager.subscribe_current();
|
||||
|
||||
public LadderSettings get_save_state() {
|
||||
// TODO: save an actual OrderDefinition to preserve order params.
|
||||
if (_settings_manager.instance_settings == null)
|
||||
return null;
|
||||
var settings_clone = SparkUtils.clone_proto(_settings_manager.instance_settings);
|
||||
settings_clone.default_order_type = order_properties_vm.selected_order_type?.order_type ?? settings_clone.default_order_type;
|
||||
return settings_clone;
|
||||
var state = _settings_manager.current_settings();
|
||||
state = state.with_window_specifics(new LadderSettings.WindowSpecifics(left_panel_width, orders_column_width, bid_column_width, price_column_width, ask_column_width, last_column_width, volume_column_width, zoom_factor));
|
||||
state.use_defaults = _settings_manager.instance_settings == null;
|
||||
return state;
|
||||
}
|
||||
|
||||
public void key_down(KeyEventArgs e) {
|
||||
@@ -299,6 +364,7 @@ namespace mbts.spark {
|
||||
_change.set(mid - md.settlement_price);
|
||||
}
|
||||
_volume.set(volume);
|
||||
_misc_time = _model.md.misc_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<UserControl x:Class="mbts.spark.LadderView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mbts="clr-namespace:mbts.ui"
|
||||
xmlns:controls="clr-namespace:mbts.ui.controls"
|
||||
xmlns:spark="clr-namespace:mbts.spark"
|
||||
xmlns:uag="clr-namespace:mbts.ui.spark.algo"
|
||||
xmlns:converters="clr-namespace:mbts.ui.converters"
|
||||
xmlns:txt="clr-namespace:mbts.ui.utility.text"
|
||||
Height="600"
|
||||
Width="420"
|
||||
Background="{x:Static mbts:BTSColors.common_background_brush}"
|
||||
TextElement.FontSize="{x:Static mbts:FontSizes.Small}">
|
||||
<UserControl
|
||||
x:Class="mbts.spark.LadderView"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mbts="clr-namespace:mbts.ui"
|
||||
xmlns:controls="clr-namespace:mbts.ui.controls"
|
||||
xmlns:spark="clr-namespace:mbts.spark"
|
||||
xmlns:uag="clr-namespace:mbts.ui.spark.algo"
|
||||
xmlns:converters="clr-namespace:mbts.ui.converters"
|
||||
xmlns:txt="clr-namespace:mbts.ui.utility.text"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
Height="600"
|
||||
Width="420"
|
||||
Background="{x:Static mbts:BTSColors.common_background_brush}"
|
||||
TextElement.FontSize="{x:Static mbts:FontSizes.Small}"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
@@ -27,6 +31,13 @@
|
||||
</converters:BooleanToObjectConverter>
|
||||
<converters:EnumCharacterCasingConverter x:Key="casing_converter"/>
|
||||
<converters:DoubleTextConverter x:Key="PositionTextConverter" Format="0" NaNText="-"/>
|
||||
<converters:GridLengthConverter x:Key="GridLengthPixelConverter" UnitType="Pixel" />
|
||||
<converters:ResponsiveTextConverter x:Key="DayTextConverter" LongText="Day:" ShortText="D:" Threshold="99" />
|
||||
<converters:ResponsiveTextConverter x:Key="HighTextConverter" LongText="High:" ShortText="H:" Threshold="99" />
|
||||
<converters:ResponsiveTextConverter x:Key="LowTextConverter" LongText="Low:" ShortText="L:" Threshold="99" />
|
||||
<converters:ResponsiveTextConverter x:Key="OpenTextConverter" LongText="Open:" ShortText="O:" Threshold="99" />
|
||||
<converters:ResponsiveTextConverter x:Key="SettleTextConverter" LongText="Settle:" ShortText="S:" Threshold="99" />
|
||||
<converters:ResponsiveTextConverter x:Key="VolumeTextConverter" LongText="Volume:" ShortText="V:" Threshold="99" />
|
||||
<Style TargetType="GridViewColumnHeader">
|
||||
<Setter Property="Background" Value="{mbts:BTSColor header}"/>
|
||||
<Setter Property="Foreground" Value="{mbts:BTSColor header_text}"/>
|
||||
@@ -34,6 +45,7 @@
|
||||
<Setter Property="BorderThickness" Value="0,0,1,0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="Height" Value="23" />
|
||||
<Setter Property="MinWidth" Value="30" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
|
||||
@@ -57,8 +69,8 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Canvas>
|
||||
<Thumb x:Name="PART_HeaderGripper">
|
||||
<Canvas MinWidth="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=MinWidth, Mode=OneTime}">
|
||||
<Thumb x:Name="PART_HeaderGripper" DragDelta="Thumb_DragDelta">
|
||||
<Thumb.Style>
|
||||
<Style TargetType="{x:Type Thumb}">
|
||||
<Setter Property="Canvas.Right" Value="-9"/>
|
||||
@@ -123,7 +135,7 @@
|
||||
<Style x:Key="LadderMenuButton" TargetType="Button">
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="Margin" Value="5,0,5,5"/>
|
||||
<Setter Property="Margin" Value="2,0,2,5"/>
|
||||
<Setter Property="Padding" Value="2"/>
|
||||
</Style>
|
||||
<Style x:Key="CancelBuys" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
|
||||
@@ -244,8 +256,7 @@
|
||||
Background="{mbts:BTSColor UI.Button.normal_background}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="True"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,12">
|
||||
Margin="2,0,2,10">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -263,108 +274,166 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<sys:Double x:Key="MaxZoomFactor">1.5</sys:Double>
|
||||
<sys:Double x:Key="MinZoomFactor">0.5</sys:Double>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<AdornerDecorator>
|
||||
<Grid x:Name="_root">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="112"/>
|
||||
<ColumnDefinition Width="2"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid RowSpan="2" Background="{mbts:BTSColor UI.spark_menu_background}">
|
||||
<Grid.LayoutTransform>
|
||||
<ScaleTransform CenterX="0" CenterY="0"
|
||||
ScaleX="{Binding ElementName=_ui_scaler, Path=Value}"
|
||||
ScaleY="{Binding ElementName=_ui_scaler, Path=Value}"
|
||||
/>
|
||||
</Grid.LayoutTransform>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="{Binding left_panel_width, Mode=TwoWay, Converter={StaticResource GridLengthPixelConverter}}"
|
||||
MinWidth="60" MaxWidth="150"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Margin="5,0">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<Grid Margin="8,4,0,0">
|
||||
<ScrollViewer Grid.Row="0" Grid.RowSpan="2" VerticalScrollBarVisibility="Hidden">
|
||||
<Grid RowSpan="2" Background="{mbts:BTSColor UI.spark_menu_background}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Grid.Row="0" Text="Day"/>
|
||||
<TextBlock Grid.Column="0" Grid.Row="2" Text="High"/>
|
||||
<TextBlock Grid.Column="0" Grid.Row="4" Text="Low"/>
|
||||
<TextBlock Grid.Column="0" Grid.Row="6" Text="Open"/>
|
||||
<TextBlock Grid.Column="0" Grid.Row="8" Text="Settle"/>
|
||||
<TextBlock Grid.Column="0" Grid.Row="10" Text="Volume"/>
|
||||
|
||||
<TextBlock Grid.Column="2" Grid.Row="0" Foreground="{Binding change, Converter={StaticResource NumericSignBrushConverter}}" FontWeight="Bold">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="change"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2" Grid.Row="2" Foreground="{mbts:BTSColor UI.default_text}" FontWeight="Bold">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="high"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2" Grid.Row="4" Foreground="{mbts:BTSColor UI.default_text}" FontWeight="Bold">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="low"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2" Grid.Row="6" FontWeight="Bold">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="open"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2" Grid.Row="8" FontWeight="Bold">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="settlement"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2" Grid.Row="10" Text="{Binding volume, Mode=OneWay}" FontWeight="Bold" />
|
||||
</Grid>
|
||||
<ComboBox DataContext="{Binding order_properties_vm}" Margin="2,10,2,2" ItemsSource="{Binding accounts}" SelectedItem="{Binding selected_account}"/>
|
||||
<ComboBox DataContext="{Binding order_properties_vm}" ItemsSource="{Binding time_in_forces}" SelectedItem="{Binding selected_time_in_force}" Margin="2">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource casing_converter}, ConverterParameter=Upper}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<uag:OrderDetailsPopupControl
|
||||
<StackPanel Margin="2,0,5,0">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<StackPanel x:Name="_slider_stack_panel" Margin="5,5,5,0">
|
||||
<Slider x:Name="_ui_scaler" Value="{Binding zoom_factor}"
|
||||
Minimum="{StaticResource MinZoomFactor}"
|
||||
Maximum="{StaticResource MaxZoomFactor}"
|
||||
Width="{Binding Path=ActualWidth, ElementName=_slider_stack_panel}"
|
||||
Visibility="{Binding show_zoom, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
<Grid Margin="2,4,2,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Day -->
|
||||
<TextBlock Grid.Column="0" Grid.Row="0"
|
||||
Text="{Binding left_panel_width, Converter={StaticResource DayTextConverter}}"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_day, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Grid.Column="2" Grid.Row="0" FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Foreground="{Binding change, Converter={StaticResource NumericSignBrushConverter}}"
|
||||
Visibility="{Binding show_day, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="change"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<!-- High -->
|
||||
<TextBlock Grid.Column="0" Grid.Row="1"
|
||||
Text="{Binding left_panel_width, Converter={StaticResource HighTextConverter}}"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_high, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Grid.Column="2" Grid.Row="1" Foreground="{mbts:BTSColor UI.default_text}" FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_high, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="high"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<!-- Low -->
|
||||
<TextBlock Grid.Column="0" Grid.Row="2"
|
||||
Text="{Binding left_panel_width, Converter={StaticResource LowTextConverter}}"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_low, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Grid.Column="2" Grid.Row="2" Foreground="{mbts:BTSColor UI.default_text}" FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_low, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="low"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<!-- Open -->
|
||||
<TextBlock Grid.Column="0" Grid.Row="3"
|
||||
Text="{Binding left_panel_width, Converter={StaticResource OpenTextConverter}}"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_open, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Grid.Column="2" Grid.Row="3" FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_open, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="open"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<!-- Settle -->
|
||||
<TextBlock Grid.Column="0" Grid.Row="4"
|
||||
Text="{Binding left_panel_width, Converter={StaticResource SettleTextConverter}}"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_settle, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Grid.Column="2" Grid.Row="4" FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_settle, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
|
||||
<Binding Path="settlement"/>
|
||||
<Binding Path="security.ContractSpec"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<!-- Volume -->
|
||||
<TextBlock Grid.Column="0" Grid.Row="5"
|
||||
Text="{Binding left_panel_width, Converter={StaticResource VolumeTextConverter}}"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Grid.Column="2" Grid.Row="5" Text="{Binding volume, Mode=OneWay}" FontWeight="Bold"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
</Grid>
|
||||
<!-- Account combobox -->
|
||||
<ComboBox DataContext="{Binding order_properties_vm}" Margin="2,10,2,2"
|
||||
ItemsSource="{Binding accounts}" SelectedItem="{Binding selected_account}"
|
||||
Visibility="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}},
|
||||
Path = DataContext.show_account,
|
||||
Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<!-- TIF combobox-->
|
||||
<ComboBox DataContext="{Binding order_properties_vm}" ItemsSource="{Binding time_in_forces}"
|
||||
SelectedItem="{Binding selected_time_in_force}" Margin="2"
|
||||
Visibility="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}},
|
||||
Path = DataContext.show_tif,
|
||||
Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource casing_converter}, ConverterParameter=Upper}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<!-- Order Type combobox -->
|
||||
<uag:OrderDetailsPopupControl
|
||||
DataContext="{Binding order_properties_vm}"
|
||||
Margin="2"
|
||||
ExpandDirection="Left"
|
||||
@@ -372,19 +441,25 @@
|
||||
IsOpen="{Binding selected_order_type.editable_properties_open, FallbackValue=False}"
|
||||
ItemsSource="{Binding order_types}"
|
||||
SelectedItem="{Binding selected_order_type}"
|
||||
Visibility="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}},
|
||||
Path = DataContext.show_order_type,
|
||||
Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
/>
|
||||
<Button
|
||||
<!-- Position into qty button -->
|
||||
<Button
|
||||
Margin="2"
|
||||
Command="{Binding position_click_command}"
|
||||
Content="{Binding position, Converter={StaticResource PositionTextConverter}}"
|
||||
FontWeight="Bold"
|
||||
Foreground="{Binding position, Converter={StaticResource NumericSignBrushConverter}}"
|
||||
Visibility="{Binding show_account_position, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
>
|
||||
<Button.ToolTip>
|
||||
<TextBlock Text="{Binding order_properties_vm.selected_account, StringFormat='{}{0} total position (click to copy to size)'}"/>
|
||||
</Button.ToolTip>
|
||||
</Button>
|
||||
<TextBox
|
||||
<Button.ToolTip>
|
||||
<TextBlock Text="{Binding order_properties_vm.selected_account, StringFormat='{}{0} total position (click to copy to size)'}"/>
|
||||
</Button.ToolTip>
|
||||
</Button>
|
||||
<!-- Qty textbox -->
|
||||
<TextBox
|
||||
mbts:TextBoxBehavior.InputFilter="{mbts:RegexInputFilter Regex={x:Static Member=Integer, MemberType={mbts:Nested txt:TextUtils.RegexFilters}}}"
|
||||
mbts:TextBoxBehavior.IncrementDecrementOnMouseClick="True"
|
||||
HorizontalContentAlignment="Right"
|
||||
@@ -392,42 +467,64 @@
|
||||
Text="{Binding size_calculator.value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ContextMenu="{x:Null}"
|
||||
/>
|
||||
<ItemsControl ItemsSource="{Binding size_calculator.buttons}" Focusable="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" Focusable="False"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding}" Content="{Binding}" Focusable="False"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Margin="2" HorizontalAlignment="Stretch" Command="{Binding size_calculator.clear}">Clear</Button>
|
||||
<!-- reset to defaults after order -->
|
||||
<CheckBox Margin="2" IsChecked="{Binding auto_clear}">Auto Clear</CheckBox>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Margin="0,17,0,0" VerticalAlignment="Top">
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="Bids" Style="{StaticResource CancelBuys}"/>
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="Asks" Style="{StaticResource CancelSells}"/>
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="All" Style="{StaticResource CancelAll}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Row="1" HorizontalContentAlignment="Stretch" Command="{Binding settings_command}" Style="{StaticResource SettingsButton}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Border Grid.Column="2" HorizontalAlignment="Stretch">
|
||||
<GridViewHeaderRowPresenter x:Name="_column_headers_presenter" AllowsColumnReorder="False"/>
|
||||
</Border>
|
||||
<spark:LadderGridHost x:Name="_ladder" Grid.Column="2" Grid.Row="1"
|
||||
Command="{Binding send_order_command}"
|
||||
Orders="{Binding orders}">
|
||||
<spark:LadderGridHost.Resources>
|
||||
<!-- Qty quick buttons -->
|
||||
<ItemsControl ItemsSource="{Binding size_calculator.buttons}" Focusable="False"
|
||||
Visibility="{Binding show_size_preset, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" Focusable="False"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding}" Content="{Binding}" Focusable="False"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!-- Clear button -->
|
||||
<Button Margin="2" HorizontalAlignment="Stretch" Command="{Binding size_calculator.clear}"
|
||||
Visibility="{Binding show_clear, Converter={StaticResource BooleanToVisibilityConverter}}">Clear</Button>
|
||||
<!-- Auto Clear button - reset to defaults after order -->
|
||||
<CheckBox Margin="2" IsChecked="{Binding auto_clear}"
|
||||
Visibility="{Binding show_auto_clear, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
Auto Clear
|
||||
</CheckBox>
|
||||
<StackPanel Margin="0,17,0,0" VerticalAlignment="Top">
|
||||
<!-- Cancel Buttons -->
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="Bids" Style="{StaticResource CancelBuys}"
|
||||
Visibility="{Binding show_cancel_buys, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="Asks" Style="{StaticResource CancelSells}"
|
||||
Visibility="{Binding show_cancel_sells, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="All" Style="{StaticResource CancelAll}"
|
||||
Visibility="{Binding show_cancel_all, Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Margin="0,17,0,0" VerticalAlignment="Bottom">
|
||||
<!-- Settings Button -->
|
||||
<Button Command="{Binding settings_command}" Style="{StaticResource SettingsButton}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<!-- Ladder Column Headers -->
|
||||
<Border Grid.Column="2" HorizontalAlignment="Stretch">
|
||||
<GridViewHeaderRowPresenter x:Name="_column_headers_presenter" AllowsColumnReorder="False" />
|
||||
</Border>
|
||||
<!-- LadderGridHost -->
|
||||
<spark:LadderGridHost
|
||||
x:Name="_ladder"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Command="{Binding send_order_command}"
|
||||
Orders="{Binding orders}"
|
||||
orders_column_width="{Binding orders_column_width, Mode=TwoWay}"
|
||||
bid_column_width="{Binding bid_column_width, Mode=TwoWay}"
|
||||
price_column_width="{Binding price_column_width, Mode=TwoWay}"
|
||||
ask_column_width="{Binding ask_column_width, Mode=TwoWay}"
|
||||
LastColumnWidth="{Binding last_column_width, Mode=TwoWay}"
|
||||
VolumeColumnWidth="{Binding volume_column_width, Mode=TwoWay}"
|
||||
>
|
||||
<spark:LadderGridHost.Resources>
|
||||
<Style TargetType="controls:Badge">
|
||||
<Setter Property="Background" Value="{Binding background}" />
|
||||
<Setter Property="BorderBrush" Value="{Binding border}" />
|
||||
@@ -467,7 +564,7 @@
|
||||
</Style>
|
||||
</spark:LadderGridHost.Resources>
|
||||
</spark:LadderGridHost>
|
||||
<spark:RecenterCountdown Grid.Column="2" Grid.Row="1" Margin="0,0,18,0"
|
||||
<spark:RecenterCountdown Grid.Column="2" Grid.Row="1" Margin="0,0,18,0"
|
||||
VerticalAlignment="Bottom" HorizontalAlignment="Right"
|
||||
Width="30" Height="30"
|
||||
Mode="Pie" Delay="0:0:10"
|
||||
@@ -483,14 +580,19 @@
|
||||
<Pen Thickness="0" Brush="MediumPurple" />
|
||||
</spark:RecenterCountdown.PieStroke>
|
||||
</spark:RecenterCountdown>
|
||||
|
||||
<Button x:Name="_status" Grid.ColumnSpan="4" Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Command="{Binding status_click_command}">
|
||||
<Button.Template>
|
||||
<ControlTemplate>
|
||||
<TextBlock Text="{Binding status_message}" Background="{Binding status_background}" Padding="5,0" />
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
<!-- Since the GridSplitter will share a cell with other elements,
|
||||
we add it in toward the end, but we still want the ladder status
|
||||
message to have the topmost Z order
|
||||
-->
|
||||
<GridSplitter Grid.Column="0" Grid.RowSpan="2" Width="5" />
|
||||
<!-- Ladder Status message -->
|
||||
<Button x:Name="_status" Grid.ColumnSpan="4" Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Command="{Binding status_click_command}">
|
||||
<Button.Template>
|
||||
<ControlTemplate>
|
||||
<TextBlock Text="{Binding status_message}" Background="{Binding status_background}" Padding="5,0" />
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
</AdornerDecorator>
|
||||
</UserControl>
|
||||
@@ -3,10 +3,14 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace mbts.spark {
|
||||
public partial class LadderView : UserControl {
|
||||
partial class LadderView : UserControl {
|
||||
readonly double _max_zoom, _min_zoom;
|
||||
|
||||
internal LadderView(LadderVM vm) {
|
||||
InitializeComponent();
|
||||
_column_headers_presenter.Columns = new GridViewColumnCollection();
|
||||
@@ -15,12 +19,87 @@ namespace mbts.spark {
|
||||
this.vm = vm;
|
||||
DataContext = vm;
|
||||
DataContextChanged += datacontext_changed;
|
||||
if (TryFindResource("MaxZoomFactor") is double found_max) {
|
||||
_max_zoom = found_max;
|
||||
} else {
|
||||
// is there a better way of not specifying the constant twice ?
|
||||
// once in the resource dictionary here and again in the xaml, seems like there has to be a better way
|
||||
_max_zoom = 1.5;
|
||||
}
|
||||
// ditto
|
||||
if (TryFindResource("MinZoomFactor") is double found_min) {
|
||||
_min_zoom = found_min;
|
||||
} else {
|
||||
_min_zoom = 0.5;
|
||||
}
|
||||
_ui_scaler.MouseDoubleClick += _ui_scaler_MouseDoubleClick;
|
||||
this.PreviewMouseWheel += LadderView_PreviewMouseWheel;
|
||||
this.PreviewMouseDown += LadderView_PreviewMouseDown;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// (Left|Right) CTRL + Wheel to zoom
|
||||
// This is neat b/c the user doesn't even need to have the slider visible to zoom
|
||||
private void LadderView_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e) {
|
||||
base.OnPreviewMouseWheel(e);
|
||||
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
|
||||
var delta = (e.Delta > 0) ? 0.1 : -0.1;
|
||||
var proposed_scaler_value = _ui_scaler.Value + delta;
|
||||
// Here we enforce the same contraints that are on the gui slider regarding min and max
|
||||
if (_min_zoom < proposed_scaler_value && proposed_scaler_value < _max_zoom) {
|
||||
_ui_scaler.Value = proposed_scaler_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (Left|Right) CTRL + Wheel Click to reset
|
||||
private void LadderView_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
|
||||
base.OnPreviewMouseDown(e);
|
||||
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) {
|
||||
if (e.MiddleButton == MouseButtonState.Pressed) {
|
||||
_ui_scaler_MouseDoubleClick(_ui_scaler, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Double click on scaler itself to reset
|
||||
private void _ui_scaler_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) {
|
||||
if (sender is Slider _the_slider) {
|
||||
_the_slider.Value = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void datacontext_changed(object sender, DependencyPropertyChangedEventArgs e) {
|
||||
this.vm = DataContext as LadderVM;
|
||||
}
|
||||
|
||||
// I found this on StackOverflow (and the XAML) and thought it was a neat
|
||||
// way of enforcing a MinWidth on the columns. This is so that the columns
|
||||
// cannot be resized to nothing with the mouse.
|
||||
private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) {
|
||||
if (sender != null) {
|
||||
DependencyObject parent = sender as DependencyObject;
|
||||
if (parent != null) {
|
||||
try {
|
||||
do {
|
||||
parent = System.Windows.Media.VisualTreeHelper.GetParent(parent as DependencyObject);
|
||||
} while (parent.GetType() != typeof(Canvas));
|
||||
|
||||
Canvas canvas = parent as Canvas;
|
||||
if (canvas != null) {
|
||||
if (canvas.ActualWidth + e.HorizontalChange < canvas.MinWidth) {
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal LadderVM vm { get; private set; }
|
||||
|
||||
protected override void OnDragOver(DragEventArgs e) {
|
||||
@@ -40,31 +119,31 @@ namespace mbts.spark {
|
||||
switch (type) {
|
||||
case LadderGridHost.ColumnType.Order:
|
||||
header = "Orders";
|
||||
width_prop = nameof(LadderGridHost.orders_column_width);
|
||||
width_prop = nameof(LadderVM.orders_column_width);
|
||||
break;
|
||||
case LadderGridHost.ColumnType.Bid:
|
||||
header = "Bid";
|
||||
width_prop = nameof(LadderGridHost.bid_column_width);
|
||||
width_prop = nameof(LadderVM.bid_column_width);
|
||||
break;
|
||||
case LadderGridHost.ColumnType.Price:
|
||||
header = "Price";
|
||||
width_prop = nameof(LadderGridHost.price_column_width);
|
||||
width_prop = nameof(LadderVM.price_column_width);
|
||||
break;
|
||||
case LadderGridHost.ColumnType.Ask:
|
||||
header = "Ask";
|
||||
width_prop = nameof(LadderGridHost.ask_column_width);
|
||||
width_prop = nameof(LadderVM.ask_column_width);
|
||||
break;
|
||||
case LadderGridHost.ColumnType.Volume:
|
||||
header = "Volume";
|
||||
width_prop = nameof(LadderGridHost.VolumeColumnWidth);
|
||||
width_prop = nameof(LadderVM.volume_column_width);
|
||||
break;
|
||||
case LadderGridHost.ColumnType.Last:
|
||||
header = "Last";
|
||||
width_prop = nameof(LadderGridHost.LastColumnWidth);
|
||||
width_prop = nameof(LadderVM.last_column_width);
|
||||
break;
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(type, nameof(type));
|
||||
}
|
||||
return _create(host, header, width_prop);
|
||||
return _create(host, header, $"DataContext.{width_prop}");
|
||||
}
|
||||
|
||||
static GridViewColumn _create(LadderGridHost host, string header, string width_prop) {
|
||||
@@ -77,5 +156,6 @@ namespace mbts.spark {
|
||||
return gvc;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace mbts.spark {
|
||||
class PnLSummaryRow : ViewModelBase, IEquatable<PnLSummaryRow>, DIYGrid.IRowVM {
|
||||
static readonly SignedDataStore<Brush> _brushes;
|
||||
readonly PnLDoubleBrushCell _start_pnl, _day_pnl, _total_pnl;
|
||||
readonly PnLPriceCell _pnl_price, _weighted_average_buy, _weighted_average_sell;
|
||||
readonly CellProperty<double> _pnl_price, _weighted_average_buy, _weighted_average_sell;
|
||||
(string account, Venue? exchange, string symbol, SecurityImmutable? security, SecurityMD? sec_md, SecurityType? sec_type) key;
|
||||
IEnumerable<FillImmutable> _fills;
|
||||
double _driver;
|
||||
@@ -38,9 +38,9 @@ namespace mbts.spark {
|
||||
day_short_quantity = day_sell_quant;
|
||||
total_net_position = net_pos;
|
||||
start_net_position = sod_pos;
|
||||
_pnl_price = new PnLPriceCell(this, nameof(pnl_price));
|
||||
_weighted_average_buy = new PnLPriceCell(this, nameof(weighted_average_buy), wavg_buy);
|
||||
_weighted_average_sell = new PnLPriceCell(this, nameof(weighted_average_sell), wavg_sell);
|
||||
_pnl_price = new CellProperty<double>(this, nameof(pnl_price), price => this.security.spec_or_default().format(price), 0);
|
||||
_weighted_average_buy = new CellProperty<double>(this, nameof(weighted_average_buy), price => this.security.spec_or_default().format(price), wavg_buy);
|
||||
_weighted_average_sell = new CellProperty<double>(this, nameof(weighted_average_sell), price => this.security.spec_or_default().format(price), wavg_sell);
|
||||
_fills = fills;
|
||||
|
||||
// set the initial pnl and pnl_price when there has been a
|
||||
@@ -193,19 +193,13 @@ namespace mbts.spark {
|
||||
class PnLDoubleBrushCell : CellProperty<double> {
|
||||
Brush _background;
|
||||
|
||||
public PnLDoubleBrushCell(PnLSummaryRow owner, string prop) : base(d => double.IsNaN(d) ? string.Empty : d.ToString("N2"), double.NaN) =>
|
||||
public PnLDoubleBrushCell(PnLSummaryRow owner, string prop) : base(owner, prop, d => double.IsNaN(d) ? string.Empty : d.ToString("N2"), double.NaN) =>
|
||||
_value_set += _ => {
|
||||
_background = null;
|
||||
owner.RaisePropertyChanged(prop);
|
||||
};
|
||||
|
||||
public Brush background => _background ?? (_background = _brushes.get(value));
|
||||
}
|
||||
|
||||
class PnLPriceCell : CellProperty<double> {
|
||||
public PnLPriceCell(PnLSummaryRow owner, string prop, double init = 0)
|
||||
: base(price => owner.security.spec_or_default().format(price), init) =>
|
||||
_value_set += _ => owner.RaisePropertyChanged(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,8 @@ namespace mbts.spark.utilities {
|
||||
}
|
||||
#endregion
|
||||
|
||||
public OrderDefinition order(SecurityImmutable security) => order(security, _admin_model.get_default_account());
|
||||
public OrderDefinitionImmutable order(SecurityImmutable security) =>
|
||||
new OrderDefinitionImmutable(order(security, _admin_model.get_default_account()));
|
||||
public OrderDefinition order(SecurityImmutable security, AccountTuple? account) {
|
||||
var rv = new OrderDefinition {
|
||||
SecurityKey = security.Id,
|
||||
|
||||
@@ -17,7 +17,9 @@ namespace mbts.spark {
|
||||
readonly DelegateProperty<IEnumerable<DialogSkirtItem>> _left_skirt_items;
|
||||
readonly List<IDisposable> _resources = new List<IDisposable>(2); // initialize with 2 capacity bc the only potential disposables are the instance/default vms
|
||||
|
||||
public WindowSettingsDialogVM(IWindowSettingsManager<T> settings_manager, Func<UserSettings, T> get_factory_settings, string settings_file_ext, Func<T, TVM> vm_factory) {
|
||||
public WindowSettingsDialogVM(IWindowSettingsManager<T> settings_manager, Func<UserSettings, T> get_factory_settings, string settings_file_ext, Func<T, TVM> vm_factory, Func<T, T> merge_with_current = null) {
|
||||
if (merge_with_current == null)
|
||||
merge_with_current = s => s;
|
||||
// by default, each new window will use their own instance settings
|
||||
_use_default = backing(nameof(use_default), settings_manager.instance_settings == null);
|
||||
default_vm = vm_factory(settings_manager.default_settings);
|
||||
@@ -59,15 +61,14 @@ namespace mbts.spark {
|
||||
_resources.Add(dispose_dvm);
|
||||
// local methods
|
||||
void apply() {
|
||||
var result = merge_with_current(instance_vm.get_result());
|
||||
if (!use_default) {
|
||||
settings_manager.instance_settings = instance_vm.get_result();
|
||||
settings_manager.instance_settings = result;
|
||||
} else {
|
||||
settings_manager.instance_settings = null;
|
||||
|
||||
// copy the current instance settings and persist
|
||||
var my_default = instance_vm.get_result();
|
||||
if (!my_default.safe_equals(settings_manager.default_settings))
|
||||
settings_manager.save_default_settings(my_default).ignore();
|
||||
if (!result.safe_equals(settings_manager.default_settings))
|
||||
settings_manager.save_default_settings(result).ignore();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File renamed without changes.
+4
-9
@@ -13,14 +13,9 @@ namespace mbts.co2
|
||||
{
|
||||
internal static unsafe T deserialize<T>(byte* buffer, ulong length) where T : new() {
|
||||
var stream = new System.IO.UnmanagedMemoryStream(buffer, (long)length);
|
||||
if (typeof(IMessage).IsAssignableFrom(typeof(T))) {
|
||||
IMessage result = (IMessage)new T();
|
||||
result.MergeFrom(stream);
|
||||
return (T)result;
|
||||
}
|
||||
else {
|
||||
return ProtoBuf.Serializer.Deserialize<T>(stream);
|
||||
}
|
||||
IMessage result = (IMessage)new T();
|
||||
result.MergeFrom(stream);
|
||||
return (T)result;
|
||||
}
|
||||
|
||||
[DllImport("libspark")]
|
||||
@@ -81,7 +76,7 @@ namespace mbts.co2
|
||||
[DllImport("libspark")]
|
||||
internal unsafe static extern IntPtr add_relay_symbol(IntPtr relay, UserSymbol symbol, bts.co2.MDRequest.Types.Flags flags);
|
||||
[DllImport("libspark")]
|
||||
internal unsafe static extern IntPtr set_relay_token(IntPtr relay, string user_name);
|
||||
internal unsafe static extern IntPtr set_relay_token(IntPtr relay, byte[] token, long token_size);
|
||||
|
||||
[DllImport("libspark")]
|
||||
internal unsafe static extern void post_on_scan_security_thread(Action callback);
|
||||
+5
-6
@@ -14,17 +14,16 @@ namespace mbts.co2
|
||||
{
|
||||
MDConsumer _consumer;
|
||||
IntPtr _relay;
|
||||
public Co2RelayControl(MDConsumer consumer) {
|
||||
public Co2RelayControl(MDConsumer consumer, uint naggle_ms) {
|
||||
_consumer = consumer;
|
||||
Log.assert(consumer.native_pointer() != IntPtr.Zero);
|
||||
_relay =
|
||||
Co2NativeMethods.start_private_relay(consumer.native_pointer(),
|
||||
config.Configuration.get<bts.md.RelayOptions>().naggle_ms);
|
||||
_relay = Co2NativeMethods.start_private_relay(consumer.native_pointer(), naggle_ms);
|
||||
Log.assert(_relay != IntPtr.Zero);
|
||||
}
|
||||
|
||||
public void set_token(AuthToken token) {
|
||||
Co2NativeMethods.set_relay_token(_relay, token.Token);
|
||||
public void set_token(AuthTokenImmutable token) {
|
||||
var buffer = token.Token.ToByteArray();
|
||||
Co2NativeMethods.set_relay_token(_relay, buffer, buffer.LongLength);
|
||||
}
|
||||
|
||||
public async Task preload_securities(MDRequest.Types.Flags flags=MDRequest.Types.Flags.Security) {
|
||||
File renamed without changes.
@@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using security_key = System.UInt32;
|
||||
using bts;
|
||||
|
||||
namespace mbts.co2
|
||||
{
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -64,7 +64,7 @@ namespace bts.api.core {
|
||||
internal static extern void api_client_send(IntPtr nchannel, byte[] buffer, int length, raw_pb_callback p, IntPtr extra);
|
||||
|
||||
[DllImport("libspark")]
|
||||
internal static extern IntPtr api_client_subscribe(IntPtr client, string method_and_qualifier, raw_subscription_callback cb, IntPtr extra);
|
||||
internal static extern IntPtr api_client_subscribe(IntPtr client, string method, string qualifier, raw_subscription_callback cb, IntPtr extra);
|
||||
[DllImport("libspark")]
|
||||
internal static extern void api_client_unsubscribe(IntPtr client, IntPtr subscription);
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace bts.api.core {
|
||||
}
|
||||
|
||||
[DllImport("libspark")]
|
||||
internal static extern IntPtr api_server_create_table(IntPtr server, string method_and_qualifier);
|
||||
internal static extern IntPtr api_server_create_table(IntPtr server, string method, string qualifier);
|
||||
[DllImport("libspark")]
|
||||
internal static extern void api_server_update_table(IntPtr table, [In] StreamUpdate[] changes, long change_count);
|
||||
[DllImport("libspark")]
|
||||
|
||||
@@ -17,15 +17,6 @@ namespace bts.api.core
|
||||
|
||||
internal abstract class ApiServer : IApiServer
|
||||
{
|
||||
static JsonFormatter _formatter = new JsonFormatter(new JsonFormatter.Settings(true));
|
||||
static Regex _fmt_cleanup = new Regex(@"[\s_+]", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
public static string format_qualifier<T>(string method, T arg) where T : IMessage {
|
||||
if (arg != null) {
|
||||
string arg_fmt = _fmt_cleanup.Replace(_formatter.Format(arg), "");
|
||||
return method + arg_fmt;
|
||||
}
|
||||
return method;
|
||||
}
|
||||
public abstract void dispatch(RequestHeader header, Stream request, Action<byte[]> on_response);
|
||||
|
||||
protected void respond<T>(int request_id, Task<T> task, Action<byte[]> on_response) where T : IMessage, IImmutableMessage {
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
using Google.Protobuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.api {
|
||||
internal static class ApiUtils {
|
||||
|
||||
|
||||
public static byte[] serialize<T>(this RequestHeader header, T body) where T : IImmutableMessage {
|
||||
int body_size = body?.CalculateSize() ?? 0;
|
||||
int header_size = header.CalculateSize() + 1; // 1 byte header length
|
||||
var buffer = new byte[header_size+body_size];
|
||||
using bts.api.core;
|
||||
using Google.Protobuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.api {
|
||||
internal static class ApiUtils {
|
||||
|
||||
public static byte[] serialize<T>(this RequestHeader header, T body) where T : IImmutableMessage {
|
||||
int body_size = body?.CalculateSize() ?? 0;
|
||||
int header_size = header.CalculateSize() + 1; // 1 byte header length
|
||||
var buffer = new byte[header_size + body_size];
|
||||
using (var stream = new MemoryStream(buffer)) {
|
||||
checked { stream.WriteByte((byte)header_size); }
|
||||
header.WriteTo(stream);
|
||||
if (body != null) body.WriteTo(stream);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public struct DeserializeResult<THeader,TBody> {
|
||||
public THeader header;
|
||||
public TBody body;
|
||||
}
|
||||
|
||||
private static DeserializeResult<THeader, TBody> mkresult<THeader,TBody>(THeader header, TBody body) {
|
||||
return new DeserializeResult<THeader, TBody>() { header = header, body = body }; }
|
||||
|
||||
public static unsafe DeserializeResult<THeader, TBody> deserialize<THeader,TBody>(byte* buffer, long buffer_length) where THeader : IMessage,new() where TBody : class,IMessage,new() {
|
||||
var header_size = buffer[0];
|
||||
Debug.Assert(header_size <= buffer_length);
|
||||
THeader header = new THeader();
|
||||
header.MergeFrom(new UnmanagedMemoryStream(buffer+1, header_size - 1));
|
||||
TBody body = new TBody();
|
||||
if (buffer_length > header_size) {
|
||||
body.MergeFrom(new UnmanagedMemoryStream(buffer + header_size, buffer_length - header_size));
|
||||
}
|
||||
return mkresult(header, body);
|
||||
}
|
||||
|
||||
public static byte[] serialize<T>(this ResponseHeader header, T body) where T : IImmutableMessage {
|
||||
int header_size = 1 + header.CalculateSize();
|
||||
int body_size = body == null ? 0 : body.CalculateSize();
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public struct DeserializeResult<THeader, TBody> {
|
||||
public THeader header;
|
||||
public TBody body;
|
||||
}
|
||||
|
||||
private static DeserializeResult<THeader, TBody> mkresult<THeader, TBody>(THeader header, TBody body) {
|
||||
return new DeserializeResult<THeader, TBody>() { header = header, body = body }; }
|
||||
|
||||
public static unsafe DeserializeResult<THeader, TBody> deserialize<THeader, TBody>(byte* buffer, long buffer_length) where THeader : IMessage, new() where TBody : class, IMessage, new() {
|
||||
var header_size = buffer[0];
|
||||
Debug.Assert(header_size <= buffer_length);
|
||||
THeader header = new THeader();
|
||||
header.MergeFrom(new UnmanagedMemoryStream(buffer + 1, header_size - 1));
|
||||
TBody body = new TBody();
|
||||
if (buffer_length > header_size) {
|
||||
body.MergeFrom(new UnmanagedMemoryStream(buffer + header_size, buffer_length - header_size));
|
||||
}
|
||||
return mkresult(header, body);
|
||||
}
|
||||
|
||||
public static byte[] serialize<T>(this ResponseHeader header, T body) where T : IImmutableMessage {
|
||||
int header_size = 1 + header.CalculateSize();
|
||||
int body_size = body == null ? 0 : body.CalculateSize();
|
||||
var buffer = new byte[header_size + body_size];
|
||||
using (var stream = new MemoryStream(buffer)) {
|
||||
checked { stream.WriteByte((byte)header_size); }
|
||||
header.WriteTo(stream);
|
||||
if (body != null) body.WriteTo(stream);
|
||||
using (var stream = new MemoryStream(buffer)) {
|
||||
checked { stream.WriteByte((byte)header_size); }
|
||||
header.WriteTo(stream);
|
||||
if (body != null) body.WriteTo(stream);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
public static byte[] serialize(this ResponseHeader header) {
|
||||
int header_size = 1 + header.CalculateSize();
|
||||
var buffer = new byte[header_size];
|
||||
using (var stream = new MemoryStream(buffer)) {
|
||||
checked { stream.WriteByte((byte)header_size); }
|
||||
header.WriteTo(stream);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public static byte[] serialize(this ResponseHeader header) {
|
||||
int header_size = 1 + header.CalculateSize();
|
||||
var buffer = new byte[header_size];
|
||||
using (var stream = new MemoryStream(buffer)) {
|
||||
checked { stream.WriteByte((byte)header_size); }
|
||||
header.WriteTo(stream);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace bts.api.core {
|
||||
var header = new RequestHeader() {
|
||||
Method = method_id,
|
||||
RequestId = id,
|
||||
Token = _context.token.HasValue ? new AuthToken() { Token = _context.token?.Token } : null
|
||||
};
|
||||
var buffer = header.serialize(request);
|
||||
var tcs = new TaskCompletionSource<Result<Response>>();
|
||||
@@ -56,60 +57,99 @@ namespace bts.api.core {
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public unsafe override IObservable<RawUpdate<TOut>> subscribe<TParse,TOut>(string method_and_qualifier, MessageParser<TParse> parser, Func<TParse,TOut> converter) {
|
||||
var result = Observable.Create<RawUpdate<TOut>>(observer => {
|
||||
bool first_update = true;
|
||||
// Memory optimization. There is only one update per subscription.
|
||||
RawUpdate<TOut> the_update = new RawUpdate<TOut>();
|
||||
ApiNativeMethods.raw_subscription_callback cb = (event_type, changes, change_count, unused) => {
|
||||
the_update.Clear();
|
||||
try {
|
||||
if (event_type == ApiNativeMethods.EventType.TABLE_UPDATED) {
|
||||
foreach (var change in changes) {
|
||||
switch (change.type) {
|
||||
case ApiNativeMethods.ChangeType.REMOVE:
|
||||
the_update.removed_keys.Add(change.key);
|
||||
break;
|
||||
case ApiNativeMethods.ChangeType.REPLACE:
|
||||
the_update.changed_items.Add(converter(parser.ParseFrom(new UnmanagedMemoryStream(change.buffer, change.buffer_len))));
|
||||
the_update.changed_keys.Add(change.key);
|
||||
break;
|
||||
case ApiNativeMethods.ChangeType.CREATE:
|
||||
the_update.added_items.Add(converter(parser.ParseFrom(new UnmanagedMemoryStream(change.buffer, change.buffer_len))));
|
||||
the_update.added_keys.Add(change.key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
observer.OnNext(the_update);
|
||||
public override IObservable<RawUpdate<TOut>> subscribe<TParse,TOut>(int method_id, string method, string qualifier, MessageParser<TParse> parser, Func<TParse,TOut> converter) {
|
||||
var result = Observable.Create<RawUpdate<TOut>>(async observer => {
|
||||
IntPtr temp_subscription = IntPtr.Zero;
|
||||
void unsubscribe_temp_sub() {
|
||||
// Unsubscribe late to keep qb refcount > 0 continuously
|
||||
if (temp_subscription != IntPtr.Zero) ApiNativeMethods.api_client_unsubscribe(_client, temp_subscription);
|
||||
}
|
||||
while (true) {
|
||||
var response = await check_stream_access(method_id, qualifier);
|
||||
if (response.success) {
|
||||
if (response.value.Authorized) {
|
||||
// success. Break loop and subscribe
|
||||
break;
|
||||
}
|
||||
else if (event_type == ApiNativeMethods.EventType.TABLE_CREATED) {
|
||||
if (!first_update) {
|
||||
observer.OnNext(the_update.MarkAsReset());
|
||||
}
|
||||
first_update = false;
|
||||
else {
|
||||
// Not authorized. return empty stream
|
||||
unsubscribe_temp_sub();
|
||||
observer.OnCompleted();
|
||||
return System.Reactive.Disposables.Disposable.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ApiNativeMethods.api_client_log_error($"Exception in subscription callback for sub: '{method_and_qualifier}' \n--\n '{e}'");
|
||||
// Sleep to let the log message get asyncronously written to disk
|
||||
Thread.Sleep(100);
|
||||
Environment.Exit(-1);
|
||||
else {
|
||||
// Wait for stream to fire once before trying again
|
||||
unsubscribe_temp_sub();
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
ApiNativeMethods.raw_subscription_callback cb = (event_type, changes, change_count, unused) => {
|
||||
tcs.TrySetResult(true);
|
||||
};
|
||||
temp_subscription = ApiNativeMethods.api_client_subscribe(_client, method, qualifier, cb, IntPtr.Zero);
|
||||
await tcs.Task;
|
||||
GC.KeepAlive(cb);
|
||||
}
|
||||
};
|
||||
var subscription = ApiNativeMethods.api_client_subscribe(_client, method_and_qualifier, cb, IntPtr.Zero);
|
||||
return FinalizeableAnonymousDisposable.Create((disposing) => {
|
||||
ApiNativeMethods.api_client_unsubscribe(_client, subscription);
|
||||
subscription = IntPtr.Zero;
|
||||
GC.KeepAlive(cb);
|
||||
cb = null;
|
||||
// Cannot (ever) call logging from the finalizer thread
|
||||
// if (!disposing) {
|
||||
// ApiNativeMethods.api_client_log_error($"Subscription has been terminated because the returned IDisposable has been finalized. Subscription: {method_and_qualifier}");
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
// Authorized. Stream events
|
||||
unsubscribe_temp_sub();
|
||||
return SubscribeNative(method, qualifier, parser, converter, observer);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
private Task<Result<StreamAccessResponse>> check_stream_access(int method_id, string qualifier) {
|
||||
var request = new StreamAccessRequest() { Qualifier = qualifier ?? "" };
|
||||
return send<StreamAccessRequest, StreamAccessResponse>(method_id, request);
|
||||
}
|
||||
|
||||
private unsafe IDisposable SubscribeNative<TParse, TOut>(string method, string qualifier, MessageParser<TParse> parser, Func<TParse, TOut> converter, IObserver<RawUpdate<TOut>> observer) where TParse : IMessage<TParse> {
|
||||
bool first_update = true;
|
||||
// Memory optimization. There is only one update per subscription.
|
||||
RawUpdate<TOut> the_update = new RawUpdate<TOut>();
|
||||
ApiNativeMethods.raw_subscription_callback cb = (event_type, changes, change_count, unused) => {
|
||||
the_update.Clear();
|
||||
try {
|
||||
if (event_type == ApiNativeMethods.EventType.TABLE_UPDATED) {
|
||||
foreach (var change in changes) {
|
||||
switch (change.type) {
|
||||
case ApiNativeMethods.ChangeType.REMOVE:
|
||||
the_update.removed_keys.Add(change.key);
|
||||
break;
|
||||
case ApiNativeMethods.ChangeType.REPLACE:
|
||||
the_update.changed_items.Add(converter(parser.ParseFrom(new UnmanagedMemoryStream(change.buffer, change.buffer_len))));
|
||||
the_update.changed_keys.Add(change.key);
|
||||
break;
|
||||
case ApiNativeMethods.ChangeType.CREATE:
|
||||
the_update.added_items.Add(converter(parser.ParseFrom(new UnmanagedMemoryStream(change.buffer, change.buffer_len))));
|
||||
the_update.added_keys.Add(change.key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
observer.OnNext(the_update);
|
||||
}
|
||||
else if (event_type == ApiNativeMethods.EventType.TABLE_CREATED) {
|
||||
if (!first_update) {
|
||||
observer.OnNext(the_update.MarkAsReset());
|
||||
}
|
||||
first_update = false;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ApiNativeMethods.api_client_log_error($"Exception in subscription callback for sub: '{method}/{qualifier}' \n--\n '{e}'");
|
||||
// Sleep to let the log message get asyncronously written to disk
|
||||
Thread.Sleep(100);
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
};
|
||||
var subscription = ApiNativeMethods.api_client_subscribe(_client, method, qualifier, cb, IntPtr.Zero);
|
||||
return FinalizeableAnonymousDisposable.Create((disposing) => {
|
||||
ApiNativeMethods.api_client_unsubscribe(_client, subscription);
|
||||
subscription = IntPtr.Zero;
|
||||
GC.KeepAlive(cb);
|
||||
cb = null;
|
||||
});
|
||||
}
|
||||
|
||||
private class FinalizeableAnonymousDisposable : IDisposable {
|
||||
private Action<bool> _action;
|
||||
public FinalizeableAnonymousDisposable(Action<bool> dispose) {
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Threading.Tasks;
|
||||
namespace bts.api.core {
|
||||
internal abstract class ClientChannel : IDisposable
|
||||
{
|
||||
readonly ServiceContext _context;
|
||||
protected readonly ServiceContext _context;
|
||||
|
||||
public ClientChannel(ServiceContext context) {
|
||||
_context = context;
|
||||
@@ -13,7 +13,7 @@ namespace bts.api.core {
|
||||
public abstract void Dispose();
|
||||
public abstract Task<Result<Response>> send<Request, Response>(int method_id, Request request)
|
||||
where Request: IImmutableMessage where Response : class,IMessage,new();
|
||||
public abstract IObservable<RawUpdate<TOut>> subscribe<TParse,TOut>(string method_and_qualifier, MessageParser<TParse> parser, Func<TParse,TOut> converter=null)
|
||||
public abstract IObservable<RawUpdate<TOut>> subscribe<TParse,TOut>(int method_id, string method, string qualifier, MessageParser<TParse> parser, Func<TParse,TOut> converter=null)
|
||||
where TParse : IMessage<TParse>;
|
||||
|
||||
public abstract bool is_sync_call_ok();
|
||||
|
||||
@@ -37,7 +37,12 @@ namespace bts.api.core
|
||||
public T value {
|
||||
get {
|
||||
if (success) return _result;
|
||||
else throw new ApiException(error_message);
|
||||
else {
|
||||
if (typeof(T).IsValueType) { throw new ApiException(error_message); }
|
||||
else {
|
||||
return default(T);// always null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool success { get { return _code == ApiError.Success; } }
|
||||
|
||||
@@ -34,8 +34,8 @@ namespace bts.api.core {
|
||||
}
|
||||
public Action<RequestHeader, Stream, Action<byte[]>> on_request;
|
||||
|
||||
public IntPtr create_table(string method_and_qualifier) {
|
||||
return ApiNativeMethods.api_server_create_table(_server, method_and_qualifier);
|
||||
public IntPtr create_table(string method, string qualifier) {
|
||||
return ApiNativeMethods.api_server_create_table(_server, method, qualifier);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
|
||||
@@ -4,12 +4,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using bts.api.user;
|
||||
|
||||
namespace bts.api.core
|
||||
{
|
||||
namespace bts.api.core {
|
||||
public class ServiceContext : IDisposable
|
||||
{
|
||||
IntPtr _qbus;
|
||||
AuthToken _token;
|
||||
|
||||
public ServiceContext() {
|
||||
_qbus = ApiNativeMethods.api_create_quickbus(null, 0);
|
||||
}
|
||||
@@ -22,7 +24,6 @@ namespace bts.api.core
|
||||
Type[] arg_types = new[] { typeof(ClientChannel) };
|
||||
ConstructorInfo ctor = typeof(T).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).First(ci => ci.GetParameters().SingleOrDefault()?.ParameterType == typeof(ClientChannel));
|
||||
return (T)ctor.Invoke(new object[] { channel });
|
||||
//return (T)Activator.CreateInstance(typeof(T), channel);
|
||||
}
|
||||
|
||||
internal ServerChannel publish<T>(T service, string service_type, string qualifier) where T : IApiServer {
|
||||
@@ -31,6 +32,38 @@ namespace bts.api.core
|
||||
return ch;
|
||||
}
|
||||
|
||||
public AuthTokenImmutable? token => _token ?? (AuthTokenImmutable?)null;
|
||||
public string user { get; private set; }
|
||||
public uint user_id { get; private set; }
|
||||
public bool is_authenticated => token.HasValue;
|
||||
|
||||
public async Task<LoginResult> login(string username, string password) {
|
||||
var auth_service = locate<AuthService>();
|
||||
var result = await auth_service.authenticate_login_async(new AuthenticateLoginRequest() {
|
||||
Login = username ?? "",
|
||||
Password = password ?? ""
|
||||
});
|
||||
if (result.success) {
|
||||
if (result.value.IsValid) {
|
||||
_token = result.value.Token;
|
||||
user = username;
|
||||
user_id = result.value.UserId;
|
||||
return new LoginResult();
|
||||
}
|
||||
else {
|
||||
return new LoginResult(result.value.Reason);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return new LoginResult(result.error_message);
|
||||
}
|
||||
}
|
||||
public void login(AuthTokenImmutable token) {
|
||||
this.user = "";
|
||||
this.user_id = 0;
|
||||
this._token = token.CloneToMutable();
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
|
||||
protected virtual void Dispose(bool disposing) {
|
||||
@@ -52,4 +85,11 @@ namespace bts.api.core
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class LoginResult {
|
||||
public LoginResult() { success = true; }
|
||||
public LoginResult(string reason) { success = false; this.reason = reason; }
|
||||
public bool success { get; }
|
||||
public string reason { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using bts.api;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
|
||||
namespace bts.md
|
||||
{
|
||||
interface IMDUpdate
|
||||
{
|
||||
bool update(SecurityMD smd, ref ulong last_update_time);
|
||||
}
|
||||
|
||||
public interface IMDData
|
||||
{
|
||||
SecurityImmutable security { get; }
|
||||
}
|
||||
|
||||
public interface IMDBook : IMDData
|
||||
{
|
||||
List<PriceSize> bids { get; }
|
||||
List<PriceSize> asks { get; }
|
||||
}
|
||||
|
||||
class MDBook : IMDBook, IMDUpdate
|
||||
{
|
||||
public MDBook()
|
||||
{
|
||||
}
|
||||
public SecurityImmutable security { get; private set; }
|
||||
|
||||
public List<PriceSize> bids { get; private set; }
|
||||
public List<PriceSize> asks { get; private set; }
|
||||
|
||||
bool IMDUpdate.update(SecurityMD smd, ref ulong last_update_time)
|
||||
{
|
||||
this.security = smd.security;
|
||||
NativePriceSize[] bids = null, asks = null;
|
||||
if (smd.md.get_depth(ref last_update_time, ref bids, ref asks, NativePriceHelpers.COCONUT_MAX_DEPTH))
|
||||
{
|
||||
this.bids = bids.Select(x => new PriceSize(x.size, x.direct_size, x.implied_size, x.price)).ToList();
|
||||
this.asks = asks.Select(x => new PriceSize(x.size, x.direct_size, x.implied_size, x.price)).ToList();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return security.ToString() + Environment.NewLine +
|
||||
"Bid BidSz BidISz" + Environment.NewLine +
|
||||
string.Join("\n", bids.Select(x => x.ToString())) + Environment.NewLine +
|
||||
"Ask AskSz AskISz" + Environment.NewLine +
|
||||
string.Join("\n", asks.Select(x => x.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using bts.api;
|
||||
using mbts;
|
||||
|
||||
namespace bts.md
|
||||
{
|
||||
public interface IMDCombined : IMDData, IMDMisc, IMDLast, IMDBook
|
||||
{
|
||||
}
|
||||
|
||||
class MDCombined : IMDCombined, IMDUpdate
|
||||
{
|
||||
MDBook _book = new MDBook();
|
||||
MDLast _last = new MDLast();
|
||||
MDMisc _misc = new MDMisc();
|
||||
public MDCombined()
|
||||
{
|
||||
}
|
||||
public SecurityImmutable security { get; private set; }
|
||||
|
||||
public ulong total_volume => _misc.total_volume;
|
||||
public double settlement_price => _misc.settlement_price;
|
||||
public DateTime settlement_date => _misc.settlement_date;
|
||||
public double previous_settlement_price => _misc.previous_settlement_price;
|
||||
public DateTime previous_settlement_date => _misc.previous_settlement_date;
|
||||
|
||||
public double open_price => _misc.open_price;
|
||||
public DateTime open_date => _misc.open_date;
|
||||
public double close_price => _misc.close_price;
|
||||
public DateTime close_date => _misc.close_date;
|
||||
public double session_high => _misc.session_high;
|
||||
public double session_low => _misc.session_low;
|
||||
public double fixing_price => _misc.fixing_price;
|
||||
|
||||
public PriceSize last => _last.last;
|
||||
public ulong total_volume2 => _last.total_volume;
|
||||
public DateTime last_time => _last.last_time;
|
||||
|
||||
public List<PriceSize> bids => _book.bids;
|
||||
public List<PriceSize> asks => _book.asks;
|
||||
|
||||
|
||||
bool IMDUpdate.update(SecurityMD smd, ref ulong last_update_time)
|
||||
{
|
||||
this.security = smd.security;
|
||||
var bl = last_update_time;
|
||||
bool b = ((IMDUpdate)_book).update(smd, ref bl);
|
||||
|
||||
var ll = last_update_time;
|
||||
bool l = ((IMDUpdate)_last).update(smd, ref ll);
|
||||
|
||||
var ml = last_update_time;
|
||||
bool m = ((IMDUpdate)_misc).update(smd, ref ml);
|
||||
|
||||
last_update_time = new[] { bl, ll, ml }.Max();
|
||||
return b || l || m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using bts.api;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
|
||||
namespace bts.md
|
||||
{
|
||||
public interface IMDLast : IMDData
|
||||
{
|
||||
PriceSize last{ get; }
|
||||
ulong total_volume { get; }
|
||||
DateTime last_time { get; }
|
||||
}
|
||||
|
||||
class MDLast : IMDLast, IMDUpdate
|
||||
{
|
||||
public MDLast()
|
||||
{
|
||||
}
|
||||
public SecurityImmutable security { get; private set; }
|
||||
|
||||
public PriceSize last { get; private set; }
|
||||
public ulong total_volume { get; private set; }
|
||||
public DateTime last_time { get; private set; }
|
||||
|
||||
bool IMDUpdate.update(SecurityMD smd, ref ulong last_update_time)
|
||||
{
|
||||
this.security = smd.security;
|
||||
NativePriceSize x; DateTime ts; ulong vol;
|
||||
if (smd.md.get_last_trade(ref last_update_time, out x, out ts, out vol))
|
||||
{
|
||||
last = new PriceSize(x.size, x.direct_size, x.implied_size, x.price);
|
||||
total_volume = vol;
|
||||
last_time = ts;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using bts.api;
|
||||
using mbts;
|
||||
|
||||
namespace bts.md
|
||||
{
|
||||
public interface IMDMisc : IMDData
|
||||
{
|
||||
ulong total_volume { get; }
|
||||
double settlement_price { get; }
|
||||
DateTime settlement_date { get; }
|
||||
double previous_settlement_price { get; }
|
||||
DateTime previous_settlement_date { get; }
|
||||
|
||||
double open_price { get; }
|
||||
DateTime open_date { get; }
|
||||
double close_price { get; }
|
||||
DateTime close_date { get; }
|
||||
double session_high { get; }
|
||||
double session_low { get; }
|
||||
double fixing_price { get; }
|
||||
}
|
||||
|
||||
class MDMisc : IMDMisc, IMDUpdate
|
||||
{
|
||||
public MDMisc()
|
||||
{
|
||||
}
|
||||
public SecurityImmutable security { get; private set; }
|
||||
|
||||
public ulong total_volume { get; private set; }
|
||||
public double settlement_price { get; private set; }
|
||||
public DateTime settlement_date { get; private set; }
|
||||
public double previous_settlement_price { get; private set; }
|
||||
public DateTime previous_settlement_date { get; private set; }
|
||||
|
||||
public double open_price { get; private set; }
|
||||
public DateTime open_date { get; private set; }
|
||||
public double close_price { get; private set; }
|
||||
public DateTime close_date { get; private set; }
|
||||
public double session_high { get; private set; }
|
||||
public double session_low { get; private set; }
|
||||
public double fixing_price { get; private set; }
|
||||
|
||||
bool IMDUpdate.update(SecurityMD smd, ref ulong last_update_time)
|
||||
{
|
||||
this.security = smd.security;
|
||||
if (smd.md.misc_timestamp > last_update_time)
|
||||
{
|
||||
total_volume = smd.md.total_volume;
|
||||
settlement_price = smd.md.settlement_price;
|
||||
settlement_date = smd.md.settlement_date;
|
||||
previous_settlement_price = smd.md.previous_settlement_price;
|
||||
previous_settlement_date = smd.md.previous_settlement_date;
|
||||
|
||||
open_price = smd.md.open_price;
|
||||
open_date = smd.md.open_date;
|
||||
close_price = smd.md.close_price;
|
||||
close_date = smd.md.close_date;
|
||||
session_high = smd.md.session_high;
|
||||
session_low = smd.md.session_low;
|
||||
fixing_price = smd.md.fixing_price;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using bts.api;
|
||||
using bts.api.synthetic;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using SecurityKey = System.UInt32;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using bts.api.user;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace bts.md
|
||||
{
|
||||
class MDSource : IObserver<List<SecurityAndHandle>>, IDisposable
|
||||
{
|
||||
protected readonly MDConsumer _consumer = new MDConsumer();
|
||||
protected readonly Co2RelayControl _relay;
|
||||
protected long _version = 0;
|
||||
protected readonly ConcurrentDictionary<SecurityKey, SecurityMD> _exchange_sec_map = new ConcurrentDictionary<SecurityKey, SecurityMD>();
|
||||
protected List<IDisposable> _dispose = new List<IDisposable>();
|
||||
|
||||
public MDSource(uint naggle_ms, bool defer_init = false)
|
||||
{
|
||||
_relay = new Co2RelayControl(_consumer, naggle_ms);
|
||||
_relay.preload_securities((co2.MDRequest.Types.Flags)0xff).ignore();
|
||||
_dispose.Add(_consumer.on_security.Subscribe(this));
|
||||
if(!defer_init)
|
||||
initialize();
|
||||
}
|
||||
|
||||
readonly BehaviorSubject<ConcurrentDictionary<SecurityKey, SecurityMD>> _on_securities =
|
||||
new BehaviorSubject<ConcurrentDictionary<SecurityKey, SecurityMD>>( new ConcurrentDictionary<SecurityKey, SecurityMD>() );
|
||||
public IObservable<ConcurrentDictionary<SecurityKey, SecurityMD>> on_securities => _on_securities;
|
||||
|
||||
virtual public void initialize()
|
||||
{
|
||||
Task.Run(run);
|
||||
}
|
||||
|
||||
public void set_token(AuthTokenImmutable token)
|
||||
{
|
||||
_relay.set_token(token);
|
||||
}
|
||||
|
||||
public void OnCompleted() { }
|
||||
public void OnError(Exception error) => Log.Md.error(error.ToString());
|
||||
public void OnNext(List<SecurityAndHandle> values)
|
||||
{
|
||||
if (values.Count > 0)
|
||||
{
|
||||
foreach (var v in values)
|
||||
{
|
||||
var smd = new SecurityMD(new SecurityImmutable(v.security), v.handle);
|
||||
_exchange_sec_map.AddOrUpdate(smd.security.Id, smd, (_, __) => smd);
|
||||
}
|
||||
_version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Disposable.dispose(_dispose);
|
||||
async protected Task run()
|
||||
{
|
||||
long run_version = 0;
|
||||
while (true)
|
||||
{
|
||||
var exch = _exchange_sec_map;
|
||||
long current_version = _version;
|
||||
if (current_version > run_version)
|
||||
{
|
||||
run_version = current_version;
|
||||
try
|
||||
{
|
||||
Log.Md.info($"MDSource publishing {exch.Count} securities");
|
||||
_on_securities.OnNext(exch);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Md.error($"Error in publish_securities: {e}");
|
||||
}
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,104 +11,224 @@ using System.Reactive.Subjects;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.co2;
|
||||
using mbts;
|
||||
using SecurityKey = System.UInt32;
|
||||
|
||||
namespace bts.api.md
|
||||
namespace bts.md
|
||||
{
|
||||
using security_key = System.UInt32;
|
||||
public class MarketDataClient : IDisposable
|
||||
|
||||
public class MarketDataClient : IDisposable
|
||||
{
|
||||
class Data
|
||||
{
|
||||
public long last_update_time;
|
||||
public ChannelUpdate data;
|
||||
}
|
||||
static uint naggle_ms = 100;
|
||||
readonly static bts.md.MDSource _subscription = new MDSource(naggle_ms);
|
||||
|
||||
const UInt32 _all_flags = (uint)(
|
||||
MDRequest.Types.Flags.Security |
|
||||
MDRequest.Types.Flags.Bbo |
|
||||
MDRequest.Types.Flags.Direct |
|
||||
MDRequest.Types.Flags.Implied |
|
||||
MDRequest.Types.Flags.LastTrade);
|
||||
|
||||
readonly ConcurrentDictionary<security_key, Data> _data = new ConcurrentDictionary<security_key, Data>();
|
||||
readonly ConcurrentDictionary<security_key, string> _security_key_to_desc= new ConcurrentDictionary<security_key, string>();
|
||||
readonly ConcurrentDictionary<string, security_key> _security_desc_to_key = new ConcurrentDictionary<string, security_key>();
|
||||
readonly Lazy<MDRequest> _subscription;
|
||||
readonly Venue _venue;
|
||||
readonly ConcurrentDictionary<SecurityKey, SecurityMD> _data = new ConcurrentDictionary<SecurityKey, SecurityMD>();
|
||||
readonly List<IDisposable> _disp = new List<IDisposable>();
|
||||
readonly uint _default_pump_ms;
|
||||
public MarketDataClient(uint default_pump_ms = 250, AuthTokenImmutable? token = null)
|
||||
{
|
||||
_default_pump_ms = default_pump_ms;
|
||||
if (token != null)
|
||||
set_auth_token(token);
|
||||
|
||||
readonly static Lazy<MarketDataRelay> _md = new Lazy<MarketDataRelay>(() => locate(), true);
|
||||
readonly static ConcurrentDictionary<api.Venue, MarketDataClient> _venue_cache = new ConcurrentDictionary<api.Venue, api.md.MarketDataClient>();
|
||||
static readonly List<Product> _all_products;
|
||||
static MarketDataClient()
|
||||
{
|
||||
_all_products = _md.Value.request_products(new VoidMessage()).Products.ToList();
|
||||
}
|
||||
public static bool create_or_get(api.Venue exchange, out MarketDataClient md)
|
||||
{
|
||||
if (!_venue_cache.TryGetValue(exchange, out md))
|
||||
_disp.Add(_subscription.on_securities.Subscribe( md =>
|
||||
{
|
||||
_venue_cache[exchange] = md = new MarketDataClient(exchange);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
foreach (var entry in md)
|
||||
_data.AddOrUpdate(entry.Key, entry.Value, (_, __) => entry.Value);
|
||||
}));
|
||||
}
|
||||
public IEnumerable<SecurityImmutable> securities => _data.Values.Select(x => x.security);
|
||||
public IObservable<IEnumerable<SecurityImmutable>> on_securities
|
||||
{
|
||||
get { return _subscription.on_securities.Select(x => x.Values.Select(xx => xx.security)); }
|
||||
}
|
||||
|
||||
public void set_auth_token(AuthTokenImmutable? toke)
|
||||
{
|
||||
if(toke.HasValue)
|
||||
_subscription.set_token(toke.Value);
|
||||
}
|
||||
public IObservable< IEnumerable<IMDBook> > subscribe_book(Func<SecurityImmutable, bool> filter,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDBook>(filter, update_interval, scheduler);
|
||||
}
|
||||
public IObservable< IEnumerable<IMDLast> > subscribe_last(Func<SecurityImmutable, bool> filter,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDLast>(filter, update_interval, scheduler);
|
||||
}
|
||||
public IObservable< IEnumerable<IMDMisc> > subscribe_misc(Func<SecurityImmutable, bool> filter,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDMisc>(filter, update_interval, scheduler);
|
||||
}
|
||||
public IObservable< IEnumerable<IMDCombined> > subscribe_combined(Func<SecurityImmutable, bool> filter,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDCombined>(filter, update_interval, scheduler);
|
||||
}
|
||||
|
||||
IObservable<IEnumerable<T>> _subscribe<T>(Func<SecurityImmutable, bool> filter,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null) where T : IMDData, IMDUpdate, new()
|
||||
{
|
||||
List<Slot<T>> slots = new List<Slot<T>>(); int data_size = 0;
|
||||
return timer_pump<IEnumerable<T>>((last_timer_tick, observer) =>
|
||||
{
|
||||
//if the number of securities has changed, or this is the first call
|
||||
if (data_size != _data.Count)
|
||||
{
|
||||
slots = _data.Values.Where(x => filter(x.security)).Select(xx => new Slot<T>(xx)).ToList();
|
||||
data_size = _data.Count;
|
||||
}
|
||||
List<T> updates = new List<T>();
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
bool did_update = slot.data.update(slot.secmd, ref slot.last_update_time);
|
||||
//if (slot.last_update_time > last_timer_tick)
|
||||
if(did_update)
|
||||
updates.Add(slot.data);
|
||||
}
|
||||
if(updates.Count > 0)
|
||||
observer.OnNext(updates);
|
||||
},
|
||||
update_interval, scheduler);
|
||||
}
|
||||
|
||||
public IObservable<IEnumerable<IMDBook>> subscribe_book(IEnumerable<SecurityKey> ids, TimeSpan? update_interval = null,
|
||||
IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDBook>(ids, update_interval, scheduler);
|
||||
}
|
||||
public IObservable<IEnumerable<IMDLast>> subscribe_last(IEnumerable<SecurityKey> ids, TimeSpan? update_interval = null,
|
||||
IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDLast>(ids, update_interval, scheduler);
|
||||
}
|
||||
public IObservable<IEnumerable<IMDMisc>> subscribe_misc(IEnumerable<SecurityKey> ids, TimeSpan? update_interval = null,
|
||||
IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDMisc>(ids, update_interval, scheduler);
|
||||
}
|
||||
public IObservable<IEnumerable<IMDCombined>> subscribe_combined(IEnumerable<SecurityKey> ids, TimeSpan? update_interval = null,
|
||||
IScheduler scheduler = null)
|
||||
{
|
||||
return _subscribe<MDCombined>(ids, update_interval, scheduler);
|
||||
}
|
||||
|
||||
IObservable<IEnumerable<T>> _subscribe<T>(IEnumerable<SecurityKey> ids, TimeSpan? update_interval = null,
|
||||
IScheduler scheduler = null) where T : IMDData, IMDUpdate, new()
|
||||
{
|
||||
List<Slot<T>> slots = ids.Where(ii => _data.TryGetValue(ii, out SecurityMD _)).Select(x =>
|
||||
{
|
||||
_data.TryGetValue(x, out SecurityMD smd);
|
||||
return new Slot<T>(smd);
|
||||
}).ToList();
|
||||
return timer_pump<IEnumerable<T>>((last_timer_tick, observer) =>
|
||||
{
|
||||
List<T> updates = new List<T>();
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
bool did_update = slot.data.update(slot.secmd, ref slot.last_update_time);
|
||||
//if (slot.last_update_time > last_timer_tick)
|
||||
if(did_update)
|
||||
updates.Add(slot.data);
|
||||
}
|
||||
if(updates.Count > 0)
|
||||
observer.OnNext(updates);
|
||||
},
|
||||
update_interval, scheduler);
|
||||
}
|
||||
|
||||
public MarketDataClient(bts.api.Venue exchange = Venue.Unset)
|
||||
class Slot<T> where T : IMDData, IMDUpdate, new()
|
||||
{
|
||||
_venue = exchange;
|
||||
_subscription = new Lazy<MDRequest>(() => load_request(exchange));
|
||||
}
|
||||
|
||||
MDRequest load_request(bts.api.Venue exchange)
|
||||
{
|
||||
var req = new MDRequest();
|
||||
var prods = _all_products.Where(x=>x.Exchange == exchange || exchange == bts.api.Venue.Unset);
|
||||
foreach(var prod in prods)
|
||||
req.Channels.Add(new SecurityChannel() { Exchange = prod.Exchange, Type = prod.Type, Symbol = prod.Symbol, Flags = 0 });
|
||||
return req;
|
||||
}
|
||||
|
||||
static MarketDataRelay locate()
|
||||
{
|
||||
using (var ctx = new api.core.ServiceContext())
|
||||
public Slot(SecurityMD md)
|
||||
{
|
||||
return ctx.locate<MarketDataRelay>();
|
||||
secmd = md;
|
||||
last_update_time = 0;
|
||||
data = new T();
|
||||
}
|
||||
public SecurityMD secmd;
|
||||
public ulong last_update_time;
|
||||
public T data;
|
||||
}
|
||||
|
||||
IObservable<T> timer_pump<T>(Action<ulong, IObserver<T>> tick, TimeSpan? interval, IScheduler scheduler = null)
|
||||
{
|
||||
if(!interval.HasValue)
|
||||
interval = TimeSpan.FromMilliseconds(_default_pump_ms);
|
||||
ulong last_update_time = 0;
|
||||
return Observable.Create<T>(observer =>
|
||||
{
|
||||
return Observable.Timer(interval.Value, interval.Value, scheduler ?? Scheduler.Default).Subscribe(_ =>
|
||||
{
|
||||
tick(last_update_time, observer);
|
||||
last_update_time = (ulong)DateTime.UtcNow.Ticks;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disp.ForEach(x => x.Dispose());
|
||||
}
|
||||
|
||||
public Security lookup(security_key key)
|
||||
#region security search
|
||||
|
||||
public SecurityImmutable? lookup(SecurityKey key)
|
||||
{
|
||||
Data value;
|
||||
if(_data.TryGetValue(key, out value))
|
||||
return value.data.Security;
|
||||
if (_data.TryGetValue(key, out SecurityMD value))
|
||||
return value.security;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool lookup_security_desc(security_key key, out string security_desc)
|
||||
public bool lookup_security_desc(SecurityKey key, out string security_desc)
|
||||
{
|
||||
return _security_key_to_desc.TryGetValue(key, out security_desc);
|
||||
security_desc = null;
|
||||
var sec = lookup(key);
|
||||
if (sec.HasValue)
|
||||
{
|
||||
security_desc = sec.Value.glbx_desc();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool lookup_security_key_from_desc(string security_desc, out security_key key)
|
||||
public bool lookup_security_key_from_desc(string security_desc, out SecurityKey key)
|
||||
{
|
||||
return _security_desc_to_key.TryGetValue(security_desc, out key);
|
||||
key = 0;
|
||||
var fnd = find(x => x.glbx_desc() == security_desc).TryFirst();
|
||||
if (fnd.HasValue)
|
||||
{
|
||||
key = fnd.Value.security.Id;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Security find_security(Func<Security, bool> filter)
|
||||
IEnumerable<SecurityMD> find(Func<SecurityImmutable, bool> filter)
|
||||
{
|
||||
return _data.Values.Select(x => x.data.Security).Where(xx => filter(xx)).FirstOrDefault();
|
||||
Func<SecurityMD, bool> filt = (SecurityMD secmd) => filter(secmd.security);
|
||||
return _data.Values.Where(filt);
|
||||
}
|
||||
|
||||
public async Task<Security> find_security_async(Func<Security, bool> filter, TimeSpan? timeout = null)
|
||||
public SecurityImmutable? find_security(Func<SecurityImmutable, bool> filter)
|
||||
{
|
||||
if(!timeout.HasValue) {
|
||||
timeout = TimeSpan.FromMilliseconds(int.MaxValue-1);
|
||||
var fnd = find(filter).TryFirst();
|
||||
return fnd?.security;
|
||||
}
|
||||
|
||||
public async Task<SecurityImmutable?> find_security_async(Func<SecurityImmutable, bool> filter, TimeSpan? timeout = null)
|
||||
{
|
||||
if (!timeout.HasValue)
|
||||
{
|
||||
timeout = TimeSpan.FromMilliseconds(10000);
|
||||
}
|
||||
CancellationTokenSource src = new CancellationTokenSource(timeout.Value);
|
||||
Security security = await Task.Run<Security>(async () =>
|
||||
SecurityImmutable? security = await Task.Run<SecurityImmutable?>(async () =>
|
||||
{
|
||||
Security sec = null;
|
||||
SecurityImmutable? sec = null;
|
||||
do
|
||||
{
|
||||
sec = find_security(filter);
|
||||
@@ -118,195 +238,11 @@ namespace bts.api.md
|
||||
while (sec == null);
|
||||
return sec;
|
||||
}, src.Token);
|
||||
|
||||
|
||||
return security ?? throw new KeyNotFoundException("Can't find security for filter"); ;
|
||||
}
|
||||
|
||||
public IObservable<ChannelUpdate> subscribe(Product product,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
return subscribe(product, _all_flags, update_interval, scheduler);
|
||||
}
|
||||
|
||||
|
||||
public IObservable<ChannelUpdate> subscribe(Product product, uint flags,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
check_subscriptions(flags, x =>
|
||||
(x.Exchange == product.Exchange && x.Type == product.Type && x.Symbol == product.Symbol));
|
||||
return _subscribe(
|
||||
x => (x.Exchange == product.Exchange && ((SecurityImmutable)x).security_type() == product.Type && x.Symbol == product.Symbol),
|
||||
update_interval,
|
||||
scheduler);
|
||||
}
|
||||
|
||||
IObservable<ChannelUpdate> _subscribe(Func<Security, bool> filter,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
Data[] slots = new Data[0]; long local_security_count = 0;
|
||||
return timer_pump<Data>((last_update, observer) =>
|
||||
{
|
||||
//if the number of securities has changed, or this is the first call
|
||||
if (slots.Length == 0 || local_security_count != _data.Count)
|
||||
slots = _data.Values.Where(xx => filter(xx.data.Security)).ToArray();
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
Data data;
|
||||
if (_data.TryGetValue(slot.data.Security.Id, out data))
|
||||
{
|
||||
if (data.last_update_time > last_update)
|
||||
observer.OnNext(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
update_interval, scheduler).Select(x => x.data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Poll for updates for a security on a specified update_interval, on the specified scheduler with default flags
|
||||
/// </summary>
|
||||
/// <param name="security"></param>
|
||||
/// <param name="update_interval"></param>
|
||||
/// <param name="scheduler"></param>
|
||||
/// <returns></returns>
|
||||
public IObservable<ChannelUpdate> subscribe(Security security,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
return subscribe(security, _all_flags, update_interval, scheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Poll for updates for a security on a specified update_interval, on the specified scheduler
|
||||
/// </summary>
|
||||
/// <param name="security"></param>
|
||||
/// <param name="flags"></param>
|
||||
/// <param name="update_interval"></param>
|
||||
/// <param name="scheduler"></param>
|
||||
/// <returns></returns>
|
||||
public IObservable<ChannelUpdate> subscribe(Security security, uint flags,
|
||||
TimeSpan? update_interval = null, IScheduler scheduler = null)
|
||||
{
|
||||
check_subscriptions(flags, x =>
|
||||
(x.Exchange == security.Exchange && x.Type == ((SecurityImmutable)security).security_type() && security.Symbol == security.Symbol));
|
||||
return timer_pump<Data>( (last_update, observer) =>
|
||||
{
|
||||
Data data;
|
||||
if (_data.TryGetValue(security.Id, out data))
|
||||
{
|
||||
if (data.last_update_time > last_update)
|
||||
observer.OnNext(data);
|
||||
}
|
||||
},
|
||||
update_interval, scheduler).Select(x => x.data);
|
||||
}
|
||||
|
||||
IObservable<T> timer_pump<T>(Action<long, IObserver<T>> tick, TimeSpan? interval, IScheduler scheduler = null)
|
||||
{
|
||||
if(!interval.HasValue)
|
||||
interval = TimeSpan.FromMilliseconds(25);
|
||||
long last_update_time = 0;
|
||||
return Observable.Create<T>(observer =>
|
||||
{
|
||||
return Observable.Timer(interval.Value, interval.Value, scheduler ?? Scheduler.Default).Subscribe(_ =>
|
||||
{
|
||||
tick(last_update_time, observer);
|
||||
last_update_time = DateTime.UtcNow.Ticks;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bool check_subscriptions(uint flags, Func<SecurityChannel, bool> selector)
|
||||
{
|
||||
foreach (var sub in _subscription.Value.Channels)
|
||||
{
|
||||
if (selector(sub))
|
||||
{
|
||||
sub.Flags |= flags;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start pumping market data for all subscribed channels and securities. Call this without await
|
||||
/// </summary>
|
||||
/// <param name="interval"></param>
|
||||
/// <returns></returns>
|
||||
public async Task start_pump(TimeSpan? interval = null)
|
||||
{
|
||||
var rep = await _md.Value.request_names_async(new VoidMessage());
|
||||
int count = 0;
|
||||
do
|
||||
{
|
||||
foreach (var s in rep.value.Securities)
|
||||
{
|
||||
foreach (var ss in s.Names)
|
||||
{
|
||||
_security_desc_to_key[ss.Value] = ss.Key;
|
||||
_security_key_to_desc[ss.Key] = ss.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!rep.success && count++ < 5);
|
||||
while (true)
|
||||
{
|
||||
var reply = await _md.Value.requestmd_async(_subscription.Value);
|
||||
if (reply.success)
|
||||
{
|
||||
foreach (var channel in reply.value.Channels)
|
||||
{
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
foreach (var update in channel.Updates)
|
||||
{
|
||||
Data data;
|
||||
if (_data.TryGetValue(update.Security.Id, out data))
|
||||
{
|
||||
//mergefrom concats repeated fields
|
||||
if (update.Direct != null)
|
||||
data.data.Direct = null;
|
||||
//this can tear, but the book won't tear, so maybe it's ok
|
||||
data.data.MergeFrom(update);
|
||||
data.last_update_time = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
_data[update.Security.Id] = new Data() { last_update_time = now, data = update };
|
||||
}
|
||||
}
|
||||
}
|
||||
await Task.Delay(interval ?? TimeSpan.FromMilliseconds(100), _cts.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task _pump_task;
|
||||
public void run_pump(TimeSpan? interval = null)
|
||||
{
|
||||
if (_pump_task == null)
|
||||
{
|
||||
_pump_task = this.start_pump(interval)
|
||||
.ContinueWith(ex =>
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(ex == null, ex.ToString());
|
||||
}
|
||||
, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
}
|
||||
|
||||
public async void run_pump_async(TimeSpan? interval = null)
|
||||
{
|
||||
await this.start_pump(interval);
|
||||
}
|
||||
|
||||
|
||||
readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
if (_md.IsValueCreated)
|
||||
_md.Value.Dispose();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using bts.api;
|
||||
using mbts.co2;
|
||||
|
||||
namespace bts.md
|
||||
{
|
||||
public class PriceSize
|
||||
{
|
||||
public uint size { get; }
|
||||
public uint direct_size { get; }
|
||||
public uint implied_size { get; }
|
||||
public double price { get; }
|
||||
public PriceSize(uint sz, uint direct_sz, uint implied_sz, double prx)
|
||||
{
|
||||
size = sz;
|
||||
direct_size = direct_sz;
|
||||
implied_size = implied_sz;
|
||||
price = prx;
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{price} {size} {implied_size} ";
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
namespace mbts {
|
||||
struct SecurityMD : IEquatable<SecurityMD> {
|
||||
public SecurityImmutable security { get; }
|
||||
public MDConsumerHandle md { get; }
|
||||
@@ -37,17 +37,17 @@ namespace bts.api {
|
||||
$"{symbol}{DateFormatter.Format(future.ExpirationYear, future.ExpirationMonth, future.ExpirationDay, DateFormat.MonthCodeLastDigitYear)}" :
|
||||
exch_desc;
|
||||
|
||||
static string _format(SecurityImmutable security) {
|
||||
switch (security.ContractCase) {
|
||||
case ContractOneofCase.Future: return format_future(security.Symbol, security.glbx_desc(), security.Future);
|
||||
static string _format(SecurityImmutable? security) {
|
||||
switch (security?.ContractCase) {
|
||||
case ContractOneofCase.Future: return format_future(security?.Symbol, security?.glbx_desc(), security.Value.Future);
|
||||
case ContractOneofCase.Spread:
|
||||
if (security.HasGlbx) {
|
||||
if (security?.HasGlbx == true) {
|
||||
// In case we ever bring it back, pretty printed spread names can be found here:
|
||||
// GlbxSecurityFormatter.FormatSpread(security, out string result)
|
||||
return security.Glbx.SecurityDesc;
|
||||
return security?.Glbx.SecurityDesc;
|
||||
}
|
||||
return string.Join("", _format_future_legs(SecurityHelper.get_flat_future_legs(security.Spread.Legs)));
|
||||
case ContractOneofCase.Synthetic: return security.Synthetic.Name;
|
||||
return string.Join("", _format_future_legs(SecurityHelper.get_flat_future_legs(security?.Spread.Legs)));
|
||||
case ContractOneofCase.Synthetic: return security?.Synthetic.Name;
|
||||
default: return "Unknown-security";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,24 +66,10 @@ namespace bts.api {
|
||||
public static readonly IComparer<SecurityImmutable> InstanceImmutable = Comparer<SecurityImmutable>.Create(Compare);
|
||||
|
||||
public static int Compare(Security x, Security y) {
|
||||
if (x.ContractCase == ContractOneofCase.Future) return y.ContractCase == ContractOneofCase.Future ? Compare(x.Symbol, x.Future, y.Symbol, y.Future) : -1;
|
||||
else if (y.ContractCase == ContractOneofCase.Future) return 1;
|
||||
var xlegs = get_flat_future_legs(x).ToList();
|
||||
var ylegs = get_flat_future_legs(y).ToList();
|
||||
int max = Math.Min(xlegs.Count, ylegs.Count);
|
||||
for (int i = 0; i < max; i++) {
|
||||
int rv = Compare(xlegs[i].symbol, xlegs[i].future, ylegs[i].symbol, ylegs[i].future);
|
||||
if (rv != 0) return rv;
|
||||
}
|
||||
for (int i = 0; i < max; i++) {
|
||||
int rv = xlegs[i].quantity.CompareTo(ylegs[i].quantity);
|
||||
if (rv != 0) return rv;
|
||||
}
|
||||
for (int i = 0; i < max; i++) {
|
||||
int rv = xlegs[i].exchange.CompareTo(ylegs[i].exchange);
|
||||
if (rv != 0) return rv;
|
||||
}
|
||||
return xlegs.Count.CompareTo(ylegs.Count);
|
||||
if (ReferenceEquals(x, y)) return 0; // x ==y
|
||||
if (x == null) return 1; // y > x
|
||||
if (y == null) return -1; // x > y
|
||||
return Compare(new SecurityImmutable(x), new SecurityImmutable(y));
|
||||
}
|
||||
|
||||
public static int Compare(SecurityImmutable x, SecurityImmutable y) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts
|
||||
{
|
||||
static class APIExtensions
|
||||
{
|
||||
public static T? TryFirst<T>(this IEnumerable<T> src) where T : struct
|
||||
{
|
||||
foreach (var v in src)
|
||||
return v;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static DateTime lldatetime(this ulong ll)
|
||||
{
|
||||
// Warning: Do not remove these consts, or try to simplify this method.
|
||||
// System.DateTime() requires bits 63-64 to be empty in the "DateTime(long ticks)" constructor.
|
||||
// The only way to set the DateTimeKind is through the "DateTime(long ticks, DateTimeKind kind)" constructor.
|
||||
// (Yes, it is annoying. Internally, System.DateTime stores the DateTimeKind in bits 63-64 - in the exact format we already have!!!)
|
||||
const ulong ticks_mask = 0x3FFFFFFFFFFFFFFF;
|
||||
const int kind_shift = 62;
|
||||
return new DateTime((long)(ll & ticks_mask), (DateTimeKind)(ll >> kind_shift));
|
||||
}
|
||||
|
||||
public static string fmt(this string input, params object[] vals)
|
||||
{
|
||||
return string.Format(input, vals);
|
||||
}
|
||||
|
||||
public static void ignore(this Task t) { }
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,17 @@
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.utils
|
||||
{
|
||||
public class AsyncLazy<T> : Lazy<Task<T>>
|
||||
{
|
||||
public AsyncLazy(Func<T> valueFactory) :
|
||||
base(() => Task.Factory.StartNew(valueFactory))
|
||||
{ }
|
||||
|
||||
public AsyncLazy(Func<Task<T>> taskFactory) :
|
||||
base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
|
||||
namespace bts.api
|
||||
{
|
||||
public static class Locator
|
||||
{
|
||||
readonly static Lazy<ServiceContext> _context = new Lazy<ServiceContext>(() => new ServiceContext(), true);
|
||||
public static T locate<T>(string qualifier="") where T : class, IApiClient
|
||||
{
|
||||
return _context.Value.locate<T>(qualifier);
|
||||
}
|
||||
static public async Task<LoginResult> login(string username, string password)
|
||||
{
|
||||
return await _context.Value.login(username, password);
|
||||
}
|
||||
static public void login_with_token(AuthTokenImmutable token)
|
||||
{
|
||||
_context.Value.login(token);
|
||||
}
|
||||
static public AuthTokenImmutable? token => _context.Value.token;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace bts {
|
||||
enum Level {
|
||||
NoLog,
|
||||
Diag,
|
||||
Info,
|
||||
Warn,
|
||||
@@ -32,12 +33,14 @@ namespace bts {
|
||||
Testing,
|
||||
Latency,
|
||||
DataFlow,
|
||||
Plugin
|
||||
Plugin,
|
||||
ClientSdk
|
||||
// Please keep this list in sync with logging.h/logging.cpp
|
||||
}
|
||||
readonly struct Logger {
|
||||
|
||||
public readonly struct Logger {
|
||||
readonly Subsystem _sys;
|
||||
public Logger(Subsystem sys) {
|
||||
internal Logger(Subsystem sys) {
|
||||
_sys = sys;
|
||||
}
|
||||
|
||||
@@ -61,19 +64,20 @@ namespace bts {
|
||||
static extern void Logger_log (Level level, Subsystem subsystem, string message, string file, int line);
|
||||
}
|
||||
|
||||
static class Log {
|
||||
public static Logger Gen => new Logger(Subsystem.gen);
|
||||
public static Logger Config => new Logger(Subsystem.Config);
|
||||
public static Logger QBus => new Logger(Subsystem.QBus);
|
||||
public static Logger Md => new Logger(Subsystem.md);
|
||||
public static Logger Api => new Logger(Subsystem.Api);
|
||||
public static Logger Persist => new Logger(Subsystem.Persist);
|
||||
public static Logger User => new Logger(Subsystem.User);
|
||||
public static Logger OrderMan => new Logger(Subsystem.OrderMan);
|
||||
public static Logger Assert => new Logger(Subsystem.Assert);
|
||||
public static Logger Ui => new Logger(Subsystem.Ui);
|
||||
public static Logger Testing => new Logger(Subsystem.Testing);
|
||||
public static Logger DataFlow => new Logger(Subsystem.DataFlow);
|
||||
public static class Log {
|
||||
internal static Logger Gen => new Logger(Subsystem.gen);
|
||||
internal static Logger Config => new Logger(Subsystem.Config);
|
||||
internal static Logger QBus => new Logger(Subsystem.QBus);
|
||||
internal static Logger Md => new Logger(Subsystem.md);
|
||||
internal static Logger Api => new Logger(Subsystem.Api);
|
||||
internal static Logger Persist => new Logger(Subsystem.Persist);
|
||||
internal static Logger User => new Logger(Subsystem.User);
|
||||
internal static Logger OrderMan => new Logger(Subsystem.OrderMan);
|
||||
internal static Logger Assert => new Logger(Subsystem.Assert);
|
||||
internal static Logger Ui => new Logger(Subsystem.Ui);
|
||||
internal static Logger Testing => new Logger(Subsystem.Testing);
|
||||
internal static Logger DataFlow => new Logger(Subsystem.DataFlow);
|
||||
public static Logger ClientSdk => new Logger(Subsystem.ClientSdk);
|
||||
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
@@ -99,10 +103,10 @@ namespace bts {
|
||||
assert(o == null);
|
||||
}
|
||||
delegate void on_crash_callback([MarshalAs(UnmanagedType.LPStr)]string msg, [MarshalAs(UnmanagedType.LPStr)] string details);
|
||||
public static void system_is_crashing(string message, object details) {
|
||||
internal static void system_is_crashing(string message, object details) {
|
||||
OnSystemCrashing?.Invoke(message, details);
|
||||
}
|
||||
public static event Action<string, object> OnSystemCrashing;
|
||||
internal static event Action<string, object> OnSystemCrashing;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -61,7 +61,7 @@ namespace bts.collections {
|
||||
if (_nursery.TryPopValue(k, out SortedList<Node> children)) {
|
||||
// remove orphans from _tree (orphan-nodes must reside in the main _tree until parents arrive)
|
||||
foreach (var n in children)
|
||||
_tree.Remove(n);
|
||||
_assert_remove(_tree, n);
|
||||
node = new Node(item, children);
|
||||
} else node = new Node(item, _comparer);
|
||||
var pk = parent(item);
|
||||
@@ -84,13 +84,10 @@ namespace bts.collections {
|
||||
if (!_index.TryGetValue(k, out Node node)) {
|
||||
return false;
|
||||
} else {
|
||||
var newval = replace(node.value);
|
||||
Log.assert(id(newval).Equals(k));
|
||||
_replace(node, newval);
|
||||
_replace(node, replace);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Remove(T item) => Remove(id(item));
|
||||
|
||||
@@ -185,44 +182,97 @@ namespace bts.collections {
|
||||
}
|
||||
}
|
||||
|
||||
void _replace(Node node, T newval) {
|
||||
TKey pk = parent(newval);
|
||||
TKey old_pk = parent(node.value);
|
||||
if (pk.Equals(old_pk)) {
|
||||
SortedList<Node> node_list;
|
||||
if (pk.Equals(_root_id))
|
||||
node_list = _tree;
|
||||
else if (_index.TryGetValue(pk, out Node pnode)) {
|
||||
node_list = pnode.children;
|
||||
} else if (!_nursery.TryGetValue(pk, out node_list)) {
|
||||
throw new KeyNotFoundException($"Replace was called with a parent-key ({pk}) that was not found in _index or _nursery; item-key was ({id(newval)}).");
|
||||
}
|
||||
node_list.Remove(node);
|
||||
node.value = newval;
|
||||
node_list.Add(node);
|
||||
} else {
|
||||
if (old_pk.Equals(_root_id)) {
|
||||
_tree.Remove(node);
|
||||
} else {
|
||||
if (_index.TryGetValue(old_pk, out Node pnode))
|
||||
pnode.children.Remove(node);
|
||||
else {
|
||||
_remove_from_nursery(old_pk, node);
|
||||
}
|
||||
}
|
||||
node.value = newval;
|
||||
if (pk.Equals(_root_id)) {
|
||||
void _replace(Node node, T new_value) =>
|
||||
_replace(node, _ => new_value);
|
||||
|
||||
void _replace(Node node, Func<T, T> replace_value) {
|
||||
/// STEPS (outline)
|
||||
/// 1) remove node with current value
|
||||
/// 2) replace node.value
|
||||
/// 3) re-insert node with new value
|
||||
|
||||
/// Implementation (details):
|
||||
/// there are 3 scenarios in which step 1 (remove node...) may occur
|
||||
/// a) node is a root node
|
||||
/// - remove it from <see cref="_tree"/>.
|
||||
/// b) node is a child node who's parent is present
|
||||
/// - remove it from the parent <see cref="Node.children"/>
|
||||
/// c) the node is a child node who's parent is NOT present
|
||||
/// - remove it from <see cref="_nursery"/> AND <see cref="_tree"/>.
|
||||
/// In each scenario, after the node is removed, the new parent will be assigned (<see cref="new_pk"/>).
|
||||
/// If the parent has NOT changed, assign the node back to the same list(s), then return.
|
||||
/// If the parent HAS changed, determine the list(s) in which the node needs to be inserted, insert the node, then return.
|
||||
TKey old_pk = parent(node.value), new_pk;
|
||||
if (old_pk.Equals(_root_id)) {
|
||||
// a)
|
||||
_assert_remove(_tree, node);
|
||||
_assert_replace(ref node, replace_value);
|
||||
new_pk = parent(node.value);
|
||||
if (new_pk.Equals(old_pk)) {
|
||||
// the parent has NOT changed; re-insert the node, then return.
|
||||
_tree.Add(node);
|
||||
} else if (_index.TryGetValue(pk, out Node pnode)) {
|
||||
pnode.children.Add(node);
|
||||
} else {
|
||||
if (!_nursery.TryGetValue(pk, out SortedList<Node> nursery_list)) {
|
||||
_nursery.Add(pk, nursery_list = new SortedList<Node>(_comparer));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (_index.TryGetValue(old_pk, out Node old_pnode)) {
|
||||
// b)
|
||||
_assert_remove(old_pnode.children, node);
|
||||
_assert_replace(ref node, replace_value);
|
||||
new_pk = parent(node.value);
|
||||
if (new_pk.Equals(old_pk)) {
|
||||
// the parent has NOT changed; re-insert the node, then return.
|
||||
old_pnode.children.Add(node);
|
||||
return;
|
||||
}
|
||||
} else if (_nursery.TryGetValue(old_pk, out SortedList<Node> nursery_list)) {
|
||||
// c)
|
||||
_assert_remove(nursery_list, node);
|
||||
_assert_remove(_tree, node);
|
||||
_assert_replace(ref node, replace_value);
|
||||
new_pk = parent(node.value);
|
||||
if (new_pk.Equals(old_pk)) {
|
||||
// the parent has NOT changed; re-insert the node, then return.
|
||||
nursery_list.Add(node);
|
||||
_tree.Add(node);
|
||||
return;
|
||||
} else {
|
||||
// the parent HAS changed; if the old nursery_list is empty, remove it.
|
||||
if (nursery_list.Count == 0)
|
||||
_nursery.Remove(old_pk);
|
||||
}
|
||||
} else {
|
||||
// error)
|
||||
throw new InvalidOperationException($"Unknown parent key '{old_pk}'.");
|
||||
}
|
||||
// the parent HAS changed. Determine the list(s) in which the node needs to inserted.
|
||||
if (new_pk.Equals(_root_id)) {
|
||||
_tree.Add(node);
|
||||
} else if (_index.TryGetValue(new_pk, out Node new_pnode)) {
|
||||
new_pnode.children.Add(node);
|
||||
} else {
|
||||
if (!_nursery.TryGetValue(new_pk, out SortedList<Node> new_nursery_list)) {
|
||||
_nursery.Add(new_pk, new_nursery_list = new SortedList<Node>(_comparer));
|
||||
}
|
||||
new_nursery_list.Add(node);
|
||||
_tree.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to removes the node from the list <see cref="SortedList{T}.Remove"/> and throws an exception if it does not succeed.
|
||||
/// </summary>
|
||||
static void _assert_remove(SortedList<Node> list, Node node) {
|
||||
if (!list.Remove(node))
|
||||
throw new InvalidOperationException("Failed to remove node.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces <see cref="Node.value"/> using the specified delegate, and throws an exception if the new value's id is different from the old value's id.
|
||||
/// </summary>
|
||||
void _assert_replace(ref Node node, Func<T, T> replace_value) {
|
||||
TKey old_key = id(node.value);
|
||||
node.value = replace_value(node.value);
|
||||
if (!id(node.value).Equals(old_key))
|
||||
throw new InvalidOperationException("Replace op changed Node's id.");
|
||||
}
|
||||
|
||||
void _remove_from_nursery(TKey pk, Node node) {
|
||||
|
||||
@@ -9,12 +9,12 @@ using System.Threading.Tasks;
|
||||
namespace bts.test {
|
||||
public static class InProcessTestRunner {
|
||||
|
||||
public static void run_test_syncronously(string v, TestEnumerator enumerator, Action<TestResult> report) {
|
||||
public static void run_test_synchronously(string v, TestEnumerator enumerator, Action<TestResult> report) {
|
||||
foreach (var t in enumerator.find(v)) {
|
||||
run_test_syncronously(t, report);
|
||||
run_test_synchronously(t, report);
|
||||
}
|
||||
}
|
||||
public static void run_test_syncronously(TestMethod test, Action<TestResult> report) {
|
||||
public static void run_test_synchronously(TestMethod test, Action<TestResult> report) {
|
||||
var result = new TestResult() {
|
||||
TestName = test.full_name,
|
||||
Status = TestResult.Types.Step.Setup,
|
||||
|
||||
Loaded 100 of 467 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user