Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3e4e1385 | ||
|
|
0864925031 | ||
|
|
d122d5704b | ||
|
|
8a1a75098e | ||
|
|
7063035e7b | ||
|
|
0db69f1b1b | ||
|
|
efa07fe96e | ||
|
|
a36f9278d5 | ||
|
|
727f39b6a6 | ||
|
|
d0c978b71c | ||
|
|
6e9d164d32 | ||
|
|
50a96fc22a | ||
|
|
c6499976a9 | ||
|
|
4b8ca8d97e | ||
|
|
7e77e510fc | ||
|
|
bd24381986 | ||
|
|
5830eee68d | ||
|
|
f3928f58dd | ||
|
|
72d57b242e | ||
|
|
9a6e43e4a4 | ||
|
|
ffb443fcb2 | ||
|
|
f22e68f4bc | ||
|
|
09f08f0a57 | ||
|
|
53ca6aade8 | ||
|
|
b968ebe492 | ||
|
|
bbef942f86 | ||
|
|
7859b7923e | ||
|
|
8202b51cb9 | ||
|
|
ddd41ab317 | ||
|
|
d175a53ef8 | ||
|
|
904e238a78 | ||
|
|
0fb1425766 | ||
|
|
2104fd5ad8 | ||
|
|
26a58e82bf | ||
|
|
01e96c8241 | ||
|
|
ab931bb527 | ||
|
|
e756897f88 | ||
|
|
05ea25a86f | ||
|
|
a6ab49a911 | ||
|
|
33ee7ac114 | ||
|
|
61981a0ea1 | ||
|
|
204e9383fa | ||
|
|
d424032757 | ||
|
|
0fcdf3f4ff | ||
|
|
1b48739e1e | ||
|
|
8b96b1d835 | ||
|
|
3569500111 | ||
|
|
05cfcb5648 | ||
|
|
e425064730 | ||
|
|
4a50e7f86d | ||
|
|
15cbacd1d2 | ||
|
|
cdf4a63ae8 | ||
|
|
f35a674b9f | ||
|
|
03a4691bc0 | ||
|
|
a990eac146 | ||
|
|
27f06d0f9d | ||
|
|
7a05ae1714 | ||
|
|
72549a7bcb | ||
|
|
4f46aa5360 | ||
|
|
b07e985e8b | ||
|
|
0c072b018e | ||
|
|
d7456c0528 | ||
|
|
60c5159a4a | ||
|
|
7388472f6a | ||
|
|
c24c31d8d3 | ||
|
|
be006051b5 | ||
|
|
4590dbc056 | ||
|
|
b5e751e625 | ||
|
|
b5a8098d9e | ||
|
|
3d1de2849c | ||
|
|
887850a612 | ||
|
|
632a86f670 | ||
|
|
cdbda8250f | ||
|
|
43fd07dcfa | ||
|
|
efcb4f471c | ||
|
|
d2f2e03e64 | ||
|
|
cfeba74900 | ||
|
|
41bc4f7950 | ||
|
|
963bd15b35 | ||
|
|
b080d4a9c4 | ||
|
|
282e10cba3 | ||
|
|
fa1b436fb3 | ||
|
|
191518d8bc | ||
|
|
50768750d6 | ||
|
|
450ef7dd56 | ||
|
|
89a4f9264a | ||
|
|
55ade15574 | ||
|
|
36b8b35e47 | ||
|
|
23bb872052 | ||
|
|
5a36af1ed8 | ||
|
|
bc12a3bd42 | ||
|
|
4e831b5f89 | ||
|
|
d886327c1a | ||
|
|
533ff8acf7 | ||
|
|
e64eaec240 | ||
|
|
02a30142b7 | ||
|
|
3937939c0a | ||
|
|
be4bfb6152 | ||
|
|
4ff1be11e2 | ||
|
|
dad4c1bf9b | ||
|
|
40aa6c518a | ||
|
|
bf8df1d396 | ||
|
|
9f575fcdec | ||
|
|
a807a09bea | ||
|
|
d064af817c | ||
|
|
7b3c4b76bc | ||
|
|
5de431f279 | ||
|
|
7b63d951f0 | ||
|
|
018887980b | ||
|
|
8b4d649bd4 | ||
|
|
441d3c886b | ||
|
|
472e32f558 | ||
|
|
472c9252f9 | ||
|
|
98a9ad3ef2 | ||
|
|
7b237fedea | ||
|
|
7281aa8ff1 | ||
|
|
d915e2f064 | ||
|
|
beb5d855b1 | ||
|
|
120b86aaae | ||
|
|
44eeff98e7 | ||
|
|
dc420a097a | ||
|
|
69ee85dea9 | ||
|
|
ebe29fec00 | ||
|
|
aee344fc2c | ||
|
|
bd451f6f65 | ||
|
|
7476b40bf1 | ||
|
|
c5cf90017e | ||
|
|
c142ce2a8e | ||
|
|
d082c47e49 | ||
|
|
44d0b96f25 | ||
|
|
cdb8bccb5a | ||
|
|
0f355dfa8f | ||
|
|
47cb8e0b04 | ||
|
|
a75784ca02 | ||
|
|
0e35ab3a52 | ||
|
|
1bb5ea8aaa | ||
|
|
83adfb8361 | ||
|
|
94e77ce2b8 | ||
|
|
d4a97f4b0c | ||
|
|
aa62338401 | ||
|
|
ef5dc90406 | ||
|
|
ee94289f22 | ||
|
|
45ba5746a6 | ||
|
|
a327ffe7e7 | ||
|
|
2feef62b65 | ||
|
|
4b1afd914e | ||
|
|
3048cabb56 | ||
|
|
3ad41adc48 | ||
|
|
e2d0d8bbae | ||
|
|
a6b803b4c3 | ||
|
|
1d68aa7fa8 | ||
|
|
d726018b15 | ||
|
|
437158e7f3 | ||
|
|
c670d0012b | ||
|
|
8751a8e496 | ||
|
|
273145feaf | ||
|
|
fe71b3081c | ||
|
|
8000ccb595 | ||
|
|
b9297a86a9 | ||
|
|
a68926bb4a | ||
|
|
f4eb427241 | ||
|
|
4c9d7ef54a | ||
|
|
359a9be900 | ||
|
|
6430f6e2df | ||
|
|
adac484f25 | ||
|
|
c1f7b030ba | ||
|
|
a363421de0 | ||
|
|
7428f9dbfb | ||
|
|
72e35a5914 | ||
|
|
6756ba44eb | ||
|
|
d8b8a5d710 | ||
|
|
5536f5f585 | ||
|
|
7367673ba4 | ||
|
|
901988ec2b | ||
|
|
b676d22f1d | ||
|
|
9b7e877a0b | ||
|
|
16d5341285 | ||
|
|
2a16ac77fb | ||
|
|
9eb76bdfbc | ||
|
|
a658ef7711 | ||
|
|
5ae750b2f1 | ||
|
|
23bbe82080 | ||
|
|
bd30c3f270 | ||
|
|
4d316034aa | ||
|
|
6b582e224c | ||
|
|
e3a366a386 | ||
|
|
f0003ff3c6 | ||
|
|
df5f8f6638 | ||
|
|
3ff26f6bb0 | ||
|
|
c8badf5689 | ||
|
|
58533bf16f | ||
|
|
6b3c3ded68 | ||
|
|
d65e00fade | ||
|
|
53e787ab7f | ||
|
|
68e5d07aef | ||
|
|
68345a181e | ||
|
|
69c121a19c | ||
|
|
780ce7416f | ||
|
|
9fe338a62a | ||
|
|
f027af66c8 | ||
|
|
6b2708b160 | ||
|
|
534474621f | ||
|
|
554614ab72 | ||
|
|
2ede69a263 | ||
|
|
ffd7a9379f | ||
|
|
9def94aceb | ||
|
|
0af30b3cc0 | ||
|
|
f3f34269d3 | ||
|
|
595c2df65c | ||
|
|
a91c2d9bc6 | ||
|
|
40f6a1db08 | ||
|
|
481c1fd2de | ||
|
|
e80fc807e0 | ||
|
|
c8e054d079 | ||
|
|
9480e76d4b | ||
|
|
538411353a | ||
|
|
acf51e1178 | ||
|
|
da6998c391 | ||
|
|
2710cdd32b | ||
|
|
60786b105c | ||
|
|
f488248abf | ||
|
|
8165d1c9d7 | ||
|
|
46dce3ac2b | ||
|
|
e24d8bac7d | ||
|
|
158d63383f | ||
|
|
465b7f8bdf | ||
|
|
c434374f74 | ||
|
|
5ba1299ddf | ||
|
|
ea39014307 | ||
|
|
830b590e5b | ||
|
|
8c58454e32 | ||
|
|
feab43ee3f | ||
|
|
15fb97c688 | ||
|
|
1d275b942d | ||
|
|
c9c93b3cf0 | ||
|
|
c40eef7b03 | ||
|
|
2bafc12502 | ||
|
|
9e5d43f44a | ||
|
|
389359dbfc | ||
|
|
390208327b | ||
|
|
5c1965f534 | ||
|
|
8553229119 | ||
|
|
96b63ff713 | ||
|
|
4903c12533 | ||
|
|
88f23530c6 | ||
|
|
c9d371fb77 | ||
|
|
b173f80703 | ||
|
|
5b6edc455f | ||
|
|
0b7d18301d | ||
|
|
9fac15b473 | ||
|
|
76ff36e514 | ||
|
|
740c14c261 | ||
|
|
ceafab93ab | ||
|
|
a8f86c38e4 | ||
|
|
46cb249016 | ||
|
|
6b799bf93b | ||
|
|
33a71f5f47 | ||
|
|
2940601f4b | ||
|
|
1f7939f49a | ||
|
|
48e273a607 | ||
|
|
e35634020d | ||
|
|
1ede053f59 | ||
|
|
93824786c7 | ||
|
|
25b8c0a728 | ||
|
|
0e4fe1b47e | ||
|
|
cd744963ff | ||
|
|
fb9d877938 | ||
|
|
087b001510 | ||
|
|
02a7c0ddca | ||
|
|
6afa1d581d | ||
|
|
5e04abe573 | ||
|
|
3422a44c87 | ||
|
|
d5fdcc6796 | ||
|
|
f12217d76c | ||
|
|
d85a914046 | ||
|
|
4fdc890cb5 | ||
|
|
3ed0bd9052 | ||
|
|
dbdc915ff2 | ||
|
|
10476c6662 | ||
|
|
74a91a7d2e | ||
|
|
72a6689cd4 | ||
|
|
05e09cb977 | ||
|
|
dab4f5acbb | ||
|
|
a20c53b23d | ||
|
|
8206f457b5 | ||
|
|
d4e6d6293d | ||
|
|
d1a2197ba4 | ||
|
|
369fb55e90 | ||
|
|
37dacff74d | ||
|
|
dc922a0299 | ||
|
|
fb08533488 | ||
|
|
d297b703c7 | ||
|
|
33f533af9b | ||
|
|
085d6402f4 | ||
|
|
f4395ef833 | ||
|
|
e7a9412124 | ||
|
|
da4ee38abd | ||
|
|
af39fac368 | ||
|
|
4f997505f3 | ||
|
|
fcbb66361d | ||
|
|
fa16321691 | ||
|
|
4ecbfe5f5b | ||
|
|
ceb3542e15 | ||
|
|
32e55d07ee | ||
|
|
e079acf91f | ||
|
|
fecedb203a | ||
|
|
6ac05a47e1 | ||
|
|
36ed80dc87 | ||
|
|
729acc4ad5 | ||
|
|
f5bbc4245f | ||
|
|
7e1463683f | ||
|
|
96fc54ff0f | ||
|
|
25b69e4f7d | ||
|
|
d871ffca6d | ||
|
|
213567bcdb | ||
|
|
b7e5c3a374 | ||
|
|
ae4a8bbf46 | ||
|
|
850386c1d4 | ||
|
|
2d4e94a636 | ||
|
|
4d559b8ec1 | ||
|
|
e447096733 | ||
|
|
cd68b95a3e | ||
|
|
2561cb3960 | ||
|
|
c4ba25bdae | ||
|
|
ebad5b389f | ||
|
|
160c49514a | ||
|
|
64430e4368 | ||
|
|
4f7fcc5634 | ||
|
|
500e5b5fe4 | ||
|
|
9e7da44cce | ||
|
|
7f2988ec57 | ||
|
|
08c2762500 | ||
|
|
509a33dc53 | ||
|
|
4e6e97d90b | ||
|
|
9d924fa4ba | ||
|
|
092e63e300 | ||
|
|
4654cd4739 | ||
|
|
f2f06d76de | ||
|
|
6bc293a58e | ||
|
|
fdb462eda4 | ||
|
|
cc579034b2 | ||
|
|
4b76e91757 | ||
|
|
53ad85cce0 | ||
|
|
8d34279ab0 | ||
|
|
7973b927a2 | ||
|
|
6997aed9bd | ||
|
|
b5d7d98098 | ||
|
|
8f22a326ab | ||
|
|
b2c3488c5e | ||
|
|
23f10eac70 | ||
|
|
cdabf3b023 | ||
|
|
8248bd7db8 | ||
|
|
cb31269a70 | ||
|
|
5b75b8718a | ||
|
|
5a256d4634 | ||
|
|
db2de3e8c4 | ||
|
|
b9dcad9549 | ||
|
|
ce104479e1 | ||
|
|
d3ae9bc891 | ||
|
|
ac90a21314 | ||
|
|
2d56eedf3e | ||
|
|
049381908c | ||
|
|
1104f4db1d | ||
|
|
0067e52155 | ||
|
|
8ed6d297a9 | ||
|
|
a06df5099a | ||
|
|
8ddd5bdbb5 | ||
|
|
5cbe00beab | ||
|
|
b8a9116ea6 | ||
|
|
40cd826260 | ||
|
|
8bd63f11b1 | ||
|
|
2198a8d6ed | ||
|
|
8023dc7aa8 | ||
|
|
0dcdbd540e | ||
|
|
226fcc279b | ||
|
|
a6ad70fc05 | ||
|
|
e2b0da8103 | ||
|
|
aea233d537 | ||
|
|
5756c090f3 | ||
|
|
5ab0acb0ef | ||
|
|
3950e5b634 | ||
|
|
7ce8f1ed97 | ||
|
|
5d53492ab5 | ||
|
|
90fd5bd382 | ||
|
|
beff060a4f | ||
|
|
bc3418734f | ||
|
|
ab255c8777 | ||
|
|
cc2bde1064 | ||
|
|
94d66a8e99 | ||
|
|
92f08b89cc | ||
|
|
02769479ea | ||
|
|
4ada322c56 | ||
|
|
7de7f6e496 | ||
|
|
0700e9e80d | ||
|
|
ccca85d1c5 | ||
|
|
60e933fca9 | ||
|
|
e2aa6034ab | ||
|
|
8b4c64e8f4 | ||
|
|
b5ea399e7e | ||
|
|
62baa6aa31 | ||
|
|
87433aa7c3 | ||
|
|
3252cab4bb | ||
|
|
fbe9b570a3 | ||
|
|
1bcb209961 | ||
|
|
b0726ad3ea | ||
|
|
e0ba27a0a9 | ||
|
|
345e45c5bf | ||
|
|
490add84d9 | ||
|
|
94fc2c4248 | ||
|
|
8a7e2db4ac | ||
|
|
71c576bbc0 | ||
|
|
2e4dff4d91 | ||
|
|
73336df814 | ||
|
|
633edb43eb | ||
|
|
29d14404e6 | ||
|
|
60b7966f2f | ||
|
|
1059351334 | ||
|
|
179ee000a3 | ||
|
|
43129427f6 | ||
|
|
7afe1e103d | ||
|
|
7c6a26ebd9 | ||
|
|
b2766c283d | ||
|
|
6b5b4ea2b1 | ||
|
|
6f7c6db918 | ||
|
|
a1ead3d1c1 | ||
|
|
1fc35ee3cd | ||
|
|
a014dcc984 | ||
|
|
6e07a0c109 | ||
|
|
e936b80dc6 | ||
|
|
60ab67213f | ||
|
|
4055a06601 | ||
|
|
e47af3d7bd | ||
|
|
c3c427742d | ||
|
|
132bee0d27 | ||
|
|
83c432d8e2 | ||
|
|
6a4930767b | ||
|
|
685e69adcc | ||
|
|
2b7f5572db | ||
|
|
e9f61dc3c0 | ||
|
|
f329470165 | ||
|
|
d7d319f1e3 | ||
|
|
cddadcb278 | ||
|
|
419833d398 | ||
|
|
9883553fb5 | ||
|
|
be5d037ca9 | ||
|
|
21f5fd919d | ||
|
|
159e8fba23 | ||
|
|
3254775eb9 | ||
|
|
d4c2eb2bbc | ||
|
|
7563eb7afe | ||
|
|
81bbafd3a1 | ||
|
|
dfe008922b | ||
|
|
d3de1c2c41 | ||
|
|
7428331504 | ||
|
|
5b818bf276 | ||
|
|
d8559276d0 | ||
|
|
3d11aba8ef | ||
|
|
c34d262d1a | ||
|
|
717c90867a | ||
|
|
90178908ba | ||
|
|
01956e92b6 | ||
|
|
b1e19294e6 | ||
|
|
1567ca475d | ||
|
|
938a572a54 | ||
|
|
e8bc07360d | ||
|
|
91989e6baf | ||
|
|
81ae503aa2 | ||
|
|
0a33fb37b9 | ||
|
|
729986d53d | ||
|
|
e19bb7c621 | ||
|
|
ea232fbaf1 | ||
|
|
6fc3c0b601 | ||
|
|
8aee612bfa | ||
|
|
5f125790cf | ||
|
|
117e107116 | ||
|
|
c8c6aea188 | ||
|
|
e1f590127e | ||
|
|
3a516df593 | ||
|
|
1b4275db4c | ||
|
|
91bc135c61 | ||
|
|
5135a21dc2 | ||
|
|
592ad39a32 | ||
|
|
1fd66a17ca | ||
|
|
3ec896d609 | ||
|
|
8f1eab57b7 |
No files matched your search
+36
-34
@@ -1,34 +1,36 @@
|
||||
nohup.out
|
||||
build/
|
||||
cmake-build-debug/
|
||||
.idea/
|
||||
.dir-locals.el
|
||||
*.pyc
|
||||
.vscode/*
|
||||
!.vscode/*.json
|
||||
.vs
|
||||
obj/
|
||||
*.csproj.user
|
||||
*.vspx
|
||||
*.psess
|
||||
msbuild.log
|
||||
*.orig
|
||||
vcxproj.user
|
||||
src/client/spark/Properties/launchSettings.json
|
||||
compile_commands.json
|
||||
.cquery_cached_index/
|
||||
src/client/sparkcmd/Properties/launchSettings.json
|
||||
node_modules
|
||||
paket-files
|
||||
project.lock.json
|
||||
src/server/sparkd/orderman/perf_analysis/data
|
||||
src/server/sparkd/orderman/perf_analysis/logs/
|
||||
src/server/sparkd/orderman/perf_analysis/trace_data/
|
||||
.fake
|
||||
*.fsx.lock
|
||||
*.code-workspace
|
||||
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
|
||||
/doc/user/public/*
|
||||
nohup.out
|
||||
build/
|
||||
cmake-build-debug/
|
||||
.idea/
|
||||
.dir-locals.el
|
||||
*.pyc
|
||||
.vscode/*
|
||||
!.vscode/*.json
|
||||
.vs
|
||||
obj/
|
||||
*.csproj.user
|
||||
*.vspx
|
||||
*.psess
|
||||
msbuild.log
|
||||
*.orig
|
||||
vcxproj.user
|
||||
src/client/spark/Properties/launchSettings.json
|
||||
compile_commands.json
|
||||
.cquery_cached_index/
|
||||
src/client/sparkcmd/Properties/launchSettings.json
|
||||
node_modules
|
||||
paket-files
|
||||
project.lock.json
|
||||
src/server/sparkd/orderman/perf_analysis/data
|
||||
src/server/sparkd/orderman/perf_analysis/logs/
|
||||
src/server/sparkd/orderman/perf_analysis/trace_data/
|
||||
.fake
|
||||
*.fsx.lock
|
||||
*.code-workspace
|
||||
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
|
||||
/doc/user/public/*
|
||||
.ionide/*
|
||||
src/web/ui/.ionide/symbolCache.db
|
||||
Vendored
+4
-4
@@ -118,7 +118,7 @@
|
||||
"program": "${workspaceRoot}/build/Debug/bin/net471/sparkcmd.exe",
|
||||
"args": [
|
||||
"test",
|
||||
"test_filter"
|
||||
"ApiUpdateTests"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"stopAtEntry": false,
|
||||
@@ -135,13 +135,13 @@
|
||||
"name": "Spreader Tests",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "/home/bts/spark/build/ninja_debug/src/server/sparkd/sparkd",
|
||||
"program": "/home/bts/spark/build/ninja_debug/server/sparkd",
|
||||
"args": [
|
||||
"test",
|
||||
"[spreader]"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "/home/bts/spark/build/ninja_debug/src/server/sparkd/",
|
||||
"cwd": "/home/bts/spark/build/ninja_debug/server/",
|
||||
"environment": [],
|
||||
"externalConsole": true,
|
||||
"MIMode": "gdb",
|
||||
@@ -249,7 +249,7 @@
|
||||
"program": "/home/bts/spark/build/ninja_debug/server/sparkd",
|
||||
"args": [
|
||||
"test",
|
||||
"SingleWriterConcurrentVector"
|
||||
"[algo]"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "/home/bts/spark/build/ninja_debug/server/",
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
+++
|
||||
title = "BTS Basic Algos"
|
||||
weight = 10
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
|
||||
|
||||
## Lean
|
||||
|
||||
The goal of the lean order is to watch the opposite-inside market size and cross when that size goes below the threshold (this also implies a cross if that size goes to zero and the market trades away from you) If the market trades toward you, you can opt into several different behaviors
|
||||
|
||||
##### Parameters
|
||||
* Cross Thresh - size thresh in absolute or ratio terms
|
||||
* Max Chase Ticks - max levels to pay up, in ticks
|
||||
|
||||
|
||||
## Join
|
||||
The goal of the join order is to watch the opposite-inside market and join when the market flips (that is, when the best offer becomes the best bid) and has at least threshold size.
|
||||
|
||||
##### Parameters
|
||||
* Join Thresh - size thresh in absolute or ratio terms
|
||||
|
||||
|
||||
## Top
|
||||
|
||||
The goal of the top order is to not trade by yourself, but the intent at the time of order entry is to get filled.If the order is on the market (say current bid price) AND the quantity on that bid price is below top thresh then cancel the order else remain working.
|
||||
|
||||
##### Parameters
|
||||
* Top Thresh - size thresh in absolute or ratio terms
|
||||
|
||||
|
||||
## Cover
|
||||
A Cover order’s purpose is to fire a hedge order (of any type) in response to fills from it’s child order on the opposite side of the market N ticks away
|
||||
|
||||
##### Parameters
|
||||
* Profit Ticks - ticks above or below fill price to place profit / cover order
|
||||
|
||||
|
||||
## OCO
|
||||
An OCO order creates a relationship between a child order and a 'stop' or 'other' order such that when one of these is cancelled, both cancel, or when one is filled, the other order's size is debited in the amount of the fill.
|
||||
|
||||
Composing this order with Cover simulates scalping functionality
|
||||
|
||||
##### Parameters
|
||||
* Stop Ticks - ticks below or above main order to place stop / other order
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,77 @@
|
||||
+++
|
||||
title = "Basic Algos"
|
||||
weight = 1
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
aliases = ["./bts_spark/Algos/"]
|
||||
+++
|
||||
|
||||
## Lean
|
||||
|
||||
The goal of the lean order is to watch the opposite-inside market size and cross when that size goes below the threshold (this also implies a cross if that size goes to zero and the market trades away from you) If the market trades toward you, you can opt into several different behaviors
|
||||
|
||||
##### Parameters
|
||||
* Cross Thresh - size thresh in absolute or ratio terms
|
||||
* Max Chase Ticks - max levels to pay up, in ticks
|
||||
|
||||
|
||||
## Join
|
||||
The goal of the join order is to watch the opposite-inside market and join when the market flips (that is, when the best offer becomes the best bid) and has at least threshold size.
|
||||
|
||||
##### Parameters
|
||||
* Join Thresh - size thresh in absolute or ratio terms
|
||||
|
||||
|
||||
## Top
|
||||
|
||||
The goal of the top order is to not trade by yourself, but the intent at the time of order entry is to get filled.If the order is on the market (say current bid price) AND the quantity on that bid price is below top thresh then cancel the order else remain working.
|
||||
|
||||
##### Parameters
|
||||
* Top Thresh - size thresh in absolute or ratio terms
|
||||
|
||||
|
||||
## Cover
|
||||
A Cover order’s purpose is to fire a hedge order (of any type) in response to fills from it’s child order on the opposite side of the market N ticks away
|
||||
|
||||
##### Parameters
|
||||
* Profit Ticks - ticks above or below fill price to place profit / cover order
|
||||
|
||||
|
||||
## OCO
|
||||
An OCO order creates a relationship between a child order and a 'stop' or 'other' order such that when one of these is cancelled, both cancel, or when one is filled, the other order's size is debited in the amount of the fill.
|
||||
|
||||
Composing this order with Cover simulates scalping functionality
|
||||
|
||||
##### Parameters
|
||||
* Stop Ticks - ticks below or above main order to place stop / other order
|
||||
|
||||
## Splitter
|
||||
The splitter is useful for pro-rata markets where you want to vary your exposure independent of the order quantity that you want to fill. The splitter will complete when the number of fills reaches or exceeds the order quantity but it will put exposed_qty total size on the market. Splitter favors order size over queue position.
|
||||
|
||||
If exposed_qty is set to 0 the splitter will continue to exist in the Order Book but will cancel all of its child orders. This makes it a useful building block for custom algos that need to implement a 'paused' state.
|
||||
|
||||
##### Parameters
|
||||
* exposed_qty - The total quantity to expose on the market.
|
||||
* max_order_qty - The maximum size of a single order.
|
||||
|
||||
|
||||
## Scratch
|
||||
The scratch algo is similar to Lean, but instead of watching the opposite inside market, it watches the same side market and pays up when this quantity recedes below the scratch_threshold
|
||||
|
||||
##### Parameters
|
||||
* scratch_threshold - The quantity on the same side market below which to scratch
|
||||
* chase_ticks - The number of ticks through the opposite market to pay up
|
||||
|
||||
|
||||
## ProRata
|
||||
The prorata algo is a special case hedging algo for pro-rata markets. Users can specify two hedge types-- normal hedge and overfill hedge, the latter is spawned when you receive more fills than your fill_limit. Further, the algo attempts to scratch when in an overfilled state AND optionally the same side size falls below stay_in_threshold
|
||||
|
||||
##### Parameters
|
||||
* hedge_ticks - The amount of ticks better hedges are placed on the opposite side market as the fill
|
||||
* chase_ticks - How many ticks through the opposite market on a scratch
|
||||
* fill_limit - The limit after which the algo considers further fills to be 'overfills'
|
||||
* stay_in_threshold - A same side size threshold that enables the algo to 'stay in' even if overfilled
|
||||
* enable_scratch_on_overfill - When true, overfills are immediately scratched upon the algo exiting
|
||||
* enable_stay_in - When true, the algo will 'stay in' so long as the same side size is above stay_in_threshold, when false the algo attempts to 'get out' (and potentially scratch overfills) as soon as it reaches overfill status
|
||||
* hedge - Child order type to send for normal hedges
|
||||
* overfill_hedge - Child order type to send for overfill hedges
|
||||
File renamed without changes.
@@ -4,6 +4,85 @@ weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.8.0
|
||||
### User Visible Changes
|
||||
- Added ProRata and Scratch algos
|
||||
- Ladder badge now grows/shrinks to fill the containing cell
|
||||
- Attached algo parameters are now displayed in the update panel
|
||||
- Fixed a bug where the update panel would show '-' values when only
|
||||
1 order was selected
|
||||
- Fixed a bug that cause cause a failure on startup if the user
|
||||
loaded a workspace with a Ladder that had a dynamic order type
|
||||
selected by default
|
||||
- Increased width of login panel to better show login error messages
|
||||
- Added optional 'Maker/Taker' pricing to Spreader algo
|
||||
- Algo buttons in ladder can now be made to deactivate after launching an algo or remain 'sticky'
|
||||
### Internal changes
|
||||
- Made it easier to generate simple UI's for new order types
|
||||
### API Changes
|
||||
- Dynamic orders can now use QuantityMeasure parameter types
|
||||
|
||||
## 2.7.2
|
||||
### User Visible Changes
|
||||
- Changed the login dialog to avoid leaking workspace names from other users.
|
||||
- Added new Splitter algo. From the manual: The splitter is useful
|
||||
for pro-rata markets where you want to vary your exposure
|
||||
independent of the order quantity that you want to fill. The
|
||||
splitter will complete when the number of fills reaches or exceeds
|
||||
the order quantity but it will put exposed_qty total size on the
|
||||
market. Splitter favors order size over queue position.
|
||||
### Internal Changes
|
||||
- Made spark more resilient to lack of connectivity to the CME web site for downloading of templates.
|
||||
|
||||
## 2.7.1
|
||||
### User Visible Changes
|
||||
- Fixed some issues with the Cover algo, where in some circumstances
|
||||
it would erroneously cancel launched hedges on completion.
|
||||
|
||||
## 2.7.0
|
||||
### User Visible Changes
|
||||
- Allowed algos to process "in-flight" fills while shutting down
|
||||
- Changed the cover algo to continue to hedge while shutting down
|
||||
### API Changes
|
||||
- Changed the API so that algos do not fully terminate until all
|
||||
their children are terminated. Algos that are in the process of
|
||||
shutting down in response to a complete or cancel call will still
|
||||
receive child updates, which they can use to calculate and report
|
||||
an accurate fill count (during this shutdown stage algos cannot
|
||||
create, cancel or update children). A new IAlgoServices method,
|
||||
is_shutting_down(), can be used to determine if an algo is
|
||||
shutting down.
|
||||
|
||||
## 2.6.2
|
||||
### User Visible Changes
|
||||
- Fixed an issue in the ladder where in some cases the mini order book could not be brought up from a badge
|
||||
- Fixed a rare race condition that could lead to erroneous limit enforcement
|
||||
- Fixed an issue with the margin limits where an error could occur if an account name with more than 12 chars was used
|
||||
|
||||
## 2.6.1
|
||||
### User Visible Changes
|
||||
- Fixed an issue in the ladder where an error could occur when trying to change ladder settings for the first time
|
||||
|
||||
## 2.6.0
|
||||
### User Visible Changes
|
||||
- In the ladder made last trade more distinctive when overlayed on the volumn column
|
||||
- In the ladder, corrected display of volume tool tip
|
||||
- In the ladder, changed the consolidation of last trade to consider only the price ignoring aggressor changes
|
||||
- Added an option for the Top Algo to delete itself if within N ticks from top of book
|
||||
- Address a rare issue that could lead to an error report on UI exit
|
||||
- Added margin risk limits
|
||||
- Added the "Offset" algo. Offset prices a child algo at a parameterizable distance from the top of the book
|
||||
- Avoided switching the current account and order type on open ladders when limits are changed
|
||||
### Internal Changes
|
||||
- Ensure that admin logins are always allowed to login, regardless of keycode status.
|
||||
|
||||
## 2.5.0
|
||||
### User Visible Changes
|
||||
- Added loss limit risk check
|
||||
- Made algos, synthetics and user names private so that multiple users can share a single system privately.
|
||||
- Allowed the mini order book defaults for column widths to be changed from the mini order book
|
||||
- Allowed the mini order book to shrink to the minimum size that will be needed to display the selected columns
|
||||
- Improved accuracy of the directional arrow when displaying last trades in the volumn column
|
||||
## 2.4.0
|
||||
### User Visible Settings
|
||||
- In the ladder, changed the last trade column to accumulate successive trades at the same price
|
||||
@@ -163,7 +242,7 @@ insert_anchor_links = "right"
|
||||
- Integrated stunnel into UI to make VPN setup unnecessary in unsecured environments.
|
||||
## 1.5.7
|
||||
### User Visible Changes
|
||||
- New videos have been added to the manual describing the [Algo Studio](./bts_spark/Trading/index.md#algo-studio) and [Spreader](./bts_spark/Spreader/index.md)
|
||||
- New videos have been added to the manual describing the [Algo Studio](./bts_spark/Trading/_index.md#algo-studio) and [Spreader](./bts_spark/Spreader/_index.md)
|
||||
- Fixed a bug that occasionally would cause windows to be position outside the visible screen on startup
|
||||
- Fixed a bug in the order book form that would occasionally cause the Recreate Order option to be unavailable
|
||||
- Added a confirmation step to Cancel All, Cancel Buys and Cancel Sells commands
|
||||
@@ -287,6 +366,7 @@ insert_anchor_links = "right"
|
||||
- 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
|
||||
@@ -391,7 +471,8 @@ insert_anchor_links = "right"
|
||||
- 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
|
||||
- These settings can be saved on a per-window basis or as a Default for newly-launched windows␍
|
||||
- These settings can be saved on a per-window basis or as a Default for newly-launched windows
|
||||
|
||||
|
||||
## 1.3.1 Through 1.3.4
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using bts.api.core;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace mbts.spark {
|
||||
[Export]
|
||||
class AdminModel : IDisposable {
|
||||
readonly string _user_name;
|
||||
public string user_name => _user_name;
|
||||
public readonly UserTuple user;
|
||||
|
||||
readonly BehaviorSubject<IReadOnlyDictionary<uint, string>>
|
||||
_accounts_subject = new BehaviorSubject<IReadOnlyDictionary<uint, string>>(new Dictionary<uint, string>()),
|
||||
@@ -24,14 +24,14 @@ namespace mbts.spark {
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
AdminModel(AdminService service, [Import("username")]string user_name) {
|
||||
_user_name = user_name;
|
||||
AdminModel(AdminService service, [Import]UserTuple user) {
|
||||
this.user = user;
|
||||
_dispose = service.subscribei_admin_data().Select(update => {
|
||||
_wrapper_subject.OnNext(new AdminModelWrapper(update.all.TryFirst(), _user_name));
|
||||
_wrapper_subject.OnNext(new AdminModelWrapper(update.all.TryFirst(), user.name));
|
||||
IEnumerable<BTSUserImmutable> active_users =
|
||||
from admin_data in update.all
|
||||
from bts_user in admin_data.Users
|
||||
where bts_user.Name == user_name && bts_user.HasUserData && bts_user.UserData.HasAccountPermissions
|
||||
where bts_user.Name == user.name && bts_user.HasUserData && bts_user.UserData.HasAccountPermissions
|
||||
select bts_user;
|
||||
|
||||
IEnumerable<BTSAccountImmutable> active_accounts =
|
||||
@@ -62,14 +62,6 @@ namespace mbts.spark {
|
||||
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToList());
|
||||
public IObservable<AdminModelWrapper> on_admin_data => _wrapper_subject.AsObservable();
|
||||
|
||||
public UserTuple? get_current_user() {
|
||||
foreach (var kv in _user_map) {
|
||||
// just for good measure, there should never be > 1 element in _user_map, and it should always match _user_name
|
||||
if (kv.Value == _user_name)
|
||||
return new UserTuple(kv.Key, kv.Value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public AccountTuple? get_default_account() {
|
||||
foreach (var kv in _account_map) {
|
||||
return new AccountTuple(kv.Key, kv.Value);
|
||||
|
||||
@@ -3,6 +3,7 @@ using mbts;
|
||||
using mbts.config;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
@@ -22,7 +23,7 @@ namespace bts.ui {
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public AboutDaemon(IBus bus, [Import("username")]string username) {
|
||||
public AboutDaemon(IBus bus, [Import(LoginContainer.Exports.UserName)]string username) {
|
||||
_user = username;
|
||||
_bus = bus;
|
||||
_dispose = bus.subscribe(SparkTags.About).OnDispatcher().Subscribe(_ => show_about());
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Reactive.Linq;
|
||||
using System.Windows.Threading;
|
||||
using System.Windows;
|
||||
using System.Threading.Tasks;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace bts.spark
|
||||
{
|
||||
@@ -20,7 +21,7 @@ namespace bts.spark
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public ErrorReporter(ErrorReportService svc, IBus bus, [Import("username")] string username) {
|
||||
public ErrorReporter(ErrorReportService svc, IBus bus, [Import(LoginContainer.Exports.UserName)] string username) {
|
||||
_svc = svc;
|
||||
_username = username;
|
||||
_dispose = bus.subscribe(SparkTags.Support).OnDispatcher().Subscribe(msg => show_support_window(msg, "Report a problem"));
|
||||
@@ -82,7 +83,6 @@ namespace bts.spark
|
||||
}
|
||||
}
|
||||
|
||||
//public static async Task<Result<T>>
|
||||
public static async Task<(T value, bool success)> safe_execute<T>(this ErrorReporter error_reporter, Func<Task<Result<T>>> execute, Func<T, bool> get_success, Func<T, string> get_error_msg, string desc) {
|
||||
try {
|
||||
var result = await execute();
|
||||
@@ -101,5 +101,31 @@ namespace bts.spark
|
||||
void _report_error(string msg) =>
|
||||
error_reporter.report_error($"Error {desc}: {msg}", AlgoStudioWindow.NAME, MessageBoxImage.Warning);
|
||||
}
|
||||
|
||||
public static async Task<TResult> safe_execute<T, TResult>(
|
||||
this ErrorReporter error_reporter,
|
||||
Func<Task<Result<T>>> execute,
|
||||
Func<T, bool> get_success,
|
||||
Func<T, string> get_error_msg,
|
||||
Func<T, bool, TResult> result_selector,
|
||||
string desc) {
|
||||
//
|
||||
try {
|
||||
var result = await execute();
|
||||
if (!result.success) {
|
||||
_report_error(result.error_message);
|
||||
} else if (!get_success(result.value)) {
|
||||
_report_error(get_error_msg(result.value));
|
||||
} else {
|
||||
return result_selector(result.value, true);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
_report_error(ex.ToString());
|
||||
}
|
||||
return result_selector(default, false);
|
||||
//
|
||||
void _report_error(string msg) =>
|
||||
error_reporter.report_error($"Error {desc}: {msg}", AlgoStudioWindow.NAME, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using bts.api.user;
|
||||
using bts.md;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace bts {
|
||||
[Export(typeof(IInitializable))]
|
||||
@@ -27,8 +28,8 @@ namespace bts {
|
||||
static uint get_naggle() => mbts.config.Configuration.get<md.RelayOptions>().naggle_ms;
|
||||
|
||||
[ImportingConstructor]
|
||||
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss) :
|
||||
base(get_naggle(), MDSourceOptions.DeferInit) {
|
||||
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss, [Import(LoginContainer.Exports.UserID)]uint user_id) :
|
||||
base(get_naggle(), MDSourceOptions.DeferInit, user_id) {
|
||||
_bus = bus;
|
||||
_token = token;
|
||||
_ss = ss;
|
||||
|
||||
@@ -3,6 +3,7 @@ using bts.metrics;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
@@ -23,7 +24,7 @@ namespace bts.perf {
|
||||
Ping _pinger = new Ping();
|
||||
|
||||
[ImportingConstructor]
|
||||
public ShellLatencyDaemon(ShellLatencyService service, [Import("username")] string user) {
|
||||
public ShellLatencyDaemon(ShellLatencyService service, [Import(LoginContainer.Exports.UserName)] string user) {
|
||||
_service = service;
|
||||
_version = Configuration.get<bts.config.System>().version;
|
||||
_username = user;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using bts.api.core;
|
||||
using bts.api.md.auxiliary;
|
||||
using bts.api.orderman;
|
||||
using bts.api.orderman.algo;
|
||||
using bts.api.persistence;
|
||||
using bts.api.position;
|
||||
using bts.api.synthetic;
|
||||
@@ -45,41 +46,11 @@ namespace Spark {
|
||||
|
||||
[Export]
|
||||
public MarketDataRelay md_replay => context.locate<MarketDataRelay>();
|
||||
//[Export]
|
||||
//public AlgoService algo_service => context.locate<AlgoService>();
|
||||
|
||||
AlgoServiceWrapper _algo_service_wrapper;
|
||||
[Export]
|
||||
public AlgoServiceWrapper algo_service_wrapper => _algo_service_wrapper ?? (_algo_service_wrapper = new AlgoServiceWrapper(context));
|
||||
public AlgoDefinitionService algo_definition_service => context.locate<AlgoDefinitionService>();
|
||||
|
||||
[Export]
|
||||
public MBOService mbo_service => context.locate<MBOService>();
|
||||
}
|
||||
|
||||
class AlgoServiceWrapper {
|
||||
public enum Mode {
|
||||
None,
|
||||
Hierarchical,
|
||||
Flat
|
||||
}
|
||||
|
||||
public AlgoServiceWrapper(ServiceContext context) {
|
||||
var flat_model_options = mbts.config.Configuration.get<bts.config.AlgoStudioFlatDefinitionModelOptions>();
|
||||
if (flat_model_options.enabled) {
|
||||
flat = context.locate<bts.api.orderman.algo.AlgoDefinitionService>();
|
||||
} else {
|
||||
hierarhical = context.locate<AlgoService>();
|
||||
}
|
||||
}
|
||||
|
||||
public AlgoService hierarhical { get; }
|
||||
public bts.api.orderman.algo.AlgoDefinitionService flat { get; }
|
||||
public Mode mode {
|
||||
get {
|
||||
if (hierarhical != null) return Mode.Hierarchical;
|
||||
if (flat != null) return Mode.Flat;
|
||||
return Mode.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,9 +40,11 @@ namespace mbts.ui {
|
||||
EditDocumentAltGeometryKey = nameof(EditDocumentAltGeometryKey),
|
||||
EditDocumentGeometryKey = nameof(EditDocumentGeometryKey),
|
||||
GearGeometryKey = nameof(GearGeometryKey),
|
||||
KeyGeometryKey = nameof(KeyGeometryKey),
|
||||
LetterAKey = nameof(LetterAKey),
|
||||
LetterDKey = nameof(LetterDKey),
|
||||
OrdersGeometryKey = nameof(OrdersGeometryKey),
|
||||
PauseGeometryKey = nameof(PauseGeometryKey),
|
||||
PaymentGeometryKey = nameof(PaymentGeometryKey),
|
||||
PencilGeometryKey = nameof(PencilGeometryKey),
|
||||
PlayGeometryKey = nameof(PlayGeometryKey),
|
||||
@@ -169,9 +171,12 @@ namespace mbts.ui {
|
||||
public static ImageSource FilterError { get { return load_resource_and_freeze("FilterError"); } }
|
||||
public static ImageSource Gear => create_icon(GearGeometryKey);
|
||||
public static ImageSource Import => load_resource_and_freeze("Import");
|
||||
public static ImageSource Key => create_icon(KeyGeometryKey);
|
||||
public static ImageSource Knight { get { return load_resource_and_freeze("Knight"); } }
|
||||
public static ImageSource Pause => create_icon(PauseGeometryKey);
|
||||
public static ImageSource Payment => create_icon(PaymentGeometryKey);
|
||||
public static ImageSource Pencil => create_icon(PencilGeometryKey);
|
||||
public static ImageSource Play => create_icon(PlayGeometryKey);
|
||||
public static ImageSource Prohibited => create_icon(ProhibitedGeometryKey);
|
||||
public static ImageSource RubberStamp => create_icon(RubberStampGeometryKey);
|
||||
public static ImageSource RedoCircular => create_icon(UndoCircularGeometryKey, Brushes.Black, null, new ScaleTransform(-1, 1));
|
||||
|
||||
@@ -274,10 +274,18 @@
|
||||
M16.000015,9.6000061C12.500012,9.6000061 9.5999844,12.5 9.5999844,16 9.5999844,19.5 12.500012,22.399994 16.000015,22.399994 19.500019,22.399994 22.400015,19.5 22.400015,16 22.400015,12.5 19.500019,9.6000061 16.000015,9.6000061z M14.199995,0L17.800005,0C18.199999,0,18.599993,0.29998779,18.599993,0.79998779L18.599993,4.7999878C19.900013,5.1000061,21.000019,5.6000061,22.099997,6.2999878L25.000023,3.3999939C25.099999,3.2999878 25.300011,3.1999817 25.500025,3.1999817 25.700007,3.1999817 25.900019,3.2999878 26.000025,3.3999939L28.600033,6C28.900021,6.2999878,28.900021,6.7999878,28.600033,7.1000061L25.700007,10C26.400019,11.100006,26.900019,12.299988,27.200007,13.5L31.20001,13.5C31.600037,13.5,31.999998,13.799988,31.999998,14.299988L31.999998,17.899994C31.999998,18.299988,31.700012,18.699982,31.20001,18.699982L27.200007,18.699982C26.900019,20,26.400019,21.100006,25.700007,22.199982L28.600033,25.100006C28.700009,25.199982 28.799984,25.399994 28.799984,25.600006 28.799984,25.799988 28.700009,26 28.600033,26.100006L26.000025,28.600006C25.900019,28.699982 25.700007,28.799988 25.500025,28.799988 25.300011,28.799988 25.099999,28.699982 25.000023,28.600006L22.099997,25.699982C21.000019,26.399994,19.800007,26.899994,18.599993,27.199982L18.599993,31.199982C18.599993,31.600006,18.300005,32,17.800005,32L14.199995,32C13.8,32,13.400006,31.699982,13.400006,31.199982L13.400006,27.199982C12.099987,26.899994,11.000011,26.399994,9.9000027,25.699982L7.0000064,28.600006C6.9000003,28.699982 6.6999881,28.799988 6.5000059,28.799988 6.2999937,28.799988 6.099981,28.699982 6.0000054,28.600006L3.3999967,26C3.0999784,25.699982,3.0999784,25.199982,3.3999967,24.899994L6.2999937,22C5.5999805,20.899994,5.0999801,19.699982,4.7999923,18.5L0.79998828,18.5C0.39999409,18.5,-1.933513E-07,18.199982,0,17.699982L0,14.100006C-1.933513E-07,13.699982,0.2999879,13.299988,0.79998828,13.299988L4.7999923,13.299988C5.0999801,12,5.5999805,10.899994,6.2999937,9.7999878L3.3999967,7C3.2999908,6.8999939 3.1999845,6.6999817 3.1999845,6.5 3.1999845,6.2999878 3.2999908,6.1000061 3.3999967,6L6.0000054,3.3999939C6.099981,3.2999878 6.2999937,3.1999817 6.5000059,3.1999817 6.6999881,3.1999817 6.9000003,3.2999878 7.0000064,3.3999939L9.9000027,6.2999878C11.000011,5.6000061,12.199993,5.1000061,13.400006,4.7999878L13.400006,0.79998779C13.400006,0.29998779,13.8,0,14.199995,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.KeyGeometryKey}">
|
||||
M22.399981,2.4000006C22.099982,2.4000006 21.899982,2.5 21.599982,2.6999998 21.199983,3.1000004 21.199983,3.8000002 21.599982,4.2000003L26.199979,8.9000001C26.599978,9.3000002 27.299977,9.3000002 27.699977,8.9000001 28.099977,8.5000002 28.099977,7.8000002 27.699977,7.4000006L23.099981,2.6999998C22.899981,2.5,22.699981,2.4000006,22.399981,2.4000006z M21.499982,0C22.299982,0,23.199981,0.30000019,23.79998,0.90000057L29.299976,6.4000001C30.599975,7.7000003,30.599975,9.7000003,29.299976,11L22.599981,17.7C21.299983,19,19.299984,19,17.999985,17.7L17.299985,17 12.699989,21.6 10.399991,20.900001 10.899991,23.400001 8.4999928,22.8 8.9999924,25.5 6.2999945,25.1 6.7999945,27.6 4.2999964,27 4.7999954,29.6 2.4999971,32 0,26.6 13.499989,13.1 16.999986,16.6 12.699989,12.3C11.399991,11,11.399991,9.0000002,12.699989,7.7000003L19.399984,1C19.899983,0.30000019,20.699983,0,21.499982,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.OrdersGeometryKey}">
|
||||
M50.14375,33.389126C50.416863,33.394688,50.653496,33.587773,50.705998,33.864617L50.015995,45.114388C49.810295,46.291462,49.130394,45.097489,49.130394,45.097489L47.65659,42.640538C42.747776,49.635197 34.633152,54.221001 25.433924,54.221001 14.884193,54.221001 5.7437552,48.204125 1.2449617,39.419205 0.80087067,38.552021 0.21358696,37.705738 0.052086769,36.736958 -0.49735315,33.490925 3.4507786,32.901036 4.8049124,34.432404 5.3674143,35.067692 6.034036,36.16407 6.5079177,37.026051 10.106778,43.799416 17.2321,48.413819 25.433924,48.413819 32.656544,48.413819 39.026364,44.826695 42.903977,39.351506L40.229367,38.854115C40.229367,38.854115,38.677464,38.647118,39.742466,37.996033L50.024895,33.398526C50.064972,33.391365,50.104733,33.388333,50.14375,33.389126z M23.030291,9.0310004L27.889918,9.0310004 27.889918,13.028299C30.539786,13.205498,32.324932,13.958096,33.464161,14.528294L33.964176,14.775793 32.417435,20.038878 31.706417,19.68068C30.579987,19.11958 28.839241,18.244583 26.149073,18.244583 24.006916,18.244583 22.723183,19.122181 22.723183,20.590876 22.723183,21.894272 23.943314,22.804369 27.551308,24.187165 32.706444,26.124659 34.911,28.702853 34.911,32.817341 34.911,36.892929 32.090527,40.05172 27.673611,40.989117L27.673611,45.540001 22.767485,45.540001 22.767485,41.330414C20.252919,41.155815,17.695551,40.424118,16.101811,39.404721L15.683999,39.137619 17.221739,33.731438 17.969059,34.180634C19.088988,34.851235 21.402748,35.97873 24.340325,35.97873 26.734886,35.97873 28.276728,34.921635 28.276728,33.291336 28.276728,31.986641 27.547409,30.853846 23.986016,29.57255 20.083815,28.144253 16.095511,26.04406 16.095511,21.239174 16.095511,17.280985 18.737379,14.291395 23.030291,13.329098z M25.268765,0C35.819447,2.7347414E-07 44.961449,6.0155495 49.458798,14.804573 49.902695,15.669173 50.490094,16.516972 50.651287,17.48427 51.198281,20.734265 47.255726,21.318962 45.901341,19.789065 45.336346,19.153567 44.669552,18.051968 44.19566,17.19517 40.5957,10.421881 33.473275,5.8072398 25.268765,5.8072393 18.047545,5.8072398 11.678013,9.3918336 7.8003562,14.867073L10.474627,15.367072C10.474627,15.367072,12.026909,15.574172,10.962921,16.223872L0.67933281,20.826762C0.36037083,20.880062,0.057029126,20.669165,0.00099972012,20.354063L0.69093342,9.1118043C0.89552094,7.9296158,1.5738537,9.124924,1.5738539,9.124924L3.0452772,11.583379C7.9566038,4.584462,16.073666,2.7347414E-07,25.268765,0z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.PauseGeometryKey}">
|
||||
M17.94,0L29.150001,0 29.150001,32 17.94,32z M0,0L11.210002,0 11.210002,32 0,32z
|
||||
</sys:String>
|
||||
|
||||
<sys:String x:Key="{x:Static mbts:Icons.PaymentGeometryKey}">
|
||||
M28.000001,6.0000026L28.000001,15 6.0010006,15 6.0010006,17.000003 30.001,17.000003 30.001,6.0000026z M14.231,2.9540056L14.979999,2.9540056 14.979999,3.987005C15.704998,4.009005,16.200997,4.178005,16.550998,4.3600049L16.248998,5.0620046C15.994998,4.9340047 15.499998,4.7010047 14.725999,4.7010047 13.796,4.7010047 13.445001,5.1900046 13.445001,5.6160046 13.445001,6.1710043 13.893,6.4470041 14.943999,6.8310037 16.187998,7.2780037 16.816998,7.8320036 16.816998,8.7800031 16.816998,9.6210027 16.151998,10.409003 14.907999,10.591002L14.907999,11.698002 14.146,11.698002 14.146,10.622003C13.445001,10.611002,12.720001,10.420002,12.297002,10.154003L12.587001,9.4400029C13.022001,9.696003 13.651,9.8980026 14.328,9.8980026 15.185999,9.8980026 15.765998,9.4620028 15.765998,8.8540034 15.765998,8.2680035 15.293999,7.9060035 14.4,7.5870035 13.167001,7.1610038 12.406001,6.6710041 12.406001,5.7440041 12.406001,4.8600047 13.119,4.189005 14.231,4.0290052z M2,2.0000002L2,13 26.000001,13 26.000001,2.0000002z M0,0L28.000001,0 28.000001,4.0000031 32.001,4.0000031 32.001,19.000003 4.0010006,19.000003 4.0010006,15 0,15z
|
||||
</sys:String>
|
||||
|
||||
@@ -5,6 +5,7 @@ using mbts.co2;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
@@ -18,7 +19,7 @@ namespace bts.shell
|
||||
class MarketShellModule : IShellModule
|
||||
{
|
||||
[ImportingConstructor]
|
||||
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model, [Import("username")]string user_name) {
|
||||
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model, [Import(LoginContainer.Exports.UserName)]string user_name) {
|
||||
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub);
|
||||
_mvf = new MarketViewWindowFactory(this, bus, settings_hub, orders_model);
|
||||
}
|
||||
|
||||
@@ -374,6 +374,72 @@
|
||||
Height="{TemplateBinding Height}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
>
|
||||
<Border.Resources>
|
||||
<!--Leg Count States-->
|
||||
<sys:String x:Key="LegCountStates">LegCountStates</sys:String>
|
||||
<sys:String x:Key="LegCountStates.TwoOrFewer">TwoOrFewerLegsState</sys:String>
|
||||
<sys:String x:Key="LegCountStates.ThreeOrMore">ThreeOrMoreLegsState</sys:String>
|
||||
<cv:ValueConverterPipe x:Key="LegCountStateConverter">
|
||||
<cv:IntGreaterThanConverter Value="2"/>
|
||||
<cv:BooleanToObjectConverter ValueForFalse="{StaticResource LegCountStates.TwoOrFewer}" ValueForTrue="{StaticResource LegCountStates.ThreeOrMore}"/>
|
||||
</cv:ValueConverterPipe>
|
||||
<!--Persistance States-->
|
||||
<sys:String x:Key="PersistanceStates">PersistanceStates</sys:String>
|
||||
<sys:String x:Key="PersistanceStates.Unpersisted">UnpersistedState</sys:String>
|
||||
<sys:String x:Key="PersistanceStates.Persisted">PersistedState</sys:String>
|
||||
<cv:BooleanToObjectConverter x:Key="PersistanceStateConverter" ValueForFalse="{StaticResource PersistanceStates.Persisted}" ValueForTrue="{StaticResource PersistanceStates.Unpersisted}"/>
|
||||
</Border.Resources>
|
||||
<u:StateManager.Groups>
|
||||
<u:StateGroupsManager AttachToMembers="True">
|
||||
<u:StateDefinitionCollection>
|
||||
<u:StateDefinition State="{Binding Group.Target.TemplatedParent.LegTuples.Count, RelativeSource={RelativeSource Self}, Converter={StaticResource LegCountStateConverter}}"/>
|
||||
<u:StateDefinition State="{Binding Group.Target.TemplatedParent.IsNew, RelativeSource={RelativeSource Self}, Converter={StaticResource PersistanceStateConverter}}"/>
|
||||
</u:StateDefinitionCollection>
|
||||
</u:StateGroupsManager>
|
||||
</u:StateManager.Groups>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="{StaticResource LegCountStates}">
|
||||
<VisualState Name="{StaticResource LegCountStates.TwoOrFewer}"/>
|
||||
<VisualState Name="{StaticResource LegCountStates.ThreeOrMore}">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_HedgePricing_Label" Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_HedgePricing_DropDown" Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup Name="{StaticResource PersistanceStates}">
|
||||
<VisualState Name="{StaticResource PersistanceStates.Unpersisted}"/>
|
||||
<VisualState Name="{StaticResource PersistanceStates.Persisted}">
|
||||
<Storyboard>
|
||||
<!--Name-->
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Name_Label" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Name_TextBox" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<!--Tick Factors-->
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_TickFactors_Label" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_TickFactors_Picker" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<!--Pricing-->
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Pricing_Label" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PART_Pricing_DropDown" Storyboard.TargetProperty="(UIElement.IsEnabled)">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False"/>
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
@@ -388,8 +454,9 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" IsEnabled="{TemplateBinding IsNew}">Name</TextBlock>
|
||||
<TextBlock x:Name="PART_Name_Label" VerticalAlignment="Center" IsEnabled="{TemplateBinding IsNew}">Name</TextBlock>
|
||||
<TextBox
|
||||
x:Name="PART_Name_TextBox"
|
||||
Grid.Column="1"
|
||||
@@ -408,20 +475,37 @@
|
||||
</u:Behavior.BindingUpdateGroups>
|
||||
</TextBox>
|
||||
<TextBlock
|
||||
Grid.Column="2" VerticalAlignment="Center" Margin="{StaticResource Margin.LHeaderPanelItem}" Text="Ticks" IsEnabled="{TemplateBinding IsNew}"
|
||||
x:Name="PART_TickFactors_Label"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Text="Ticks"
|
||||
IsEnabled="True"
|
||||
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
|
||||
/>
|
||||
<c:TickFactorsPicker
|
||||
x:Name="PART_TickFactorsPicker" Grid.Column="3" Margin="{StaticResource Margin.LHeaderPanelItem}" IsEnabled="{TemplateBinding IsNew}" HorizontalAlignment="Stretch"
|
||||
x:Name="PART_TickFactors_Picker"
|
||||
Grid.Column="3"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsEnabled="True"
|
||||
SelectedTickFactors="{Binding TickFactors, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
|
||||
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
|
||||
/>
|
||||
<TextBlock Grid.Column="4" VerticalAlignment="Center" Margin="{StaticResource Margin.LHeaderPanelItem}" IsEnabled="{TemplateBinding IsNew}" >Pricing</TextBlock>
|
||||
<TextBlock
|
||||
x:Name="PART_Pricing_Label"
|
||||
Grid.Column="4"
|
||||
VerticalAlignment="Center"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
IsEnabled="True"
|
||||
Text="Pricing"
|
||||
/>
|
||||
<ComboBox
|
||||
x:Name="PART_Pricing_DropDown"
|
||||
Grid.Column="5"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Width="90"
|
||||
IsEnabled="{TemplateBinding IsNew}"
|
||||
IsEnabled="True"
|
||||
ItemsSource="{x:Static usp:ExecutionStrategyEditor.PriceCalcItems}"
|
||||
SelectedItem="{Binding price_calc}"
|
||||
>
|
||||
@@ -431,6 +515,24 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock
|
||||
Grid.Column="6"
|
||||
x:Name="PART_HedgePricing_Label"
|
||||
VerticalAlignment="Center"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Text="Hedge"
|
||||
Visibility="Collapsed"
|
||||
/>
|
||||
<ComboBox
|
||||
x:Name="PART_HedgePricing_DropDown"
|
||||
Grid.Column="7"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
ItemsSource="{TemplateBinding HedgePricingItemsSource}"
|
||||
ItemTemplate="{StaticResource EnumDescriptionTemplate}"
|
||||
SelectedItem="{Binding HedgePricing, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Visibility="Collapsed"
|
||||
/>
|
||||
</Grid>
|
||||
<c:DataGridEx
|
||||
x:Name="PART_DataGrid"
|
||||
|
||||
@@ -8,6 +8,7 @@ using mbts.ui.utility;
|
||||
using mbts.user.settings;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
@@ -183,6 +184,29 @@ namespace mbts.ui.spark {
|
||||
public mbts.spark.ContractPickerModel ContractPickerModel { get => GetValue(ContractPickerModelProperty) as mbts.spark.ContractPickerModel; set => SetValue(ContractPickerModelProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region HedgePricingItemsSource
|
||||
public static readonly DependencyProperty HedgePricingItemsSourceProperty =
|
||||
WPFHelper.create_dp<IEnumerable, ExecutionStrategyEditor>(
|
||||
nameof(HedgePricingItemsSource));
|
||||
|
||||
public IEnumerable HedgePricingItemsSource {
|
||||
get => GetValue(HedgePricingItemsSourceProperty) as IEnumerable;
|
||||
set => SetValue(HedgePricingItemsSourceProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HedgePricing
|
||||
public static readonly DependencyProperty HedgePricingProperty =
|
||||
WPFHelper.create_dp<object, ExecutionStrategyEditor>(
|
||||
nameof(HedgePricing),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault);
|
||||
|
||||
public object HedgePricing {
|
||||
get => GetValue(HedgePricingProperty);
|
||||
set => SetValue(HedgePricingProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override void OnApplyTemplate() =>
|
||||
_connect_leg_tuples();
|
||||
|
||||
@@ -251,7 +275,7 @@ namespace mbts.ui.spark {
|
||||
|
||||
public ExecutionStrategyErrorAdorner(ExecutionStrategyEditor editor) : base(editor) {
|
||||
_name_text_box = child_property<TextBox>("PART_Name_TextBox");
|
||||
_tick_factors_picker = child_property<TickFactorsPicker>("PART_TickFactorsPicker");
|
||||
_tick_factors_picker = child_property<TickFactorsPicker>("PART_TickFactors_Picker");
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext drawing_context) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts.api;
|
||||
using bts.api.synthetic;
|
||||
using bts.spark;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
@@ -14,12 +15,14 @@ namespace mbts.spark {
|
||||
readonly IBus _bus;
|
||||
readonly SyntheticSecurityService _service;
|
||||
readonly ErrorReporter _error;
|
||||
readonly uint _user_id;
|
||||
|
||||
[ImportingConstructor]
|
||||
public SpreaderModel(IBus bus, SyntheticSecurityService service, ErrorReporter error) {
|
||||
public SpreaderModel(IBus bus, SyntheticSecurityService service, ErrorReporter error, [Import(LoginContainer.Exports.UserID)]uint user_id) {
|
||||
_bus = bus;
|
||||
_service = service;
|
||||
_error = error;
|
||||
_user_id = user_id;
|
||||
}
|
||||
|
||||
public IObservable<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>> strategy => _bus.subscribe(SparkTags.SyntheticstrategiesTag);
|
||||
@@ -39,29 +42,40 @@ namespace mbts.spark {
|
||||
return (result, success);
|
||||
});
|
||||
|
||||
public async Task<ExecutionStrategyImmutable?> lookup_strategy_async(SecurityImmutable security) =>
|
||||
await lookup_strategy_async(security.Id);
|
||||
public Task<ExecutionStrategyImmutable?> lookup_strategy_async(SecurityImmutable security) =>
|
||||
lookup_strategy_async(security.Id);
|
||||
|
||||
public async Task<ExecutionStrategyImmutable?> lookup_strategy_async(uint id) {
|
||||
var result = await _service.query_security_metadata_async(new MetadataQuery { SecurityKey = { id } });
|
||||
if (result.success) {
|
||||
var metadata = result.value.Data.FirstOrDefault(i => i.SecurityKey == id);
|
||||
if (metadata != null) {
|
||||
return metadata.Strategy;
|
||||
}
|
||||
public Task<ExecutionStrategyImmutable?> lookup_strategy_async(uint id) =>
|
||||
_error.safe_execute(
|
||||
() => _service.query_security_metadata_async(new MetadataQuery { SecurityKey = { id } }),
|
||||
mdc => mdc.Data.Count == 1 && mdc.Data[0].SecurityKey == id,
|
||||
mdc => $"did not find strategy for security id '{id}.'",
|
||||
(mdc, success) => success ? mdc.Data[0].Strategy : (ExecutionStrategyImmutable?)null,
|
||||
"lookup strategy");
|
||||
|
||||
public Task<uint?> create_synthetic(SecurityImmutable synthetic) {
|
||||
if (synthetic.ContractCase != Security.ContractOneofCase.Synthetic)
|
||||
return null;
|
||||
if (synthetic.Synthetic.UserId != _user_id) {
|
||||
var msynth = synthetic.CloneToMutable();
|
||||
msynth.Synthetic.UserId = _user_id;
|
||||
synthetic = new SecurityImmutable(msynth);
|
||||
}
|
||||
return null;
|
||||
return _error.safe_execute(
|
||||
() => _service.create_synthetic_async(synthetic),
|
||||
r => r.Error == CreateResult.Types.Error.Ok,
|
||||
r => r.Error.ToString(),
|
||||
(r, success) => success ? r.Key : (uint?)null,
|
||||
"creating 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;
|
||||
}
|
||||
|
||||
public async Task<bool> save_strategy(uint synthetic, ExecutionStrategyImmutable strategy) {
|
||||
var result = await _service.update_security_metadata_async(new Metadata { SecurityKey = synthetic, Strategy = strategy.CloneToMutable() });
|
||||
return _error.handle_error(result, "Error saving execution strategy: {0}", r => r.Error, e => e == UpdateResult.Types.Error.Ok);
|
||||
}
|
||||
public Task<bool> save_strategy(uint synthetic, ExecutionStrategyImmutable strategy) =>
|
||||
_error.safe_execute(
|
||||
() => _service.update_security_metadata_async(new Metadata { SecurityKey = synthetic, Strategy = strategy.CloneToMutable() }),
|
||||
r => r.Error == UpdateResult.Types.Error.Ok,
|
||||
r => r.Error.ToString(),
|
||||
(r, success) => success,
|
||||
"saving strategy");
|
||||
|
||||
public async Task<bool> verify_name_is_unique(string name) {
|
||||
var (result, success) = await strategy_tuple.FirstAsyncTask(SparkUtils.DefaultTimeOut);
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
LegTuples="{Binding leg_tuples}"
|
||||
RemoveLegCommand="{Binding remove_leg}"
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
HedgePricing="{Binding hedge_pricing}"
|
||||
HedgePricingItemsSource="{Binding hedge_pricing_options}"
|
||||
/>
|
||||
<c:DialogSkirtOKCancel Grid.Row="1" OKCommand="{Binding commit}" CloseOnOK="False"/>
|
||||
</Grid>
|
||||
|
||||
@@ -17,6 +17,7 @@ using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using HedgePricing = bts.spark.ExecutionStrategy.Types.HedgePricing;
|
||||
|
||||
namespace mbts.spark {
|
||||
/// <summary>
|
||||
@@ -32,6 +33,7 @@ namespace mbts.spark {
|
||||
bool _set_name_automatically, _set_ticks_automatically, _set_quantities_automatically, _set_price_mults_automatically, _is_setting_quantities, _is_setting_price_mults;
|
||||
TickFactors _tick_factors;
|
||||
ExecutionStrategyImmutable _strategy;
|
||||
HedgePricing _hedge_pricing;
|
||||
|
||||
/// <summary>
|
||||
/// Inititalizes a new instance of SyntheticEditorVM class. This constructor may only be used for already-existing synthetic securities (id must be non-zero).
|
||||
@@ -134,7 +136,7 @@ namespace mbts.spark {
|
||||
_synthetic.ContractSpec.PriceBreak = double.MaxValue;
|
||||
_synthetic.ContractSpec.MainFraction = (uint)MathUtils.factor_out(_tick_factors.tick_base, 10);
|
||||
_synthetic.ContractSpec.PriceDisplayFormat = (uint)_tick_factors.display_digits();
|
||||
uint? id = await model.create_synthetic(_synthetic);
|
||||
uint? id = await model.create_synthetic(new SecurityImmutable(_synthetic));
|
||||
if (id.HasValue && id != 0) {
|
||||
if (await model.save_strategy(id.Value, get_strategy())) {
|
||||
var (smd, success) = await SparkUtils.get_security_md_async(bus, id.Value);
|
||||
@@ -294,6 +296,9 @@ namespace mbts.spark {
|
||||
public IRaiseCanExecuteChangedCommand commit { get; }
|
||||
public ICommand remove_leg { get; }
|
||||
public ContractPickerModel contract_picker_model { get; }
|
||||
// hedge pricing
|
||||
public HedgePricing hedge_pricing { get => _hedge_pricing; set => Set(ref _hedge_pricing, value); }
|
||||
public IEnumerable<HedgePricing> hedge_pricing_options => EnumHelper.get_values<HedgePricing>();
|
||||
|
||||
public ExecutionStrategyImmutable get_strategy() {
|
||||
var mutable_strategy = _strategy.CloneToMutable();
|
||||
@@ -301,6 +306,7 @@ namespace mbts.spark {
|
||||
foreach (var lt in leg_tuples) {
|
||||
mutable_strategy.LegParamsMap.Add(lt.leg.Security.Id, lt.leg_params);
|
||||
}
|
||||
mutable_strategy.HedgePricing = hedge_pricing;
|
||||
return new ExecutionStrategyImmutable(mutable_strategy);
|
||||
}
|
||||
|
||||
@@ -308,6 +314,7 @@ namespace mbts.spark {
|
||||
_synthetic = synthetic.CloneToMutable();
|
||||
_synthetic.Exchange = Venue.Bts;
|
||||
_strategy = strategy;
|
||||
hedge_pricing = strategy.HedgePricing;
|
||||
leg_tuples.SetItems(_get_legs(_synthetic, strategy));
|
||||
RaisePropertyChanged(nameof(name), nameof(tick_factors));
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
using bts.api.core;
|
||||
using bts.collections;
|
||||
using mbts.collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
/// <summary>
|
||||
/// This class's primary purpose is to handle async AlgoService persistence calls for the AlgoStudioVM/OrderPropertiesEditorVM.
|
||||
/// When a new definition/template item is persisted for the first time, it's corresponding viewmodel (<see cref="AlgoDefinitionVM"/>, <see cref="AlgoDefinitionTemplateVM"/>)
|
||||
/// must be removed and replaced with a new one that has the brand new persisted id (<see cref="AlgoKey"/>). As part of its duty, this class makes sure that the new persisted items are inserted into the same index as their unpersisted predecessor.
|
||||
/// </summary>
|
||||
static class AlgoPersistenceHelper {
|
||||
public static (int? def_index, int? tmp_index) selection_snapshot(IList<AlgoDefinitionVM> definitions, AlgoDefinitionVM selected_definition, AlgoDefinitionTemplateVM selected_template) {
|
||||
var def = selected_definition;
|
||||
var tmp = selected_template;
|
||||
int? def_index = def == null ? (int?)null : definitions.IndexOf(def);
|
||||
int? tmp_index = tmp == null ? (int?)null : tmp.owner.templates.IndexOf(tmp);
|
||||
return (def_index, tmp_index);
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyDictionary<AlgoKey, AlgoKey>> persist_definition(OrdersModel model, IList<AlgoDefinitionVM> definitions, AlgoDefinitionVM definition) {
|
||||
int def_index = definitions.IndexOf(definition);
|
||||
return await persist(model, definitions.WrapSubRange(def_index, 1).WrapSelector(def => (def, definition.templates.AsList()), t => t.def));
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyDictionary<AlgoKey, AlgoKey>> persist_template(OrdersModel model, IList<AlgoDefinitionVM> definitions, AlgoDefinitionTemplateVM template) {
|
||||
switch (template.owner.key.mode) {
|
||||
case AlgoKey.Mode.Unpersisted: return await persist_definition(model, definitions, template.owner);
|
||||
case AlgoKey.Mode.Persisted:
|
||||
int def_index = definitions.IndexOf(template.owner);
|
||||
int tmp_index = template.owner.templates.IndexOf(template);
|
||||
return await persist(model, definitions.WrapSubRange(def_index, 1).WrapSelector(def => (def, def.templates.WrapSubRange(tmp_index, 1)), t => t.def));
|
||||
default: return DictionaryHelper.empty<AlgoKey, AlgoKey>();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyDictionary<AlgoKey, AlgoKey>> persist_all(OrdersModel model, ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey> definitions) =>
|
||||
await persist(model, definitions.WrapSelector(def => (def, def.templates.AsList()), t => t.def));
|
||||
|
||||
/// <summary>
|
||||
/// Persists each pair of definition/templates, then returns a map of non-persisted keys to persisted ones (<see cref="AlgoKey"/>, <see cref="AlgoDefinitionVM.key"/>, <see cref="AlgoDefinitionTemplateVM.key"/>).
|
||||
/// The key-map gives callers the ability to replace unpersisted items with persisted ones before the next update arrives.
|
||||
/// <para>Note: use a <see cref="mbts.utils.TableUpdateBuffer{T, TKey}"/> to suspend updates while awaiting this task; don't resume them until all unpersisted items have been replaced.
|
||||
/// <see cref="AlgoDefinitionCollection"/> caters to this pre-emptive adding behavior, in it's <see cref="TableUpdate{T, TKey}"/> callback, it checks if added items are already present, and if so, treats them as changes instead.</para>
|
||||
/// <para>PS: the reason for all this madness is so newly persisted items are inserted in the same index as their placeholder counterpart. The only way to connect the non-persisted keys with their persisted counterparts
|
||||
/// is to await each save-op individually, and build a map with the results..hence the returned key-map.</para>
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <param name="definition_templates"></param>
|
||||
/// <returns>An AlgoKey->AlgoKey map of old placeholder ids to newly persisted ones.</returns>
|
||||
public static async Task<IReadOnlyDictionary<AlgoKey, AlgoKey>> persist(OrdersModel model, IList<(AlgoDefinitionVM definition, IList<AlgoDefinitionTemplateVM> templates)> definition_templates) {
|
||||
var placeholder_to_persisted_keys = new Dictionary<AlgoKey, AlgoKey>();
|
||||
for (int i = 0; i < definition_templates.Count; i++) {
|
||||
(var def, var templates) = definition_templates[i];
|
||||
var persisted_def = def;
|
||||
if (def.is_dirty) {
|
||||
var def_record = await model.algo_model.save_definition(def.create_algo_def());
|
||||
if (def_record.HasValue) {
|
||||
switch (def.key.mode) {
|
||||
case AlgoKey.Mode.Factory:
|
||||
case AlgoKey.Mode.Unpersisted:
|
||||
persisted_def = def.to_persisted(def_record.Value);
|
||||
foreach (var tmp in templates) {
|
||||
AlgoDefinitionTemplateVM persisted_tmp;
|
||||
var tmp_record = await model.algo_model.save_template(tmp.create(def_record.Value));
|
||||
if (tmp_record.HasValue) {
|
||||
persisted_def.templates.Add(persisted_tmp = tmp.to_persisted(persisted_def, new AlgoData(tmp_record.Value.value, tmp_record.Value.name, tmp_record.Value.key)));
|
||||
} else {
|
||||
/// If the save-op fails, the unpersisted template is cloned and added to the persisted def's template items, so the user may try to save it again without having to re-enter all the templated-values.
|
||||
/// Q: "why don't we just re-add the same template to the persisted def, the save-op failed, so nothing changed...why do we need a new one?"
|
||||
/// A: "A new template vm must be created because its owner has changed (it's a readonly prop, <see cref="AlgoDefinitionTemplateVM.owner"/>)."
|
||||
persisted_def.templates.Add(persisted_tmp = new AlgoDefinitionTemplateVM(persisted_def, model, tmp.create_def_data()));
|
||||
}
|
||||
placeholder_to_persisted_keys[tmp.key] = persisted_tmp.key;
|
||||
}
|
||||
persisted_def.connect();
|
||||
if (def.key.mode == AlgoKey.Mode.Unpersisted) {
|
||||
definition_templates[i] = (persisted_def, persisted_def.templates);
|
||||
} else {
|
||||
definition_templates.Add((persisted_def, persisted_def.templates));
|
||||
}
|
||||
placeholder_to_persisted_keys[def.key] = persisted_def.key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// so the def isn't dirty, but it still might have dirty templates
|
||||
using (persisted_def.buffer_updates()) {
|
||||
for (int j = 0; j < templates.Count; j++) {
|
||||
var tmp = templates[j];
|
||||
if (!tmp.is_dirty)
|
||||
continue;
|
||||
var tmp_record = await model.algo_model.save_template(tmp.create());
|
||||
if (tmp_record.HasValue) {
|
||||
switch (tmp.key.mode) {
|
||||
case AlgoKey.Mode.Unpersisted:
|
||||
var persisted_tmp = tmp.to_persisted(persisted_def, new AlgoData(tmp_record.Value.value, tmp_record.Value.name, tmp_record.Value.key));
|
||||
templates[j] = persisted_tmp;
|
||||
placeholder_to_persisted_keys[tmp.key] = persisted_tmp.key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return placeholder_to_persisted_keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,11 @@
|
||||
xmlns:uag="clr-namespace:mbts.ui.spark.algo"
|
||||
xmlns:uspcv="clr-namespace:mbts.ui.spark.converters"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
u:StateManager.VisualState="{Binding content_source.algo_root, Converter={x:Static usp:AlgoStudioView.EditPaneVisualStateConverter}}"
|
||||
u:StateManager.VisualState="{Binding selected_definition.algo_root, Converter={x:Static usp:AlgoStudioView.EditPaneVisualStateConverter}}"
|
||||
Background="{StaticResource CommonBackground}"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/view/Controls/Order/AlgoTemplateTreeView.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!--Primitive Constants-->
|
||||
<sys:Double x:Key="HorizontalPush1">4</sys:Double>
|
||||
<sys:Double x:Key="HorizontalPush2">8</sys:Double>
|
||||
@@ -33,9 +30,6 @@
|
||||
<Thickness x:Key="Margin.TP1" Top="{StaticResource VerticalPush1}"/>
|
||||
<Thickness x:Key="Margin.TP2" Top="{StaticResource VerticalPush2}"/>
|
||||
<!--Converters-->
|
||||
<cv:ValueMap x:Key="AlgoTemplateFavoriteToggleButtonToolTipConverter" Default="Add to Favorites">
|
||||
<cv:KeyValue Key="{u:True}" Value="Remove from Favorites"/>
|
||||
</cv:ValueMap>
|
||||
<sys:String x:Key="AlgoAbbreviationActiveState">AbbreviationActive</sys:String>
|
||||
<sys:String x:Key="AlgoAbbreviationInactiveState">AbbreviationInactive</sys:String>
|
||||
<cv:ValueMap x:Key="AlgoAbbreviationVisualStateConverter" Default="{StaticResource AlgoAbbreviationActiveState}">
|
||||
@@ -130,70 +124,8 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="AlgoTemplateFavoriteToggleButtonStyle" TargetType="ToggleButton">
|
||||
<Setter Property="Background" Value="LightGray"/>
|
||||
<Setter Property="BorderBrush" Value="Black"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="IsThreeState" Value="False"/>
|
||||
<Setter Property="IsChecked" Value="{Binding is_favorite}"/>
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="UseLayoutRounding" Value="True"/>
|
||||
<Setter Property="Width" Value="16"/>
|
||||
<Setter Property="Height" Value="16"/>
|
||||
<Setter Property="ToolTip" Value="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static uag:AlgoTemplateToolBar.FavoriteToggleButtonToolTipConverter}}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Grid UseLayoutRounding="{TemplateBinding UseLayoutRounding}">
|
||||
<Ellipse x:Name="ellipse" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="{TemplateBinding Background}" Width="0" Height="0"/>
|
||||
<Image SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" UseLayoutRounding="{TemplateBinding UseLayoutRounding}" MaxHeight="13" MaxWidth="13">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<GeometryDrawing Geometry="{Binding Source={StaticResource ResourceKey={x:Static u:Icons.StarGeometryKey}}}" Brush="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}" >
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen
|
||||
Brush="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Thickness="{Binding BorderThickness.Left, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
/>
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Foreground" Value="Gold"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="ellipse" Storyboard.TargetProperty="Width" To="16" Duration="0:0:0.03" />
|
||||
<DoubleAnimation Storyboard.TargetName="ellipse" Storyboard.TargetProperty="Height" To="16" Duration="0:0:0.03" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="ellipse" Storyboard.TargetProperty="Width" To="0" Duration="0:0:0.0" />
|
||||
<DoubleAnimation Storyboard.TargetName="ellipse" Storyboard.TargetProperty="Height" To="0" Duration="0:0:0.0" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="uag:AlgoPickerNodeView" BasedOn="{StaticResource {x:Type uag:AlgoPickerNodeView}}">
|
||||
<Setter Property="DropDownStyleKey" Value="{Binding DataContext.content_source, RelativeSource={RelativeSource AncestorType=usp:AlgoStudioView}, Converter={x:Static usp:AlgoContentTemplateSelector.AlgoDropDownStyleKeyConverter}}"/>
|
||||
<Setter Property="DropDownStyleKey" Value="{x:Static usp:AlgoContentTemplateSelector.DefinitionDropDownStyleKey}"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
@@ -221,94 +153,26 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TreeView
|
||||
<ListBox
|
||||
x:Name="_tree_view"
|
||||
u:TreeViewEx.SelectedItemChangedHandler="{Binding}"
|
||||
ItemsSource="{Binding definitions_view}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
MinWidth="180"
|
||||
Background="Transparent"
|
||||
Style="{StaticResource AlgoTemplateTreeViewStyle}"
|
||||
SelectedItem="{Binding selected_definition}"
|
||||
>
|
||||
<TreeView.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:EditableCollectionViewEx.AddNewItemCommand}" Executed="{x:Static u:EditableCollectionViewEx.AddNewItemExecuted}" CanExecute="{x:Static u:EditableCollectionViewEx.AddNewItemCanExecute}"/>
|
||||
<CommandBinding Command="{x:Static u:EditableCollectionViewEx.CommitAnyCommand}" Executed="{x:Static u:EditableCollectionViewEx.CommitAnyExecuted}"/>
|
||||
</TreeView.CommandBindings>
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type sp:AlgoDefinitionVM}" ItemsSource="{Binding templates_view}">
|
||||
<Grid u:StateManager.VisualStateCollection="{Binding tree_view_visual_states}">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="{x:Static GroupName, MemberType={u:Nested usp:AlgoStudioView.VisualStateNames.TreeViewBadge}}">
|
||||
<VisualState Name="{x:Static Active, MemberType={u:Nested usp:AlgoStudioView.VisualStateNames.TreeViewBadge}}"/>
|
||||
<VisualState Name="{x:Static Inactive, MemberType={u:Nested usp:AlgoStudioView.VisualStateNames.TreeViewBadge}}">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="badge" Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle
|
||||
x:Name="badge"
|
||||
Fill="{Binding badge_fill}"
|
||||
Style="{StaticResource RectangleBadgeStyle}"
|
||||
Visibility="{DynamicResource ResourceKey={x:Static usp:AlgoStudioView.BadgesInTreeViewVisibilityKey}}"
|
||||
/>
|
||||
<TextBlock x:Name="text_view" Grid.Column="1" Margin="{StaticResource Margin.LP1}" Text="{Binding name}"/>
|
||||
</Grid>
|
||||
<HierarchicalDataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding is_dirty}" Value="True">
|
||||
<Setter TargetName="text_view" Property="Text" Value="{Binding name, StringFormat='{}{0}*'}"/>
|
||||
</DataTrigger>
|
||||
</HierarchicalDataTemplate.Triggers>
|
||||
</HierarchicalDataTemplate>
|
||||
<DataTemplate DataType="{x:Type sp:AlgoDefinitionTemplateVM}">
|
||||
<Grid u:StateManager.VisualStateCollection="{Binding tree_view_visual_states}" Margin="-6,0,0,0">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup Name="{x:Static GroupName, MemberType={u:Nested usp:AlgoStudioView.VisualStateNames.FavoriteToggleButton}}">
|
||||
<VisualState Name="{x:Static Active, MemberType={u:Nested usp:AlgoStudioView.VisualStateNames.FavoriteToggleButton}}"/>
|
||||
<VisualState Name="{x:Static Inactive, MemberType={u:Nested usp:AlgoStudioView.VisualStateNames.FavoriteToggleButton}}">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="favorite_toggle" Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ToggleButton x:Name="favorite_toggle" Grid.Column="1" Margin="4,0,0,0" Style="{StaticResource AlgoTemplateFavoriteToggleButtonStyle}"/>
|
||||
</Grid>
|
||||
<TextBlock x:Name="text_view" Grid.Column="1" Margin="{StaticResource Margin.LP1}" Text="{Binding name}" ToolTip="Double click to set as default"/>
|
||||
</Grid>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type sp:AlgoDefinitionVM}">
|
||||
<TextBlock x:Name="text_view" Grid.Column="1" Margin="{StaticResource Margin.LP1}" Text="{Binding name}"/>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Value="{u:True}">
|
||||
<DataTrigger.Binding>
|
||||
<MultiBinding Converter="{StaticResource OrConverter}">
|
||||
<Binding Path="is_dirty"/>
|
||||
<Binding Path="is_persisted" Converter="{StaticResource NotConverter}"/>
|
||||
</MultiBinding>
|
||||
</DataTrigger.Binding>
|
||||
<DataTrigger Binding="{Binding is_dirty}" Value="True">
|
||||
<Setter TargetName="text_view" Property="Text" Value="{Binding name, StringFormat='{}{0}*'}"/>
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</TreeView.Resources>
|
||||
</TreeView>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<ScrollViewer Grid.Column="1" Grid.IsSharedSizeScope="True" Margin="8,4">
|
||||
<Grid x:Name="edit_pane">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -430,7 +294,7 @@
|
||||
Margin="{StaticResource Margin.LP1}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding content_source.name, UpdateSourceTrigger=PropertyChanged, Delay=25}"
|
||||
Text="{Binding selected_definition.name, UpdateSourceTrigger=PropertyChanged, Delay=25}"
|
||||
/>
|
||||
<Grid x:Name="abbreviation_root" Grid.Column="2">
|
||||
<Grid.ToolTip>
|
||||
@@ -452,7 +316,7 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="abbreviation_label" FontStyle="Italic" Margin="{StaticResource Margin.LP1}" VerticalAlignment="Center">Abbrev.</TextBlock>
|
||||
<TextBox Grid.Column="1" Margin="{StaticResource Margin.LP1}" Text="{Binding content_source.abbreviation, UpdateSourceTrigger=PropertyChanged, Delay=25}"/>
|
||||
<TextBox Grid.Column="1" Margin="{StaticResource Margin.LP1}" Text="{Binding selected_definition.abbreviation, UpdateSourceTrigger=PropertyChanged, Delay=25}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid x:Name="badge_selector_radio_button_root" Grid.Row="1" Margin="{StaticResource Margin.TP2}">
|
||||
@@ -478,8 +342,8 @@
|
||||
<TextBlock>Hint: the default color is that of the algo-root (</TextBlock>
|
||||
<usp:OrderTypeItem
|
||||
Margin="2,0"
|
||||
BadgeFill="{Binding content_source.algo_root.meta.Color, Converter={StaticResource ColorToBrushConverter}}"
|
||||
DisplayName="{Binding content_source.algo_root.display_name}"
|
||||
BadgeFill="{Binding selected_definition.algo_root.meta.Color, Converter={StaticResource ColorToBrushConverter}}"
|
||||
DisplayName="{Binding selected_definition.algo_root.display_name}"
|
||||
/>
|
||||
<TextBlock>).</TextBlock>
|
||||
</StackPanel>
|
||||
@@ -496,7 +360,7 @@
|
||||
x:Name="default_badge"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin.LP1}"
|
||||
Fill="{Binding content_source.algo_root.meta.Color, Converter={StaticResource ColorToBrushConverter}}"
|
||||
Fill="{Binding selected_definition.algo_root.meta.Color, Converter={StaticResource ColorToBrushConverter}}"
|
||||
Style="{StaticResource RectangleBadgeStyle}"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -506,7 +370,7 @@
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="{StaticResource Margin.LP2}"
|
||||
IsChecked="{Binding content_source.custom_badge_color.enabled}"
|
||||
IsChecked="{Binding selected_definition.custom_badge_color.enabled}"
|
||||
GroupName="Badge"
|
||||
>
|
||||
<RadioButton.ToolTip>
|
||||
@@ -526,7 +390,7 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="3,0"
|
||||
Fill="{Binding content_source.custom_badge_color.value, Converter={StaticResource ColorToBrushConverter}}"
|
||||
Fill="{Binding selected_definition.custom_badge_color.value, Converter={StaticResource ColorToBrushConverter}}"
|
||||
Style="{StaticResource RectangleBadgeStyle}"
|
||||
/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" FontStyle="Italic">to open the color picker popup.</TextBlock>
|
||||
@@ -541,7 +405,7 @@
|
||||
<Button
|
||||
x:Name="custom_badge_color_picker_button"
|
||||
Grid.Column="1"
|
||||
Background="{Binding content_source.custom_badge_color.value, Converter={StaticResource ColorToBrushConverter}}"
|
||||
Background="{Binding selected_definition.custom_badge_color.value, Converter={StaticResource ColorToBrushConverter}}"
|
||||
Margin="{StaticResource Margin.LP1}"
|
||||
Style="{StaticResource CustomBadgeColorPickerButtonStyle}"
|
||||
/>
|
||||
@@ -549,8 +413,8 @@
|
||||
<Border Style="{StaticResource CustomBadgeColorPickerPopupRootStyle}">
|
||||
<cp:ColorPickerControl
|
||||
Background="Transparent"
|
||||
Palettes="{Binding DataContext.content_source.custom_badge_color_palettes, RelativeSource={RelativeSource AncestorType=Popup}}"
|
||||
SelectedColor="{Binding DataContext.content_source.custom_badge_color.value, RelativeSource={RelativeSource AncestorType=Popup}, Mode=TwoWay}"
|
||||
Palettes="{Binding DataContext.selected_definition.custom_badge_color_palettes, RelativeSource={RelativeSource AncestorType=Popup}}"
|
||||
SelectedColor="{Binding DataContext.selected_definition.custom_badge_color.value, RelativeSource={RelativeSource AncestorType=Popup}, Mode=TwoWay}"
|
||||
/>
|
||||
</Border>
|
||||
</Popup>
|
||||
@@ -559,7 +423,7 @@
|
||||
</Grid>
|
||||
<usp:AlgoEditorView
|
||||
Grid.Row="2"
|
||||
DataContext="{Binding content_source}"
|
||||
DataContext="{Binding selected_definition}"
|
||||
Background="Transparent"
|
||||
BorderBrush="{Binding algo_root, Converter={StaticResource OrderTypeBrushConverter}}"
|
||||
Margin="0,6,0,0"
|
||||
@@ -569,10 +433,10 @@
|
||||
<StackPanel.Margin>
|
||||
<Thickness Left="{StaticResource DialogSkirtPadding.Left}" Top="{StaticResource VerticalPush2}" Right="{StaticResource DialogSkirtButtonPadding.Right}" Bottom="{StaticResource DialogSkirtPadding.Bottom}"/>
|
||||
</StackPanel.Margin>
|
||||
<Button ToolTipService.ShowOnDisabled="True" Margin="{StaticResource DialogSkirtButtonMargin}" Command="{Binding save_cmd}" CommandParameter="{Binding content_source}" Width="100" ToolTip="{Binding save_cmd_tooltip}" IsDefault="True">
|
||||
<Button ToolTipService.ShowOnDisabled="True" Margin="{StaticResource DialogSkirtButtonMargin}" Command="{Binding save_cmd}" CommandParameter="{Binding selected_definition}" Width="100" ToolTip="{Binding save_cmd_tooltip}" IsDefault="True">
|
||||
<TextBlock Text="{Binding selected_definition.name, StringFormat='{}Save {0}'}"/>
|
||||
</Button>
|
||||
<Button ToolTipService.ShowOnDisabled="True" Margin="{StaticResource DialogSkirtButtonMargin}" Command="{Binding revert_cmd}" CommandParameter="{Binding content_source}" Width="100" ToolTip="{Binding revert_cmd_tooltip}">
|
||||
<Button ToolTipService.ShowOnDisabled="True" Margin="{StaticResource DialogSkirtButtonMargin}" Command="{Binding revert_cmd}" CommandParameter="{Binding selected_definition}" Width="100" ToolTip="{Binding revert_cmd_tooltip}">
|
||||
<TextBlock Text="{Binding selected_definition.name, StringFormat='{}Revert {0}'}"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
@@ -580,4 +444,4 @@
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</AdornerDecorator>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
@@ -1,43 +1,24 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.converters;
|
||||
using System;
|
||||
using mbts.ui.extensions;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
partial class AlgoStudioView : UserControl {
|
||||
public const string BadgesInTreeViewVisibilityKey = nameof(BadgesInTreeViewVisibilityKey);
|
||||
public static readonly IValueConverter EditPaneVisualStateConverter =
|
||||
ValueConverter.create(obj => obj == null ? VisualStates.Inactive : VisualStates.Active);
|
||||
|
||||
IAlgoStudioViewController _controller;
|
||||
|
||||
public AlgoStudioView() {
|
||||
InitializeComponent();
|
||||
DataContextChanged += (s, e) => set_controller();
|
||||
set_controller();
|
||||
// local methods
|
||||
void set_controller() => Disposable.replace(ref _controller, AlgoStudioViewControllerFactory.create(DataContext, _tree_view, name_text_box, custom_badge_color_picker_button, custom_badge_color_picker_popup));
|
||||
PopupViewController.attach(custom_badge_color_picker_popup, custom_badge_color_picker_button);
|
||||
this.ConnectDataContext<AlgoStudioVM>(vm => vm.request_view_focus_name += focus_name, vm => vm.request_view_focus_name -= focus_name);
|
||||
void focus_name(AlgoDefinitionVM target) =>
|
||||
Dispatcher.InvokeAsync(() => Keyboard.Focus(name_text_box));
|
||||
}
|
||||
|
||||
public new AlgoStudioVM DataContext {
|
||||
get => base.DataContext as AlgoStudioVM;
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
|
||||
public void add_definition() => _controller.add_definition();
|
||||
public void add_definition_clone(AlgoDefinitionVM src_definition) => _controller.add_definition_clone(src_definition);
|
||||
public void add_template(AlgoDefinitionVM definition) => _controller.add_template(definition);
|
||||
public void rename_definition(AlgoDefinitionVM definition) => _controller.rename_definition(definition);
|
||||
public void rename_template(AlgoDefinitionTemplateVM template) => _controller.rename_template(template);
|
||||
public void select_definition(AlgoDefinitionVM definition) => _controller.select_definition(definition);
|
||||
public void select_template(AlgoDefinitionTemplateVM template) => _controller.select_template(template);
|
||||
public IDisposable preserve_selected_definition() => _controller.preserve_selected_definition();
|
||||
|
||||
void _end_edit_executed(object s, ExecutedRoutedEventArgs e) =>
|
||||
_controller.end_edit(e);
|
||||
|
||||
public static class VisualStateNames {
|
||||
public static class EditPane {
|
||||
public const string GroupName = nameof(EditPane);
|
||||
@@ -75,14 +56,6 @@ namespace mbts.ui.spark {
|
||||
Inactive = $"{GroupName}{VisualStates.Inactive}";
|
||||
}
|
||||
|
||||
public static class TreeViewBadge {
|
||||
public const string GroupName = nameof(TreeViewBadge);
|
||||
|
||||
public static readonly string
|
||||
Active = $"{GroupName}{VisualStates.Active}",
|
||||
Inactive = $"{GroupName}{VisualStates.Inactive}";
|
||||
}
|
||||
|
||||
public static class FavoriteToggleButton {
|
||||
public const string GroupName = nameof(FavoriteToggleButton);
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
interface IAlgoStudioViewController : IDisposable {
|
||||
void add_definition();
|
||||
void add_definition_clone(AlgoDefinitionVM src_definition);
|
||||
void add_template(AlgoDefinitionVM definition);
|
||||
void rename_definition(AlgoDefinitionVM definition);
|
||||
void rename_template(AlgoDefinitionTemplateVM template);
|
||||
void end_edit(ExecutedRoutedEventArgs e);
|
||||
void select_definition(AlgoDefinitionVM definition);
|
||||
void select_template(AlgoDefinitionTemplateVM template);
|
||||
IDisposable preserve_selected_definition();
|
||||
}
|
||||
|
||||
class AlgoStudioViewControllerFactory {
|
||||
static readonly IAlgoStudioViewController _empty = new EmptyController();
|
||||
|
||||
public static IAlgoStudioViewController create(AlgoStudioVM vm, TreeView tree_view, TextBox name_text_box, Button color_picker_button, Popup color_picker_popup) =>
|
||||
vm == null || tree_view == null || name_text_box == null || color_picker_button == null || color_picker_popup == null ?
|
||||
_empty : new Controller(vm, tree_view, name_text_box, color_picker_button, color_picker_popup);
|
||||
|
||||
/// <summary>
|
||||
/// This wrapper class is returned by <see cref="create(AlgoStudioVM, TreeView)"/> when the vm or tree_view is null.
|
||||
/// This further separates the view from the controller-logic; it doesn't even need to check for null when accessing <see cref="IAlgoStudioViewController"/> methods.
|
||||
/// </summary>
|
||||
class EmptyController : IAlgoStudioViewController {
|
||||
public void add_definition() { }
|
||||
public void add_definition_clone(AlgoDefinitionVM src_definition) { }
|
||||
public void add_template(AlgoDefinitionVM definition) { }
|
||||
public void rename_definition(AlgoDefinitionVM definition) { }
|
||||
public void rename_template(AlgoDefinitionTemplateVM template) { }
|
||||
public void end_edit(ExecutedRoutedEventArgs e) { }
|
||||
public void select_definition(AlgoDefinitionVM definition) { }
|
||||
public void select_template(AlgoDefinitionTemplateVM template) { }
|
||||
public IDisposable preserve_selected_definition() => Disposable.empty;
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
class Controller : IAlgoStudioViewController {
|
||||
readonly AlgoStudioVM _vm;
|
||||
readonly TreeView _tree_view;
|
||||
readonly TextBox _name_text_box;
|
||||
IDisposable _dispose;
|
||||
|
||||
public Controller(AlgoStudioVM vm, TreeView tree_view, TextBox name_text_box, Button color_picker_button, Popup color_picker_popup) {
|
||||
_vm = vm;
|
||||
_tree_view = tree_view;
|
||||
_name_text_box = name_text_box;
|
||||
_dispose = PopupViewController.attach(color_picker_popup, color_picker_button);
|
||||
}
|
||||
|
||||
public void add_definition() {
|
||||
_vm.add_new_definition();
|
||||
_begin_edit();
|
||||
}
|
||||
|
||||
public void add_definition_clone(AlgoDefinitionVM src_definition) {
|
||||
_vm.add_new_definition_clone(src_definition);
|
||||
_begin_edit();
|
||||
}
|
||||
|
||||
public void add_template(AlgoDefinitionVM def) {
|
||||
var new_tmp = def.template_factory();
|
||||
def.templates.Add(new_tmp);
|
||||
_begin_edit();
|
||||
}
|
||||
|
||||
public void end_edit(ExecutedRoutedEventArgs e) {
|
||||
if (e.OriginalSource is EditableTextBlock etb) {
|
||||
etb.end_edit();
|
||||
_get_container(etb.DataContext)?.SetCurrentValue(TreeViewItem.IsSelectedProperty, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void rename_definition(AlgoDefinitionVM definition) {
|
||||
if (_tree_view.ItemContainerGenerator.ContainerFromItem(definition) is TreeViewItem tvi) {
|
||||
var etb = tvi.descendants_of_type<EditableTextBlock>().FirstOrDefault();
|
||||
etb.SetCurrentValue(EditableTextBlock.IsEditingProperty, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void rename_template(AlgoDefinitionTemplateVM template) {
|
||||
if (_tree_view.ItemContainerGenerator.ContainerFromItem(template.owner) is TreeViewItem tvi) {
|
||||
if (tvi.ItemContainerGenerator.ContainerFromItem(template) is TreeViewItem ttvi) {
|
||||
var etb = ttvi.descendants_of_type<EditableTextBlock>().FirstOrDefault();
|
||||
etb.SetCurrentValue(EditableTextBlock.IsEditingProperty, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void select_definition(AlgoDefinitionVM definition) =>
|
||||
_tree_view.ItemContainerGenerator.ContainerFromItem(definition)?.SetCurrentValue(TreeViewItem.IsSelectedProperty, true);
|
||||
|
||||
public void select_template(AlgoDefinitionTemplateVM template) {
|
||||
if (_tree_view.ItemContainerGenerator.ContainerFromItem(template.owner) is TreeViewItem def_view) {
|
||||
def_view.SetCurrentValue(TreeViewItem.IsSelectedProperty, true);
|
||||
def_view.ItemContainerGenerator.ContainerFromItem(template)?.SetCurrentValue(TreeViewItem.IsSelectedProperty, true);
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable preserve_selected_definition() =>
|
||||
Disposable.Create(() => {
|
||||
var name = _vm.selected_definition.name;
|
||||
if (_vm.definitions.TryGetFirst(d => d.name == name, out AlgoDefinitionVM def))
|
||||
_tree_view.ItemContainerGenerator.ContainerFromItem(def)?.SetCurrentValue(TreeViewItem.IsSelectedProperty, true);
|
||||
});
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
|
||||
void _begin_edit() =>
|
||||
WPFHelper.push_frame_until(() => Keyboard.Focus(_name_text_box) == _name_text_box);
|
||||
|
||||
TreeViewItem _get_container(object obj) =>
|
||||
obj is AlgoDefinitionTemplateVM t ? _get_container(t) :
|
||||
obj is AlgoDefinitionVM d ? _get_container(d) : null;
|
||||
|
||||
TreeViewItem _get_container(AlgoDefinitionVM d) =>
|
||||
_tree_view.ItemContainerGenerator.ContainerFromItem(d) as TreeViewItem;
|
||||
|
||||
TreeViewItem _get_container(AlgoDefinitionTemplateVM t) =>
|
||||
_get_container(t.owner) is TreeViewItem dview ? dview.ItemContainerGenerator.ContainerFromItem(t) as TreeViewItem : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,35 +62,19 @@ namespace mbts.spark {
|
||||
_vm = new AlgoStudioVM(_model);
|
||||
_vm.load_from(_state);
|
||||
_view = new AlgoStudioView { DataContext = _vm };
|
||||
_view.Resources[AlgoStudioView.BadgesInTreeViewVisibilityKey] = _ui_configuration.show_badges_in_tree_view ? Visibility.Visible : Visibility.Collapsed;
|
||||
_resources = new CompositeDisposable();
|
||||
// observable sources
|
||||
var content_src = _vm.observe_selected().Select(_ => _vm.content_source);
|
||||
// add definition
|
||||
_commands.Add(new SparkCommandCommand("Add Algo", Icons.AddBoxes, new DelegateCommand(_view.add_definition)));
|
||||
// add definition clone
|
||||
var add_definition_clone_spark_cmd = new ObservantSparkCommandCommand<AlgoDefinitionVM>(Icons.DocumentCopy, d => $"Clone '{d?.name}'", d => d != null, _view.add_definition_clone);
|
||||
_commands.Add(add_definition_clone_spark_cmd);
|
||||
// add template
|
||||
var add_template_spark_cmd = new ObservantSparkCommandCommand<AlgoDefinitionVM>(Icons.AddFile, d => $"Add '{d?.name}' Template", d => d != null, _view.add_template) {
|
||||
show_in_toolbar = (_ui_configuration.add_template_button & bts.config.AlgoStudioOptions.AddTemplateButton.MainToolBar) == bts.config.AlgoStudioOptions.AddTemplateButton.MainToolBar,
|
||||
show_in_context_menu = (_ui_configuration.add_template_button & bts.config.AlgoStudioOptions.AddTemplateButton.DefinitionContextMenu) == bts.config.AlgoStudioOptions.AddTemplateButton.DefinitionContextMenu
|
||||
};
|
||||
_commands.Add(add_template_spark_cmd);
|
||||
_resources.Add(_vm.subscribe_definition().OnDispatcher().Subscribe(add_definition_clone_spark_cmd, add_template_spark_cmd));
|
||||
_commands.Add(new SparkCommandCommand("Add Algo", Icons.AddBoxes, _vm.add_definition_cmd));
|
||||
// clone definition
|
||||
var clone_selected_definition_spark_cmd = new SparkCommandCommand(string.Empty, Icons.DocumentCopy, _vm.clone_selected_definition_cmd);
|
||||
_commands.Add(clone_selected_definition_spark_cmd);
|
||||
_resources.Add(_vm.subscribe_definition().OnDispatcher().Subscribe(
|
||||
def => clone_selected_definition_spark_cmd.title = def == null ? $"Clone (None Selected)" : $"Clone '{def.name}'"));
|
||||
// save
|
||||
if (_ui_configuration.can_save_specific) {
|
||||
var content_is_dirty_src = _vm.observe_selected().SelectManyInSeries(t => {
|
||||
if (t.template != null) {
|
||||
return t.template.observe_is_dirty();
|
||||
} else if (t.definition != null) {
|
||||
return t.definition.subscribe_is_definition_dirty();
|
||||
} else return Observable.Return(false);
|
||||
});
|
||||
_commands.Add(new ReactiveSparkCommand(content_src.OnDispatcher().Select(c => $"Save '{c?.name}'"), Icons.DiskFile, () => {
|
||||
if (_vm.selected_template is AlgoDefinitionTemplateVM t)
|
||||
_vm.save_template(t).ignore();
|
||||
else if (_vm.selected_definition is AlgoDefinitionVM d)
|
||||
var content_is_dirty_src = _vm.subscribe_definition().SelectManyInSeries(def => def?.subscribe_is_definition_dirty() ?? Observable.Return(false));
|
||||
_commands.Add(new ReactiveSparkCommand(_vm.subscribe_definition().OnDispatcher().Select(c => $"Save '{c?.name}'"), Icons.DiskFile, () => {
|
||||
if (_vm.selected_definition is AlgoDefinitionVM d)
|
||||
_vm.save_definition(d).ignore();
|
||||
}, content_is_dirty_src));
|
||||
}
|
||||
@@ -101,48 +85,39 @@ namespace mbts.spark {
|
||||
// undo/redo
|
||||
if (_ui_configuration.can_undo_redo) {
|
||||
// undo
|
||||
var undo_cmd = new ObservantCommand(() => _vm.content_source?.try_undo());
|
||||
var undo_cmd = new ObservantCommand(() => _vm.selected_definition?.try_undo());
|
||||
var undo_spark_cmd = new SparkCommandCommand("Undo", Icons.UndoCircular, undo_cmd);
|
||||
var can_undo_source = _vm.observe_selected().SelectManyInSeries(tup => {
|
||||
IAlgoStudioContentVM content = (IAlgoStudioContentVM)tup.template ?? tup.definition;
|
||||
return content != null ?
|
||||
var can_undo_source = _vm.subscribe_definition().SelectManyInSeries(def =>
|
||||
def != null ?
|
||||
Observable.Create<bool>(o => {
|
||||
content.can_undo_changed += notify;
|
||||
def.can_undo_changed += notify;
|
||||
notify();
|
||||
return Disposable.Create(() => content.can_undo_changed -= notify);
|
||||
void notify() => o.OnNext(content.can_undo);
|
||||
return Disposable.Create(() => def.can_undo_changed -= notify);
|
||||
void notify() => o.OnNext(def.can_undo);
|
||||
}) :
|
||||
Observable.Return(false);
|
||||
});
|
||||
Observable.Return(false));
|
||||
_commands.Add(undo_spark_cmd);
|
||||
_resources.Add(can_undo_source.Subscribe(undo_cmd));
|
||||
// redo
|
||||
var redo_cmd = new ObservantCommand(() => _vm.content_source?.try_redo());
|
||||
var redo_cmd = new ObservantCommand(() => _vm.selected_definition?.try_redo());
|
||||
var redo_spark_cmd = new SparkCommandCommand("Redo", Icons.RedoCircular, redo_cmd);
|
||||
var can_redo_source = _vm.observe_selected().SelectManyInSeries(tup => {
|
||||
IAlgoStudioContentVM content = (IAlgoStudioContentVM)tup.template ?? tup.definition;
|
||||
return content != null ?
|
||||
var can_redo_source = _vm.subscribe_definition().SelectManyInSeries(def =>
|
||||
def != null ?
|
||||
Observable.Create<bool>(o => {
|
||||
content.can_redo_changed += notify;
|
||||
def.can_redo_changed += notify;
|
||||
notify();
|
||||
return Disposable.Create(() => content.can_redo_changed -= notify);
|
||||
void notify() => o.OnNext(content.can_redo);
|
||||
return Disposable.Create(() => def.can_redo_changed -= notify);
|
||||
void notify() => o.OnNext(def.can_redo);
|
||||
}) :
|
||||
Observable.Return(false);
|
||||
});
|
||||
Observable.Return(false));
|
||||
_commands.Add(redo_spark_cmd);
|
||||
_resources.Add(can_redo_source.Subscribe(redo_cmd));
|
||||
}
|
||||
// remove
|
||||
var remove_spark_cmd = new ObservantSparkCommandCommand<IAlgoStudioContentVM>(Icons.Trash, v => $"Remove '{v?.name}'", v => v != null, value => {
|
||||
if (value is AlgoDefinitionTemplateVM t) {
|
||||
_vm.remove_template(t).ignore();
|
||||
} else if (value is AlgoDefinitionVM d) {
|
||||
if (value is AlgoDefinitionVM d) {
|
||||
string prompt; MessageBoxImage icon;
|
||||
if (_model.algo_model.templates_enabled() && d.templates.Count > 0) {
|
||||
prompt = $"Deleting this definition ({d.name}) will also delete its templates ({d.templates.Count}).\n\nDo you wan to Continue?";
|
||||
icon = MessageBoxImage.Warning;
|
||||
} else if (d.key.mode == AlgoKey.Mode.Persisted) {
|
||||
if (d.key.mode == AlgoKey.Mode.Persisted) {
|
||||
prompt = $"Are you sure you want to delete '{d.name}?'";
|
||||
icon = MessageBoxImage.Question;
|
||||
} else {
|
||||
@@ -162,7 +137,7 @@ namespace mbts.spark {
|
||||
}
|
||||
});
|
||||
_commands.Add(remove_spark_cmd);
|
||||
_resources.Add(content_src.OnDispatcher().Subscribe(remove_spark_cmd));
|
||||
_resources.Add(_vm.subscribe_definition().OnDispatcher().Subscribe(remove_spark_cmd));
|
||||
}
|
||||
|
||||
void _dispose_core() {
|
||||
|
||||
@@ -1,31 +1,57 @@
|
||||
using bts.api.orderman;
|
||||
using bts.api.orderman.algo;
|
||||
using bts.utils;
|
||||
using mbts.spark.utilities;
|
||||
using System;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.spark {
|
||||
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
|
||||
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
|
||||
struct AlgoData : IEquatable<AlgoData> {
|
||||
public readonly CustomOrderDefinitionImmutable value;
|
||||
public readonly string name;
|
||||
public readonly AlgoKey key;
|
||||
|
||||
public AlgoData(CustomOrderDefinitionImmutable value)
|
||||
: this(value, value.display_name(), OrderDataHelper.get_shallow_factory_key(value)) { }
|
||||
|
||||
public AlgoData(CustomOrderDefinitionImmutable value, string name, AlgoKey key) {
|
||||
AlgoData(CustomOrderDefinitionImmutable value, string name, AlgoKey key) {
|
||||
this.value = value;
|
||||
this.name = name;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public AlgoData with(CustomOrderDefinitionImmutable? value = null, string name = null) =>
|
||||
new AlgoData(value ?? this.value, name ?? this.name, key);
|
||||
/// <summary>
|
||||
/// This is the only <see cref="AlgoData"/> factory method that allows callers to explicitly specify a name.
|
||||
/// It should only be used within the context of creating, or renaming, studio algos.
|
||||
/// </summary>
|
||||
public static AlgoData create(CustomOrderDefinitionImmutable def, string name, AlgoKey key) =>
|
||||
new AlgoData(def, name, key);
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs <see cref="AlgoData"/> from a <see cref="CustomOrderDefinitionImmutable"/>, with a special caveat in the case of <see cref="CompositeOrderDefinition"/>:
|
||||
/// a fallback/default name is used instead of the composite algo's true name, which must be obtained from the <see cref="IAlgoStudioModel.definitions"/> stream.
|
||||
/// </summary>
|
||||
public static AlgoData from_custom_def(CustomOrderDefinitionImmutable def) {
|
||||
switch (def.CustomCase) {
|
||||
case CustomOrderDefinition.CustomOneofCase.Composite:
|
||||
string name = $"({OrderDataHelper.abbreviate_algo_type(def.CustomCase)}){OrderDataHelper.abbreviate_algo_type(def.Composite.Definition.CustomCase)}";
|
||||
return new AlgoData(def.Composite.Definition, name, AlgoKey.from_composite_key(def.Composite.AlgoTemplateKey));
|
||||
case CustomOrderDefinition.CustomOneofCase.Dynamic:
|
||||
return new AlgoData(def,def.Dynamic.DynamicOrderType, AlgoKey.from_dynamic_type(def.Dynamic.DynamicOrderType));
|
||||
default:
|
||||
return new AlgoData(def, def.CustomCase.display_name(), AlgoKey.from_known_type(def.CustomCase));
|
||||
}
|
||||
}
|
||||
|
||||
public static AlgoData from_record(AlgoDefinitionRecordImmutable record) =>
|
||||
new AlgoData(record.Algo.Definition, record.Algo.Name, AlgoKey.from_composite_key(record.Key));
|
||||
|
||||
public static AlgoData create_unpersisted(CustomOrderDefinitionImmutable custom_def, string name) =>
|
||||
new AlgoData(custom_def, name, AlgoKey.create_unpersisted());
|
||||
|
||||
public bool Equals(AlgoData other) =>
|
||||
key == other.key &&
|
||||
name == other.name &&
|
||||
OrderDataHelper.EqualsHelpers.custom_order_def_compareri.Equals(value, other.value);
|
||||
OrderDataHelper.EqualsHelpers.custom_order_defi_comparer.Equals(value, other.value);
|
||||
|
||||
public override bool Equals(object obj) => EqualityComparer.equals_obj(this, obj);
|
||||
public override int GetHashCode() =>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using bts.api.orderman;
|
||||
using bts.persist.algo;
|
||||
using bts.utils;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.spark {
|
||||
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
|
||||
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
|
||||
[ProtoContract]
|
||||
struct AlgoID : IEquatable<AlgoID> {
|
||||
[ProtoMember(1)]
|
||||
@@ -47,24 +48,23 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public static AlgoID from_algo_key(AlgoKey key, IMaskIndexInt32<string> dynamic_type_index) {
|
||||
public static AlgoID from_algo_key(AlgoKey key) {
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Factory:
|
||||
return AlgoStructureSerializer.to_algo_id(key.factory.structure, dynamic_type_index);
|
||||
case AlgoKey.Mode.Persisted:
|
||||
return new AlgoID(key.persisted.id_uint);
|
||||
case AlgoKey.Mode.Factory: return new AlgoID(key.factory);
|
||||
case AlgoKey.Mode.Dynamic: return new AlgoID(key.dynamic_type);
|
||||
case AlgoKey.Mode.Persisted: return new AlgoID(key.persisted);
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key.mode, nameof(key));
|
||||
}
|
||||
return new AlgoID(CustomOrderDefinition.CustomOneofCase.None);
|
||||
}
|
||||
|
||||
public static AlgoKey to_algo_key(AlgoID id, IMaskIndexInt32<string> dynamic_type_index) {
|
||||
public static AlgoKey to_algo_key(AlgoID id) {
|
||||
if (id._factory_key.HasValue)
|
||||
return new AlgoKey(id._factory_key.Value);
|
||||
return AlgoKey.from_known_type(id._factory_key.Value);
|
||||
if (id._persisted_key.HasValue)
|
||||
return new AlgoKey(new AlgoPersistedKey(id._persisted_key.Value));
|
||||
return AlgoKey.from_composite_key(id._persisted_key.Value);
|
||||
if (id._dynamic_key != null)
|
||||
return new AlgoKey(new AlgoNameStructureKey(id._dynamic_key, AlgoStructureSerializer.structure_key(dynamic_type_index.to_int(id._dynamic_key))));
|
||||
return default;
|
||||
return AlgoKey.from_dynamic_type(id._dynamic_key);
|
||||
throw new ArgumentException("ID is empty.", nameof(id));
|
||||
}
|
||||
|
||||
public bool Equals(AlgoID other) =>
|
||||
@@ -85,55 +85,93 @@ namespace mbts.spark {
|
||||
public static bool operator !=(AlgoID a, AlgoID b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
/// TODO: Remove <see cref="AlgoKey"/>, and redirect references to <see cref="AlgoID"/>.
|
||||
struct AlgoKey : IEquatable<AlgoKey> {
|
||||
readonly AlgoPersistedKey? _persisted_key;
|
||||
readonly AlgoNameStructureKey? _factory_key;
|
||||
readonly uint? _composite_key;
|
||||
readonly Guid? _unpersisted;
|
||||
readonly string _dynamic_type;
|
||||
readonly CustomOrderDefinition.CustomOneofCase? _factory_type;
|
||||
|
||||
public AlgoKey(Guid id_guid) {
|
||||
_persisted_key = null;
|
||||
_factory_key = null;
|
||||
AlgoKey(Guid id_guid) {
|
||||
_composite_key = null;
|
||||
_unpersisted = id_guid;
|
||||
_dynamic_type = null;
|
||||
_factory_type = null;
|
||||
}
|
||||
|
||||
public AlgoKey(CustomOrderDefinition.CustomOneofCase factory_root_algo_type)
|
||||
: this(new AlgoNameStructureKey(factory_root_algo_type)) { }
|
||||
|
||||
public AlgoKey(AlgoNameStructureKey factory_key) {
|
||||
_persisted_key = null;
|
||||
_factory_key = factory_key;
|
||||
public AlgoKey(uint composite_key) {
|
||||
_composite_key = composite_key;
|
||||
_unpersisted = null;
|
||||
_dynamic_type = null;
|
||||
_factory_type = null;
|
||||
}
|
||||
|
||||
public AlgoKey(AlgoPersistedKey persisted_key) {
|
||||
_persisted_key = persisted_key;
|
||||
_factory_key = null;
|
||||
public AlgoKey(string dynamic_type) {
|
||||
_composite_key = null;
|
||||
_unpersisted = null;
|
||||
_dynamic_type = dynamic_type;
|
||||
_factory_type = null;
|
||||
}
|
||||
|
||||
public AlgoKey(CustomOrderDefinition.CustomOneofCase factory_type) {
|
||||
_composite_key = null;
|
||||
_unpersisted = null;
|
||||
_dynamic_type = null;
|
||||
_factory_type = factory_type;
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
None,
|
||||
Persisted,
|
||||
Unpersisted,
|
||||
Dynamic,
|
||||
Factory
|
||||
}
|
||||
|
||||
public bool is_persisted => mode == Mode.Persisted;
|
||||
|
||||
public AlgoPersistedKey persisted => _persisted_key.Value;
|
||||
public AlgoNameStructureKey factory => _factory_key ?? new AlgoNameStructureKey(CustomOrderDefinition.CustomOneofCase.None);
|
||||
public uint persisted => _composite_key.Value;
|
||||
public Guid unpersisted => _unpersisted.Value;
|
||||
public CustomOrderDefinition.CustomOneofCase factory => _factory_type.Value;
|
||||
public string dynamic_type => _dynamic_type;
|
||||
|
||||
public Mode mode {
|
||||
get {
|
||||
if (_persisted_key.HasValue) return Mode.Persisted;
|
||||
if (_factory_type.HasValue) return Mode.Factory;
|
||||
if (_composite_key.HasValue) return Mode.Persisted;
|
||||
if (_dynamic_type != null) return Mode.Dynamic;
|
||||
if (_unpersisted.HasValue) return Mode.Unpersisted;
|
||||
if (_factory_key.HasValue) return Mode.Factory;
|
||||
return Mode.None;
|
||||
}
|
||||
}
|
||||
|
||||
public static AlgoKey create_unpersisted() =>
|
||||
new AlgoKey(Guid.NewGuid());
|
||||
|
||||
public static AlgoKey from_custom_def(CustomOrderDefinitionImmutable custom_def) {
|
||||
switch (custom_def.CustomCase) {
|
||||
case CustomOrderDefinition.CustomOneofCase.Dynamic: return new AlgoKey(custom_def.Dynamic.DynamicOrderType);
|
||||
case CustomOrderDefinition.CustomOneofCase.Composite: return new AlgoKey(custom_def.Composite.AlgoTemplateKey);
|
||||
default: return new AlgoKey(custom_def.CustomCase);
|
||||
}
|
||||
}
|
||||
|
||||
public static AlgoKey from_known_type(CustomOrderDefinition.CustomOneofCase known_type) {
|
||||
if (known_type == CustomOrderDefinition.CustomOneofCase.Dynamic)
|
||||
throw new ArgumentException(nameof(known_type), "Potential loss of data: no dynamic type name specified.");
|
||||
if (known_type == CustomOrderDefinition.CustomOneofCase.Composite)
|
||||
throw new ArgumentException(nameof(known_type), "Potential loss of data: no composite id specified.");
|
||||
return new AlgoKey(known_type);
|
||||
}
|
||||
|
||||
public static AlgoKey from_dynamic_type(string dynamic_type) {
|
||||
if (string.IsNullOrEmpty(dynamic_type))
|
||||
throw ExceptionBuilder.Argument.null_or_empty(nameof(dynamic_type));
|
||||
return new AlgoKey(dynamic_type);
|
||||
}
|
||||
|
||||
public static AlgoKey from_composite_key(uint composite_key) =>
|
||||
new AlgoKey(composite_key);
|
||||
|
||||
public bool Equals(AlgoKey other) {
|
||||
Mode m = mode;
|
||||
if (m == other.mode) {
|
||||
@@ -141,6 +179,7 @@ namespace mbts.spark {
|
||||
case Mode.Persisted: return persisted == other.persisted;
|
||||
case Mode.Unpersisted: return unpersisted == other.unpersisted;
|
||||
case Mode.Factory: return factory == other.factory;
|
||||
case Mode.Dynamic: return dynamic_type == other.dynamic_type;
|
||||
case Mode.None: return true;
|
||||
}
|
||||
}
|
||||
@@ -155,6 +194,7 @@ namespace mbts.spark {
|
||||
case Mode.Persisted: hash = persisted.GetHashCode(); phase_mult = 2; break;
|
||||
case Mode.Unpersisted: hash = unpersisted.GetHashCode(); phase_mult = 4; break;
|
||||
case Mode.Factory: hash = factory.GetHashCode(); phase_mult = 3; break;
|
||||
case Mode.Dynamic: hash = dynamic_type.GetHashCode(); phase_mult = 5; break;
|
||||
default: return 0;
|
||||
}
|
||||
return EqualityComparer.phase_hash(hash, phase_mult);
|
||||
@@ -165,4 +205,4 @@ namespace mbts.spark {
|
||||
|
||||
InvalidOperationException _unexpected_mode_ex() => new InvalidOperationException($"Unexpected Mode '{mode}.'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ namespace bts.persist.algo {
|
||||
public bool is_persisted => mode != Mode.None;
|
||||
|
||||
public mbts.spark.AlgoKey to_algo_key() =>
|
||||
new mbts.spark.AlgoKey(this);
|
||||
new mbts.spark.AlgoKey(_id_uint ?? 0);
|
||||
|
||||
public bool Equals(AlgoPersistedKey other) {
|
||||
Mode m = mode;
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
using bts.api.orderman;
|
||||
using bts.utils;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.spark {
|
||||
struct AlgoNameStructureKey : IEquatable<AlgoNameStructureKey> {
|
||||
public readonly string name;
|
||||
public readonly AlgoStructureKey structure;
|
||||
|
||||
public AlgoNameStructureKey(CustomOrderDefinition.CustomOneofCase base_type)
|
||||
: this(base_type.type_id(), new AlgoStructureKey(base_type)) { }
|
||||
|
||||
public AlgoNameStructureKey(string name, AlgoStructureKey structure) {
|
||||
this.name = name;
|
||||
this.structure = structure;
|
||||
}
|
||||
|
||||
public bool Equals(AlgoNameStructureKey other) => name == other.name && structure == other.structure;
|
||||
public override bool Equals(object obj) => EqualityComparer.equals_obj(this, obj);
|
||||
public override int GetHashCode() => EqualityComparer.aggregate_hash(EqualityComparer.safe_get_hash_code(name), structure.GetHashCode());
|
||||
public static bool operator ==(AlgoNameStructureKey a, AlgoNameStructureKey b) => a.Equals(b);
|
||||
public static bool operator !=(AlgoNameStructureKey a, AlgoNameStructureKey b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
struct AlgoStructureKey : IEquatable<AlgoStructureKey> {
|
||||
readonly int[] _structure;
|
||||
|
||||
public AlgoStructureKey(CustomOrderDefinition.CustomOneofCase base_type)
|
||||
: this(new[] { (int)base_type }) { }
|
||||
|
||||
public AlgoStructureKey(CustomOrderDefinitionImmutable definition, IMaskIndexInt32<string> dynamic_type_index) =>
|
||||
_structure = AlgoStructureSerializer.to_array(definition, dynamic_type_index);
|
||||
|
||||
public AlgoStructureKey(int[] structure) =>
|
||||
_structure = structure;
|
||||
|
||||
public IReadOnlyList<int> structure => _structure ?? Array.Empty<int>();
|
||||
|
||||
public bool Equals(AlgoStructureKey other) {
|
||||
if (_structure == null) {
|
||||
return other._structure == null;
|
||||
} else {
|
||||
/// <see cref="Enumerable.SequenceEqual{TSource}(IEnumerable{TSource}, IEnumerable{TSource})"/> is not optimized to check Count/Length, since the args are <see cref="IEnumerable{T}"/>.
|
||||
return other._structure != null && _structure.Length == other._structure.Length && _structure.SequenceEqual(other._structure);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is AlgoStructureKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_structure == null ? 0 : EqualityComparer.aggregate_hash(_structure);
|
||||
|
||||
public static bool operator ==(AlgoStructureKey a, AlgoStructureKey b) => a.Equals(b);
|
||||
public static bool operator !=(AlgoStructureKey a, AlgoStructureKey b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
static class AlgoStructureSerializer {
|
||||
const int
|
||||
_dynamic_mask = 1 << 31,
|
||||
_child_mask_offset = 30;
|
||||
|
||||
public static CustomOrderDefinitionImmutable build_algoi(this AlgoStructureKey structure_key, DynamicOrderHelper dynamic_helper) =>
|
||||
parsei(structure_key.structure, dynamic_helper);
|
||||
|
||||
public static CustomOrderDefinitionImmutable build_algo(this AlgoStructureKey structure_key, DynamicOrderHelper dynamic_helper) =>
|
||||
parsei(structure_key.structure, dynamic_helper);
|
||||
|
||||
public static CustomOrderDefinition.CustomOneofCase order_type(this AlgoStructureKey structure_key) =>
|
||||
structure_key.structure.Count > 0 ? (CustomOrderDefinition.CustomOneofCase)structure_key.structure[0] : CustomOrderDefinition.CustomOneofCase.None;
|
||||
|
||||
public static AlgoStructureKey structure_key(int dynamic_type) =>
|
||||
new AlgoStructureKey(new[] { dynamic_type | _dynamic_mask });
|
||||
|
||||
public static AlgoStructureKey structure_key(CustomOrderDefinitionImmutable algo, IMaskIndexInt32<string> dynamic_type_index) =>
|
||||
new AlgoStructureKey(to_array(algo, dynamic_type_index));
|
||||
|
||||
public static AlgoStructureKey structure_key(CustomOrderDefinition algo, IMaskIndexInt32<string> dynamic_type_index) =>
|
||||
new AlgoStructureKey(to_array(algo, dynamic_type_index));
|
||||
|
||||
public static int[] to_array(CustomOrderDefinitionImmutable algo, IMaskIndexInt32<string> dynamic_type_index) =>
|
||||
to_array(algo.CloneToMutable(), dynamic_type_index);
|
||||
|
||||
public static int[] to_array(CustomOrderDefinition algo, IMaskIndexInt32<string> dynamic_type_index) {
|
||||
int[] s = new[] { (int)algo.CustomCase };
|
||||
_build_structure(algo, ref s, dynamic_type_index);
|
||||
return s;
|
||||
}
|
||||
|
||||
public static CustomOrderDefinitionImmutable from_array(int[] structure, DynamicOrderHelper dynamic_helper) =>
|
||||
parsei(structure, dynamic_helper);
|
||||
|
||||
public static CustomOrderDefinitionImmutable parsei(IReadOnlyList<int> structure, DynamicOrderHelper dynamic_helper) =>
|
||||
new CustomOrderDefinitionImmutable(parse(structure, dynamic_helper));
|
||||
|
||||
public static CustomOrderDefinition parse(IReadOnlyList<int> structure, DynamicOrderHelper dynamic_helper) {
|
||||
if (structure == null || structure.Count == 0)
|
||||
return new CustomOrderDefinition();
|
||||
int pos = 1;
|
||||
var mdef = OrderDataHelper.create_algo((CustomOrderDefinition.CustomOneofCase)structure[0]);
|
||||
_build_def(structure, ref pos, ref mdef, dynamic_helper);
|
||||
return mdef;
|
||||
}
|
||||
|
||||
public static AlgoID to_algo_id(AlgoStructureKey key, IMaskIndexInt32<string> dynamic_type_index) {
|
||||
if (key.structure.Count == 0)
|
||||
return new AlgoID(CustomOrderDefinition.CustomOneofCase.None);
|
||||
int i = key.structure[0];
|
||||
return BitHelper.remove_flags(ref i, _dynamic_mask) ? new AlgoID(dynamic_type_index.from_int(i)) : new AlgoID((CustomOrderDefinition.CustomOneofCase)i);
|
||||
}
|
||||
|
||||
static void _build_structure(CustomOrderDefinition algo, ref int[] structure, IMaskIndexInt32<string> dynamic_type_index) {
|
||||
if (structure == null || structure.Length == 0)
|
||||
structure = new int[1];
|
||||
structure[0] = (int)algo.CustomCase;
|
||||
int p = 1;
|
||||
_build_structure(algo, ref structure, ref p, dynamic_type_index);
|
||||
ArrayHelper.trim(ref structure, p);
|
||||
}
|
||||
|
||||
static void _build_structure(CustomOrderDefinition algo, ref int[] structure, ref int pos, IMaskIndexInt32<string> dynamic_type_index) {
|
||||
IEnumerable<(CustomOrderDefinition child_algo, int field_pos)> children;
|
||||
if (algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic) {
|
||||
ArrayHelper.ensure_capacity(ref structure, pos + 1);
|
||||
structure[pos++] = dynamic_type_index.to_int(algo.Dynamic.DynamicOrderType) | _dynamic_mask;
|
||||
children = algo.Dynamic.OrderParams.Where(kv => kv.Value.ValueCase == DynamicOrderParam.ValueOneofCase.AsOrderDef).Select((kv, i) => (kv.Value.AsOrderDef, i));
|
||||
} else {
|
||||
var cad = OrderDataHelper.get_child_algo_data(algo);
|
||||
children = cad.Select((c, i) => (c.get_child(algo), i));
|
||||
children = children.Where(c => c.child_algo != null);
|
||||
}
|
||||
foreach ((var child, int field_pos) in children) {
|
||||
ArrayHelper.ensure_capacity(ref structure, pos + 1);
|
||||
structure[pos++] = EnumMaskIndexInt32<CustomOrderDefinition.CustomOneofCase>.to_int(child.CustomCase) | _child_algo_mask(field_pos);
|
||||
_build_structure(child, ref structure, ref pos, dynamic_type_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a mask for a particular child-algo field of a particular algo definition.
|
||||
/// </summary>
|
||||
/// <param name="field_pos">The index wrt the deterministic enumeration of child algos for a particular definition (e.g., Hedge = 0, Child = 1).</param>
|
||||
static int _child_algo_mask(int field_pos) => 1 << (_child_mask_offset - field_pos);
|
||||
|
||||
static void _build_def(IReadOnlyList<int> structure, ref int pos, ref CustomOrderDefinition algo, DynamicOrderHelper dynamic_helper) {
|
||||
var local_algo = algo;
|
||||
IEnumerable<(Action<CustomOrderDefinition> assign_child, int field_pos)> children;
|
||||
if (algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic) {
|
||||
var dyn_def = algo.Dynamic;
|
||||
dyn_def.DynamicOrderType = dynamic_helper.dynamic_type_index.from_int(structure[pos++] ^ _dynamic_mask);
|
||||
dynamic_helper.add_dynamic_params(ref dyn_def);
|
||||
children = dyn_def.OrderParams
|
||||
.Where(kv => kv.Value.ValueCase == DynamicOrderParam.ValueOneofCase.AsOrderDef)
|
||||
.Select((kv, i) => (new Action<CustomOrderDefinition>(child_algo => kv.Value.AsOrderDef = child_algo), i));
|
||||
} else {
|
||||
children = OrderDataHelper.get_child_algo_data(algo)
|
||||
.Select((c, i) => (new Action<CustomOrderDefinition>(child_algo => c.assign_child(local_algo, child_algo)), i));
|
||||
}
|
||||
using (var children_en = children.GetEnumerator()) {
|
||||
while (pos < structure.Count && children_en.MoveNext()) {
|
||||
(var assign_child, int field_pos) = children_en.Current;
|
||||
int mask = _child_algo_mask(field_pos);
|
||||
if ((structure[pos] & mask) == mask) {
|
||||
var type = EnumMaskIndexInt32<CustomOrderDefinition.CustomOneofCase>.from_int(structure[pos++] ^ mask);
|
||||
var child_algo = OrderDataHelper.create_algo(type);
|
||||
_build_def(structure, ref pos, ref child_algo, dynamic_helper);
|
||||
assign_child(child_algo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-345
@@ -1,345 +0,0 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.persist.algo;
|
||||
using bts.spark;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
class AlgoStudioDefinitionTemplateModel : IAlgoStudioModel, IDisposable {
|
||||
readonly AlgoService _service;
|
||||
readonly AdminModel _admin_model;
|
||||
readonly ErrorReporter _error_reporter;
|
||||
readonly ObservableDemuxer<AlgoKey, AlgoDefinitionMetadata> _metadata_demuxer = new ObservableDemuxer<AlgoKey, AlgoDefinitionMetadata>();
|
||||
readonly TableUpdateDemuxer<AlgoTemplateData, AlgoKey, AlgoKey> _definition_templates_demuxer;
|
||||
IReadOnlyDictionary<AlgoKey, AlgoData> _algos = DictionaryHelper.empty<AlgoKey, AlgoData>();
|
||||
IReadOnlyDictionary<AlgoKey, AlgoTemplateData> _algo_templates = DictionaryHelper.empty<AlgoKey, AlgoTemplateData>();
|
||||
IDisposable _dispose;
|
||||
|
||||
public AlgoStudioDefinitionTemplateModel(AlgoService service, IAlgoDefinitionMetadataService meta, AdminModel admin_model, ErrorReporter error_reporter, bts.config.AlgoStudioOptions ui_configuration) {
|
||||
_service = service;
|
||||
metadata_service = meta;
|
||||
_admin_model = admin_model;
|
||||
_error_reporter = error_reporter;
|
||||
this.ui_configuration = ui_configuration;
|
||||
_definition_templates_demuxer = new TableUpdateDemuxer<AlgoTemplateData, AlgoKey, AlgoKey>(t => t.key, t => t.definition_key);
|
||||
definitions = _service.subscribei_algos()
|
||||
.Select(u => new Update<AlgoData, AlgoKey>(
|
||||
u.all.Select(_Utils.definition_data).ToList(),
|
||||
u.added.Select(_Utils.definition_data).ToList(),
|
||||
u.changes.Select(ch => new Change<AlgoData>(_Utils.definition_data(ch.Old), _Utils.definition_data(ch.New))).ToList(),
|
||||
u.removed.Select(d => _Utils.definition_data(d)).ToList(),
|
||||
def => def.key))
|
||||
.Table()
|
||||
.Cache();
|
||||
templates = _service.subscribei_algo_templates()
|
||||
.Select(u => new Update<AlgoTemplateData, AlgoKey>(
|
||||
u.all.Select(_Utils.template_data).ToList(),
|
||||
u.added.Select(_Utils.template_data).ToList(),
|
||||
u.changes.Select(ch => new Change<AlgoTemplateData>(_Utils.template_data(ch.Old), _Utils.template_data(ch.New))).ToList(),
|
||||
u.removed.Select(_Utils.template_data).ToList(),
|
||||
def => def.key))
|
||||
.Table()
|
||||
.Cache();
|
||||
_dispose = new CompositeDisposable(
|
||||
definitions.OnDispatcher().Subscribe(t => _algos = t.state),
|
||||
templates.OnDispatcher().Subscribe(t => _algo_templates = t.state),
|
||||
templates.Subscribe(_definition_templates_demuxer),
|
||||
metadata_service.subscribe_metadata().Subscribe(_metadata_demuxer));
|
||||
}
|
||||
|
||||
public IObservable<TableUpdate<AlgoData, AlgoKey>> definitions { get; }
|
||||
public IObservable<TableUpdate<AlgoTemplateData, AlgoKey>> templates { get; }
|
||||
public IAlgoDefinitionMetadataService metadata_service { get; }
|
||||
public bts.config.AlgoStudioOptions ui_configuration { get; }
|
||||
|
||||
public IObservable<TableUpdate<AlgoTemplateData, AlgoKey>> subscribe_definition_templates(AlgoKey definition) =>
|
||||
_definition_templates_demuxer.subscribe(definition);
|
||||
|
||||
public async Task<AlgoData?> save_definition(AlgoData definition_data) {
|
||||
if (definition_data.key.is_persisted) {
|
||||
// TODO: implement update op when backend ready
|
||||
} else {
|
||||
// create op
|
||||
return await _create_definition(new CompositeAlgoDefinitionImmutable(_Utils.to_definition(definition_data)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async Task<AlgoTemplateData?> save_template(AlgoTemplateData template_data) {
|
||||
if (template_data.key.is_persisted) {
|
||||
// update op
|
||||
return await _update_template(_Utils.to_recordi(template_data));
|
||||
} else {
|
||||
// create op
|
||||
return await _create_template(_Utils.to_templatei(template_data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The api is not used here (there are some unresolved dependency issues with definitions on the backend).
|
||||
/// The definition is marked as hidden in the AlgoMetadata stored on the server.
|
||||
/// <para/>
|
||||
/// For implementation details, see: <see cref="IAlgoDefinitionMetadataService"/>, <see cref="AlgoTemplateMetadataService"/>
|
||||
/// </summary>
|
||||
public async Task<bool> remove_definition(AlgoKey key) {
|
||||
if (_algos.TryGetValue(key, out AlgoData curr)) {
|
||||
if (_definition_templates_demuxer.try_get_values(key, out IReadOnlyDictionary<AlgoKey, AlgoTemplateData> tmps)) {
|
||||
foreach (var t in tmps.Values)
|
||||
await remove_template(t.key);
|
||||
}
|
||||
await metadata_service.hide_definition(key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> remove_template(AlgoKey template_key) {
|
||||
var req = new AlgoServiceRemoveRequest();
|
||||
_Utils.set_key(ref req, template_key);
|
||||
var reply = await _safe_execute(() => _service.remove_template_async(req), ActionMetadata.remove_template);
|
||||
return reply.success && reply.value.Success;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
|
||||
async Task<AlgoData?> _create_definition(CompositeAlgoDefinitionImmutable definition) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
return await _create_definition(definition, user_id);
|
||||
} else {
|
||||
_report_no_user_id(ActionMetadata.CreateDefinition);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async Task<AlgoTemplateData?> _create_template(AlgoTemplateImmutable template) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.create_template_async(
|
||||
new TemplateCreateRequest { UserId = user_id, NewTemplate = template.CloneToMutable() }),
|
||||
r => r.Success, r => r.ErrMsg, ActionMetadata.CreateTemplate);
|
||||
if (success)
|
||||
return _Utils.template_data(value.Record);
|
||||
} else {
|
||||
_report_no_user_id(ActionMetadata.CreateTemplate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async Task<AlgoTemplateData?> _update_template(AlgoTemplateRecordImmutable template) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.update_template_async(
|
||||
new TemplateUpdateRequest { Key = template.Key, UpdatedTemplate = template.AlgoTemplate.CloneToMutable() }),
|
||||
r => r.Success, r => r.ErrMsg.ToString(), ActionMetadata.UpdateTemplate);
|
||||
if (success)
|
||||
return _Utils.template_data(template);
|
||||
} else {
|
||||
_report_no_user_id(ActionMetadata.UpdateTemplate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async Task<AlgoData?> _create_definition(CompositeAlgoDefinitionImmutable definition, uint user_id) {
|
||||
(var result, var success) = await _error_reporter.safe_execute(() => _service.create_composite_algo_async(
|
||||
new CompositeAlgoCreateRequest {
|
||||
NewAlgo = definition.CloneToMutable(),
|
||||
UserId = user_id
|
||||
}), r => r.Success, r => r.ErrMsg, "create definition");
|
||||
return success ? _Utils.definition_data(new CompositeAlgoRecordImmutable(result.Record)) : (AlgoData?)null;
|
||||
}
|
||||
|
||||
async Task<Result<T>> _safe_execute<T>(Func<Task<Result<T>>> execute, IActionMetadata<T> am) {
|
||||
try {
|
||||
var result = await execute();
|
||||
if (!result.success) {
|
||||
_report_error(am.present_progressive_desc, result.error_message);
|
||||
} else if (!am.get_success(result.value)) {
|
||||
_report_error(am.present_progressive_desc, am.get_error_msg(result.value));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
_report_error(am.present_progressive_desc, ex.ToString());
|
||||
}
|
||||
return new Result<T>(bts.api.ApiError.UnexpectedResponse);
|
||||
}
|
||||
|
||||
void _report_error(string present_progressive_desc, string msg) =>
|
||||
_error_reporter.report_error($"Error {present_progressive_desc}: {msg}", AlgoStudioWindow.NAME, System.Windows.MessageBoxImage.Warning);
|
||||
|
||||
void _report_no_user_id(string desc) =>
|
||||
_error_reporter.report_error($"{desc} failed: did not find current user id");
|
||||
|
||||
bool _try_get_user(out uint id) {
|
||||
var user = _admin_model.get_current_user();
|
||||
if (user.HasValue) {
|
||||
id = user.Value.id;
|
||||
return true;
|
||||
} else {
|
||||
id = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// used for error handling/reporting
|
||||
interface IActionMetadata {
|
||||
string desc { get; }
|
||||
string present_progressive_desc { get; }
|
||||
}
|
||||
|
||||
interface IActionMetadata<TReply> : IActionMetadata {
|
||||
bool get_success(TReply reply);
|
||||
string get_error_msg(TReply reply);
|
||||
}
|
||||
|
||||
static class ActionMetadata {
|
||||
public const string
|
||||
CreateDefinition = "create definition",
|
||||
UpdateDefinition = "update definition",
|
||||
CreateTemplate = "create template",
|
||||
UpdateTemplate = "update template";
|
||||
public static readonly IActionMetadata<AlgoServiceReply> update_template = new Impl<AlgoServiceReply>(_update, _template, r => r.Success, r => r.ErrMsg.ToString());
|
||||
public static readonly IActionMetadata<AlgoServiceReply> remove_template = new Impl<AlgoServiceReply>(_delete, _template, r => r.Success, r => r.ErrMsg.ToString());
|
||||
public static readonly IActionMetadata<TemplateCreateResult> create_template = new Impl<TemplateCreateResult>(_create, _template, r => r.Success, r => r.ErrMsg);
|
||||
public static readonly IActionMetadata<CompositeAlgoCreateResult> create_definition = new Impl<CompositeAlgoCreateResult>(_create, _definition, r => r.Success, r => r.ErrMsg);
|
||||
|
||||
static readonly DescTuple _create = new DescTuple("create", "creating"), _update = new DescTuple("update", "updating"), _delete = new DescTuple("delete", "deleting");
|
||||
static readonly string _definition = "definition", _template = "template";
|
||||
|
||||
class Impl<T> : IActionMetadata<T> {
|
||||
readonly DescTuple _desc;
|
||||
readonly string _subject;
|
||||
readonly Func<T, bool> _get_success;
|
||||
readonly Func<T, string> _get_error_msg;
|
||||
|
||||
public Impl(DescTuple desc, string subject, Func<T, bool> get_success, Func<T, string> get_error_msg) {
|
||||
_desc = desc;
|
||||
_subject = subject;
|
||||
_get_success = get_success;
|
||||
_get_error_msg = get_error_msg;
|
||||
}
|
||||
public string desc => _desc.desc;
|
||||
public string present_progressive_desc => _desc.present_progressive_desc;
|
||||
public bool get_success(T reply) => _get_success(reply);
|
||||
public string get_error_msg(T reply) => _get_error_msg(reply);
|
||||
}
|
||||
|
||||
struct DescTuple {
|
||||
public readonly string desc, present_progressive_desc;
|
||||
public DescTuple(string desc, string present_progressive_desc) {
|
||||
this.desc = desc;
|
||||
this.present_progressive_desc = present_progressive_desc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The AlgoService protobuf types have been extracted out of the viewmodels (<see cref="AlgoStudioVM"/>, <see cref="OrderPropertiesEditorVM"/>, <see cref="AlgoDefinitionVM"/>, <see cref="AlgoDefinitionTemplateVM"/>).
|
||||
/// The viewmodels instead use these types to represent the data: <see cref="AlgoData"/> is used in place of <see cref="CompositeAlgoRecordImmutable"/>, and <see cref="AlgoTemplateData"/> is used in place of <see cref="AlgoTemplateRecordImmutable"/>.
|
||||
/// This thin abstraction isolates the server-side from the client-side, so if one day the server side types change, we won't need to change any client-side code except the methods below which handle the conversion from server type to corresponding client type.
|
||||
/// There are a couple other methods in here as well (this is an extensions class afterall), but [John]I am going out of town soon (1/6/18) and did not get a chance to organize these methods beyond containing them in <see cref="Utils"/>.
|
||||
/// </summary>
|
||||
static class _Utils {
|
||||
// AlgoTemplateImmutable custom_order_definition(i)
|
||||
public static CustomOrderDefinitionImmutable custom_order_definitioni(AlgoTemplateImmutable templatei) {
|
||||
switch (templatei.AlgoParamsCase) {
|
||||
case AlgoTemplate.AlgoParamsOneofCase.CompositeAlgoParams: return templatei.CompositeAlgoParams.Algo;
|
||||
case AlgoTemplate.AlgoParamsOneofCase.SystemAlgoParams: return templatei.SystemAlgoParams;
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(templatei.AlgoParamsCase, nameof(templatei));
|
||||
}
|
||||
}
|
||||
|
||||
public static AlgoData definition_data(AlgoTemplateImmutable templatei) {
|
||||
switch (templatei.AlgoParamsCase) {
|
||||
case AlgoTemplate.AlgoParamsOneofCase.CompositeAlgoParams: return definition_data(templatei.CompositeAlgoParams);
|
||||
case AlgoTemplate.AlgoParamsOneofCase.SystemAlgoParams:
|
||||
return new AlgoData(
|
||||
templatei.SystemAlgoParams,
|
||||
templatei.SystemAlgoParams.CustomCase.display_name(),
|
||||
get_key(templatei.SystemAlgoParams));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(templatei.AlgoParamsCase, nameof(templatei));
|
||||
}
|
||||
}
|
||||
|
||||
public static AlgoData definition_data(CompositeAlgoRecordImmutable recordi) => new AlgoData(recordi.AlgoDefinition.Algo, recordi.AlgoDefinition.Name, get_key(recordi));
|
||||
|
||||
public static AlgoData definition_data(CompositeAlgoDefinitionImmutable definitioni) =>
|
||||
new AlgoData(definitioni.Algo, definitioni.Name, get_key(definitioni));
|
||||
|
||||
public static AlgoData? definition_data(Result<CompositeAlgoCreateResult> result) {
|
||||
if (result.success && result.value.Success) {
|
||||
var r = result.value.Record;
|
||||
return new AlgoData(new CustomOrderDefinitionImmutable(r.AlgoDefinition.Algo), r.AlgoDefinition.Name, new AlgoKey(new AlgoPersistedKey(r.Key)));
|
||||
} else return null;
|
||||
}
|
||||
|
||||
// AlgoTemplateRecord(Immutable) template data
|
||||
public static AlgoTemplateData template_data(AlgoTemplateRecord record) => template_data(new AlgoTemplateRecordImmutable(record));
|
||||
public static AlgoTemplateData template_data(AlgoTemplateRecordImmutable recordi) => new AlgoTemplateData(custom_order_definitioni(recordi.AlgoTemplate), recordi.AlgoTemplate.Name, get_key(recordi), definition_data(recordi.AlgoTemplate));
|
||||
|
||||
public static CompositeAlgoDefinition to_definition(AlgoData definition_data) =>
|
||||
new CompositeAlgoDefinition {
|
||||
Name = definition_data.name,
|
||||
Algo = definition_data.value.CloneToMutable()
|
||||
};
|
||||
|
||||
// AlgoTemplateData to_record(i)
|
||||
public static AlgoTemplateRecord to_record(AlgoTemplateData template) {
|
||||
var record = new AlgoTemplateRecord {
|
||||
AlgoTemplate = new AlgoTemplate {
|
||||
Name = template.name,
|
||||
CompositeAlgoParams = new CompositeAlgoDefinition {
|
||||
Name = template.definition.name,
|
||||
Algo = template.value.CloneToMutable()
|
||||
}
|
||||
}
|
||||
};
|
||||
set_key(ref record, template.key);
|
||||
return record;
|
||||
}
|
||||
public static AlgoTemplateRecordImmutable to_recordi(AlgoTemplateData template) => new AlgoTemplateRecordImmutable(to_record(template));
|
||||
|
||||
// AlgoTemplateData to_template(i)
|
||||
public static AlgoTemplate to_template(AlgoTemplateData template) =>
|
||||
new AlgoTemplate {
|
||||
Name = template.name,
|
||||
CompositeAlgoParams = new CompositeAlgoDefinition {
|
||||
Name = template.definition.name,
|
||||
Algo = template.value.CloneToMutable()
|
||||
}
|
||||
};
|
||||
public static AlgoTemplateImmutable to_templatei(AlgoTemplateData template) => new AlgoTemplateImmutable(to_template(template));
|
||||
|
||||
public static AlgoKey get_key(CompositeAlgoRecordImmutable recordi) => new AlgoKey(new AlgoPersistedKey(recordi.Key));
|
||||
public static AlgoKey get_key(AlgoTemplateRecordImmutable recordi) => new AlgoKey(new AlgoPersistedKey(recordi.Key));
|
||||
public static AlgoKey get_key(CompositeAlgoDefinitionImmutable definitioni) => new AlgoKey(new AlgoPersistedKey(definitioni.Name));
|
||||
public static AlgoKey get_key(CustomOrderDefinitionImmutable custom_order_definitioni) => new AlgoKey(custom_order_definitioni.CustomCase);
|
||||
|
||||
public static void set_key(ref AlgoTemplateRecord record, AlgoKey key) {
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
switch (key.persisted.mode) {
|
||||
case AlgoPersistedKey.Mode.String:
|
||||
record.Key = key.persisted.id_string; break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
public static void set_key(ref AlgoServiceRemoveRequest request, AlgoKey key) {
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
switch (key.persisted.mode) {
|
||||
case AlgoPersistedKey.Mode.String:
|
||||
request.Key = key.persisted.id_string; break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-32
@@ -1,18 +1,16 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman.algo;
|
||||
using bts.persist.algo;
|
||||
using bts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
[Export(typeof(IAlgoStudioModel))]
|
||||
class AlgoStudioFlatDefinitionModel : IAlgoStudioModel, IDisposable {
|
||||
readonly AlgoDefinitionService _service;
|
||||
readonly AdminModel _admin_model;
|
||||
@@ -21,33 +19,34 @@ namespace mbts.spark {
|
||||
IReadOnlyDictionary<AlgoKey, AlgoTemplateData> _algo_templates = DictionaryHelper.empty<AlgoKey, AlgoTemplateData>();
|
||||
IDisposable _dispose;
|
||||
|
||||
public AlgoStudioFlatDefinitionModel(AlgoDefinitionService service, IAlgoDefinitionMetadataService metadata_service, AdminModel admin_model, ErrorReporter error_reporter, bts.config.AlgoStudioOptions ui_configuration) {
|
||||
[ImportingConstructor]
|
||||
public AlgoStudioFlatDefinitionModel(AlgoDefinitionService service, IAlgoDefinitionMetadataService metadata_service, AdminModel admin_model, ErrorReporter error_reporter) {
|
||||
_service = service;
|
||||
this.metadata_service = metadata_service;
|
||||
_admin_model = admin_model;
|
||||
_error_reporter = error_reporter;
|
||||
this.ui_configuration = ui_configuration;
|
||||
definitions = service.subscribei_algo_definitions().Filter(r => r.Metadata.IsActive).SelectKeyValue(_definition_data, d => d.key).Table().Cache();
|
||||
definitions =
|
||||
service.subscribei_algo_definitions()
|
||||
.Filter(r => r.Metadata.IsActive && r.Metadata.CreatorUserId == _admin_model.user.id)
|
||||
.SelectKeyValue(_definition_data, d => d.key)
|
||||
.Table()
|
||||
.Cache();
|
||||
}
|
||||
|
||||
public IObservable<TableUpdate<AlgoData, AlgoKey>> definitions { get; }
|
||||
public IObservable<TableUpdate<AlgoTemplateData, AlgoKey>> templates => Observable.Empty<TableUpdate<AlgoTemplateData, AlgoKey>>();
|
||||
public IAlgoDefinitionMetadataService metadata_service { get; }
|
||||
public bts.config.AlgoStudioOptions ui_configuration { get; }
|
||||
public bts.config.AlgoStudioOptions ui_configuration { get; } = config.Configuration.get<bts.config.AlgoStudioOptions>();
|
||||
|
||||
public IObservable<TableUpdate<AlgoTemplateData, AlgoKey>> subscribe_definition_templates(AlgoKey definition_key) =>
|
||||
Observable.Return(TableUpdate<AlgoTemplateData, AlgoKey>.mk_empty(t => t.key));
|
||||
|
||||
public async Task<AlgoData?> save_definition(AlgoData definition_data) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.save_algo_definition_async(
|
||||
_with_key(new AlgoDefinitionUpdateRequest { UserId = user_id, Algo = _template(definition_data) }, definition_data.key)),
|
||||
r => r.Success, r => r.ErrorMessage, "saving template");
|
||||
if (success)
|
||||
return _definition_data(new AlgoDefinitionRecordImmutable(value.Record));
|
||||
} else {
|
||||
_report_no_user_id("save template");
|
||||
}
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.save_algo_definition_async(
|
||||
_with_key(new AlgoDefinitionUpdateRequest { UserId = _admin_model.user.id, Algo = _template(definition_data) }, definition_data.key)),
|
||||
r => r.Success, r => r.ErrorMessage, "saving template");
|
||||
if (success)
|
||||
return _definition_data(new AlgoDefinitionRecordImmutable(value.Record));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -73,10 +72,10 @@ namespace mbts.spark {
|
||||
new AlgoDefinitionImmutable(_template(definition_data));
|
||||
|
||||
static AlgoData _definition_data(AlgoDefinitionRecordImmutable recordi) =>
|
||||
new AlgoData(recordi.Algo.Definition, recordi.Algo.Name, _key(recordi));
|
||||
AlgoData.from_record(recordi);
|
||||
|
||||
static AlgoKey _key(AlgoDefinitionRecordImmutable recordi) =>
|
||||
new AlgoKey(new AlgoPersistedKey(recordi.Key));
|
||||
AlgoKey.from_composite_key(recordi.Key);
|
||||
|
||||
static AlgoDefinitionUpdateRequest _with_key(AlgoDefinitionUpdateRequest request, AlgoKey key) {
|
||||
_set_key(ref request, key);
|
||||
@@ -85,9 +84,10 @@ namespace mbts.spark {
|
||||
|
||||
static void _set_key(ref AlgoDefinitionUpdateRequest request, AlgoKey key) {
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Persisted: request.Key = key.persisted.id_uint; break;
|
||||
case AlgoKey.Mode.Persisted: request.Key = key.persisted; break;
|
||||
case AlgoKey.Mode.Unpersisted:
|
||||
case AlgoKey.Mode.Factory:
|
||||
case AlgoKey.Mode.Dynamic:
|
||||
case AlgoKey.Mode.None: request.Key = 0; break;
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key.mode, nameof(key));
|
||||
}
|
||||
@@ -95,7 +95,7 @@ namespace mbts.spark {
|
||||
|
||||
static void _set_key(ref AlgoDefinitionRemoveRequest request, AlgoKey key) {
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Persisted: request.Key = key.persisted.id_uint; break;
|
||||
case AlgoKey.Mode.Persisted: request.Key = key.persisted; break;
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key.mode, nameof(key));
|
||||
}
|
||||
}
|
||||
@@ -105,16 +105,5 @@ namespace mbts.spark {
|
||||
|
||||
void _report_no_user_id(string desc) =>
|
||||
_error_reporter.report_error($"{desc} failed: did not find current user id");
|
||||
|
||||
bool _try_get_user(out uint id) {
|
||||
var user = _admin_model.get_current_user();
|
||||
if (user.HasValue) {
|
||||
id = user.Value.id;
|
||||
return true;
|
||||
} else {
|
||||
id = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.spark;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
@@ -20,34 +17,13 @@ namespace mbts.spark {
|
||||
bts.config.AlgoStudioOptions ui_configuration { get; }
|
||||
}
|
||||
|
||||
class AlgoStudioModelExporter {
|
||||
[ImportingConstructor]
|
||||
AlgoStudioModelExporter(Spark.AlgoServiceWrapper service_wrapper, IAlgoDefinitionMetadataService meta, AdminModel admin_model, ErrorReporter error_reporter) {
|
||||
var ui_configuration = config.Configuration.get<bts.config.AlgoStudioOptions>();
|
||||
switch (service_wrapper.mode) {
|
||||
case Spark.AlgoServiceWrapper.Mode.Flat: model = new AlgoStudioFlatDefinitionModel(service_wrapper.flat, meta, admin_model, error_reporter, ui_configuration); break;
|
||||
case Spark.AlgoServiceWrapper.Mode.Hierarchical: model = new AlgoStudioDefinitionTemplateModel(service_wrapper.hierarhical, meta, admin_model, error_reporter, ui_configuration); break;
|
||||
}
|
||||
}
|
||||
|
||||
[Export]
|
||||
public IAlgoStudioModel model { get; }
|
||||
}
|
||||
|
||||
static class AlgoStudioModelUtils {
|
||||
public static bool templates_enabled(this IAlgoStudioModel model) =>
|
||||
get_templates_enabled(model.ui_configuration);
|
||||
|
||||
public static bool get_templates_enabled(bts.config.AlgoStudioOptions ui_configuration) =>
|
||||
ui_configuration.add_template_button != bts.config.AlgoStudioOptions.AddTemplateButton.None;
|
||||
|
||||
public static IReadOnlyDictionary<object, object> get_dynamic_resources(bts.config.AlgoStudioOptions ui_configuration) =>
|
||||
new Dictionary<object, object> {
|
||||
{ AlgoStudioWindow.ShowAlgoToolBarRenameDefinitionKey, ui_configuration.can_modify_definition_name && ui_configuration.show_algo_toolbar_rename },
|
||||
{ AlgoStudioWindow.ShowAlgoToolBarSaveDefinitionKey, ui_configuration.can_modify_definition_values && ui_configuration.show_algo_toolbar_save },
|
||||
{ AlgoStudioWindow.ShowAlgoToolBarRevertDefinitionKey, ui_configuration.show_algo_toolbar_revert },
|
||||
{ AlgoStudioWindow.ShowAlgoToolBarUndoRedoDefinitionKey, ui_configuration.show_algo_toolbar_undo_redo },
|
||||
{ AlgoStudioWindow.TemplatesEnabledKey, get_templates_enabled(ui_configuration) }
|
||||
{ AlgoStudioWindow.ShowAlgoToolBarUndoRedoDefinitionKey, ui_configuration.show_algo_toolbar_undo_redo }
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -35,9 +35,9 @@ namespace mbts.spark {
|
||||
internal AlgoDefinitionPersistedMetadata to_persisted() =>
|
||||
id.mode != AlgoKey.Mode.Persisted ?
|
||||
throw new InvalidOperationException("Metadata does not have a persisted key.") :
|
||||
new AlgoDefinitionPersistedMetadata(id.persisted,
|
||||
default_template.mode == AlgoKey.Mode.Persisted ? default_template.persisted : default,
|
||||
favorite_templates.Where(t => t.mode == AlgoKey.Mode.Persisted).Select(t => t.persisted).ToArray(),
|
||||
new AlgoDefinitionPersistedMetadata(new AlgoPersistedKey(id.persisted),
|
||||
default_template.mode == AlgoKey.Mode.Persisted ? new AlgoPersistedKey(default_template.persisted) : default,
|
||||
favorite_templates.Where(t => t.mode == AlgoKey.Mode.Persisted).Select(t => new AlgoPersistedKey(t.persisted)).ToArray(),
|
||||
is_favorite, is_hidden, abbreviation, new NullableColorProto(badge_color));
|
||||
|
||||
public AlgoDefinitionMetadata with(AlgoKey? id = null, AlgoKey? default_template = null, IEnumerable<AlgoKey> favorite_templates = null, bool? is_favorite = null, bool? is_hidden = null, string abbreviation = null, NullableColorProto? badge_color = null) =>
|
||||
|
||||
@@ -83,7 +83,8 @@ namespace mbts.spark {
|
||||
|
||||
public static AlgoDisplayGroup display_group(this AlgoKey key) {
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Factory: return display_group(key.factory.structure.order_type());
|
||||
case AlgoKey.Mode.Factory: return display_group(key.factory);
|
||||
case AlgoKey.Mode.Dynamic: return AlgoDisplayGroup.API;
|
||||
case AlgoKey.Mode.Unpersisted:
|
||||
case AlgoKey.Mode.Persisted:
|
||||
return AlgoDisplayGroup.Studio;
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
using bts;
|
||||
using bts.api.orderman;
|
||||
using bts.utils;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Input;
|
||||
using VisualStateNames = mbts.ui.spark.AlgoStudioView.VisualStateNames;
|
||||
|
||||
namespace mbts.spark {
|
||||
class AlgoDefinitionTemplateVM : DisposableViewModelBase, IAlgoStudioContentVM, IEquatable<AlgoDefinitionTemplateVM> {
|
||||
/// <see cref="CustomType"/> is referenced by an x:Static extension in OrderDetailsPopupControl.xaml <see cref="mbts.ui.spark.algo.OrderDetailsPopupControl"/>.
|
||||
/// In most cases, <see cref="mbts.ui.NestedExtension"/> works just like <see cref="System.Windows.Markup.TypeExtension"/>, but for nested types, such as <see cref="CustomTemplatePlaceholderVM"/>.
|
||||
/// However, when the former is used to denote the DataType of a DataTemplate, a strange XamlReader exception is thrown at runtime.
|
||||
/// After spending ~15 minutes investigating what was happening, [John]I decided it was more trouble than it was worth and opted for this workaround.
|
||||
public static readonly Type CustomType = typeof(CustomTemplatePlaceholderVM);
|
||||
readonly OrdersModel _model;
|
||||
readonly VisualStateModel _edit_pane_vs_model = new VisualStateModel(), _tree_view_vs_model = new VisualStateModel();
|
||||
/// Note: the difference between <see cref="EditStack{T}"/> and <see cref="VersionContainer{T}"/> is that the former implements push/pop functionality so the user may undo/redo (like ctrl+z/ctrl+y in text editors),
|
||||
/// while the latter is a lazy delegate-based container for the most recent value (<see cref="VersionContainer{T}.increment_version"/>).
|
||||
readonly EditStack<Snapshot> _edit_stack = new EditStack<Snapshot>();
|
||||
readonly VersionContainer<Snapshot> _version;
|
||||
readonly DeferrableActionWrapper _refresh_on_local_change;
|
||||
readonly Property<IAlgoVM> _algo_root;
|
||||
readonly DelegateProperty<bool> _can_undo, _can_redo, _is_dirty;
|
||||
readonly Property<bool> _is_default, _is_favorite, _is_selected;
|
||||
readonly Property<string> _name, _abbreviation;
|
||||
// maintains the state of each algo-root, so the user may change the definition root back and forth without any loss of state amongst the templates
|
||||
Snapshot _committed;
|
||||
|
||||
public AlgoDefinitionTemplateVM(AlgoDefinitionVM owner, OrdersModel model, AlgoData data, AlgoEditingPermissions editing_permissions = AlgoEditingPermissions.AllowEditValues) {
|
||||
// init core data
|
||||
this.owner = owner;
|
||||
_model = model;
|
||||
key = data.key;
|
||||
_version = new VersionContainer<Snapshot>(_snapshot);
|
||||
// maintain states for different algo roots, so they aren't lost when the user changes the root algo type
|
||||
var algo_map = new Dictionary<AlgoKey, IAlgoVM> {
|
||||
{ data.key, algo_vm_factory(data.value) }
|
||||
};
|
||||
// init backing props
|
||||
_name = backing(nameof(this.name), name);
|
||||
_abbreviation = backing(nameof(abbreviation), string.Empty);
|
||||
_is_default = backing(nameof(is_default), false);
|
||||
_is_favorite = backing(nameof(is_favorite), false);
|
||||
_is_selected = backing(nameof(is_selected), false);
|
||||
_algo_root = backing(nameof(algo_root), get_algo_root(owner.algo_root));
|
||||
_committed = _version.value;
|
||||
_can_redo = backing(nameof(can_redo), () => _edit_stack.get_can_redo(_version.value));
|
||||
_can_undo = backing(nameof(can_undo), () => _edit_stack.get_can_undo(_version.value));
|
||||
_is_dirty = backing(nameof(is_dirty), () => !_committed.Equals(_version.value));
|
||||
_can_redo.Changed += on_can_redo_changed;
|
||||
_can_undo.Changed += on_can_undo_changed;
|
||||
set_as_default_cmd = new DelegateCommand(() => {
|
||||
if (!is_default && key.mode == AlgoKey.Mode.Persisted)
|
||||
_model.algo_model.metadata_service.set_as_default(owner.key, key).ignore();
|
||||
});
|
||||
// init visual states
|
||||
// TODO: implement these features in IAlgoMetadataService; until then, set the appropriate visual states.
|
||||
_edit_pane_vs_model.set_state(VisualStateNames.Abbreviation.GroupName, VisualStateNames.Abbreviation.NotSupportedForTemplates);
|
||||
_tree_view_vs_model.set_state(VisualStateNames.FavoriteToggleButton.GroupName, VisualStateNames.FavoriteToggleButton.Inactive);
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
_edit_stack.push(_committed);
|
||||
_edit_pane_vs_model.set_state(VisualStateNames.Name.GroupName, VisualStateNames.Name.Active);
|
||||
break;
|
||||
case AlgoKey.Mode.Unpersisted: _edit_pane_vs_model.set_state(VisualStateNames.Name.GroupName, VisualStateNames.Name.Active); break;
|
||||
case AlgoKey.Mode.Factory: _edit_pane_vs_model.set_state(VisualStateNames.Name.GroupName, VisualStateNames.Name.Inactive); break;
|
||||
}
|
||||
// set hooks/subscriptions
|
||||
_refresh_on_local_change = new DeferrableActionWrapper(on_local_changed);
|
||||
_dispose = new System.Reactive.Disposables.CompositeDisposable(
|
||||
as_demuxer().subscribe_many(_refresh_on_local_change.execute, nameof(name), nameof(abbreviation), nameof(is_default), nameof(is_favorite)),
|
||||
owner.observe_algo_root().Subscribe(r => _algo_root.set(get_algo_root(r))),
|
||||
_algo_root.observe_current(true).SelectManyInSeries(r => r?.subscribe_definition()).Subscribe(_ => _refresh_on_local_change.execute()),
|
||||
Disposable.wrap_collection(algo_map.Values)) {
|
||||
() => {
|
||||
_can_redo.Changed -= on_can_redo_changed;
|
||||
_can_undo.Changed -= on_can_undo_changed;
|
||||
}
|
||||
};
|
||||
// local methods
|
||||
IAlgoVM algo_vm_factory(CustomOrderDefinitionImmutable def) => OrderTypeVMFactory.create(new AlgoData(def, def.CustomCase.display_name(), OrderDataHelper.get_shallow_factory_key(def)), editing_permissions);
|
||||
IAlgoVM get_algo_root(IAlgoVM owner_root) {
|
||||
var key = new AlgoKey(owner_root.order_type);
|
||||
if (algo_map.TryGetValue(key, out IAlgoVM vm)) {
|
||||
vm.load_structure(owner_root.definition);
|
||||
} else {
|
||||
vm = algo_map.GetOrAdd(key, ot => algo_vm_factory(owner_root.definition));
|
||||
}
|
||||
return vm;
|
||||
}
|
||||
void on_can_undo_changed(bool oldval, bool newval) => can_undo_changed?.Invoke();
|
||||
void on_can_redo_changed(bool oldval, bool newval) => can_redo_changed?.Invoke();
|
||||
void on_local_changed() {
|
||||
_version.increment_version();
|
||||
_edit_stack.push_distinct(_version.value);
|
||||
_is_dirty.refresh();
|
||||
_refresh_undo_redo();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action can_redo_changed, can_undo_changed;
|
||||
public event PropertyChange<IAlgoVM> algo_root_changed { add => _algo_root.Changed += value; remove => _algo_root.Changed -= value; }
|
||||
|
||||
public AlgoDefinitionVM owner { get; }
|
||||
public AlgoKey owner_key => owner.key;
|
||||
public AlgoKey key { get; }
|
||||
public string name { get => _name.Value; set => _name.set(value); }
|
||||
public string abbreviation { get => _abbreviation.Value; set => _abbreviation.set(value); }
|
||||
public bool is_dirty => _is_dirty.Value;
|
||||
public bool can_redo => _can_redo.Value;
|
||||
public bool can_undo => _can_undo.Value;
|
||||
public bool is_default => _is_default.Value;
|
||||
public bool is_favorite { get => _is_favorite.Value; set => _is_favorite.set(value); }
|
||||
public IAlgoVM algo_root => _algo_root.Value;
|
||||
public virtual bool is_custom_placeholder => false;
|
||||
public bool is_selected { get => _is_selected.Value; set => _is_selected.set(value); }
|
||||
public bool is_expanded => false;
|
||||
public bool is_persisted => key.mode == AlgoKey.Mode.Persisted;
|
||||
public ObservableCollection<string> edit_pane_visual_states => _edit_pane_vs_model.states;
|
||||
public ObservableCollection<string> tree_view_visual_states => _tree_view_vs_model.states;
|
||||
public ICommand set_as_default_cmd { get; }
|
||||
|
||||
public IObservable<bool> observe_is_dirty() => _is_dirty.observe_current(true);
|
||||
public void undo() => _restore(_edit_stack.try_undo);
|
||||
public void redo() => _restore(_edit_stack.try_redo);
|
||||
|
||||
public void on_meta(bool is_default, bool is_favorite) {
|
||||
_is_default.set(is_default);
|
||||
_is_favorite.set(is_favorite);
|
||||
}
|
||||
|
||||
public AlgoData create_def_data() =>
|
||||
new AlgoData(algo_root.definition, name, key);
|
||||
|
||||
public AlgoTemplateData create() =>
|
||||
create(owner.create_algo_def());
|
||||
|
||||
public AlgoTemplateData create(AlgoData record) {
|
||||
if (!OrderDataHelper.EqualsHelpers.structure_equals(algo_root.definition, record.value))
|
||||
throw new InvalidOperationException("AlgoTemplate's algo structure does not match that of its parent.");
|
||||
return new AlgoTemplateData(algo_root.definition, name, key, record);
|
||||
}
|
||||
|
||||
public AlgoDefinitionTemplateVM to_persisted(AlgoDefinitionVM persisted_owner, AlgoData template) =>
|
||||
new AlgoDefinitionTemplateVM(persisted_owner, _model, template);
|
||||
|
||||
public void load_committed() => _load_from(_committed.name, _committed.definition, true);
|
||||
|
||||
public void load_from(AlgoTemplateData value) {
|
||||
if (key.mode != AlgoKey.Mode.Persisted)
|
||||
throw new InvalidOperationException("AlgoStructure has not been saved yet.");
|
||||
else if (value.key != key)
|
||||
throw new InvalidOperationException("Value id does not match this id.");
|
||||
_load_from(value.name, value.value, true);
|
||||
}
|
||||
|
||||
public bool Equals(AlgoDefinitionTemplateVM other) => other != null && key == other.key;
|
||||
public override bool Equals(object obj) => Equals(obj as AlgoDefinitionTemplateVM);
|
||||
public override int GetHashCode() => key.GetHashCode();
|
||||
|
||||
void _load_from(string new_name, CustomOrderDefinitionImmutable new_def, bool update_committed) {
|
||||
using (_refresh_on_local_change.defer_execution()) {
|
||||
name = new_name;
|
||||
algo_root.load_from(new_def);
|
||||
}
|
||||
if (update_committed) {
|
||||
_committed = _version.value;
|
||||
_is_dirty.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void _refresh_undo_redo() {
|
||||
_can_redo.refresh();
|
||||
_can_undo.refresh();
|
||||
}
|
||||
|
||||
void _restore(TryGetValue<Snapshot> try_get_snapshot) {
|
||||
while (try_get_snapshot(out Snapshot next_state)) {
|
||||
if (!_version.value.Equals(next_state)) {
|
||||
_load_from(next_state.name, next_state.definition, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Snapshot _snapshot() =>
|
||||
new Snapshot(name, abbreviation, algo_root?.definition ?? new CustomOrderDefinitionImmutable(new CustomOrderDefinition()), is_default, is_favorite);
|
||||
|
||||
struct Snapshot : IEquatable<Snapshot> {
|
||||
public readonly string name, abbreviation;
|
||||
public readonly CustomOrderDefinitionImmutable definition;
|
||||
public readonly bool is_default, is_favorite;
|
||||
|
||||
public Snapshot(string name, string abbreviation, CustomOrderDefinitionImmutable definition, bool is_default, bool is_favorite) {
|
||||
this.name = name;
|
||||
this.abbreviation = abbreviation;
|
||||
this.definition = definition;
|
||||
this.is_default = is_default;
|
||||
this.is_favorite = is_favorite;
|
||||
}
|
||||
|
||||
public bool Equals(Snapshot other) =>
|
||||
name == other.name &&
|
||||
abbreviation == other.abbreviation &&
|
||||
is_default == other.is_default &&
|
||||
is_favorite == other.is_favorite &&
|
||||
OrderDataHelper.EqualsHelpers.equals(definition, other.definition);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
EqualityComparer.equals_obj(this, obj);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
EqualityComparer.safe_aggregate_hash(name),
|
||||
EqualityComparer.safe_aggregate_hash(abbreviation),
|
||||
is_default.GetHashCode(),
|
||||
is_favorite.GetHashCode(),
|
||||
OrderDataHelper.EqualsHelpers.custom_order_def_compareri.GetHashCode(definition));
|
||||
|
||||
public static bool operator ==(Snapshot a, Snapshot b) => a.Equals(b);
|
||||
public static bool operator !=(Snapshot a, Snapshot b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
public class CustomTemplatePlaceholderVM : AlgoDefinitionTemplateVM {
|
||||
const string _custom_name = "(Custom)";
|
||||
|
||||
public CustomTemplatePlaceholderVM(AlgoDefinitionVM owner, OrdersModel model, CustomOrderDefinitionImmutable definition)
|
||||
: base(owner, model, new AlgoData(definition, _custom_name, new AlgoKey(Guid.NewGuid())), AlgoEditingPermissions.AllowEditStructure | AlgoEditingPermissions.AllowEditValues) {
|
||||
_edit_stack.reset(_snapshot());
|
||||
}
|
||||
|
||||
public override bool is_custom_placeholder => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using bts.api.core;
|
||||
using bts;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.proto;
|
||||
@@ -6,7 +6,6 @@ using bts.utils;
|
||||
using mbts.collections;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.controls.color_picker;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
@@ -18,7 +17,6 @@ using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using VisualStateNames = mbts.ui.spark.AlgoStudioView.VisualStateNames;
|
||||
|
||||
@@ -36,18 +34,14 @@ namespace mbts.spark {
|
||||
public static readonly CustomOrderDefinitionImmutable new_algo_def = OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(CustomOrderDefinition.CustomOneofCase.Iceberg);
|
||||
readonly OrdersModel _model;
|
||||
readonly VisualStateModel _edit_pane_vs_model = new VisualStateModel(), _tree_view_vs_model = new VisualStateModel();
|
||||
readonly ObservableKeyedCollection<AlgoDefinitionTemplateVM, AlgoKey> _templates = new ObservableKeyedCollection<AlgoDefinitionTemplateVM, AlgoKey>(t => t.key);
|
||||
readonly EditStack<Snapshot> _edit_stack = new EditStack<Snapshot>();
|
||||
readonly VersionContainer<Snapshot> _version;
|
||||
readonly Property<bool> _is_expanded, _is_selected;
|
||||
readonly Property<IAlgoVM> _algo_root, _visible_algo_root;
|
||||
readonly Property<AlgoDefinitionTemplateVM> _selected_template;
|
||||
readonly DelegateProperty<bool> _is_favorite, _is_dirty, _can_undo, _can_redo;
|
||||
readonly Property<string> _name, _abbreviation;
|
||||
readonly ObservableKeyedCollection<IAlgoVM, AlgoKey> _algo_roots = new ObservableKeyedCollection<IAlgoVM, AlgoKey>(vm => vm.id);
|
||||
readonly ObservableAppendCollection<AlgoDefinitionTemplateVM> _templates_with_custom;
|
||||
readonly DeferrableActionWrapper _refresh_on_local_changes;
|
||||
readonly ReactiveThresholdMonitor<AlgoDefinitionTemplateVM> _any_templates_dirty_monitor = new ReactiveThresholdMonitor<AlgoDefinitionTemplateVM>(t => t.observe_is_dirty());
|
||||
readonly Property<AlgoFormats> _selected_format;
|
||||
readonly EnumMaskInt32<AlgoEditingPermissions> _permissions_mask = new EnumMaskInt32<AlgoEditingPermissions>();
|
||||
|
||||
@@ -55,18 +49,12 @@ namespace mbts.spark {
|
||||
Brush _badge_fill;
|
||||
Snapshot? _committed;
|
||||
AlgoDefinitionMetadata _metadata;
|
||||
TableUpdateBuffer<AlgoTemplateData, AlgoKey> _template_update_buffer;
|
||||
bool _connected;
|
||||
|
||||
public static AlgoDefinitionVM create(OrdersModel model, AlgoData data, bool connect = false, AlgoFormats init_format = AlgoFormats.Exchange, AlgoEditingPermissions editing_permissions = (AlgoEditingPermissions)int.MaxValue) {
|
||||
var rv = new AlgoDefinitionVM(model, data, init_format, editing_permissions);
|
||||
if (connect)
|
||||
rv.connect();
|
||||
return rv;
|
||||
}
|
||||
public static AlgoDefinitionVM create(OrdersModel model, AlgoData data, AlgoFormats init_format = AlgoFormats.Exchange, AlgoEditingPermissions editing_permissions = (AlgoEditingPermissions)int.MaxValue) =>
|
||||
new AlgoDefinitionVM(model, data, init_format, editing_permissions);
|
||||
|
||||
public static AlgoDefinitionVM create_placeholder(OrdersModel model, CustomOrderDefinitionImmutable def, string name, AlgoFormats init_format) =>
|
||||
new AlgoDefinitionVM(model, new AlgoData(def, name, new AlgoKey(Guid.NewGuid())), init_format, (AlgoEditingPermissions)int.MaxValue);
|
||||
new AlgoDefinitionVM(model, AlgoData.create_unpersisted(def, name), init_format, (AlgoEditingPermissions)int.MaxValue);
|
||||
|
||||
AlgoDefinitionVM(OrdersModel model, AlgoData def_data, AlgoFormats init_format, AlgoEditingPermissions init_permissions) {
|
||||
// init core data
|
||||
@@ -78,20 +66,8 @@ namespace mbts.spark {
|
||||
editing_permissions_mask ^= (AlgoEditingPermissions.AllowEditValues | AlgoEditingPermissions.AllowSaveValues);
|
||||
if (!model.algo_model.ui_configuration.can_modify_definition_structure)
|
||||
editing_permissions_mask ^= (AlgoEditingPermissions.AllowEditStructure | AlgoEditingPermissions.AllowSaveStructure);
|
||||
else if (model.algo_model.templates_enabled()) {
|
||||
resources.Add(_templates.Subscribe((s, e) => {
|
||||
if (_templates.Count == 0) {
|
||||
editing_permissions_mask |= AlgoEditingPermissions.AllowEditStructure | AlgoEditingPermissions.AllowSaveStructure;
|
||||
} else {
|
||||
editing_permissions_mask ^= (AlgoEditingPermissions.AllowEditStructure | AlgoEditingPermissions.AllowSaveStructure);
|
||||
}
|
||||
}));
|
||||
}
|
||||
key = def_data.key;
|
||||
algos_view = ObservableAutoWrapper.create_sorted_wrapper(_algo_roots, AlgoCollectionHelper.algo_vm_comparer);
|
||||
_template_update_buffer = new TableUpdateBuffer<AlgoTemplateData, AlgoKey>(t => t.key);
|
||||
_templates_with_custom = new ObservableAppendCollection<AlgoDefinitionTemplateVM>(templates);
|
||||
templates_view = (ListCollectionView)CollectionViewSource.GetDefaultView(_templates_with_custom);
|
||||
custom_badge_color_palettes.AddRange(ColorPalette.all_presets(model.factory).Select(p => new ColorPaletteVM(p)));
|
||||
_version = new VersionContainer<Snapshot>(_snapshot);
|
||||
var conf = _model.algo_model.ui_configuration;
|
||||
@@ -99,87 +75,80 @@ namespace mbts.spark {
|
||||
_name = backing(nameof(name), def_data.name);
|
||||
_abbreviation = backing(nameof(abbreviation), string.Empty);
|
||||
_selected_format = backing(nameof(selected_format), init_format);
|
||||
_selected_template = backing<AlgoDefinitionTemplateVM>(nameof(selected_template));
|
||||
_is_favorite = backing(nameof(is_favorite), () => _favorite_index.HasValue);
|
||||
_is_expanded = backing(nameof(is_expanded), true);
|
||||
_is_selected = backing(nameof(is_selected), true);
|
||||
_is_dirty = backing(nameof(is_dirty), () => key.mode == AlgoKey.Mode.Unpersisted || !_committed.HasValue || !_committed.Value.Equals(_version.value));
|
||||
var factory_def_data = def_data.key.mode == AlgoKey.Mode.Factory ? def_data : new AlgoData(def_data.value);
|
||||
var factory_def_data = def_data.key.mode == AlgoKey.Mode.Factory ? def_data : AlgoData.from_custom_def(def_data.value);
|
||||
_algo_root = backing(nameof(algo_root), _algo_roots.GetOrAdd(factory_def_data.key, _ => OrderTypeVMFactory.create(factory_def_data)));
|
||||
_visible_algo_root = backing<IAlgoVM>(nameof(visible_algo_root));
|
||||
// only composite/studio algos can change root
|
||||
if (key.mode == AlgoKey.Mode.Factory || key.mode == AlgoKey.Mode.Dynamic) {
|
||||
_algo_root.Changing += (oldval, newval) =>
|
||||
Log.assert(newval != null && newval.id == key);
|
||||
}
|
||||
_can_redo = backing(nameof(can_redo), () => _edit_stack.get_can_redo(_version.value));
|
||||
_can_undo = backing(nameof(can_undo), () => _edit_stack.get_can_undo(_version.value));
|
||||
_can_redo.Changed += on_can_redo_changed;
|
||||
_can_undo.Changed += on_can_undo_changed;
|
||||
_refresh_on_local_changes = new DeferrableActionWrapper(on_local_changed);
|
||||
// load initial state
|
||||
init_async();
|
||||
async void init_async() {
|
||||
// visible_algo_root is selected_template.algo_root if user can add templates, otherwise algo_root
|
||||
// - this behavior is configurable in 'AlgoStudioOptions' in UI.proto (01/28/2018)
|
||||
if (_model.algo_model.templates_enabled()) {
|
||||
resources.Add(_selected_template.observe_current(true).Subscribe(t => _visible_algo_root.set(t?.algo_root)));
|
||||
} else {
|
||||
resources.Add(_algo_root.observe_current(true).Subscribe(_visible_algo_root.set));
|
||||
// visible_algo_root is selected_template.algo_root if user can add templates, otherwise algo_root
|
||||
// - this behavior is configurable in 'AlgoStudioOptions' in UI.proto (01/28/2018)
|
||||
resources.Add(_algo_root.observe_current(true).Subscribe(_visible_algo_root.set));
|
||||
// set hooks/subscriptions
|
||||
using (_refresh_on_local_changes.supress_execution()) {
|
||||
resources.AddRange(
|
||||
custom_badge_color.observe_nullable(true).Subscribe(v => Set(ref _badge_fill, (v ?? algo_root.safe_get_badge_color()).to_brush(), nameof(badge_fill))),
|
||||
as_demuxer().subscribe_many(_refresh_on_local_changes.execute, nameof(this.name), nameof(abbreviation), nameof(badge_fill)),
|
||||
_edit_pane_vs_model.add_source(VisualStateNames.BadgeSelector.GroupName, custom_badge_color.observe_nullable(true).Select(t => t.HasValue ? VisualStateNames.BadgeSelector.Active : VisualStateNames.BadgeSelector.Inactive)),
|
||||
_edit_pane_vs_model.add_source(VisualStateNames.Abbreviation.GroupName, _abbreviation.observe_current(true).Select(a => string.IsNullOrEmpty(a) ? VisualStateNames.Abbreviation.Unpopulated : VisualStateNames.Abbreviation.Populated)),
|
||||
_algo_root.observe_current(true).SubscribeEach(vm =>
|
||||
vm == null ? null :
|
||||
Disposable.Create(
|
||||
vm.subscribe_is_dirty().Subscribe(v => {
|
||||
if (!v) {
|
||||
_version.increment_version();
|
||||
_on_committed_changed();
|
||||
}
|
||||
}),
|
||||
vm.subscribe_definition().Subscribe(_ => _refresh_on_local_changes.execute()))),
|
||||
ObservableAutoWrapper.connect_item_hooks(algo_roots, a => a as IDisposable ?? Disposable.empty, true),
|
||||
Disposable.Create(() => {
|
||||
_can_redo.Changed -= on_can_redo_changed;
|
||||
_can_undo.Changed -= on_can_undo_changed;
|
||||
}));
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
_metadata = new AlgoDefinitionMetadata(key);
|
||||
resources.Add(model.algo_model.metadata_service.subscribe_metadata(key).OnDispatcher().Subscribe(_on_metadata));
|
||||
_edit_pane_vs_model.set_state(VisualStateNames.Name.GroupName, conf.can_modify_definition_name ? VisualStateNames.Name.Active : VisualStateNames.Name.CannotRenameDefinition);
|
||||
break;
|
||||
case AlgoKey.Mode.Factory:
|
||||
_edit_pane_vs_model.set_state(VisualStateNames.Name.GroupName, VisualStateNames.Name.Inactive);
|
||||
break;
|
||||
}
|
||||
// init tree view item visual state
|
||||
_tree_view_vs_model.set_state(VisualStateNames.TreeViewBadge.GroupName, conf.show_badges_in_tree_view ? VisualStateNames.TreeViewBadge.Active : VisualStateNames.TreeViewBadge.Inactive);
|
||||
// set hooks/subscriptions
|
||||
using (_refresh_on_local_changes.supress_execution()) {
|
||||
resources.AddRange(
|
||||
custom_badge_color.observe_nullable(true).Subscribe(v => Set(ref _badge_fill, (v ?? algo_root.safe_get_badge_color()).to_brush(), nameof(badge_fill))),
|
||||
as_demuxer().subscribe_many(_refresh_on_local_changes.execute, nameof(this.name), nameof(abbreviation), nameof(badge_fill)),
|
||||
_edit_pane_vs_model.add_source(VisualStateNames.BadgeSelector.GroupName, custom_badge_color.observe_nullable(true).Select(t => t.HasValue ? VisualStateNames.BadgeSelector.Active : VisualStateNames.BadgeSelector.Inactive)),
|
||||
_edit_pane_vs_model.add_source(VisualStateNames.Abbreviation.GroupName, _abbreviation.observe_current(true).Select(a => string.IsNullOrEmpty(a) ? VisualStateNames.Abbreviation.Unpopulated : VisualStateNames.Abbreviation.Populated)),
|
||||
_algo_root.observe_current(true).SubscribeEach(vm =>
|
||||
vm == null ? null :
|
||||
Disposable.Create(
|
||||
vm.subscribe_is_dirty().Subscribe(v => {
|
||||
if (!v) {
|
||||
_version.increment_version();
|
||||
_on_committed_changed();
|
||||
}
|
||||
}),
|
||||
vm.subscribe_definition().Subscribe(_ => _refresh_on_local_changes.execute()))),
|
||||
ObservableAutoWrapper.connect_item_hooks(algo_roots, a => a as IDisposable ?? Disposable.empty, true),
|
||||
ObservableAutoWrapper.connect_item_hooks(templates, t => new CompositeDisposable(_any_templates_dirty_monitor.join(t), t), true),
|
||||
Disposable.Create(() => {
|
||||
_can_redo.Changed -= on_can_redo_changed;
|
||||
_can_undo.Changed -= on_can_undo_changed;
|
||||
}));
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
_metadata = new AlgoDefinitionMetadata(key);
|
||||
resources.Add(_template_update_buffer.OnDispatcher().Subscribe(_on_templates));
|
||||
resources.Add(model.algo_model.metadata_service.subscribe_metadata(key).OnDispatcher().Subscribe(_on_metadata));
|
||||
_edit_pane_vs_model.set_state(VisualStateNames.Name.GroupName, conf.can_modify_definition_name ? VisualStateNames.Name.Active : VisualStateNames.Name.CannotRenameDefinition);
|
||||
break;
|
||||
case AlgoKey.Mode.Factory:
|
||||
_edit_pane_vs_model.set_state(VisualStateNames.Name.GroupName, VisualStateNames.Name.Inactive);
|
||||
break;
|
||||
}
|
||||
var permission_src = _permissions_mask.observe_result();
|
||||
resources.Add(_selected_format.observe_current(true).CombineLatest(permission_src, (fmt, perms) => (fmt, perms))
|
||||
.SubscribeEach(t => {
|
||||
if ((t.perms & AlgoEditingPermissions.AllowEditStructure) != 0) {
|
||||
using (_refresh_on_local_changes.defer_execution()) {
|
||||
var algo_data = model.factory.default_algos(t.fmt);
|
||||
var default_algo = new AlgoKey(OrderDataHelper.get_default_algo(t.fmt));
|
||||
return this.load_algo_data_recursive(algo_data, default_algo);
|
||||
}
|
||||
var permission_src = _permissions_mask.observe_result();
|
||||
resources.Add(_selected_format.observe_current(true).CombineLatest(permission_src, (fmt, perms) => (fmt, perms))
|
||||
.SubscribeEach(t => {
|
||||
if ((t.perms & AlgoEditingPermissions.AllowEditStructure) != 0) {
|
||||
using (_refresh_on_local_changes.defer_execution()) {
|
||||
var algo_data = model.factory.default_algos(t.fmt);
|
||||
var default_algo = AlgoKey.from_known_type(OrderDataHelper.get_default_algo(t.fmt));
|
||||
return this.load_algo_data_recursive(algo_data, default_algo);
|
||||
}
|
||||
return Disposable.empty;
|
||||
}));
|
||||
resources.Add(permission_src.Subscribe(_ => RaisePropertyChanged(nameof(editing_permissions))));
|
||||
resources.Add(this.connect_data(permission_src));
|
||||
resources.Add(this.connect_data(Observable.Return(model)));
|
||||
resources.Add(_algo_root.Subscribe(_ => RaisePropertyChanged(nameof(IAlgoSelector.selected_algo), nameof(IAlgoParamVM.value), nameof(group))));
|
||||
_dispose = resources;
|
||||
await algo_root.load_from_async(def_data.value);
|
||||
_version.increment_version();
|
||||
_edit_stack.reset(_version.value);
|
||||
_on_committed_changed();
|
||||
}
|
||||
}
|
||||
return Disposable.empty;
|
||||
}));
|
||||
resources.Add(permission_src.Subscribe(_ => RaisePropertyChanged(nameof(editing_permissions))));
|
||||
resources.Add(this.connect_data(permission_src));
|
||||
resources.Add(this.connect_data(Observable.Return(model)));
|
||||
resources.Add(_algo_root.Subscribe(_ => RaisePropertyChanged(nameof(IAlgoSelector.selected_algo), nameof(IAlgoParamVM.value), nameof(group))));
|
||||
_dispose = resources;
|
||||
algo_root.load_from(def_data.value);
|
||||
_version.increment_version();
|
||||
_edit_stack.reset(_version.value);
|
||||
_on_committed_changed();
|
||||
}
|
||||
// local methods
|
||||
void on_can_redo_changed(bool oldval, bool newval) => can_redo_changed?.Invoke();
|
||||
@@ -206,7 +175,6 @@ namespace mbts.spark {
|
||||
public IReadOnlyObservableCollection<IAlgoVM> algos_view { get; }
|
||||
public IAlgoVM algo_root { get => _algo_root.Value; set => _algo_root.set(value); }
|
||||
public IAlgoVM visible_algo_root => _visible_algo_root.Value;
|
||||
public AlgoDefinitionTemplateVM selected_template { get => _selected_template.Value; set => _selected_template.set(value); }
|
||||
public int? favorite_index {
|
||||
get => _favorite_index;
|
||||
set {
|
||||
@@ -219,34 +187,12 @@ namespace mbts.spark {
|
||||
public bool is_selected { get => _is_selected.Value; set => _is_selected.set(value); }
|
||||
public bool is_dirty => _is_dirty.Value;
|
||||
public bool is_factory_definition => key.mode == AlgoKey.Mode.Factory;
|
||||
public AlgoDisplayGroup group {
|
||||
get {
|
||||
switch (key.mode) {
|
||||
case AlgoKey.Mode.Factory:
|
||||
if (algo_root != null)
|
||||
return algo_root.group;
|
||||
break;
|
||||
case AlgoKey.Mode.Unpersisted:
|
||||
case AlgoKey.Mode.Persisted:
|
||||
return AlgoDisplayGroup.Studio;
|
||||
}
|
||||
return AlgoDisplayGroup.BTS;
|
||||
}
|
||||
}
|
||||
public AlgoDisplayGroup group => key.display_group();
|
||||
public bool can_redo => _can_redo.Value;
|
||||
public bool can_undo => _can_undo.Value;
|
||||
public EnabledValueVM<Color> custom_badge_color { get; } = new EnabledValueVM<Color>();
|
||||
public ObservableCollection<ColorPaletteVM> custom_badge_color_palettes { get; } = new ObservableCollection<ColorPaletteVM>();
|
||||
public Brush badge_fill => _badge_fill;
|
||||
public ObservableCollection<AlgoDefinitionTemplateVM> templates => _templates;
|
||||
/// <see cref="templates_with_custom"/> has the same elements as <see cref="templates"/>, plus the custom-placeholder-item, which is always at index 0.
|
||||
/// The custom-placeholder supports legacy behavior in the ladder/ticket algo popup by allowing the user to manipulate the algo-structure (e.g. change an iceberg-limit order to iceberg-lean-cover-offset etc.).
|
||||
/// However, the custom-placeholder item cannot be used to create/save algo-templates.
|
||||
/// Algo-templates are just a set of values used to populate a concrete algo-structure; the 'structures' can be created in the AlgoStudio (<see cref="AlgoStudioWindow"/>).
|
||||
/// If we were to strictly follow this paradigm, the user would no longer be able to freely change the algo-structure in the ladder/ticket popup (they would be forced to open up a new <see cref="AlgoStudioWindow"/>, and we know how much they hate more clicks!).
|
||||
/// Some users might consider this a regression. The custom-placeholder-item addresses that concern.
|
||||
public ObservableAppendCollection<AlgoDefinitionTemplateVM> templates_with_custom => _templates_with_custom;
|
||||
public ListCollectionView templates_view { get; }
|
||||
// format
|
||||
public AlgoFormats selected_format { get => _selected_format.Value; set => _selected_format.set(value); }
|
||||
public ObservableCollection<AlgoFormats> formats { get; } = new ObservableCollection<AlgoFormats> { AlgoFormats.Exchange, AlgoFormats.Spreader, AlgoFormats.Exchange | AlgoFormats.Spreader };
|
||||
@@ -321,24 +267,7 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
// idempotent
|
||||
public void prepend_custom_template() {
|
||||
if (_templates_with_custom.appended_front.Count == 0)
|
||||
_templates_with_custom.insert_front(new AlgoDefinitionTemplateVM.CustomTemplatePlaceholderVM(this, _model, algo_root.definition));
|
||||
}
|
||||
|
||||
public AlgoDefinitionTemplateVM template_factory() =>
|
||||
new AlgoDefinitionTemplateVM(this, _model, algo_root.create_def_data().with(name: new_template_name)) {
|
||||
is_selected = true
|
||||
};
|
||||
|
||||
public IObservable<bool> subscribe_is_definition_dirty() => _is_dirty.observe_current(true);
|
||||
public IObservable<bool> subscribe_any_templates_dirty() => _any_templates_dirty_monitor;
|
||||
public IObservable<bool> subscribe_is_definition_or_any_templates_dirty() =>
|
||||
Observable.Merge(
|
||||
subscribe_is_definition_dirty().Select(v => v || _any_templates_dirty_monitor.is_above_threshold),
|
||||
subscribe_any_templates_dirty().Select(v => v || is_dirty))
|
||||
.DistinctUntilChanged();
|
||||
|
||||
public void undo() => _restore(_edit_stack.try_undo);
|
||||
public void redo() => _restore(_edit_stack.try_redo);
|
||||
@@ -347,20 +276,9 @@ namespace mbts.spark {
|
||||
_assign_from_data(_committed.Value);
|
||||
_on_committed_changed();
|
||||
}
|
||||
/// <summary>
|
||||
/// Subscribes to the templates source for this definition if it has been persisted (subscription requires a persistant id); does nothing if this definition has not been persisted, or if it is already subscribed (idempotent).
|
||||
/// </summary>
|
||||
public void connect() {
|
||||
if (!_connected) {
|
||||
_connected = true;
|
||||
concat_disposable(_model.algo_model.subscribe_definition_templates(key).OnDispatcher().Subscribe(_template_update_buffer.OnNext));
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable buffer_updates() => _template_update_buffer.buffer_updates();
|
||||
|
||||
public void load_def(CustomOrderDefinitionImmutable def) {
|
||||
var key = OrderDataHelper.get_shallow_factory_key(def);
|
||||
var key = AlgoKey.from_custom_def(def);
|
||||
if (algo_root?.id == key) {
|
||||
algo_root.load_from(def);
|
||||
} else if (_algo_roots.TryGetValue(key, out IAlgoVM vm)) {
|
||||
@@ -384,7 +302,7 @@ namespace mbts.spark {
|
||||
case 1: load_def(def_list[0]); break;
|
||||
default:
|
||||
var def = def_list[0];
|
||||
var key = OrderDataHelper.get_shallow_factory_key(def);
|
||||
var key = AlgoKey.from_custom_def(def);
|
||||
if (algo_root?.id == key) {
|
||||
algo_root.load_many(def_list);
|
||||
} else if (_algo_roots.TryGetValue(key, out IAlgoVM vm)) {
|
||||
@@ -414,7 +332,7 @@ namespace mbts.spark {
|
||||
new AlgoDefinitionVM(_model, def, selected_format, editing_permissions_mask);
|
||||
|
||||
public AlgoData create_algo_def() =>
|
||||
new AlgoData(algo_root.definition, name, key);
|
||||
AlgoData.create(algo_root == null ? new CustomOrderDefinitionImmutable(new CustomOrderDefinition()) : algo_root.definition, name, key);
|
||||
|
||||
public AlgoDefinitionMetadata create_metadata() =>
|
||||
_metadata.with(abbreviation: abbreviation, badge_color: new NullableColorProto(custom_badge_color.get_nullable()));
|
||||
@@ -443,7 +361,7 @@ namespace mbts.spark {
|
||||
|
||||
void _assign_from_data(string name, CustomOrderDefinitionImmutable definition) {
|
||||
this.name = name;
|
||||
algo_root = _algo_roots.GetOrAdd(OrderDataHelper.get_shallow_factory_key(definition), _ => _mk_algo_root(definition));
|
||||
algo_root = _algo_roots.GetOrAdd(AlgoKey.from_custom_def(definition), _ => _mk_algo_root(definition));
|
||||
algo_root.load_from(definition);
|
||||
}
|
||||
|
||||
@@ -458,18 +376,12 @@ namespace mbts.spark {
|
||||
public Snapshot current_value => _version.value;
|
||||
|
||||
IAlgoVM _mk_algo_root(CustomOrderDefinitionImmutable definition) =>
|
||||
OrderTypeVMFactory.create(new AlgoData(definition), is_expanded: true);
|
||||
OrderTypeVMFactory.create(AlgoData.from_custom_def(definition), is_expanded: true);
|
||||
|
||||
void _on_metadata(AlgoDefinitionMetadata metadata) {
|
||||
using (_refresh_on_local_changes.supress_execution()) {
|
||||
_abbreviation.set(metadata.abbreviation);
|
||||
custom_badge_color.set_nullable(metadata.badge_color);
|
||||
foreach (var t in templates) {
|
||||
if (t.key.mode == AlgoKey.Mode.Persisted)
|
||||
t.on_meta(t.key == metadata.default_template, metadata.favorite_templates.Contains(t.key));
|
||||
else
|
||||
t.on_meta(false, false);
|
||||
}
|
||||
_version.increment_version();
|
||||
if (_edit_stack.size == 1 && _edit_stack.pos == 1) {
|
||||
// this is a hack to account for the metadata being part of the committed state,
|
||||
@@ -482,22 +394,6 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
void _on_templates(TableUpdate<AlgoTemplateData, AlgoKey> table) {
|
||||
foreach (var rec in table.removed)
|
||||
_templates.Remove(rec.key);
|
||||
foreach (var rec in table.changed)
|
||||
_templates[rec.key].load_from(rec);
|
||||
foreach (var rec in table.added) {
|
||||
if (_templates.TryGetValue(rec.key, out AlgoDefinitionTemplateVM tmp)) {
|
||||
tmp.load_from(rec);
|
||||
} else {
|
||||
tmp = new AlgoDefinitionTemplateVM(this, _model, new AlgoData(rec.value, rec.name, rec.key));
|
||||
tmp.on_meta(tmp.key == _metadata.default_template, _metadata.favorite_templates.Contains(tmp.key));
|
||||
_templates.Add(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _refresh_undo_redo() {
|
||||
_can_undo.refresh();
|
||||
_can_redo.refresh();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using bts;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.collections;
|
||||
using mbts.ui;
|
||||
@@ -14,6 +13,7 @@ using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using StartupData = bts.shell.AlgoStudioWindowFactory.StartupData;
|
||||
|
||||
@@ -37,29 +37,30 @@ namespace mbts.spark {
|
||||
});
|
||||
}
|
||||
|
||||
class AlgoStudioVM : DisposableViewModelBase, IHandleSelectedItemChanged {
|
||||
class AlgoStudioVM : DisposableViewModelBase {
|
||||
static readonly AlgoFormats _studio_formats = AlgoFormats.Exchange;
|
||||
readonly OrdersModel _model;
|
||||
readonly Property<AlgoDefinitionVM> _selected_definition;
|
||||
readonly Property<AlgoDefinitionTemplateVM> _selected_template;
|
||||
readonly Property<IAlgoVM> _editable_content;
|
||||
readonly DelegateProperty<string> _save_cmd_tooltip, _revert_cmd_tooltip;
|
||||
readonly ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey> _definitions = new ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey>(d => d.key);
|
||||
readonly ListCollectionView<AlgoDefinitionVM> _definitions_view;
|
||||
readonly TableUpdateBuffer<AlgoData, AlgoKey> _definitions_update_buffer;
|
||||
readonly ReactiveThresholdMonitor<AlgoDefinitionVM> _any_dirty_monitor = new ReactiveThresholdMonitor<AlgoDefinitionVM>(d => d.subscribe_is_definition_or_any_templates_dirty());
|
||||
readonly ReactiveThresholdMonitor<AlgoDefinitionVM> _any_dirty_monitor = new ReactiveThresholdMonitor<AlgoDefinitionVM>(d => d.subscribe_is_definition_dirty());
|
||||
IReadOnlyDictionary<AlgoKey, AlgoDefinitionMetadata> _persisted_meta = new Dictionary<AlgoKey, AlgoDefinitionMetadata>();
|
||||
|
||||
public AlgoStudioVM(OrdersModel model) {
|
||||
_model = model;
|
||||
_selected_definition = backing<AlgoDefinitionVM>(nameof(selected_definition));
|
||||
_selected_template = backing<AlgoDefinitionTemplateVM>(nameof(selected_template));
|
||||
_editable_content = backing(nameof(editable_content), observe_selected().SelectManyInSeries(tup => tup.template?.observe_algo_root() ?? tup.definition?.observe_algo_root()).OnDispatcher());
|
||||
_editable_content = backing(nameof(editable_content), subscribe_definition().SelectManyInSeries(def => def?.observe_algo_root()).OnDispatcher());
|
||||
_definitions_update_buffer = new TableUpdateBuffer<AlgoData, AlgoKey>(def => def.key);
|
||||
_definitions_view = new ListCollectionView<AlgoDefinitionVM>(definitions) {
|
||||
CustomSort = AlgoCollectionHelper.algo_definition_vm_comparer.to_non_generic()
|
||||
};
|
||||
definitions_view.Filter = d => !(d.key.mode == AlgoKey.Mode.Persisted && _persisted_meta.TryGetValue(d.key, out AlgoDefinitionMetadata meta) && meta.is_hidden);
|
||||
// add/clone
|
||||
add_definition_cmd = new DelegateCommand(add_definition);
|
||||
clone_selected_definition_cmd = new DelegateCommand(clone_selected_definition);
|
||||
// save
|
||||
var save_cmd = new ObservantAsyncCommand<IAlgoStudioContentVM>(save_async);
|
||||
this.save_cmd = save_cmd;
|
||||
@@ -71,13 +72,18 @@ namespace mbts.spark {
|
||||
//
|
||||
_dispose = new CompositeDisposable(
|
||||
_model.algo_model.definitions.Subscribe(_definitions_update_buffer),
|
||||
_definitions_update_buffer.OnDispatcher().Subscribe(u => AlgoCollectionHelper.update_persisted_definitions(_definitions, u, d => AlgoDefinitionVM.create(model, d, true, _studio_formats))),
|
||||
_definitions_update_buffer.OnDispatcher().Subscribe(u => {
|
||||
var orig_selected_def = selected_definition;
|
||||
AlgoCollectionHelper.update_persisted_definitions(_definitions, u, d => AlgoDefinitionVM.create(model, d, _studio_formats));
|
||||
if (selected_definition == null)
|
||||
selected_definition = definitions.FirstOrDefault();
|
||||
}),
|
||||
_model.algo_model.metadata_service.subscribe_metadata().OnDispatcher().Subscribe(meta => {
|
||||
_persisted_meta = meta;
|
||||
definitions_view.Refresh();
|
||||
}),
|
||||
ObservableAutoWrapper.connect(observe_selected().Select(t => t.template?.edit_pane_visual_states ?? t.definition?.edit_pane_visual_states ?? new ObservableCollection<string>()), content_visual_states),
|
||||
observe_selected().SelectManyInSeries(t => t.template?.observe_is_dirty() ?? t.definition?.subscribe_is_definition_or_any_templates_dirty() ?? Observable.Return(false)).OnDispatcher()
|
||||
ObservableAutoWrapper.connect(subscribe_definition().Select(def => def?.edit_pane_visual_states ?? new ObservableCollection<string>()), content_visual_states),
|
||||
subscribe_definition().SelectManyInSeries(def => def?.subscribe_is_definition_dirty() ?? Observable.Return(false)).OnDispatcher()
|
||||
.Subscribe(new CompositeObserver<bool>(save_cmd, revert_cmd) { val => {
|
||||
_save_cmd_tooltip.refresh();
|
||||
_revert_cmd_tooltip.refresh();
|
||||
@@ -85,77 +91,64 @@ namespace mbts.spark {
|
||||
ObservableAutoWrapper.connect_item_hooks(definitions, d => new CompositeDisposable(_any_dirty_monitor.join(d), d), true));
|
||||
// local methods
|
||||
string get_revert_tooltip() {
|
||||
if (content_source is AlgoDefinitionTemplateVM tmp) {
|
||||
switch (tmp.key.mode) {
|
||||
case AlgoKey.Mode.Persisted: return tmp.is_dirty ? $"Revert changes to '{tmp.name}'" : "No changes to revert";
|
||||
}
|
||||
} else if (content_source is AlgoDefinitionVM def) {
|
||||
switch (def.key.mode) {
|
||||
case AlgoKey.Mode.Persisted: return def.is_dirty || def.templates.Any(t => t.is_dirty) ? $"Revert changes to '{def.name}' templates" : "No changes to revert";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
var def = selected_definition;
|
||||
return def != null && def.key.mode == AlgoKey.Mode.Persisted ?
|
||||
(def.is_dirty ? $"Revert changes to '{def.name}' templates" : "No changes to revert") :
|
||||
null;
|
||||
}
|
||||
async Task save_async(IAlgoStudioContentVM content) {
|
||||
if (content is AlgoDefinitionTemplateVM tmp) {
|
||||
await save_template(tmp);
|
||||
} else if (content is AlgoDefinitionVM def) {
|
||||
if (content is AlgoDefinitionVM def)
|
||||
await save_definition(def);
|
||||
}
|
||||
}
|
||||
string get_save_tooltip() {
|
||||
if (content_source is AlgoDefinitionTemplateVM tmp) {
|
||||
switch (tmp.key.mode) {
|
||||
case AlgoKey.Mode.Unpersisted: return tmp.is_dirty ? $"Save '{tmp.name}'" : $"Cannot Save '{tmp.name}'";
|
||||
case AlgoKey.Mode.Persisted: return tmp.is_dirty ? $"Save changes to '{tmp.name}'" : "No changes to save";
|
||||
}
|
||||
} else if (content_source is AlgoDefinitionVM def) {
|
||||
var def = selected_definition;
|
||||
if (def != null) {
|
||||
switch (def.key.mode) {
|
||||
case AlgoKey.Mode.Unpersisted: return def.is_dirty || def.templates.Any(t => t.is_dirty) ? $"Save '{def.name}'" : $"Cannot Save '{def.name}'";
|
||||
case AlgoKey.Mode.Persisted: return def.is_dirty || def.templates.Any(t => t.is_dirty) ? $"Save changes to '{def.name}' templates" : "No changes to save";
|
||||
case AlgoKey.Mode.Unpersisted: return def.is_dirty ? $"Save '{def.name}'" : $"Cannot Save '{def.name}'";
|
||||
case AlgoKey.Mode.Persisted: return def.is_dirty ? $"Save changes to '{def.name}' templates" : "No changes to save";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void add_definition() =>
|
||||
add_definition_common(AlgoDefinitionVM.new_algo_def, AlgoDefinitionVM.new_algo_name);
|
||||
|
||||
void clone_selected_definition() {
|
||||
if (selected_definition == null || selected_definition.algo_root == null) {
|
||||
add_definition();
|
||||
} else {
|
||||
add_definition_common(
|
||||
selected_definition.algo_root.definition,
|
||||
string.IsNullOrEmpty(selected_definition.name) ? AlgoDefinitionVM.new_algo_name : $"{selected_definition.name} - Copy");
|
||||
}
|
||||
}
|
||||
|
||||
void add_definition_common(CustomOrderDefinitionImmutable def, string name) {
|
||||
var new_def = AlgoDefinitionVM.create_placeholder(_model, def, name, _studio_formats);
|
||||
_definitions.Add(new_def);
|
||||
selected_definition = new_def;
|
||||
request_view_focus_name?.Invoke(new_def);
|
||||
}
|
||||
}
|
||||
|
||||
event Action<(AlgoDefinitionVM definition, AlgoDefinitionTemplateVM template)> _selected_changed;
|
||||
public event Action<AlgoDefinitionVM> request_view_focus_name;
|
||||
|
||||
public ObservableCollection<AlgoDefinitionVM> definitions => _definitions;
|
||||
public ObservableCollection<string> content_visual_states { get; } = new ObservableCollection<string>();
|
||||
public AlgoDefinitionVM selected_definition { get => _selected_definition.Value; set => _selected_definition.set(value); }
|
||||
public AlgoDefinitionTemplateVM selected_template { get => _selected_template.Value; set => _selected_template.set(value); }
|
||||
public IAlgoStudioContentVM content_source => (IAlgoStudioContentVM)selected_template ?? selected_definition;
|
||||
public IAlgoVM editable_content => _editable_content.Value;
|
||||
public ICollectionView<AlgoDefinitionVM> definitions_view => _definitions_view;
|
||||
public ICommand add_definition_cmd { get; }
|
||||
public ICommand clone_selected_definition_cmd { get; }
|
||||
public ICommand save_cmd { get; }
|
||||
public ICommand revert_cmd { get; }
|
||||
public string save_cmd_tooltip => _save_cmd_tooltip.Value;
|
||||
public string revert_cmd_tooltip => _revert_cmd_tooltip.Value;
|
||||
|
||||
public void add_new_definition() =>
|
||||
_add_new_definition(AlgoDefinitionVM.new_algo_def, AlgoDefinitionVM.new_algo_name);
|
||||
|
||||
public void add_new_definition_clone(AlgoDefinitionVM src_definition) {
|
||||
if (src_definition == null || src_definition.algo_root == null) {
|
||||
add_new_definition();
|
||||
} else {
|
||||
_add_new_definition(src_definition.algo_root.definition, string.IsNullOrEmpty(src_definition.name) ? AlgoDefinitionVM.new_algo_name : $"{src_definition.name} - Copy");
|
||||
}
|
||||
}
|
||||
|
||||
void _add_new_definition(CustomOrderDefinitionImmutable def, string name) =>
|
||||
_definitions.Add(AlgoDefinitionVM.create_placeholder(_model, def, name, _studio_formats));
|
||||
|
||||
public StartupData create() => new StartupData();
|
||||
public void load_from(StartupData state) { }
|
||||
public IObservable<AlgoDefinitionVM> subscribe_definition() => _selected_definition.observe_current(true);
|
||||
public IObservable<AlgoDefinitionTemplateVM> subscribe_template() => _selected_template.observe_current(true);
|
||||
public IObservable<(AlgoDefinitionVM definition, AlgoDefinitionTemplateVM template)> observe_selected() =>
|
||||
Observable.Create<(AlgoDefinitionVM definition, AlgoDefinitionTemplateVM template)>(o => {
|
||||
_selected_changed += o.OnNext;
|
||||
return Disposable.Create(() => _selected_changed -= o.OnNext);
|
||||
});
|
||||
public IObservable<bool> subscribe_dirty() => _any_dirty_monitor;
|
||||
|
||||
public async Task remove_definition(AlgoDefinitionVM definition) {
|
||||
@@ -163,87 +156,43 @@ namespace mbts.spark {
|
||||
await _model.algo_model.remove_definition(definition.key);
|
||||
} else {
|
||||
// This is a new definition, which hasn't been saved/persisted yet. No need to call the server.
|
||||
definition.templates.Clear();
|
||||
definitions.Remove(definition);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task remove_template(AlgoDefinitionTemplateVM template) {
|
||||
if (template.key.mode == AlgoKey.Mode.Persisted) {
|
||||
await _model.algo_model.remove_template(template.key);
|
||||
} else {
|
||||
// This is a new template, which hasn't been saved/persisted yet. No need to call the server.
|
||||
template.owner.templates.Remove(template);
|
||||
}
|
||||
}
|
||||
public Task save_definition(AlgoDefinitionVM definition) =>
|
||||
_save(new[] { definition });
|
||||
|
||||
public async Task save_definition(AlgoDefinitionVM definition) {
|
||||
using (_save_op()) {
|
||||
var newly_persisted_keys = await AlgoPersistenceHelper.persist_definition(_model, _definitions, definition);
|
||||
AlgoKey pkey;
|
||||
switch (definition.key.mode) {
|
||||
case AlgoKey.Mode.Unpersisted:
|
||||
if (!newly_persisted_keys.TryGetValue(definition.key, out pkey))
|
||||
return;
|
||||
break;
|
||||
case AlgoKey.Mode.Persisted:
|
||||
pkey = definition.key;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
await _model.algo_model.metadata_service.update_metadata(pkey, _ => definition.create_metadata().with(id: pkey));
|
||||
}
|
||||
}
|
||||
public Task save_all() =>
|
||||
_save(definitions);
|
||||
|
||||
public async Task save_template(AlgoDefinitionTemplateVM template) {
|
||||
using (_save_op())
|
||||
await AlgoPersistenceHelper.persist_template(_model, _definitions, template);
|
||||
}
|
||||
|
||||
public async Task save_all() =>
|
||||
await _save(definitions.WrapSelector(def => (def, def.templates.AsList()), t => t.def));
|
||||
|
||||
async Task _save(IList<(AlgoDefinitionVM definition, IList<AlgoDefinitionTemplateVM> templates)> definition_templates) {
|
||||
using (_save_op())
|
||||
await AlgoPersistenceHelper.persist(_model, definition_templates);
|
||||
}
|
||||
|
||||
IDisposable _save_op() {
|
||||
int? def_index = selected_definition == null ? (int?)null : _definitions.IndexOf(selected_definition);
|
||||
int? tmp_index = selected_template == null ? (int?)null : selected_definition.templates.IndexOf(selected_template);
|
||||
return new CompositeDisposable(_definitions_update_buffer.buffer_updates()) {
|
||||
() => {
|
||||
if (def_index.HasValue) {
|
||||
var def = _definitions[def_index.Value];
|
||||
if (tmp_index.HasValue) {
|
||||
var tmp = def.templates[tmp_index.Value];
|
||||
tmp.is_selected = true;
|
||||
} else def.is_selected = true;
|
||||
async Task _save(IEnumerable<AlgoDefinitionVM> definitions) {
|
||||
var orig_selected = selected_definition;
|
||||
var orig_selected_index = orig_selected == null ? -1 : this.definitions.IndexOf(orig_selected);
|
||||
using (_definitions_update_buffer.buffer_updates()) {
|
||||
var def_list = definitions as IList<AlgoDefinitionVM> ?? definitions.ToList();
|
||||
await Task.WhenAll(definitions.Select(async d => {
|
||||
var def = await _model.algo_model.save_definition(d.create_algo_def());
|
||||
if (def.HasValue) {
|
||||
// don't forget to save metadata
|
||||
await _model.algo_model.metadata_service.update_metadata(def.Value.key, _ => d.create_metadata().with(def.Value.key));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Despite the existence of these properties: <see cref="AlgoDefinitionVM.is_selected"/>, and <see cref="AlgoDefinitionTemplateVM.is_selected"/>,
|
||||
/// <see cref="AlgoStudioVM"/> still implements <see cref="IHandleSelectedItemChanged"/> to update the properties that depend on the <see cref="System.Windows.Controls.TreeView.SelectedItem"/>.
|
||||
/// [John]I am going to Uganda in 3 days (today is 1/3/2019), and decided against removing this legacy-dependency at the last minute.
|
||||
/// Fwiw, <see cref="AlgoDefinitionVM.is_selected"/> and <see cref="AlgoDefinitionTemplateVM.is_selected"/> are bound to <see cref="System.Windows.Controls.TreeViewItem.IsSelected"/>.
|
||||
/// They serve to prevent a quirky default behavior in TreeView: double clicking any TreeViewItem collapses the entire TreeView.
|
||||
/// This behavior wasn't just quirky either, it actually conflicted with the double-click-to-set-template-as-default behavior.
|
||||
/// (for details, <see cref="mbts.ui.spark.AlgoStudioView.EnableToggleDefaultOnDoubleClickProperty"/>)
|
||||
void IHandleSelectedItemChanged.on_selected_item_changed(object old_value, object new_value) {
|
||||
if (new_value is AlgoDefinitionVM def) {
|
||||
selected_definition = def;
|
||||
selected_template = null;
|
||||
} else if (new_value is AlgoDefinitionTemplateVM templ) {
|
||||
selected_definition = templ.owner;
|
||||
selected_template = templ;
|
||||
} else {
|
||||
selected_definition = null;
|
||||
selected_template = null;
|
||||
return def;
|
||||
})).ContinueOnDispatcher(t => {
|
||||
if (t.Status != TaskStatus.RanToCompletion) {
|
||||
Log.Ui.debug_fatal($"Algo Studio error saving definitions: unexpected Task.Status '{t.Status}'.");
|
||||
return;
|
||||
}
|
||||
var persisted_data = t.Result;
|
||||
Log.assert(persisted_data.Length == def_list.Count);
|
||||
for (int i = 0; i < persisted_data.Length; i++) {
|
||||
if (!def_list[i].key.is_persisted && persisted_data[i].HasValue)
|
||||
_definitions.Replace(def_list[i].key, def_list[i].to_persisted(persisted_data[i].Value));
|
||||
}
|
||||
if (selected_definition != orig_selected && orig_selected_index >= 0 && orig_selected_index < this.definitions.Count)
|
||||
selected_definition = this.definitions[orig_selected_index];
|
||||
});
|
||||
}
|
||||
_selected_changed?.Invoke((selected_definition, selected_template));
|
||||
RaisePropertyChanged(nameof(content_source));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,6 @@ namespace mbts.spark {
|
||||
public bool key_match(KeyEventArgs e) => _algo_root?.key_match(e) ?? false;
|
||||
public void load_from(CustomOrderDefinitionImmutable value) => _algo_root?.load_from(value);
|
||||
public void load_many(IEnumerable<CustomOrderDefinitionImmutable> values) => _algo_root?.load_many(values);
|
||||
public Task<CustomOrderDefinitionImmutable?> load_from_async(CustomOrderDefinitionImmutable definition) => _algo_root is IAlgoVM vm ? vm.load_from_async(definition) : Task.FromResult<CustomOrderDefinitionImmutable?>(null);
|
||||
void IAlgoVM.ensure_init() => _value?.algo_root?.ensure_init();
|
||||
|
||||
IDisposable _set_hooks(AlgoDefinitionVM vm) {
|
||||
|
||||
+3
@@ -513,6 +513,9 @@
|
||||
LegTuples="{Binding leg_tuples}"
|
||||
RemoveLegCommand="{Binding remove_leg}"
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
IsEnabled="{Binding IsReadOnly, RelativeSource={RelativeSource AncestorType=usp:AlgoEditorGridView}, Converter={StaticResource NotConverter}}"
|
||||
HedgePricing="{Binding hedge_pricing}"
|
||||
HedgePricingItemsSource="{Binding hedge_pricing_options}"
|
||||
/>
|
||||
</AdornerDecorator>
|
||||
</DataTemplate>
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ using System.Reactive.Disposables;
|
||||
namespace mbts.spark {
|
||||
static class AlgoParamRowsHelper {
|
||||
public static IDisposable connect(AlgoDefinitionVM algo, ObservableAggregateCollection<AlgoParamRowVM> rows, bool include_algo_type_rows) =>
|
||||
algo == null ? null : new CompositeDisposable(algo.observe_algo_root().SubscribeEach(a => {
|
||||
algo == null ? null : new CompositeDisposable(algo.observe_algo_root().OnDispatcher().SubscribeEach(a => {
|
||||
rows.clear_collections();
|
||||
if (a == null)
|
||||
return Disposable.empty;
|
||||
@@ -77,7 +77,7 @@ namespace mbts.spark {
|
||||
include_algo_type_rows ?
|
||||
(ObservableCollection<AlgoParamRowVM>)new ObservableAppendCollection<AlgoParamRowVM>(child_rows, selector_wrapper) :
|
||||
child_rows,
|
||||
s.subscribe_selected_algo().SubscribeEach(a => {
|
||||
s.subscribe_selected_algo().OnDispatcher().SubscribeEach(a => {
|
||||
s.value = a;
|
||||
child_rows.clear_collections();
|
||||
return _wrap_rows(a, selector_wrapper, child_rows, include_algo_type_rows);
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ namespace mbts.ui.spark {
|
||||
class AlgoParamTemplateSelector : DataTemplateSelector {
|
||||
public static readonly Type generic_algo_param_itype = typeof(IAlgoParamVM<>);
|
||||
|
||||
KeyedCollection<TemplateSelectorKeyValue, string> _keyed_templates = new KeyedCollection<TemplateSelectorKeyValue, string>(tsk => tsk.Key);
|
||||
KeyedCollection<TemplateSelectorKeyValue, string> _keyed_templates = new KeyedCollection<TemplateSelectorKeyValue, string>(tsk => tsk.Key.ToString());
|
||||
|
||||
public DataTemplate BoolTemplate { get; set; }
|
||||
public DataTemplate NullableBoolTemplate { get; set; }
|
||||
|
||||
@@ -194,20 +194,27 @@ namespace mbts.spark {
|
||||
readonly bts.persist.algo.AlgoPersistedKey[] _favorite_algos;
|
||||
[ProtoMember(2)]
|
||||
public readonly int column_count;
|
||||
[ProtoMember(3)]
|
||||
readonly bts.persist.algo.AlgoPersistedKey[] _stickied_algos;
|
||||
|
||||
public LadderFavoriteAlgosSettings(AlgoKey[] favorite_algos, int column_count)
|
||||
: this(ArrayHelper.safe_convert(favorite_algos, k => k.persisted), column_count) { }
|
||||
public LadderFavoriteAlgosSettings(AlgoKey[] favorite_algos, int column_count, AlgoKey[] stickied_algos)
|
||||
: this(ArrayHelper.safe_convert(favorite_algos, k => new bts.persist.algo.AlgoPersistedKey(k.persisted)),
|
||||
column_count,
|
||||
ArrayHelper.safe_convert(stickied_algos, k => new bts.persist.algo.AlgoPersistedKey(k.persisted))) { }
|
||||
|
||||
LadderFavoriteAlgosSettings(bts.persist.algo.AlgoPersistedKey[] favorite_algos, int column_count) {
|
||||
LadderFavoriteAlgosSettings(bts.persist.algo.AlgoPersistedKey[] favorite_algos, int column_count, bts.persist.algo.AlgoPersistedKey[] stickied_algos) {
|
||||
_favorite_algos = favorite_algos;
|
||||
this.column_count = column_count;
|
||||
_stickied_algos = stickied_algos;
|
||||
}
|
||||
|
||||
public IReadOnlyList<AlgoKey> favorite_algos => ArrayHelper.safe_convert(_favorite_algos, k => k.to_algo_key());
|
||||
public IReadOnlyList<AlgoKey> stickied_algos => ArrayHelper.safe_convert(_stickied_algos, k => k.to_algo_key());
|
||||
|
||||
public bool Equals(LadderFavoriteAlgosSettings other) =>
|
||||
column_count == other.column_count &&
|
||||
favorite_algos.SequenceEqual(other.favorite_algos);
|
||||
favorite_algos.SequenceEqual(other.favorite_algos) &&
|
||||
stickied_algos.SequenceEqual(other.stickied_algos);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
EqualityComparer.equals_obj(this, obj);
|
||||
@@ -215,7 +222,8 @@ namespace mbts.spark {
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
column_count,
|
||||
EqualityComparer.aggregate_hash_limited(favorite_algos));
|
||||
EqualityComparer.aggregate_hash_limited(favorite_algos),
|
||||
EqualityComparer.aggregate_hash_limited(stickied_algos));
|
||||
|
||||
public static bool operator ==(LadderFavoriteAlgosSettings a, LadderFavoriteAlgosSettings b) => a.Equals(b);
|
||||
public static bool operator !=(LadderFavoriteAlgosSettings a, LadderFavoriteAlgosSettings b) => !a.Equals(b);
|
||||
@@ -273,7 +281,7 @@ namespace mbts.spark {
|
||||
order_entry_panel_initial_width = _default_order_entry_panel_width;
|
||||
zoom_factor = _default_zoom_factor;
|
||||
use_defaults = true;
|
||||
favorite_algos = new LadderFavoriteAlgosSettings(null, 2);
|
||||
favorite_algos = new LadderFavoriteAlgosSettings(null, 2, null);
|
||||
show_warn_on_cross = _default_show_warn_on_cross;
|
||||
}
|
||||
|
||||
@@ -339,6 +347,7 @@ namespace mbts.spark {
|
||||
zoom_slider_visible = false,
|
||||
zoom_factor = _default_zoom_factor,
|
||||
net_change = NetChangeTerms.None,
|
||||
mini_ob = new LadderMiniOrderBookSettings(bts.shell.orderbook.OrderBookStartupData.default_mini_grid.general),
|
||||
};
|
||||
rv.row_height = LadderRender.measure_row_height(rv);
|
||||
return rv;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using bts.api;
|
||||
using bts.api;
|
||||
using bts.md;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
@@ -12,10 +11,10 @@ using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using LadderSettings = mbts.spark.LadderSettings;
|
||||
@@ -23,28 +22,31 @@ using LadderSettings = mbts.spark.LadderSettings;
|
||||
namespace bts.shell {
|
||||
class LadderWindowFactory : ShellWindowFactory<LadderSaveState> {
|
||||
const string _window_type = "ladder";
|
||||
const int _version_where_stickied_algos_introduced = 2;
|
||||
readonly LadderConfig _ladder_conf;
|
||||
readonly IBus _bus;
|
||||
readonly IWindowManager _wm;
|
||||
readonly MBOService _mbo;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly IShellWindowThreadIDProvider _thread_id_provider;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
|
||||
|
||||
public LadderWindowFactory(
|
||||
IShellModule owner,
|
||||
IBus bus,
|
||||
IWindowManager wm,
|
||||
UserSettingsHub settings_hub,
|
||||
IShellModule owner,
|
||||
IBus bus,
|
||||
IWindowManager wm,
|
||||
UserSettingsHub settings_hub,
|
||||
OrdersModel orders_model,
|
||||
MBOService mbo)
|
||||
|
||||
: base(owner, "SparkLadder") {
|
||||
: base(owner, "SparkLadder", 2) {
|
||||
_bus = bus;
|
||||
_wm = wm;
|
||||
_settings_hub = settings_hub;
|
||||
_orders_model = orders_model;
|
||||
_ladder_conf = new LadderConfig(Configuration.get<bts.config.LadderKnobs>(), Configuration.get<bts.config.MultiThreadedWindows>().ladder);
|
||||
_thread_id_provider = ShellWindowThreadIDFactory.create("Ladder", _ladder_conf.ladders_on_threads);
|
||||
_mbo = mbo;
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
@@ -61,6 +63,16 @@ namespace bts.shell {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override Task<IShellWindow> try_upgrade_state(Stream load_stream, int version) {
|
||||
var state = ProtoBuf.Serializer.DeserializeWithLengthPrefix<LadderSaveState>(load_stream, ProtoBuf.PrefixStyle.Fixed32);
|
||||
if (version < _version_where_stickied_algos_introduced) {
|
||||
// We want to assign any existing favorite algos to stickied algos upon setting upgrade
|
||||
var favorite_algos_array = state.settings.favorite_algos.favorite_algos.ToArray();
|
||||
state.settings.favorite_algos = new LadderFavoriteAlgosSettings(favorite_algos_array, state.settings.favorite_algos.column_count, favorite_algos_array);
|
||||
}
|
||||
return load(state);
|
||||
}
|
||||
|
||||
LadderSaveState _get_state(LadderVM vm, uint id) {
|
||||
LadderSettings s = vm.get_save_state();
|
||||
if (id != 0) {
|
||||
@@ -68,8 +80,7 @@ namespace bts.shell {
|
||||
// profile was deleted
|
||||
id = 0;
|
||||
} else {
|
||||
var curr_profile = _fit_to_sec(result.value, vm.security.ContractCase);
|
||||
if (!s.Equals(curr_profile)) {
|
||||
if (!s.Equals(result.value)) {
|
||||
// ladder's settings diverged from the profile (this happens when the user changes settings outside of the settings dialog, such as column widths, left panel width, etc.)
|
||||
id = 0;
|
||||
}
|
||||
@@ -78,10 +89,8 @@ namespace bts.shell {
|
||||
return new LadderSaveState { security_key = vm.security.Id, settings = s, settings_id = id };
|
||||
}
|
||||
|
||||
int _ladder_count = 0;
|
||||
IShellWindow create_ladder_window(SecurityMD md, LadderSettings state_settings, uint settings_id = 0) {
|
||||
IShellWindow create_ladder_window(SecurityMD md, LadderSettings last_value, uint settings_id = 0) {
|
||||
IDisposable active_settings_dialog = null;
|
||||
var thread_id = Interlocked.Increment(ref _ladder_count) % Environment.ProcessorCount;
|
||||
// on_shell_window_vm should behave like an event (do not set to null)
|
||||
Action<ShellWindowVM> on_shell_window_vm = _ => { };
|
||||
ShellWindowVM shell_window_vm = null;
|
||||
@@ -95,15 +104,28 @@ namespace bts.shell {
|
||||
lp => lp.id,
|
||||
lp => new SettingsProfile<LadderSettings>(lp.id, lp.name, LadderSettings.sanitize(lp.value), lp.security_criteria))),
|
||||
(us, lps) => us.with(ladder_profiles: lps.Values.Select(lp => new LadderSettingsProfile(lp.id, lp.name, lp.value, lp.security_criteria)).ToArray()));
|
||||
state_settings?.sanitize();
|
||||
LadderSettings last_value = fit_to_sec(state_settings);
|
||||
var settings_subject = new BehaviorSubject<LadderSettings>(last_value);
|
||||
last_value?.sanitize();
|
||||
/// When settings are adjusted outside of the settings dialog, the vm should raise the <see cref="LadderVM.settings_invalidated"/> event.
|
||||
/// In response to that event, 'needs_resync' is set to true. This causes 'last_value' to be refreshed when the next caller requests the latest settings
|
||||
/// (i.e., an IObserver subscribing to the settings_source, or the settings-dialog viewmodel).
|
||||
bool initialized = false, needs_resync = false;
|
||||
LadderVM vm = null;
|
||||
var settings_subject = new Subject<LadderSettings>();
|
||||
var settings_source = Observable.Create<LadderSettings>(o => {
|
||||
ensure_synced();
|
||||
if (last_value != null)
|
||||
o.OnNext(last_value);
|
||||
return settings_subject.Subscribe(s => {
|
||||
if (s != null)
|
||||
o.OnNext(s);
|
||||
});
|
||||
});
|
||||
// vm
|
||||
var vm = LadderVM.create(_bus, model, last_value ?? _settings_hub.current.ladder, settings_subject.Where(s => s != null));
|
||||
vm = LadderVM.create(_bus, model, LadderSettings.sanitize(last_value ?? _settings_hub.current.ladder), settings_source);
|
||||
vm.settings_invalidated += on_settings_invalidated;
|
||||
vm.order_properties_vm.open_algo_popup_on_select = _ladder_conf.open_algo_popup_on_select;
|
||||
vm.order_properties_vm.use_algo_param_grid = _ladder_conf.use_algo_param_grid;
|
||||
vm.request_settings_dialog += open_settings_dialog;
|
||||
bool initialized = false;
|
||||
var settings_subscription = _settings_hub.subscribe_default_settings().Subscribe(on_user_settings);
|
||||
vm.OnDisposed += disposing => {
|
||||
Disposable.dispose(ref settings_subscription);
|
||||
@@ -116,11 +138,25 @@ namespace bts.shell {
|
||||
lv.Resources[kv.Key] = kv.Value;
|
||||
return lv;
|
||||
// local methods
|
||||
LadderSettings fit_to_sec(LadderSettings s) =>
|
||||
_fit_to_sec(s, md.security.ContractCase);
|
||||
void on_settings_invalidated() =>
|
||||
needs_resync = true;
|
||||
|
||||
void ensure_synced() {
|
||||
if (!needs_resync)
|
||||
return;
|
||||
var curr = vm.get_save_state();
|
||||
if (settings_id == 0) {
|
||||
last_value = curr;
|
||||
} else if (!curr.Equals(last_value)) {
|
||||
settings_id = 0;
|
||||
last_value = curr;
|
||||
}
|
||||
needs_resync = false;
|
||||
}
|
||||
|
||||
void on_user_settings(UserSettings us) {
|
||||
if (initialized) {
|
||||
ensure_synced();
|
||||
if (settings_id == 0) {
|
||||
// not on a profile, don't respond to default settings changes
|
||||
return;
|
||||
@@ -128,7 +164,7 @@ namespace bts.shell {
|
||||
if (!last_value.Equals(vm.get_save_state())) {
|
||||
settings_id = 0;
|
||||
} else {
|
||||
settings_subject.OnNext(last_value = fit_to_sec(profile.value));
|
||||
settings_subject.OnNext(last_value = profile.value);
|
||||
}
|
||||
} else {
|
||||
// profile was deleted; keep the current settings and switch to the 'Local' profile.
|
||||
@@ -138,7 +174,7 @@ namespace bts.shell {
|
||||
initialized = true;
|
||||
if (settings_id != 0) {
|
||||
if (us.ladder_profiles.TryGetFirst(p => p.id == settings_id, out var profile)) {
|
||||
last_value = fit_to_sec(profile.value);
|
||||
last_value = profile.value;
|
||||
} else {
|
||||
settings_id = 0;
|
||||
}
|
||||
@@ -148,13 +184,13 @@ namespace bts.shell {
|
||||
// TODO: Use a kind of 'match priority' to resolve conflicts when the security is included in more than 1 profile's security criteria.
|
||||
// In the meantime, simply use the first match.
|
||||
if (lp.security_criteria.includes(md.security)) {
|
||||
last_value = fit_to_sec(lp.value);
|
||||
last_value = lp.value;
|
||||
settings_id = lp.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (last_value == null)
|
||||
last_value = fit_to_sec(_settings_hub.current.ladder);
|
||||
last_value = _settings_hub.current.ladder;
|
||||
}
|
||||
settings_subject.OnNext(last_value);
|
||||
}
|
||||
@@ -170,7 +206,7 @@ namespace bts.shell {
|
||||
svm.load(s);
|
||||
return svm;
|
||||
},
|
||||
fit_to_sec,
|
||||
_ => _,
|
||||
vm.get_save_state(),
|
||||
UserSettings.FileExtensions.LADDER,
|
||||
settings_id);
|
||||
@@ -226,12 +262,11 @@ namespace bts.shell {
|
||||
Log.Ui.warn($"LadderSettingsDialog error {action_desc}: {error}.");
|
||||
}
|
||||
});
|
||||
string thread = _ladder_conf.ladders_on_threads ? $"Ladder{thread_id:00}" : "";
|
||||
return shell_window = ShellWindow.create(
|
||||
module, md.security.ToString(), _window_type, view,
|
||||
(v, s) => save(_get_state(((LadderView)v).vm, settings_id), s),
|
||||
icon: Icons.Ladder,
|
||||
thread_id: thread,
|
||||
thread_id: _thread_id_provider.next_id(),
|
||||
vm => {
|
||||
shell_window_vm = vm;
|
||||
title_source.connect();
|
||||
@@ -252,20 +287,6 @@ namespace bts.shell {
|
||||
}
|
||||
}
|
||||
|
||||
LadderSettings _fit_to_sec(LadderSettings s, Security.ContractOneofCase sec_type) {
|
||||
if (s == null) return null;
|
||||
var algo_key = AlgoID.to_algo_key(s.default_algo, _orders_model.dynamic_helper.dynamic_type_index);
|
||||
if (algo_key.mode == AlgoKey.Mode.Factory) {
|
||||
var fitted_algo = OrderDataHelper.fit_to_type(algo_key.factory.structure.build_algo(_orders_model.dynamic_helper), sec_type, true);
|
||||
if (fitted_algo.HasValue) {
|
||||
// be sure to clone ('s' may be shared)
|
||||
s = SparkUtils.clone_proto(s);
|
||||
s.default_algo = AlgoID.from_custom_def(fitted_algo.Value);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
readonly struct LadderConfig {
|
||||
public readonly uint max_open_ladders;
|
||||
public readonly bool ladders_on_threads, open_algo_popup_on_select, use_algo_param_grid;
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace mbts.spark {
|
||||
readonly IBus _bus;
|
||||
readonly Property<FontFamily> _font_family;
|
||||
readonly Property<FontWeight> _font_weight;
|
||||
readonly Property<double> _font_size;
|
||||
readonly Property<double> _font_size, _grid_row_height;
|
||||
readonly Property<Brush> _foreground, _background, _hoverbackground, _border;
|
||||
readonly Property<bool> _has_multiple_orders_at_same_level, _is_hedge_order;
|
||||
readonly Property<CustomOrderDefinition.CustomOneofCase> _algo_type;
|
||||
@@ -64,6 +64,7 @@ namespace mbts.spark {
|
||||
_font_weight = backing(nameof(font_weight), owner.settings, get_font_weight, get_font_weight(fs));
|
||||
_font_size = backing(nameof(font_size), owner.settings, get_font_size, get_font_size(fs));
|
||||
_foreground = backing(nameof(foreground), owner.settings, get_foreground, get_foreground(fs));
|
||||
_grid_row_height = backing(nameof(grid_row_height), owner.settings, get_row_height, get_row_height(fs));
|
||||
_background = backing(nameof(background), _color_bg_brush);
|
||||
_hoverbackground = backing(nameof(hoverbackground), _color_bg_brush);
|
||||
_border = backing(nameof(border), _color_bg_brush);
|
||||
@@ -75,6 +76,7 @@ namespace mbts.spark {
|
||||
FontWeight get_font_weight(LadderSettings s) => ui.utility.text.TextUtils.from_open_type_weight_safe(s.orders_column.font_weight);
|
||||
double get_font_size(LadderSettings s) => s.orders_column.font_size;
|
||||
Brush get_foreground(LadderSettings s) => s.orders_column.foreground.rgb_brush();
|
||||
double get_row_height(LadderSettings s) => s.row_height;
|
||||
//
|
||||
add_order(op);
|
||||
|
||||
@@ -246,6 +248,7 @@ namespace mbts.spark {
|
||||
public FontFamily font_family => _font_family.Value;
|
||||
public FontWeight font_weight => _font_weight.Value;
|
||||
public double font_size => _font_size.Value;
|
||||
public double grid_row_height => _grid_row_height.Value;
|
||||
public Brush foreground => _foreground.Value;
|
||||
public Brush background { get => _background.Value; private set => _background.Value = value; }
|
||||
public Brush hoverbackground { get => _hoverbackground.Value; private set => _hoverbackground.Value = value; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.persist.algo;
|
||||
using bts.shell.orderbook;
|
||||
using bts.utils;
|
||||
using mbts.collections;
|
||||
@@ -135,8 +136,8 @@ namespace mbts.spark {
|
||||
foreach (var c in _observable_components)
|
||||
c.PropertyChanged -= on_prop_changed;
|
||||
}
|
||||
AccountTuple? account_factory(KeyValuePair<uint, string> kv) => new AccountTuple(kv.Key, kv.Value);
|
||||
AlgoDefinitionVM algo_factory(AlgoData data) => AlgoDefinitionVM.create(_model, data, false);
|
||||
AccountTuple? account_factory(uint key, string name) => new AccountTuple(key, name);
|
||||
AlgoDefinitionVM algo_factory(AlgoData data) => AlgoDefinitionVM.create(_model, data);
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
@@ -355,8 +356,19 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public LadderSettings get_result() {
|
||||
var clone = SparkUtils.clone_proto(_current);
|
||||
clone.mini_ob = new LadderMiniOrderBookSettings(mini_ob.create());
|
||||
LadderSettings clone;
|
||||
if (_current == null) {
|
||||
clone = new LadderSettings {
|
||||
mini_ob = new LadderMiniOrderBookSettings(mini_ob.create())
|
||||
};
|
||||
} else {
|
||||
clone = SparkUtils.clone_proto(_current);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
clone.mini_ob = new LadderMiniOrderBookSettings(_current.mini_ob.value != null ?
|
||||
mini_ob.create().with(sorting_criterion: _current.mini_ob.value.sorting_criterion) :
|
||||
mini_ob.create());
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
}
|
||||
clone.badge_style = badge_style;
|
||||
clone.alternate_order_size = alternate_order_size;
|
||||
clone.favorite_algos = favorite_algos_vm.create();
|
||||
@@ -385,7 +397,7 @@ namespace mbts.spark {
|
||||
clone.default_account = default_account.Value.id;
|
||||
// - algo
|
||||
clone.default_algo = default_algo != null ?
|
||||
AlgoID.from_algo_key(default_algo.key, _model.dynamic_helper.dynamic_type_index) :
|
||||
AlgoID.from_algo_key(default_algo.key) :
|
||||
(_desired_algo ?? LadderSettings.Default.default_algo);
|
||||
// - components
|
||||
clone.auto_clear = auto_clear;
|
||||
@@ -442,7 +454,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
void _try_select_desired_algo(bool bypass_setter = false) {
|
||||
if (_desired_algo.HasValue && (!_default_algo.setter_called || bypass_setter) && _algo_definitions.TryGetValue(AlgoID.to_algo_key(_desired_algo.Value, _model.dynamic_helper.dynamic_type_index), out var vm)) {
|
||||
if (_desired_algo.HasValue && (!_default_algo.setter_called || bypass_setter) && _algo_definitions.TryGetValue(AlgoID.to_algo_key(_desired_algo.Value), out var vm)) {
|
||||
_desired_algo = null;
|
||||
_default_algo.bypass_setter(vm);
|
||||
}
|
||||
@@ -712,12 +724,15 @@ namespace mbts.spark {
|
||||
readonly IObservableCollection<AlgoDefinitionVM> _algo_definitions;
|
||||
readonly ObservableCollection<FavoriteAlgoItem> _raw_items = new ObservableCollection<FavoriteAlgoItem>();
|
||||
int _column_count = 2;
|
||||
HashSet<AlgoPersistedKey> _sticky_algo_keys;
|
||||
IDisposable _sticky_algos_hook;
|
||||
|
||||
public FavoriteAlgosVM(IObservableCollection<AlgoDefinitionVM> algo_definitions) {
|
||||
_algo_definitions = algo_definitions;
|
||||
items = new ObservableAggregateCollection<FavoriteAlgoItem>(_raw_items, new ObservableCollection<FavoriteAlgoItem> { FavoriteAlgoItem.AddButtonPlaceholderItem });
|
||||
add_item_cmd = new DelegateCommand(() => _raw_items.Add(_item_factory()));
|
||||
remove_item_cmd = new DelegateCommand<FavoriteAlgoItem>(fai => _raw_items.Remove(fai));
|
||||
_dispose = Disposable.Create(() => Disposable.dispose(ref _sticky_algos_hook));
|
||||
}
|
||||
|
||||
public ObservableCollection<FavoriteAlgoItem> items { get; }
|
||||
@@ -734,6 +749,17 @@ namespace mbts.spark {
|
||||
fai.selector.try_select(k);
|
||||
return fai;
|
||||
}));
|
||||
_sticky_algo_keys = new HashSet<AlgoPersistedKey>(value.stickied_algos.Where(k => k.is_persisted).Select(x => new AlgoPersistedKey(x.persisted)));
|
||||
Disposable.replace(ref _sticky_algos_hook,
|
||||
ObservableAutoWrapper.connect_item_hooks(_raw_items, x =>
|
||||
x.selector.subscribe_value().Subscribe(algo => {
|
||||
if (algo != null &&
|
||||
algo.key.is_persisted &&
|
||||
(x.is_sticky = _sticky_algo_keys.Remove(new AlgoPersistedKey(algo.key.persisted))) &&
|
||||
_sticky_algo_keys.Count == 0)
|
||||
//
|
||||
Disposable.dispose(ref _sticky_algos_hook);
|
||||
})));
|
||||
}
|
||||
|
||||
public LadderFavoriteAlgosSettings create() =>
|
||||
@@ -742,7 +768,13 @@ namespace mbts.spark {
|
||||
.Where(k => k.HasValue && k.Value.is_persisted)
|
||||
.Select(k => k.Value)
|
||||
.ToArray(),
|
||||
column_count);
|
||||
column_count,
|
||||
_raw_items
|
||||
.Where(fai => fai.is_sticky)
|
||||
.Select(fai => fai.algo_definition?.key ?? fai.selector.desired_key)
|
||||
.Where(k => k.HasValue && k.Value.is_persisted)
|
||||
.Select(k => k.Value)
|
||||
.ToArray());
|
||||
|
||||
FavoriteAlgoItem _item_factory() =>
|
||||
new FavoriteAlgoItem(_algo_definitions, new ListCollectionView<AlgoDefinitionVM>(_algo_definitions) {
|
||||
@@ -755,6 +787,7 @@ namespace mbts.spark {
|
||||
public static readonly FavoriteAlgoItem AddButtonPlaceholderItem = new _AddButtonPlaceholderItem();
|
||||
|
||||
internal readonly KeyedItemSelector<AlgoDefinitionVM, AlgoKey> selector;
|
||||
bool _is_sticky;
|
||||
|
||||
protected FavoriteAlgoItem() { }
|
||||
|
||||
@@ -765,6 +798,7 @@ namespace mbts.spark {
|
||||
|
||||
public ICollectionView<AlgoDefinitionVM> algo_definitions_view { get; }
|
||||
public AlgoDefinitionVM algo_definition { get => selector.value; set => selector.value = value; }
|
||||
public bool is_sticky { get => _is_sticky; set => Set(ref _is_sticky, value); }
|
||||
|
||||
class _AddButtonPlaceholderItem : FavoriteAlgoItem, ITemplateSelectorKey {
|
||||
public string key => LadderSettingsView.FavoriteAlgoAddButtonTemplateKey;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.shell;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.co2;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
@@ -32,6 +34,7 @@ namespace mbts.spark {
|
||||
_show_tif, _show_order_type, _show_size_preset, _show_auto_clear, _show_clear, _show_cancel_buys,
|
||||
_show_cancel_sells, _show_cancel_all, _show_zoom;
|
||||
readonly PropertyAutoWrapper<LadderSettings, Brush> _bid_background, _bid_foreground, _ask_background, _ask_foreground;
|
||||
readonly HashSet<AlgoKey> _sticky_algos = new HashSet<AlgoKey>();
|
||||
double _position, _day_position;
|
||||
bool _show_account_position, _show_day_position;
|
||||
ulong _misc_time = 0;
|
||||
@@ -133,7 +136,7 @@ namespace mbts.spark {
|
||||
//_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(_show_day_position ? day_position : position)));
|
||||
send_order_command = new DelegateCommand<(double price, Side? side, MouseButton button)>(send_order);
|
||||
send_order_command = new AsyncCommand<(double price, Side? side, MouseButton button)>(send_order_async);
|
||||
cancel_command = new AsyncCommand<LadderModel.CancelType>(_model.cancel_orders, true);
|
||||
show_book_command = new DelegateCommand(_show_book);
|
||||
show_ticket_command = new DelegateCommand(_show_ticket);
|
||||
@@ -223,7 +226,10 @@ namespace mbts.spark {
|
||||
.SelectManyInSeries(a => _model.order_gateway.on_security_positions(security.Id)
|
||||
.Select(table => table.state.Values.FirstOrDefault(pos => pos.Item2.Account == a, tup => tup.Item2, (bts.api.position.PositionUnitImmutable?)null)));
|
||||
return Disposable.Create(
|
||||
settings.OnDispatcher().Subscribe(s => order_properties_vm.on_favorite_algos(s.favorite_algos)),
|
||||
settings.OnDispatcher().Subscribe(s => {
|
||||
_sticky_algos.SetItems(s.favorite_algos.stickied_algos);
|
||||
order_properties_vm.on_favorite_algos(s.favorite_algos);
|
||||
}),
|
||||
order_properties_vm,
|
||||
position_src.OnDispatcher().Subscribe(pos => {
|
||||
double total, day;
|
||||
@@ -241,7 +247,7 @@ namespace mbts.spark {
|
||||
|
||||
public IBus bus => _bus;
|
||||
|
||||
public event Action on_tick, request_settings_dialog;
|
||||
public event Action on_tick, request_settings_dialog, settings_invalidated;
|
||||
|
||||
public MDConsumerHandle md => _model.md;
|
||||
// we return only entries who have piqs and whose piqs are not uint.Max
|
||||
@@ -377,7 +383,7 @@ namespace mbts.spark {
|
||||
if (order_properties_vm.selected_account.HasValue)
|
||||
rv.default_account = order_properties_vm.selected_account.Value.id;
|
||||
if (order_properties_vm.selected_algo_definition != null)
|
||||
rv.default_algo = AlgoID.from_algo_key(order_properties_vm.selected_algo_definition.key, _model.order_gateway.dynamic_helper.dynamic_type_index);
|
||||
rv.default_algo = AlgoID.from_algo_key(order_properties_vm.selected_algo_definition.key);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
@@ -394,20 +400,25 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public void send_order((double price, Side? side, MouseButton button) args) {
|
||||
if (order_properties_vm == null || args.button == MouseButton.Middle)
|
||||
return;
|
||||
var size_from_alternate = args.button == MouseButton.Right;
|
||||
if (DoubleHelper.IsNaNOrInfinity(args.price) || !args.side.HasValue || HasErrors || order_properties_vm.HasErrors || (size_from_alternate && current_settings.alternate_order_size == 0)) {
|
||||
return;
|
||||
}
|
||||
if (!size_from_alternate && (!size_calculator.value.HasValue || size_calculator.value.Value == 0)) {
|
||||
size_calculator.flash_error();
|
||||
}
|
||||
else {
|
||||
uint size = size_from_alternate ? current_settings.alternate_order_size : (uint)Math.Abs(size_calculator.value ?? 0);
|
||||
send_order_async(args.side.Value, size, args.price).ignore();
|
||||
public void on_mini_orderbook_closed(bts.shell.orderbook.OrderBookStartupData ob_state) {
|
||||
current_settings = SparkUtils.clone_proto(current_settings);
|
||||
current_settings.mini_ob = new LadderMiniOrderBookSettings(ob_state.orderbook_grid.general);
|
||||
settings_invalidated?.Invoke();
|
||||
}
|
||||
|
||||
public Task send_order_async((double price, Side? side, MouseButton button) args) {
|
||||
if (order_properties_vm != null && args.button != MouseButton.Middle) {
|
||||
var size_from_alternate = args.button == MouseButton.Right;
|
||||
if (!DoubleHelper.IsNaNOrInfinity(args.price) && args.side.HasValue && !HasErrors && !order_properties_vm.HasErrors && (!size_from_alternate || current_settings.alternate_order_size > 0)) {
|
||||
if (!size_from_alternate && (!size_calculator.value.HasValue || size_calculator.value.Value == 0)) {
|
||||
size_calculator.flash_error();
|
||||
} else {
|
||||
uint size = size_from_alternate ? current_settings.alternate_order_size : (uint)Math.Abs(size_calculator.value ?? 0);
|
||||
return send_order_async(args.side.Value, size, args.price);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task send_order_async(Side side, uint size, double price) {
|
||||
@@ -427,6 +438,13 @@ namespace mbts.spark {
|
||||
await _model.order_gateway.send_order(order, price, size);
|
||||
if (auto_clear)
|
||||
size_calculator.value = 0;
|
||||
var fav_algo = order_properties_vm.selected_favorite_algo_definition;
|
||||
if (fav_algo != null && !_sticky_algos.Contains(fav_algo.key) && !order_properties_vm.try_pop_selected_algo_definition()) {
|
||||
var key = new AlgoKey(OrderDataHelper.get_default_algo(OrderDataHelper.get_algo_format(security.ContractCase)));
|
||||
if (order_properties_vm.try_get_algo_definition(key, out var r)) {
|
||||
order_properties_vm.selected_algo_definition = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _update_header() {
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace mbts.spark {
|
||||
LadderVM.create_readonly(_bus,
|
||||
new LadderModel(security_md.Value, _model, _model.mbo_service,
|
||||
new Func<OrderSecurity, bool>(osec => _selected_order_tree.Contains(osec.order.Key))),
|
||||
Observable.Return(LadderSettings.Default)) : null);
|
||||
Observable.Return(LadderSettings.sanitize(LadderSettings.Default))) : null);
|
||||
RaisePropertyChanged(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ namespace mbts.spark {
|
||||
NativePriceSize _last_trade;
|
||||
Func<NativePriceSize, string> _fmt_size = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default());
|
||||
int _old_tooltip_row = int.MaxValue;
|
||||
double _old_last_trade_price = 0.0;
|
||||
NativePriceSize? _old_last_trade;
|
||||
string _direction_arrow = string.Empty;
|
||||
bool _tooltips_are_hooked_up = false;
|
||||
ToolTip tooltip_view = new ToolTip();
|
||||
IDisposable _parameterized_order_popup;
|
||||
@@ -693,12 +694,14 @@ namespace mbts.spark {
|
||||
// At any rate, we need this part hiding the tooltip so that we don't leave random tooltips around
|
||||
// and we have to reset to old row so that when we go back to what could be our preivous row, the tooltip gets drawn again
|
||||
//System.Diagnostics.Debug.WriteLine(" Tooltip - Volume is 0");
|
||||
_old_tooltip_row = int.MaxValue;
|
||||
_hide_tool_tip();
|
||||
//_old_tooltip_row = Int32.MaxValue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//System.Diagnostics.Debug.WriteLine(" Tooltip - not over Volume column");
|
||||
_old_tooltip_row = int.MaxValue;
|
||||
_hide_tool_tip();
|
||||
}
|
||||
}
|
||||
@@ -861,21 +864,43 @@ namespace mbts.spark {
|
||||
void _refresh_volume_at_price() {
|
||||
if (!_settings.volume_column.general.show)
|
||||
return;
|
||||
|
||||
// if the last trade has a new size then we need to explicitly
|
||||
// mark the volume container as dirty because there is a chance
|
||||
// we have arrived here off a size change and not a volume change
|
||||
// or a random change, like applying new settings
|
||||
bool last_trade_size_changed = _old_last_trade.HasValue ? _last_trade.size != _old_last_trade.Value.size : true;
|
||||
if (last_trade_size_changed && _settings.volume_column.show_ltq) {
|
||||
_volume_container.mark_dirty();
|
||||
}
|
||||
var vapls = _volume_container.read_new_value();
|
||||
if (vapls != null) {
|
||||
Log.assert_d(_price_list != null);
|
||||
var ltp = _last_trade.price;
|
||||
var last_index = LadderRender.index_of(_price_list, ltp, _price_comparer);
|
||||
string direction_arrow = string.Empty;
|
||||
if (MathUtils.epsilon_equals(ltp, _old_last_trade_price)) {
|
||||
// do nothing if in here b/c direction_arrow already string.empty
|
||||
} else if (ltp > _old_last_trade_price) {
|
||||
direction_arrow = "▲";
|
||||
} else {
|
||||
direction_arrow = "▼";
|
||||
}
|
||||
LadderRender.render_volumes(_volume_vis_tup.visual, _volume_vis_tup.glyph_context, _price_list, vapls, _settings.row_height, security.ContractSpec, _settings.volume_column, last_index, _last_trade, direction_arrow);
|
||||
_old_last_trade_price = ltp;
|
||||
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
|
||||
LadderRender.render_volumes(_volume_vis_tup.visual, _volume_vis_tup.glyph_context, _price_list, vapls, _settings.row_height, security.ContractSpec, _settings.volume_column, last_index, _get_last_arrow_string());
|
||||
_old_last_trade = _last_trade;
|
||||
}
|
||||
|
||||
string _get_last_arrow_string() {
|
||||
// if _old_last_trade doesn't have a value, this is the first time the ladder has been launched
|
||||
// and so the direction should be drawn without an arrow since there is no basis for comparison
|
||||
if (!_old_last_trade.HasValue) {
|
||||
_direction_arrow = string.Empty;
|
||||
// if the Lasts are different then we need to reformat the direction arrow in some fashion
|
||||
} else if (_old_last_trade != _last_trade) {
|
||||
var ltp = _last_trade.price;
|
||||
if (MathUtils.epsilon_equals(ltp, _old_last_trade.Value.price)) {
|
||||
// update on the same price, so size must be different
|
||||
_direction_arrow = string.Empty;
|
||||
} else if (ltp > _old_last_trade.Value.price) {
|
||||
_direction_arrow = "▲";
|
||||
} else {
|
||||
_direction_arrow = "▼";
|
||||
}
|
||||
}
|
||||
// regardless of if we've arrived here via a new Last (and new direction arrow)
|
||||
// or we're reusing the old one, this format bit is the same
|
||||
return $"{_direction_arrow} {_last_trade.size} ";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interactivity;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.spark {
|
||||
@@ -156,7 +154,10 @@ namespace mbts.spark {
|
||||
popup.SetBinding(System.Windows.Controls.Primitives.Popup.IsOpenProperty,
|
||||
nameof(OrderBookVM.is_row_collection_populated)
|
||||
);
|
||||
popup.Closed += (sender, event_args) => (popup.DataContext as IDisposable)?.Dispose();
|
||||
popup.Closed += (sender, event_args) => {
|
||||
(_parent.DataContext as LadderVM)?.on_mini_orderbook_closed(obvm.create());
|
||||
(popup.DataContext as IDisposable)?.Dispose();
|
||||
};
|
||||
popup.MouseUp += (sender, event_args) => {
|
||||
if (event_args.ChangedButton == MouseButton.Middle) {
|
||||
popup.IsOpen = false;
|
||||
|
||||
@@ -196,8 +196,7 @@ namespace mbts.spark {
|
||||
ContractSpecificationImmutable spec,
|
||||
VolumeColumnSettings settings,
|
||||
int last_index,
|
||||
NativePriceSize last_trade,
|
||||
string direction_arrow
|
||||
string last_size_and_direction
|
||||
) {
|
||||
if (!data.Any()) {
|
||||
_clear_visual(dv);
|
||||
@@ -216,16 +215,16 @@ namespace mbts.spark {
|
||||
Brush total_brush = null;
|
||||
if (settings.total.bars.result.HasValue) {
|
||||
total_brush = new SolidColorBrush(settings.total.bars.result.Value);
|
||||
} else if (components.Count == 0) {
|
||||
} else if (components.Count == 0 && !settings.show_ltq) {
|
||||
_clear_visual(dv);
|
||||
return;
|
||||
}
|
||||
// inflate the max so that the graph never fills the available space
|
||||
double max = data.Max(v => v.Volume) * 1.1;
|
||||
Brush text_brush = settings.general.foreground.rgb_brush();
|
||||
_render_cells(dv, row_height, data.Select(d => (d.Price, d)), settings.general, prices, spec, render_cell);
|
||||
/// local methods
|
||||
void render_cell(DrawingContext dc, int r, VolumeAtPriceLevelImmutable vap) {
|
||||
Brush text_brush = settings.general.foreground.rgb_brush();
|
||||
double y = row_height * r;
|
||||
var last_trade_here = r == last_index;
|
||||
|
||||
@@ -259,7 +258,8 @@ namespace mbts.spark {
|
||||
}
|
||||
if (settings.show_ltq && last_trade_here) {
|
||||
numbers_text.Clear();
|
||||
numbers_text.Add($"{direction_arrow} {last_trade.size} ");
|
||||
numbers_text.Add(last_size_and_direction);
|
||||
text_brush = BTSCoreColors.white;
|
||||
}
|
||||
if (numbers_text.Count > 0) {
|
||||
string str = string.Join("/", numbers_text);
|
||||
|
||||
@@ -805,6 +805,7 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c1"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Command="{Binding DataContext.remove_item_cmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding}" Style="{StaticResource RoundCloseButton}"/>
|
||||
<ComboBox Grid.Column="1" ItemsSource="{Binding algo_definitions_view}" SelectedItem="{Binding algo_definition}">
|
||||
@@ -814,6 +815,7 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<CheckBox Grid.Column="2" VerticalAlignment="Center" VerticalContentAlignment="Center" IsChecked="{Binding is_sticky}"> Stay Enabled?</CheckBox>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</u:KeyedTemplateSelector.Default>
|
||||
@@ -823,6 +825,7 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c1"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="1" HorizontalAlignment="Stretch" Command="{Binding DataContext.add_item_cmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}">Add</Button>
|
||||
</Grid>
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
<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" />
|
||||
<converters:DoubleSubtractionConverter x:Key="DoubleSubtractionConverter" Value="4" />
|
||||
<converters:DoubleSubtractionConverter x:Key="HeightDoubleSubtractionConverter" Value="6" />
|
||||
<Style TargetType="GridViewColumnHeader">
|
||||
<Setter Property="Background" Value="{mbts:BTSColor header}"/>
|
||||
<Setter Property="Foreground" Value="{mbts:BTSColor header_text}"/>
|
||||
@@ -579,7 +581,9 @@
|
||||
<Setter Property="FontSize" Value="{Binding font_size}"/>
|
||||
<Setter Property="Foreground" Value="{Binding foreground}"/>
|
||||
<Setter Property="TextLines" Value="{Binding text_lines}" />
|
||||
<Setter Property="Width" Value="{x:Static controls:Badge.DefaultWidth}"/>
|
||||
<!--<Setter Property="Width" Value="{x:Static controls:Badge.DefaultWidth}"/>-->
|
||||
<Setter Property="Width" Value="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type spark:LadderOrdersHost}}, Converter={StaticResource DoubleSubtractionConverter}}" />
|
||||
<Setter Property="Height" Value="{Binding grid_row_height, Converter={StaticResource HeightDoubleSubtractionConverter}}" />
|
||||
<Setter Property="HasMultipleOrdersAtLevel" Value="{Binding has_multiple_orders_at_same_level}" />
|
||||
<Setter Property="IsHedgeOrder" Value="{Binding is_hedge_order}" />
|
||||
<Setter Property="AlgoType" Value="{Binding algo_type}" />
|
||||
|
||||
@@ -79,8 +79,8 @@ namespace bts.shell.manual_fills {
|
||||
|
||||
public ManualFillsModel(OrdersModel model)
|
||||
: this(model, model.on_manual_fill_tuples,
|
||||
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, kv => new AccountTuple(kv.Key, kv.Value))).Table(u => u.id),
|
||||
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, kv => new UserTuple(kv.Key, kv.Value))).Table(a => a.id)) { }
|
||||
model.factory.subscribe_accounts().Select(d => MappedDictionary.create(d, (k, v) => new AccountTuple(k, v))).Table(u => u.id),
|
||||
model.factory.subscribe_users().Select(d => MappedDictionary.create(d, (k, v) => new UserTuple(k, v))).Table(a => a.id)) { }
|
||||
|
||||
public ManualFillsModel(OrdersModel model, IObservable<TableUpdate<ManualFillTuple, int>> manual_fills, IObservable<TableUpdate<AccountTuple, uint>> accounts, IObservable<TableUpdate<UserTuple, uint>> users) {
|
||||
_model = model;
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace bts.shell.manual_fills {
|
||||
// The security desc lookup could be better optimized, but the speed at which fills are imported is probably not a paramount concern.
|
||||
public static IEnumerable<ParsedManualFill> parse_rows(IEnumerable<string> rows, IReadOnlyDictionary<uint, SecurityImmutable> security_map, IReadOnlyDictionary<uint, string> account_map, IReadOnlyDictionary<uint, string> user_map) =>
|
||||
parse_rows(rows,
|
||||
reverse_lookup(MappedDictionary.create(security_map, kv => kv.Value.ToString())),
|
||||
reverse_lookup(MappedDictionary.create(security_map, s => s.ToString())),
|
||||
reverse_lookup(account_map),
|
||||
reverse_lookup(user_map));
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace bts.shell.manual_fills {
|
||||
var lines = File.ReadAllLines(file_name);
|
||||
var parsed_fills = ManualFillsParser.parse_rows(
|
||||
lines,
|
||||
MappedDictionary.create(security_map, kv => kv.Value.security),
|
||||
MappedDictionary.create(security_map, smd => smd.security),
|
||||
accounts.ToDictionary(a => a.Value.id, a => a.Value.name),
|
||||
users.ToDictionary(u => u.Value.id, u => u.Value.name)
|
||||
).ToList();
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
xmlns:order="clr-namespace:bts.api.orderman;assembly=spark_api"
|
||||
mc:Ignorable="d"
|
||||
MaxHeight="240"
|
||||
MinWidth="875"
|
||||
d:DesignHeight="600" d:DesignWidth="1080"
|
||||
FocusManager.IsFocusScope="True"
|
||||
Background="{StaticResource CommonBackground}">
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace mbts.spark {
|
||||
// This interface is used to hide ColumnBase.SortDirection, and offer a SortDirection member that does NOT require a DependencyProperty lookup.
|
||||
interface ICVOrderBookColumn : IKeyedColumn<Columns>, IGridColumn {
|
||||
ListSortDirection? SortDirection { get; set; }
|
||||
IDataGridColumnFilter<OrderSecurity> filter { get; }
|
||||
IDataGridColumnFilter<OrderSecurityRootPair> filter { get; }
|
||||
IComparer<OrderBookRowVM> comparer { get; }
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ namespace mbts.spark {
|
||||
public OrderBookColumnCollection(IBus bus, OrdersModel model, IObservable<(IEnumerable<SecurityImmutable> added, IEnumerable<SecurityImmutable> removed)> order_securities) {
|
||||
// security
|
||||
Add(security_column = new SecurityColumn(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, SecurityImmutable, SecurityCriteria>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, SecurityImmutable, SecurityCriteria>(
|
||||
order_securities,
|
||||
ort => ort.security ?? SecurityDataHelper.Defaults.SecurityImmutable,
|
||||
ort => ort.order.security ?? SecurityDataHelper.Defaults.SecurityImmutable,
|
||||
SecurityCriteria.from_securities,
|
||||
SecurityCriteria.get_to_securities(bus),
|
||||
nameof(OrderBookRowVM.security),
|
||||
@@ -85,9 +85,9 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.Symbol,
|
||||
r => r.symbol,
|
||||
DefaultColumnWidths.Symbol,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, string>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, string>(
|
||||
mbts.co2.UserSymbol.get_all().Select(u => u.symbol).Distinct().ToList(),
|
||||
ort => ort.security?.Symbol ?? string.Empty,
|
||||
ort => ort.order.security?.Symbol ?? string.Empty,
|
||||
nameof(OrderBookRowVM.symbol)
|
||||
),
|
||||
DataGridFilterKnownTemplates.DiscreteValues,
|
||||
@@ -98,8 +98,8 @@ namespace mbts.spark {
|
||||
"Time",
|
||||
r => r.time_created_text,
|
||||
DefaultColumnWidths.Time,
|
||||
new TimeFilterVM<OrderSecurity>(
|
||||
ort => ort.order.created(),
|
||||
new TimeFilterVM<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.created(),
|
||||
nameof(OrderBookRowVM.time_created)),
|
||||
DataGridFilterKnownTemplates.TimeFilter,
|
||||
_wrap_comparer(r => r.time_created)));
|
||||
@@ -109,8 +109,8 @@ namespace mbts.spark {
|
||||
"Last Change",
|
||||
r => r.last_change_text,
|
||||
DefaultColumnWidths.Time,
|
||||
new TimeFilterVM<OrderSecurity>(
|
||||
ort => ort.order.last_change(),
|
||||
new TimeFilterVM<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.last_change(),
|
||||
nameof(OrderBookRowVM.last_change)),
|
||||
DataGridFilterKnownTemplates.TimeFilter,
|
||||
_wrap_comparer(r => r.last_change)));
|
||||
@@ -120,9 +120,9 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.User,
|
||||
r => r.user,
|
||||
DefaultColumnWidths.User,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, string>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, string>(
|
||||
model.factory.subscribe_users().Select(d => d.Values),
|
||||
ort => ort.order.user(),
|
||||
ort => ort.order.order.user(),
|
||||
nameof(OrderBookRowVM.user)),
|
||||
DataGridFilterKnownTemplates.DiscreteValues,
|
||||
_wrap_comparer(r => r.user)));
|
||||
@@ -132,9 +132,9 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.Account,
|
||||
r => r.account,
|
||||
DefaultColumnWidths.Account,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, string, string[]>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, string, string[]>(
|
||||
model.factory.subscribe_accounts().Select(d => d.Values),
|
||||
ort => ort.order.account(),
|
||||
ort => ort.order.order.account(),
|
||||
vals => vals.ToArray(),
|
||||
vals => Observable.Return(vals),
|
||||
nameof(OrderBookRowVM.account)),
|
||||
@@ -146,9 +146,9 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.Exchange,
|
||||
r => r.exchange.ToString(),
|
||||
DefaultColumnWidths.Exchange,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, Venue>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, Venue>(
|
||||
EnumHelper.get_values_except(Venue.Unset).ToList(),
|
||||
ort => ort.security.exchange_or_default(),
|
||||
ort => ort.order.security.exchange_or_default(),
|
||||
nameof(OrderBookRowVM.exchange)),
|
||||
DataGridFilterKnownTemplates.EnumValues,
|
||||
_wrap_comparer(r => r.exchange)));
|
||||
@@ -158,14 +158,14 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.TimeInForce,
|
||||
r => r.time_in_force.ToString(),
|
||||
DefaultColumnWidths.TimeInForce,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, TimeInForce>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, TimeInForce>(
|
||||
EnumHelper.get_values<TimeInForce>().ToList(),
|
||||
ort => ort.order.time_in_force(),
|
||||
ort => ort.order.order.time_in_force(),
|
||||
nameof(OrderBookRowVM.time_in_force)),
|
||||
DataGridFilterKnownTemplates.EnumValues,
|
||||
_wrap_comparer(r => r.time_in_force)));
|
||||
// (algo items)
|
||||
var algo_items_source = model.subscribe_all_algos().SelectKeyValue(AlgoDataVM.from_data, d => AlgoID.from_algo_key(d.data.key, model.dynamic_helper.dynamic_type_index)).OnDispatcher();
|
||||
var algo_items_source = model.subscribe_all_algos().SelectKeyValue(AlgoDataVM.from_data, d => AlgoID.from_algo_key(d.data.key)).OnDispatcher();
|
||||
var algo_items_comp = AlgoCollectionHelper.algo_data_vm_comparer.to_non_generic();
|
||||
var algo_items_group_desc = new PropertyGroupDescription(nameof(AlgoDataVM.group));
|
||||
// order type
|
||||
@@ -174,9 +174,9 @@ namespace mbts.spark {
|
||||
nameof(OrderBookRowVM.badge_fill),
|
||||
nameof(OrderBookRowVM.display_name),
|
||||
_wrap_comparer(r => r.order_type),
|
||||
DiscreteValuesFilterVM.CreateTableUpdate<OrderSecurity, AlgoDataVM, AlgoID>(
|
||||
o => new AlgoDataVM(o.algo.GetValueOrDefault()),
|
||||
vm => AlgoID.from_algo_key(vm.data.key, model.dynamic_helper.dynamic_type_index), out var order_type_filter_observer, out var order_type_items_view)));
|
||||
DiscreteValuesFilterVM.CreateTableUpdate<OrderSecurityRootPair, AlgoDataVM, AlgoID>(
|
||||
o => new AlgoDataVM(o.order.algo),
|
||||
vm => AlgoID.from_algo_key(vm.data.key), out var order_type_filter_observer, out var order_type_items_view)));
|
||||
_resources.Add(algo_items_source.Subscribe(order_type_filter_observer));
|
||||
order_type_items_view.CustomSort = algo_items_comp;
|
||||
order_type_items_view.GroupDescriptions.Add(algo_items_group_desc);
|
||||
@@ -185,10 +185,10 @@ namespace mbts.spark {
|
||||
Columns.RootAlgoType,
|
||||
nameof(OrderBookRowVM.root_algo_badge_fill),
|
||||
nameof(OrderBookRowVM.root_algo_display_name),
|
||||
_wrap_comparer(r => r.order_tuple.root_algo.value.CustomCase),
|
||||
DiscreteValuesFilterVM.CreateTableUpdate<OrderSecurity, AlgoDataVM, AlgoID>(
|
||||
o => new AlgoDataVM(o.root_algo),
|
||||
vm => AlgoID.from_algo_key(vm.data.key, model.dynamic_helper.dynamic_type_index), out var root_algo_type_observer, out var root_algo_type_items_view),
|
||||
_wrap_comparer(r => r.order_tuple.root.algo.value.CustomCase),
|
||||
DiscreteValuesFilterVM.CreateTableUpdate<OrderSecurityRootPair, AlgoDataVM, AlgoID>(
|
||||
o => new AlgoDataVM(o.root.algo),
|
||||
vm => AlgoID.from_algo_key(vm.data.key), out var root_algo_type_observer, out var root_algo_type_items_view),
|
||||
DefaultColumnHeaders.RootAlgoType));
|
||||
_resources.Add(algo_items_source.Subscribe(root_algo_type_observer));
|
||||
root_algo_type_items_view.CustomSort = algo_items_comp;
|
||||
@@ -199,36 +199,36 @@ namespace mbts.spark {
|
||||
"Sub Type",
|
||||
r => r.order_sub_type_text,
|
||||
80,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, OrderSubType>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, OrderSubType>(
|
||||
EnumHelper.get_values<OrderSubType>().ToList(),
|
||||
ort => ort.order.sub_type(),
|
||||
ort => ort.order.order.sub_type(),
|
||||
nameof(OrderBookRowVM.order_sub_type)),
|
||||
OrderBookGridView.SubTypeFilterTemplateKey,
|
||||
_wrap_comparer(r => r.order_sub_type)));
|
||||
// side
|
||||
Add(side_column = new SideColumn(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, Side>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, Side>(
|
||||
new[] { Side.Buy, Side.Sell },
|
||||
ort => ort.order.side(),
|
||||
ort => ort.order.order.side(),
|
||||
nameof(OrderBookRowVM.side))));
|
||||
// quantity
|
||||
Add(qty_column = new SizeColumn(
|
||||
new UIntMinMax<OrderSecurity>(ort => ort.order.quantity(), nameof(OrderBookRowVM.quantity))));
|
||||
new UIntMinMax<OrderSecurityRootPair>(ort => ort.order.order.quantity(), nameof(OrderBookRowVM.quantity))));
|
||||
// edit quantity column
|
||||
// not usually part of the main order book, but it's useful to add it
|
||||
// rather than debugging the popup window
|
||||
//Add(edit_qty_column = new EditableQtyColumn());
|
||||
// filled quantity
|
||||
Add(filled_qty_column = new FilledColumn(
|
||||
new UIntMinMax<OrderSecurity>(ort => ort.order.filled(), nameof(OrderBookRowVM.filled_quantity))));
|
||||
new UIntMinMax<OrderSecurityRootPair>(ort => ort.order.order.filled(), nameof(OrderBookRowVM.filled_quantity))));
|
||||
// price
|
||||
Add(price_column = new SimpleTextColumn<DoubleMinMaxState>(
|
||||
Columns.Price,
|
||||
"Price",
|
||||
r => r.price_text,
|
||||
DefaultColumnWidths.Price,
|
||||
new PriceMinMax<OrderSecurity>(
|
||||
ort => ort.order.price(),
|
||||
new PriceMinMax<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.price(),
|
||||
nameof(OrderBookRowVM.price)),
|
||||
DataGridFilterKnownTemplates.DoubleMinMax,
|
||||
_wrap_comparer(r => r.price)));
|
||||
@@ -243,8 +243,8 @@ namespace mbts.spark {
|
||||
"Avg Price",
|
||||
r => r.avg_price_text,
|
||||
DefaultColumnWidths.Price,
|
||||
new PriceMinMax<OrderSecurity>(
|
||||
ort => ort.order.avg_price(),
|
||||
new PriceMinMax<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.avg_price(),
|
||||
nameof(OrderBookRowVM.avg_price)),
|
||||
DataGridFilterKnownTemplates.DoubleMinMax,
|
||||
_wrap_comparer(r => r.avg_price)));
|
||||
@@ -254,30 +254,30 @@ namespace mbts.spark {
|
||||
"Last Filled",
|
||||
r => r.last_price_text,
|
||||
DefaultColumnWidths.Price,
|
||||
new PriceMinMax<OrderSecurity>(
|
||||
ort => ort.order.last_price(),
|
||||
new PriceMinMax<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.last_price(),
|
||||
nameof(OrderBookRowVM.last_price)),
|
||||
DataGridFilterKnownTemplates.DoubleMinMax,
|
||||
_wrap_comparer(r => r.last_price)));
|
||||
// piq
|
||||
Add(piq_column = new PiqColumn(
|
||||
new UIntMinMax<OrderSecurity>(ort => ort.piq ?? uint.MaxValue, nameof(OrderBookRowVM.piq))));
|
||||
new UIntMinMax<OrderSecurityRootPair>(ort => ort.order.piq ?? uint.MaxValue, nameof(OrderBookRowVM.piq))));
|
||||
// status
|
||||
Add(status_column = new SimpleTextColumn<OrderStatus[]>(
|
||||
Columns.Status,
|
||||
"Status",
|
||||
r => r.status.ToString(),
|
||||
80,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurity, OrderStatus>(
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, OrderStatus>(
|
||||
EnumHelper.get_values<OrderStatus>().ToList(),
|
||||
ort => ort.order.status(),
|
||||
ort => ort.order.order.status(),
|
||||
nameof(OrderBookRowVM.status)),
|
||||
DataGridFilterKnownTemplates.EnumValues,
|
||||
_wrap_comparer(r => r.status)));
|
||||
// last message
|
||||
Add(last_message_column = new OrderBookLastMessageColumn(
|
||||
new RegexFilterVM<OrderSecurity>(
|
||||
ort => ort.order.last_msg(),
|
||||
new RegexFilterVM<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.last_msg(),
|
||||
nameof(OrderBookRowVM.last_msg))));
|
||||
// security key
|
||||
Add(security_key_column = new SimpleTextColumn<string>(
|
||||
@@ -285,8 +285,8 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.SecurityKey,
|
||||
r => r.security_key.ToString(),
|
||||
DefaultColumnWidths.SecurityKey,
|
||||
new RegexFilterVM<OrderSecurity>(
|
||||
ort => ort.order.security_key().ToString(),
|
||||
new RegexFilterVM<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.security_key().ToString(),
|
||||
nameof(OrderBookRowVM.security_key)),
|
||||
DataGridFilterKnownTemplates.Regex,
|
||||
_wrap_comparer(r => r.security_key)));
|
||||
@@ -296,8 +296,8 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.OrderKey,
|
||||
r => r.order_key.ToString(),
|
||||
DefaultColumnWidths.OrderKey,
|
||||
new RegexFilterVM<OrderSecurity>(
|
||||
ort => ort.order.Key.ToString(),
|
||||
new RegexFilterVM<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.Key.ToString(),
|
||||
nameof(OrderBookRowVM.order_key)),
|
||||
DataGridFilterKnownTemplates.Regex,
|
||||
_wrap_comparer(r => r.order_key)));
|
||||
@@ -307,8 +307,8 @@ namespace mbts.spark {
|
||||
"Parent Key",
|
||||
r => r.parent_order_key.ToString(),
|
||||
DefaultColumnWidths.ParentKey,
|
||||
new RegexFilterVM<OrderSecurity>(
|
||||
ort => ort.order.ParentKey.ToString(),
|
||||
new RegexFilterVM<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.ParentKey.ToString(),
|
||||
nameof(OrderBookRowVM.parent_order_key)),
|
||||
DataGridFilterKnownTemplates.Regex,
|
||||
_wrap_comparer(r => r.parent_order_key)));
|
||||
@@ -318,8 +318,8 @@ namespace mbts.spark {
|
||||
DefaultColumnHeaders.AttachedTargetOrderKey,
|
||||
r => r.order.attached_target_order_key().ToString(),
|
||||
DefaultColumnWidths.AttachedTargetOrderKey,
|
||||
new RegexFilterVM<OrderSecurity>(
|
||||
ort => ort.order.attached_target_order_key().ToString(),
|
||||
new RegexFilterVM<OrderSecurityRootPair>(
|
||||
ort => ort.order.order.attached_target_order_key().ToString(),
|
||||
nameof(OrderBookRowVM.parent_order_key)),
|
||||
DataGridFilterKnownTemplates.Regex,
|
||||
_wrap_comparer(r => r.order.attached_target_order_key())));
|
||||
@@ -327,6 +327,18 @@ namespace mbts.spark {
|
||||
|
||||
public event Action filter_changed;
|
||||
|
||||
public static IEnumerable<ColumnBase> editable_columns {
|
||||
get {
|
||||
yield return new AttachDetachColumn(typeof(MiniOrderBookView));
|
||||
yield return new CommitColumn();
|
||||
yield return new EditablePriceColumn();
|
||||
yield return new EditableQtyColumn();
|
||||
yield return new UpdateColumn(typeof(MiniOrderBookView));
|
||||
yield return new CancelColumn(nameof(OrderBookRowVM.cancel_command));
|
||||
yield return new CheckColumn(nameof(OrderBookRowVM.is_selected));
|
||||
}
|
||||
}
|
||||
|
||||
public SecurityColumn security_column { get; }
|
||||
public SimpleTextColumn<string[]> symbol_column { get; }
|
||||
public SimpleTextColumn<TimeFilterTuple> time_created_column { get; }
|
||||
@@ -428,9 +440,9 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public Func<OrderSecurity, bool> get_filter() {
|
||||
public Func<OrderSecurityRootPair, bool> get_filter() {
|
||||
var active_filters = this.OfType<ICVOrderBookColumn>().Where(c => c.filter.active.GetValueOrDefault()).Select(c => c.filter).ToArray();
|
||||
return active_filters.Length == 0 ? null : new Func<OrderSecurity, bool>(r => active_filters.All(f => f.filter(r)));
|
||||
return active_filters.Length == 0 ? null : new Func<OrderSecurityRootPair, bool>(r => active_filters.All(f => f.filter(r)));
|
||||
}
|
||||
|
||||
public OrderBookSortingData get_sorting_data() {
|
||||
@@ -531,7 +543,7 @@ namespace mbts.spark {
|
||||
GlyphContext _root_glyph_context, _child_glyph_context;
|
||||
|
||||
protected OrderbookColumnBase(Columns col_key, string header, Func<OrderBookRowVM, string> get_text, double width,
|
||||
IDataGridColumnPersistentFilter<OrderSecurity, T> filter, string filter_template, IComparer<OrderBookRowVM> comparer) : base(col_key) {
|
||||
IDataGridColumnPersistentFilter<OrderSecurityRootPair, T> filter, string filter_template, IComparer<OrderBookRowVM> comparer) : base(col_key) {
|
||||
id = key.ToString();
|
||||
Width = width;
|
||||
Header = header;
|
||||
@@ -548,10 +560,10 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public IDataGridColumnPersistentFilter<OrderSecurity, T> filter { get; }
|
||||
public IDataGridColumnPersistentFilter<OrderSecurityRootPair, T> filter { get; }
|
||||
public IComparer<OrderBookRowVM> comparer { get; }
|
||||
|
||||
IDataGridColumnFilter<OrderSecurity> ICVOrderBookColumn.filter => filter;
|
||||
IDataGridColumnFilter<OrderSecurityRootPair> ICVOrderBookColumn.filter => filter;
|
||||
IDataGridColumnFilter IFilterableColumn.filter => filter;
|
||||
string IFilterableColumn.filter_template => _filter_template;
|
||||
ListSortDirection? ICVOrderBookColumn.SortDirection { get => _cached_sort_direction; set => SortDirection = value; }
|
||||
@@ -565,7 +577,7 @@ namespace mbts.spark {
|
||||
|
||||
public class SimpleTextColumn<TFilterState> : OrderbookColumnBase<TFilterState>, IRenderColumn<OrderBookRowVM> {
|
||||
public SimpleTextColumn(Columns key, string header, Func<OrderBookRowVM, string> get_text, double width,
|
||||
IDataGridColumnPersistentFilter<OrderSecurity, TFilterState> filter, string filter_template, IComparer<OrderBookRowVM> comparer)
|
||||
IDataGridColumnPersistentFilter<OrderSecurityRootPair, TFilterState> filter, string filter_template, IComparer<OrderBookRowVM> comparer)
|
||||
: base(key, header, get_text, width, filter, filter_template, comparer) { }
|
||||
|
||||
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
|
||||
@@ -575,7 +587,7 @@ namespace mbts.spark {
|
||||
|
||||
#region Explicit Column Defs
|
||||
public class SideColumn : OrderbookColumnBase<Side[]>, IRenderColumn<OrderBookRowVM> {
|
||||
public SideColumn(IDataGridColumnPersistentFilter<OrderSecurity, Side[]> filter)
|
||||
public SideColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, Side[]> filter)
|
||||
: base(Columns.Side, "B/S", r => r.side.ToString(), DefaultColumnWidths.Side, filter, DataGridFilterKnownTemplates.EnumValues, _wrap_comparer(r => r.side)) { }
|
||||
|
||||
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
|
||||
@@ -584,7 +596,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public class SizeColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
|
||||
public SizeColumn(IDataGridColumnPersistentFilter<OrderSecurity, UIntMinMaxState> filter)
|
||||
public SizeColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
|
||||
: base(Columns.Size, "Qty", r => r.quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.quantity)) { }
|
||||
|
||||
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
|
||||
@@ -593,7 +605,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public class FilledColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
|
||||
public FilledColumn(IDataGridColumnPersistentFilter<OrderSecurity, UIntMinMaxState> filter)
|
||||
public FilledColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
|
||||
: base(Columns.Filled, "Filled Qty", r => r.filled_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.filled_quantity)) { }
|
||||
|
||||
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
|
||||
@@ -605,7 +617,7 @@ namespace mbts.spark {
|
||||
readonly BindingBase _foreground_binding;
|
||||
ListSortDirection? _cached_sort_direction;
|
||||
|
||||
public OrderBookOrderTypeColumn(Columns key, string badge_fill_binding_path, string display_name_binding_path, IComparer<OrderBookRowVM> row_comparer, IDataGridColumnPersistentFilter<OrderSecurity, AlgoID[]> filter, string header = DefaultColumnHeaders.OrderType)
|
||||
public OrderBookOrderTypeColumn(Columns key, string badge_fill_binding_path, string display_name_binding_path, IComparer<OrderBookRowVM> row_comparer, IDataGridColumnPersistentFilter<OrderSecurityRootPair, AlgoID[]> filter, string header = DefaultColumnHeaders.OrderType)
|
||||
: base(key, new Binding(badge_fill_binding_path), new Binding(display_name_binding_path), filter, header) {
|
||||
id = key.ToString();
|
||||
this.filter = filter;
|
||||
@@ -615,11 +627,11 @@ namespace mbts.spark {
|
||||
this.comparer = row_comparer;
|
||||
}
|
||||
|
||||
public new IDataGridColumnPersistentFilter<OrderSecurity, AlgoID[]> filter { get; }
|
||||
public new IDataGridColumnPersistentFilter<OrderSecurityRootPair, AlgoID[]> filter { get; }
|
||||
public override string filter_template => DataGridFilterKnownTemplates.AlgoType;
|
||||
public IComparer<OrderBookRowVM> comparer { get; }
|
||||
|
||||
IDataGridColumnFilter<OrderSecurity> ICVOrderBookColumn.filter => filter;
|
||||
IDataGridColumnFilter<OrderSecurityRootPair> ICVOrderBookColumn.filter => filter;
|
||||
ListSortDirection? ICVOrderBookColumn.SortDirection { get => _cached_sort_direction; set => SortDirection = value; }
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
@@ -637,7 +649,7 @@ namespace mbts.spark {
|
||||
public class OrderBookLastMessageColumn : TextColumn<Columns>, ICVOrderBookColumn {
|
||||
ListSortDirection? _cached_sort_direction;
|
||||
|
||||
public OrderBookLastMessageColumn(IDataGridColumnPersistentFilter<OrderSecurity, string> filter)
|
||||
public OrderBookLastMessageColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter)
|
||||
: base(Columns.LastMessage, new Binding(nameof(OrderBookRowVM.last_msg)), filter, DataGridFilterKnownTemplates.Regex) {
|
||||
id = key.ToString();
|
||||
this.filter = filter;
|
||||
@@ -646,10 +658,10 @@ namespace mbts.spark {
|
||||
tooltip_binding = new Binding(nameof(OrderBookRowVM.last_msg));
|
||||
}
|
||||
|
||||
public new IDataGridColumnPersistentFilter<OrderSecurity, string> filter { get; }
|
||||
public new IDataGridColumnPersistentFilter<OrderSecurityRootPair, string> filter { get; }
|
||||
public IComparer<OrderBookRowVM> comparer => _wrap_comparer(r => r.last_msg);
|
||||
|
||||
IDataGridColumnFilter<OrderSecurity> ICVOrderBookColumn.filter => filter;
|
||||
IDataGridColumnFilter<OrderSecurityRootPair> ICVOrderBookColumn.filter => filter;
|
||||
ListSortDirection? ICVOrderBookColumn.SortDirection { get => _cached_sort_direction; set => SortDirection = value; }
|
||||
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) {
|
||||
@@ -659,7 +671,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public class PiqColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
|
||||
public PiqColumn(IDataGridColumnPersistentFilter<OrderSecurity, UIntMinMaxState> filter)
|
||||
public PiqColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
|
||||
: base(Columns.Piq, "PIQ", r => r.piq_text, DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.piq)) { }
|
||||
|
||||
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
|
||||
@@ -674,7 +686,7 @@ namespace mbts.spark {
|
||||
|
||||
ListSortDirection? _cached_sort_direction;
|
||||
|
||||
public SecurityColumn(IDataGridColumnPersistentFilter<OrderSecurity, SecurityCriteria> filter) : base(Columns.Security) {
|
||||
public SecurityColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, SecurityCriteria> filter) : base(Columns.Security) {
|
||||
id = key.ToString();
|
||||
this.filter = filter;
|
||||
_foreground_binding = RenderUtils.get_foreground_binding(this);
|
||||
@@ -683,10 +695,10 @@ namespace mbts.spark {
|
||||
_cached_sort_direction = SortDirection;
|
||||
}
|
||||
|
||||
public IDataGridColumnPersistentFilter<OrderSecurity, SecurityCriteria> filter { get; }
|
||||
public IDataGridColumnPersistentFilter<OrderSecurityRootPair, SecurityCriteria> filter { get; }
|
||||
public IComparer<OrderBookRowVM> comparer => _wrap_comparer(r => r.security, SecurityHelper.Comparer.InstanceImmutable.to_nullable());
|
||||
|
||||
IDataGridColumnFilter<OrderSecurity> ICVOrderBookColumn.filter => filter;
|
||||
IDataGridColumnFilter<OrderSecurityRootPair> ICVOrderBookColumn.filter => filter;
|
||||
IDataGridColumnFilter IFilterableColumn.filter => filter;
|
||||
string IFilterableColumn.filter_template => DataGridFilterKnownTemplates.DiscreteValues;
|
||||
ListSortDirection? ICVOrderBookColumn.SortDirection { get => _cached_sort_direction; set => SortDirection = value; }
|
||||
@@ -731,8 +743,11 @@ namespace mbts.spark {
|
||||
Width = 30;
|
||||
_checked_binding = new Binding(checked_property);
|
||||
|
||||
var header_cb = new CheckBox();
|
||||
header_cb.ToolTip = "Select / Unselect All";
|
||||
var header_cb = new CheckBox {
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
ToolTip = "Select / Unselect All"
|
||||
};
|
||||
header_cb.SetBinding(CheckBox.IsCheckedProperty, new Binding(nameof(OrderBookVM.all_rows_selected)));
|
||||
this.Header = header_cb;
|
||||
}
|
||||
@@ -1164,6 +1179,7 @@ namespace mbts.spark {
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
var price_up_down = new PriceEntryCommitControl();
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.RealPriceProperty, new Binding(nameof(OrderBookRowVM.price)));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.SourceValueProperty, nameof(OrderBookRowVM.prospective_price));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.SourceContractSpecProperty, nameof(OrderBookRowVM.contract_spec));
|
||||
price_up_down.SetBinding(PriceEntryCommitControl.OnCommitProperty, nameof(OrderBookRowVM.modify_order_quantity_andor_price));
|
||||
@@ -1177,7 +1193,6 @@ namespace mbts.spark {
|
||||
public class CommitColumn : FrameworkElementColumnBase<Columns> {
|
||||
public CommitColumn() : base(Columns.Commit) {
|
||||
id = key.ToString();
|
||||
Header = "";
|
||||
Width = 20;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -14,7 +16,9 @@ using System.Windows.Media;
|
||||
using Columns = mbts.spark.OrderBookGridView.ColumnKeys;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class OrderBookRowVM : IEquatable<OrderBookRowVM>, DIYGrid.IRowVM, IHierarchicalRow<OrderSecurity>, INotifyPropertyChanged {
|
||||
partial class OrderBookRowVM : IEquatable<OrderBookRowVM>, DIYGrid.IRowVM, IHierarchicalRow<OrderSecurityRootPair>, INotifyPropertyChanged {
|
||||
public static readonly IComparer<OrderBookRowVM> key_comparer = Comparer<uint>.Default.wrap<uint, OrderBookRowVM>(r => r.order_key).to_safe();
|
||||
|
||||
const string _unknown_security_price_format_string = "0.#########";
|
||||
const int _unknown_security_price_num_fractional_digits = 9;
|
||||
readonly OrderBookVM _owner;
|
||||
@@ -23,13 +27,13 @@ namespace mbts.spark {
|
||||
IRaiseCanExecuteChangedCommand _show_order_modify_cmd, _toggle_expanded_cmd, _cancel_cmd, _detach_command, _attach_command, _update_command, _modify_order_quantity_andor_price, _revert_prospective_order_quantity_andor_price;
|
||||
bool _is_parent, _is_expanded, _is_selected, _is_attachable, _is_detachable;
|
||||
int _depth_level;
|
||||
SecurityImmutable? _security => order_tuple.security;
|
||||
SecurityImmutable? _security => order_tuple.order.security;
|
||||
// for mini-ob
|
||||
uint _prospective_quantity;
|
||||
double _prospective_price;
|
||||
Action<bool> _on_selection_changed;
|
||||
|
||||
public OrderBookRowVM(OrderBookVM owner, OrderSecurity order_tuple, bool is_expanded, Action<bool> on_selection_changed) {
|
||||
public OrderBookRowVM(OrderBookVM owner, OrderSecurityRootPair order_tuple, bool is_expanded, Action<bool> on_selection_changed) {
|
||||
_owner = owner;
|
||||
this.order_tuple = order_tuple;
|
||||
is_visible = !is_child;
|
||||
@@ -45,22 +49,22 @@ namespace mbts.spark {
|
||||
//// when the OrderBookVM.selected_order_changes, this emulates the selected
|
||||
//// order changing since the current_row setter invokes the selected_order_changed
|
||||
//// handler
|
||||
public IObservable<OrderSecurity> subscribe_data() {
|
||||
return Observable.FromEvent<Action, OrderSecurity>(
|
||||
public IObservable<OrderSecurityRootPair> subscribe_data() {
|
||||
return Observable.FromEvent<Action, OrderSecurityRootPair>(
|
||||
cb => () => cb(order_tuple),
|
||||
cb => invalidated += cb,
|
||||
cb => invalidated -= cb);
|
||||
}
|
||||
|
||||
public event Action invalidated;
|
||||
public event Action<(OrderSecurity oldval, OrderSecurity newval)> updated;
|
||||
public event Action<(OrderSecurityRootPair oldval, OrderSecurityRootPair newval)> updated;
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
// Core
|
||||
public OrderSecurity order_tuple { get; private set; }
|
||||
public OrderRecordImmutable order => order_tuple.order;
|
||||
public OrderSecurityRootPair order_tuple { get; private set; }
|
||||
public OrderRecordImmutable order => order_tuple.order.order;
|
||||
public OrderMetadata order_metadata => _owner.get_metadata(this);
|
||||
public Brush badge_fill => _badge_fill ?? (_badge_fill = OrderUtils.badge_fill(order_tuple.meta, order_metadata));
|
||||
public Brush badge_fill => _badge_fill ?? (_badge_fill = OrderUtils.badge_fill(order_tuple.order.meta, order_metadata));
|
||||
public uint order_key => order.Key;
|
||||
public uint parent_order_key => order.ParentKey;
|
||||
public uint security_key => order.security_key();
|
||||
@@ -71,7 +75,7 @@ namespace mbts.spark {
|
||||
public string symbol => _security.symbol_or_default();
|
||||
// Def
|
||||
public CustomOrderDefinition.CustomOneofCase order_type => order.order_type();
|
||||
public string display_name => order_tuple.display_name();
|
||||
public string display_name => order_tuple.order.display_name();
|
||||
public OrderSubType order_sub_type => order.sub_type();
|
||||
public string order_sub_type_text => _fmt_order_sub_type(order_sub_type);
|
||||
public Side side => order.side();
|
||||
@@ -93,7 +97,7 @@ namespace mbts.spark {
|
||||
public uint quantity => order.quantity();
|
||||
public uint filled_quantity => order.filled();
|
||||
public string last_msg => order.last_msg();
|
||||
public uint piq => order_tuple.piq ?? uint.MaxValue;
|
||||
public uint piq => order_tuple.order.piq ?? uint.MaxValue;
|
||||
public string piq_text => piq == uint.MaxValue ? string.Empty : piq.ToString();
|
||||
public bool is_non_terminal => order.is_non_terminal();
|
||||
public bool is_attachable { get => _is_attachable; set => SetVisual(ref _is_attachable, value); }
|
||||
@@ -101,10 +105,10 @@ namespace mbts.spark {
|
||||
public string filled_status_desc => order.filled_status_description(security.spec_or_default());
|
||||
|
||||
// Root Algo
|
||||
public CustomOrderDefinition.CustomOneofCase root_algo_order_type => order_tuple.root_algo.value.CustomCase;
|
||||
public CustomOrderDefinition.CustomOneofCase root_algo_order_type => order_tuple.root.algo.value.CustomCase;
|
||||
public OrderMetadata root_algo_order_metadata => _owner.get_metadata(root_algo_order_type);
|
||||
public Brush root_algo_badge_fill => _root_algo_badge_fill ?? (_root_algo_badge_fill = new SolidColorBrush(_root_algo_badge_color));
|
||||
public string root_algo_display_name => order_tuple.root_algo.name;
|
||||
public string root_algo_display_name => order_tuple.root.algo.name;
|
||||
|
||||
// Tree
|
||||
public bool is_parent { get => _is_parent; set => Set(ref _is_parent, value); }
|
||||
@@ -213,7 +217,7 @@ namespace mbts.spark {
|
||||
|
||||
// private props
|
||||
Color _root_algo_badge_color {
|
||||
get => _root_algo_badge_color_backing ?? (_root_algo_badge_color_backing = OrderUtils.badge_color(order_tuple.root_meta, root_algo_order_metadata)).Value;
|
||||
get => _root_algo_badge_color_backing ?? (_root_algo_badge_color_backing = OrderUtils.badge_color(order_tuple.root.meta, root_algo_order_metadata)).Value;
|
||||
set {
|
||||
if (_root_algo_badge_color_backing != value) {
|
||||
_root_algo_badge_color_backing = value;
|
||||
@@ -226,7 +230,7 @@ namespace mbts.spark {
|
||||
bool IHierarchicalRow.is_parent => is_parent;
|
||||
bool IHierarchicalRow.is_expanded => is_expanded;
|
||||
int IHierarchicalRow.depth_level => depth_level;
|
||||
OrderSecurity IHierarchicalRow<OrderSecurity>.value => order_tuple;
|
||||
OrderSecurityRootPair IHierarchicalRow<OrderSecurityRootPair>.value => order_tuple;
|
||||
|
||||
void IHierarchicalRow.on_is_parent_changed(bool is_parent) => this.is_parent = is_parent;
|
||||
/// Do NOT actually call the <see cref="is_expanded"/> setter (it has side effects) - the row collection calls this method when the row is expanded/collapsed by some **external means, outside of this class.
|
||||
@@ -238,7 +242,7 @@ namespace mbts.spark {
|
||||
/// <see cref="OperableHierarchicalCollection{TRow, T, TKey}.collapse_row(TKey)"/>
|
||||
void IHierarchicalRow.on_is_expanded_changed(bool is_expanded) => Set(ref _is_expanded, is_expanded, nameof(is_expanded));
|
||||
void IHierarchicalRow.on_depth_level_changed(int depth_level) => this.depth_level = depth_level;
|
||||
void IHierarchicalRow<OrderSecurity>.on_value_changed(OrderSecurity value) => update(value);
|
||||
void IHierarchicalRow<OrderSecurityRootPair>.on_value_changed(OrderSecurityRootPair value) => update(value);
|
||||
#endregion
|
||||
|
||||
private void _unselect_me() {
|
||||
@@ -254,8 +258,8 @@ namespace mbts.spark {
|
||||
internal void expand_internal() => Set(ref _is_expanded, true, nameof(is_expanded));
|
||||
internal void collapse_internal() => Set(ref _is_expanded, false, nameof(is_expanded));
|
||||
|
||||
public void update(OrderSecurity order_tuple) {
|
||||
Log.assert(order_tuple.order.Key == this.order.Key, "OrderBookRowVM: update_order was called with an order with the wrong key.");
|
||||
public void update(OrderSecurityRootPair order_tuple) {
|
||||
Log.assert(order_tuple.order.order.Key == this.order.Key, "OrderBookRowVM: update_order was called with an order with the wrong key.");
|
||||
// _show_order_modify_cmd is lazy (instantiated in the default_command getter); it might be null, don't not force instantiation
|
||||
bool? could_modify_execute = _show_order_modify_cmd?.CanExecute(null);
|
||||
var old_order_tuple = this.order_tuple;
|
||||
@@ -281,25 +285,25 @@ namespace mbts.spark {
|
||||
if ((Columns.Security & dirty) != 0)
|
||||
RaisePropertyChanged(nameof(security), nameof(security_text));
|
||||
// badge fill
|
||||
if (old_order_tuple.meta?.badge_color != order_tuple.meta?.badge_color) {
|
||||
if (old_order_tuple.order.meta?.badge_color != order_tuple.order.meta?.badge_color) {
|
||||
// invalidate by setting to null (will be re-instantiated in the public getter)
|
||||
_badge_fill = null;
|
||||
RaisePropertyChanged(nameof(badge_fill));
|
||||
}
|
||||
// display name
|
||||
if (old_order_tuple.display_name() != order_tuple.display_name())
|
||||
if (old_order_tuple.order.algo.name != order_tuple.order.algo.name)
|
||||
RaisePropertyChanged(nameof(display_name));
|
||||
// root badge fill
|
||||
if (_root_algo_badge_color_backing.HasValue) {
|
||||
// only need to invalidate it if it had a value (it is lazily instantiated)
|
||||
_root_algo_badge_color = OrderUtils.badge_color(order_tuple.root_meta, root_algo_order_metadata);
|
||||
_root_algo_badge_color = OrderUtils.badge_color(order_tuple.root.meta, root_algo_order_metadata);
|
||||
RaisePropertyChanged(nameof(root_algo_badge_fill));
|
||||
}
|
||||
// root display name
|
||||
if (old_order_tuple.root_algo.name != order_tuple.root_algo.name)
|
||||
if (old_order_tuple.root.algo.name != order_tuple.root.algo.name)
|
||||
RaisePropertyChanged(nameof(root_algo_display_name));
|
||||
// root order type
|
||||
if (old_order_tuple.root_algo.value.CustomCase != order_tuple.root_algo.value.CustomCase) {
|
||||
if (old_order_tuple.root.algo.value.CustomCase != order_tuple.root.algo.value.CustomCase) {
|
||||
RaisePropertyChanged(nameof(root_algo_order_type));
|
||||
}
|
||||
// status
|
||||
@@ -317,7 +321,7 @@ namespace mbts.spark {
|
||||
// always raise updated
|
||||
updated?.Invoke((old_order_tuple, order_tuple));
|
||||
// terminal state
|
||||
if (old_order_tuple.order.is_non_terminal() != order_tuple.order.is_non_terminal()) {
|
||||
if (old_order_tuple.order.order.is_non_terminal() != order_tuple.order.order.is_non_terminal()) {
|
||||
// while we call refresh_is_at/detachable in the OrderBookRowCollection.update
|
||||
// on all the children of a modified row, we used to not call it on the row istelf,
|
||||
// which should of course be done here. This manifested via the very top parent in any
|
||||
@@ -398,35 +402,35 @@ namespace mbts.spark {
|
||||
Columns.AvgFilled |
|
||||
Columns.Status;
|
||||
|
||||
public static Columns get_dirty_cells(OrderSecurity oldval, OrderSecurity newval) {
|
||||
public static Columns get_dirty_cells(OrderSecurityRootPair oldval, OrderSecurityRootPair newval) {
|
||||
var dirty = (Columns)0;
|
||||
if (oldval.security != newval.security)
|
||||
if (oldval.order.security != newval.order.security)
|
||||
dirty |= Columns.Security;
|
||||
if (oldval.order.last_change() != newval.order.last_change())
|
||||
if (oldval.order.order.last_change() != newval.order.order.last_change())
|
||||
dirty |= Columns.LastChange;
|
||||
if (oldval.order.filled() != newval.order.filled())
|
||||
if (oldval.order.order.filled() != newval.order.order.filled())
|
||||
dirty |= Columns.Filled;
|
||||
if (oldval.order.last_price() != newval.order.last_price())
|
||||
if (oldval.order.order.last_price() != newval.order.order.last_price())
|
||||
dirty |= Columns.LastPrice;
|
||||
if (oldval.order.avg_price() != newval.order.avg_price())
|
||||
if (oldval.order.order.avg_price() != newval.order.order.avg_price())
|
||||
dirty |= Columns.AvgFilled;
|
||||
if (oldval.order.price() != newval.order.price())
|
||||
if (oldval.order.order.price() != newval.order.order.price())
|
||||
dirty |= Columns.Price;
|
||||
if (oldval.order.quantity() != newval.order.quantity())
|
||||
if (oldval.order.order.quantity() != newval.order.order.quantity())
|
||||
dirty |= Columns.Size;
|
||||
if (oldval.order.ParentKey != newval.order.ParentKey)
|
||||
if (oldval.order.order.ParentKey != newval.order.order.ParentKey)
|
||||
dirty |= Columns.ParentKey;
|
||||
if (oldval.order.attached_target_order_key() != newval.order.attached_target_order_key())
|
||||
if (oldval.order.order.attached_target_order_key() != newval.order.order.attached_target_order_key())
|
||||
dirty |= Columns.AttachedTargetOrderKey;
|
||||
if (oldval.order.status() != newval.order.status())
|
||||
if (oldval.order.order.status() != newval.order.order.status())
|
||||
dirty |= Columns.Status;
|
||||
if (oldval.order.last_msg() != newval.order.last_msg())
|
||||
if (oldval.order.order.last_msg() != newval.order.order.last_msg())
|
||||
dirty |= Columns.LastMessage;
|
||||
if (oldval.piq != newval.piq) {
|
||||
if (oldval.order.piq != newval.order.piq) {
|
||||
dirty |= Columns.Piq;
|
||||
}
|
||||
return dirty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,9 @@ namespace mbts.spark {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _model;
|
||||
readonly Property<OrderBookRowVM> _current_row;
|
||||
readonly ObservableSortedCollection<OrderBookRowVM> _selected_rows = new ObservableSortedCollection<OrderBookRowVM>(OrderBookRowVM.key_comparer);
|
||||
readonly DelegateProperty<bool> _show_order_params, _show_ladder, _show_fills, _can_attach_selected_orders, _can_detach_selected_orders;
|
||||
readonly OperableHierarchicalCollection<OrderBookRowVM, OrderSecurity, uint> _row_collection;
|
||||
readonly OperableHierarchicalCollection<OrderBookRowVM, OrderSecurityRootPair, uint> _row_collection;
|
||||
readonly OrderBookColumnCollection _columns;
|
||||
readonly IWindowSettingsManager<OrderBookGridSettings> _settings_manager;
|
||||
readonly IWindowSettingsManager<GridSettings> _fills_settings_manager = new WindowSettingsManager<GridSettings>.DisconnectedImpl(FillsSettings.MiniGridSettings, FillsSettings.MiniGridSettings);
|
||||
@@ -61,26 +62,30 @@ namespace mbts.spark {
|
||||
_is_attaching_orders,
|
||||
_is_row_collection_populated,
|
||||
_all_rows_selected;
|
||||
IReadOnlyDictionary<uint, OrderSecurity> _cached_orders = DictionaryHelper.empty<uint, OrderSecurity>();
|
||||
IReadOnlyDictionary<uint, OrderSecurityRootPair> _cached_orders = DictionaryHelper.empty<uint, OrderSecurityRootPair>();
|
||||
IReadOnlyDictionary<uint, bool> _cached_attachable = DictionaryHelper.empty<uint, bool>(), _cached_detachable = DictionaryHelper.empty<uint, bool>();
|
||||
HashSet<uint>
|
||||
_attachable_selected_orders = new HashSet<uint>(),
|
||||
_detachable_selected_orders = new HashSet<uint>();
|
||||
FilterData _cached_filter_data;
|
||||
Dictionary<uint, IDisposable> _on_updated_single_callback_gatekeeper = new Dictionary<uint, IDisposable>();
|
||||
|
||||
public OrderBookVM(
|
||||
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false)
|
||||
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded) { }
|
||||
|
||||
public OrderBookVM(
|
||||
IBus bus,
|
||||
INotificationService notification_service,
|
||||
OrdersModel model,
|
||||
IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source,
|
||||
IWindowSettingsManager<OrderBookGridSettings> settings_manager,
|
||||
bool rows_are_initially_expanded = false) {
|
||||
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
_selected_params_mon = new OrderParamsAggregator(model.dynamic_helper, _selection_change_throttle);
|
||||
_selected_params_mon = new OrderParamsAggregator(model.dynamic_helper, _selection_change_throttle, k => _cached_orders.TryGetValue(k, out var o) ? o.order : (OrderSecurityAlgo?)null);
|
||||
ladder_vm = new LadderVMContainer(bus, model);
|
||||
order_mod_vm = new OrderParamsVM(this, AlgoDefinitionVM.create(model, OrderDataHelper.Defaults.AlgoData(), editing_permissions: AlgoEditingPermissions.AllowEditValues));
|
||||
order_mod_vm = new OrderParamsVM(this, AlgoFormats.Exchange, AlgoEditingPermissions.AllowEditValues);
|
||||
order_mod_vm.algo_vm.editing_permissions_mask = AlgoEditingPermissions.None;
|
||||
order_mod_vm.algo_vm.algo_root = null;
|
||||
_show_order_params = backing(nameof(show_order_params), () => (_right_panel.visible_components & OrderBookRightPanelSettings.Components.Params) != 0);
|
||||
@@ -93,16 +98,12 @@ namespace mbts.spark {
|
||||
};
|
||||
_can_attach_selected_orders = backing(nameof(can_attach_selected_orders), () => _attachable_selected_orders.Count == selected_rows.Count && selected_rows.Count > 0);
|
||||
_can_detach_selected_orders = backing(nameof(can_detach_selected_orders), () => _detachable_selected_orders.Count == selected_rows.Count && selected_rows.Count > 0);
|
||||
attach_vm = new OrderParamsVM(this,
|
||||
AlgoDefinitionVM.create(_model,
|
||||
OrderDataHelper.Defaults.AlgoData(CustomOrderDefinition.CustomOneofCase.Iceberg),
|
||||
init_format: AlgoFormats.Attachable,
|
||||
editing_permissions: AlgoEditingPermissions.AllowEditStructure | AlgoEditingPermissions.AllowEditValues));
|
||||
attach_vm = new OrderParamsVM(this, AlgoFormats.Attachable, AlgoEditingPermissions.AllowEditStructure | AlgoEditingPermissions.AllowEditValues);
|
||||
_settings_manager = settings_manager;
|
||||
_columns = new OrderBookColumnCollection(bus, model, model.on_order_securities.Select(x => (x.added.Values, x.removed.Values)));
|
||||
// we use an ObservableAppendCollection b/c the mini-ob needs a cancel column at the front
|
||||
columns = new ObservableAppendCollection<DIYGrid.ColumnBase>(_columns);
|
||||
_row_collection = new OperableHierarchicalCollection<OrderBookRowVM, OrderSecurity, uint>(
|
||||
_row_collection = new OperableHierarchicalCollection<OrderBookRowVM, OrderSecurityRootPair, uint>(
|
||||
o => new OrderBookRowVM(this, o, rows_are_initially_expanded, (row_is_selected) => {
|
||||
// all are selected and one is checked off
|
||||
if (all_rows_selected && !row_is_selected) {
|
||||
@@ -114,11 +115,11 @@ namespace mbts.spark {
|
||||
RaisePropertyChanged(nameof(this.all_rows_selected));
|
||||
}
|
||||
}) {
|
||||
is_attachable = _cached_attachable.TryGetValue(o.order.Key, out bool ia) && ia,
|
||||
is_detachable = _cached_detachable.TryGetValue(o.order.Key, out bool id)
|
||||
is_attachable = _cached_attachable.TryGetValue(o.order.order.Key, out bool ia) && ia,
|
||||
is_detachable = _cached_detachable.TryGetValue(o.order.order.Key, out bool id)
|
||||
},
|
||||
r => r.order.Key,
|
||||
r => r.order.ParentKey,
|
||||
r => r.order.order.Key,
|
||||
r => r.order.order.ParentKey,
|
||||
_columns.get_sorting_data().comparer);
|
||||
_row_collection.CollectionChanged += _row_collection_CollectionChanged;
|
||||
_current_row = backing<OrderBookRowVM>(nameof(current_row));
|
||||
@@ -161,7 +162,6 @@ namespace mbts.spark {
|
||||
begin_attaching += on_begin_attaching;
|
||||
end_attaching += on_end_attaching;
|
||||
// switch between orders and/or orders w/ piq
|
||||
var orders_source = OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root;
|
||||
concat_disposable(
|
||||
connect_order_mod_vm(),
|
||||
_can_attach_selected_orders.observe_current().Subscribe(send_attached_orders_async_cmd, begin_attach_cmd),
|
||||
@@ -189,8 +189,24 @@ namespace mbts.spark {
|
||||
RaisePropertyChanged(nameof(security_plurality));
|
||||
}),
|
||||
orders_source.OnDispatcher().Subscribe(t => {
|
||||
_cached_orders = t.state;
|
||||
_row_collection.update(t.added, t.changed, t.removed.Select(o => o.order.Key));
|
||||
if (_selected_rows.Count == 0) {
|
||||
_cached_orders = t.state;
|
||||
_row_collection.update(t.added, t.changed, t.removed.Select(OrderSecurityHelper.root_pair_key_fn));
|
||||
} else {
|
||||
using (_selected_params_mon.defer_updates())
|
||||
using (_selected_security_mon.defer_updates()) {
|
||||
_cached_orders = t.state;
|
||||
_row_collection.update(t.added, t.changed, t.removed.Select(OrderSecurityHelper.root_pair_key_fn));
|
||||
// Remove, then re-add, all selected roots from _selected_params_mon
|
||||
// to capture algo param changes at any level (this keeps the update panel in sync).
|
||||
_selected_params_mon.Clear();
|
||||
_selected_security_mon.clear();
|
||||
foreach (var r in _selected_rows) {
|
||||
_selected_params_mon.Add(r.order_tuple.root);
|
||||
_selected_security_mon.add(r.security);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
model.on_orders_can_attach.OnDispatcher().Subscribe(table => {
|
||||
_cached_attachable = new MappedDictionary<uint, KeyValuePair<uint, bool>, bool>(table.state, kv => kv.Value);
|
||||
@@ -271,11 +287,11 @@ namespace mbts.spark {
|
||||
try {
|
||||
double price = attach_vm.price;
|
||||
uint? size = attach_vm.size;
|
||||
uint algo_template_key = attach_vm.algo_vm.algo_root.id.is_persisted ? attach_vm.algo_vm.algo_root.id.persisted.id_uint : 0u;
|
||||
uint algo_template_key = attach_vm.algo_vm.algo_root.id.is_persisted ? attach_vm.algo_vm.algo_root.id.persisted : 0u;
|
||||
var order_tups = selected_rows.Select(r => r.order_tuple).ToList();
|
||||
var mod_algo_vm = attach_vm.algo_vm.algo_root;
|
||||
await Task.WhenAll(order_tups.Select(t => {
|
||||
var algo = t.order.Def.CustomDef.actual().CloneToMutable();
|
||||
var algo = t.order.order.Def.CustomDef.actual().CloneToMutable();
|
||||
mod_algo_vm.assign_to(algo);
|
||||
if (algo_template_key != 0) {
|
||||
algo = new CustomOrderDefinition {
|
||||
@@ -286,12 +302,12 @@ namespace mbts.spark {
|
||||
};
|
||||
}
|
||||
return _model.send_order(
|
||||
t.order.Def.with(
|
||||
attach_target_order_key: t.order.Key,
|
||||
t.order.order.Def.with(
|
||||
attach_target_order_key: t.order.order.Key,
|
||||
custom_def: algo
|
||||
),
|
||||
double.IsNaN(price) ? t.order.price() : price,
|
||||
size ?? t.order.quantity());
|
||||
double.IsNaN(price) ? t.order.order.price() : price,
|
||||
size ?? t.order.order.quantity());
|
||||
}));
|
||||
} catch (Exception ex) {
|
||||
Log.Ui.error($"Error sending attached orders: {ex}.");
|
||||
@@ -318,21 +334,20 @@ namespace mbts.spark {
|
||||
}),
|
||||
// custom templates (created by user)
|
||||
_model.algo_model.definitions.OnDispatcher().Subscribe(u =>
|
||||
AlgoCollectionHelper.update_persisted_definition_wrappers(attach_vm.algo_vm.algo_roots_index, u, mk_factory(true)))));
|
||||
AlgoCollectionHelper.update_persisted_definition_wrappers(attach_vm.algo_vm.algo_roots_index, u, mk_algo_def_vm))));
|
||||
// vm factory
|
||||
Func<AlgoData, AlgoDefinitionVM> mk_factory(bool connect) =>
|
||||
fd => {
|
||||
var vm = AlgoDefinitionVM.create(_model, fd, connect, OrderDataHelper.get_algo_format(_selected_params_mon.security?.ContractCase), AlgoEditingPermissions.AllowEditValues);
|
||||
vm.selected_format = OrderDataHelper.get_algo_format(_selected_params_mon.security?.ContractCase);
|
||||
return vm;
|
||||
};
|
||||
AlgoDefinitionVM mk_algo_def_vm(AlgoData fd) {
|
||||
var vm = AlgoDefinitionVM.create(_model, fd, OrderDataHelper.get_algo_format(_selected_params_mon.security?.ContractCase), AlgoEditingPermissions.AllowEditValues);
|
||||
vm.selected_format = OrderDataHelper.get_algo_format(_selected_params_mon.security?.ContractCase);
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
void on_end_attaching() => Disposable.dispose(ref attach_vm_hooks);
|
||||
|
||||
IDisposable connect_order_mod_vm() =>
|
||||
Disposable.Create(
|
||||
_selected_params_mon.subscribe_algos().Subscribe(order_mod_vm.algo_vm.load_many),
|
||||
order_mod_vm.subscribe_is_dirty().Subscribe(_ => {
|
||||
_selected_params_mon.subscribe_algos().OnDispatcher().Subscribe(order_mod_vm.algo_vm.load_many),
|
||||
order_mod_vm.subscribe_is_dirty().OnDispatcher().Subscribe(_ => {
|
||||
modify_selected_orders_async_cmd.RaiseCanExecuteChanged();
|
||||
revert_selected_orders_cmd.RaiseCanExecuteChanged();
|
||||
}),
|
||||
@@ -340,10 +355,11 @@ namespace mbts.spark {
|
||||
|
||||
IDisposable connect_common_selected_params(OrderParamsVM vm) =>
|
||||
Disposable.Create(
|
||||
_selected_params_mon.subscribe_security().to_security_md(bus).OnDispatcher().Subscribe(newval => vm.security_md = newval),
|
||||
_selected_params_mon.subscribe_price().Subscribe(vm.on_common_price),
|
||||
_selected_params_mon.subscribe_size().Subscribe(vm.on_common_size),
|
||||
_selected_params_mon.subscribe_account().Subscribe(newval => vm.selected_account = newval));
|
||||
_selected_params_mon.subscribe_security().OnDispatcher().to_security_md(bus).OnDispatcher().Subscribe(newval => vm.security_md = newval),
|
||||
_selected_params_mon.subscribe_price().OnDispatcher().Subscribe(vm.on_common_price),
|
||||
_selected_params_mon.subscribe_size().OnDispatcher().Subscribe(vm.on_common_size),
|
||||
_selected_params_mon.subscribe_account().OnDispatcher().Subscribe(newval => vm.selected_account = newval));
|
||||
|
||||
}
|
||||
|
||||
private void _row_collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
@@ -375,7 +391,7 @@ namespace mbts.spark {
|
||||
public OrderParamsVM order_mod_vm { get; }
|
||||
public FillsVMContainer fills_vm { get; } = new FillsVMContainer();
|
||||
public OrderParamsVM attach_vm { get; }
|
||||
public ObservableCollection<OrderBookRowVM> selected_rows { get; } = new ObservableCollection<OrderBookRowVM>();
|
||||
public IReadOnlyObservableCollection<OrderBookRowVM> selected_rows => _selected_rows;
|
||||
public IEnumerable<OrderRecordImmutable> selected_orders => selected_rows.Select(r => r.order);
|
||||
public IEnumerable<OrderBookRowVM> selected_hierarchical_rows => selected_rows.SelectMany(r => new[] { r }.Concat(_row_collection.get_descendent_rows(r.order_key)));
|
||||
public OrderBookGridSettingsManager grid_settings_manager { get; }
|
||||
@@ -456,65 +472,30 @@ namespace mbts.spark {
|
||||
using (_selected_params_mon.defer_updates())
|
||||
using (_selected_security_mon.defer_updates()) {
|
||||
if (e.RemovedItems != null) {
|
||||
foreach (var row_vm in e.RemovedItems.OfType<OrderBookRowVM>()) {
|
||||
row_vm.is_selected = false;
|
||||
_selected_security_mon.remove(row_vm.security);
|
||||
_selected_params_mon.Remove(_get_root(row_vm.order_tuple));
|
||||
_attachable_selected_orders.Remove(row_vm.order_key);
|
||||
_detachable_selected_orders.Remove(row_vm.order_key);
|
||||
foreach (var row in e.RemovedItems.OfType<OrderBookRowVM>()) {
|
||||
row.is_selected = false;
|
||||
_selected_rows.Remove(row);
|
||||
_selected_params_mon.Remove(row.order_tuple.root);
|
||||
_selected_security_mon.remove(row.security);
|
||||
_attachable_selected_orders.Remove(row.order_key);
|
||||
_detachable_selected_orders.Remove(row.order_key);
|
||||
}
|
||||
}
|
||||
if (e.AddedItems != null) {
|
||||
foreach (var row_vm in e.AddedItems.OfType<OrderBookRowVM>()) {
|
||||
var root = _get_root(row_vm.order_tuple);
|
||||
row_vm.is_selected = true;
|
||||
_selected_security_mon.add(row_vm.security);
|
||||
if (!_on_updated_single_callback_gatekeeper.ContainsKey(row_vm.order_key)) {
|
||||
row_vm.updated += on_updated;
|
||||
_on_updated_single_callback_gatekeeper.Add(row_vm.order_key, Disposable.Create(() => row_vm.updated -= on_updated));
|
||||
}
|
||||
_selected_params_mon.Add(root);
|
||||
if (row_vm.is_attachable)
|
||||
_attachable_selected_orders.Add(row_vm.order_key);
|
||||
if (row_vm.is_detachable)
|
||||
_detachable_selected_orders.Add(row_vm.order_key);
|
||||
void on_updated((OrderSecurity oldval, OrderSecurity newval) change) {
|
||||
if (!row_vm.is_selected) {
|
||||
// self destruct
|
||||
if (_on_updated_single_callback_gatekeeper.TryGetValue(row_vm.order_key, out var unsubscribe_updated_disposable)) {
|
||||
unsubscribe_updated_disposable.Dispose();
|
||||
_on_updated_single_callback_gatekeeper.Remove(row_vm.order_key);
|
||||
}
|
||||
_selected_params_mon.Remove(_get_root(row_vm.order_tuple));
|
||||
_selected_security_mon.remove(row_vm.security);
|
||||
return;
|
||||
}
|
||||
var new_root = _get_root(change.newval);
|
||||
if (!new_root.Equals(root)) {
|
||||
using (_selected_params_mon.defer_updates()) {
|
||||
// unselect and select in one smooth motion via on_row_updated
|
||||
// because we thought there may be gremlins afoot when doing
|
||||
// on_row_unselected and then on_row_selected
|
||||
_selected_params_mon.Remove(root);
|
||||
_selected_params_mon.Add(new_root);
|
||||
root = new_root;
|
||||
}
|
||||
}
|
||||
if (change.oldval.security?.Id != change.newval.security?.Id) {
|
||||
using (_selected_security_mon.defer_updates()) {
|
||||
_selected_security_mon.remove(change.oldval.security);
|
||||
_selected_security_mon.add(change.newval.security);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var row in e.AddedItems.OfType<OrderBookRowVM>()) {
|
||||
row.is_selected = true;
|
||||
_selected_rows.Add(row);
|
||||
_selected_params_mon.Add(row.order_tuple.root);
|
||||
_selected_security_mon.add(row.security);
|
||||
if (row.is_attachable) _attachable_selected_orders.Add(row.order_key);
|
||||
if (row.is_detachable) _detachable_selected_orders.Add(row.order_key);
|
||||
}
|
||||
}
|
||||
RowSelectionHelper.apply_changes(selected_rows, e);
|
||||
_can_attach_selected_orders.refresh();
|
||||
_can_detach_selected_orders.refresh();
|
||||
selected_order_changed?.Invoke(current_order);
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
_can_attach_selected_orders.refresh();
|
||||
_can_detach_selected_orders.refresh();
|
||||
selected_order_changed?.Invoke(current_order);
|
||||
} catch (Exception ex) {
|
||||
// [John]I observed a strange null-reference exception in this callback when the launcher was shutting down (see https://bluetradesys.atlassian.net/browse/SPARK-2089)
|
||||
Log.Ui.error($"[{nameof(OrderBookVM)}] Error handling selection change: {ex}");
|
||||
}
|
||||
@@ -584,11 +565,11 @@ namespace mbts.spark {
|
||||
if (!new_sort.Equals(old_sort)) {
|
||||
/// Both filter and sort have changed, use <see cref="OperableHierarchicalCollection{TRow, T, TKey}.defer_refresh"/> to avoid syncing twice.
|
||||
using (_row_collection.defer_refresh()) {
|
||||
_row_collection.filter = new_filter.build_predicate(_model.dynamic_helper.dynamic_type_index);
|
||||
_row_collection.filter = new_filter.build_predicate();
|
||||
_row_collection.comparer = new_sort.comparer;
|
||||
}
|
||||
} else {
|
||||
_row_collection.filter = new_filter.build_predicate(_model.dynamic_helper.dynamic_type_index);
|
||||
_row_collection.filter = new_filter.build_predicate();
|
||||
}
|
||||
} else if (!new_sort.Equals(old_sort))
|
||||
_row_collection.comparer = new_sort.comparer;
|
||||
@@ -648,17 +629,11 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
OrderSecurity _get_root(OrderSecurity os) {
|
||||
while (os.order.ParentKey != 0 && _cached_orders.TryGetValue(os.order.ParentKey, out var pos))
|
||||
os = pos;
|
||||
return os;
|
||||
}
|
||||
|
||||
void _on_filter_changed() {
|
||||
var fd = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states());
|
||||
if (!fd.Equals(_cached_filter_data)) {
|
||||
_cached_filter_data = fd;
|
||||
_row_collection.filter = fd.build_predicate(_model.dynamic_helper.dynamic_type_index);
|
||||
_row_collection.filter = fd.build_predicate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,9 +662,9 @@ namespace mbts.spark {
|
||||
// their values will be cleared (in the param grid gui), and the modifications will not be applied.
|
||||
// FYI this behavior is implemented to support 'partial edits.' (e.g., changing the 'price' for multiple orders that do not have the same 'size').
|
||||
// ----
|
||||
var update_params = selected_rows.Select(r => update_params_factory(_get_root(r.order_tuple))).ToList();
|
||||
var update_params = selected_rows.Select(r => update_params_factory(r.order_tuple.root)).ToList();
|
||||
await Task.WhenAll(update_params.Select(t => _model.update_order(t.order, new_price: price, new_size: size, new_algo: t.new_algo)));
|
||||
(OrderRecordImmutable order, CustomOrderDefinitionImmutable? new_algo) update_params_factory(OrderSecurity order_tuple) {
|
||||
(OrderRecordImmutable order, CustomOrderDefinitionImmutable? new_algo) update_params_factory(OrderSecurityAlgo order_tuple) {
|
||||
var mod_algo = order_tuple.order.Def.CustomDef.CloneToMutable();
|
||||
if (mod_algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Composite) {
|
||||
mod_algo_vm.assign_to(mod_algo.Composite.Definition);
|
||||
@@ -702,7 +677,7 @@ namespace mbts.spark {
|
||||
} else await Task.WhenAll(selected_rows.Select(r => _model.update_order(r.order, price)));
|
||||
}
|
||||
|
||||
IObservable<ObservableCollection<OrderBookRowVM>> _subscribe_throttled_selected_rows() =>
|
||||
IObservable<IReadOnlyList<OrderBookRowVM>> _subscribe_throttled_selected_rows() =>
|
||||
selected_rows.Observe().BufferLatest(TimeSpan.FromSeconds(0.2)).Select(_ => selected_rows);
|
||||
|
||||
static OrderBookFillsGridSettings _get_fills_settings(FillsVM fills_vm) =>
|
||||
@@ -752,14 +727,15 @@ namespace mbts.spark {
|
||||
_size_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write;
|
||||
SecurityMD? _security_md;
|
||||
|
||||
public OrderParamsVM(OrderBookVM owner)
|
||||
public OrderParamsVM(OrderBookVM owner, AlgoFormats init_format, AlgoEditingPermissions init_permissions)
|
||||
: this(owner,
|
||||
AlgoDefinitionVM.create(
|
||||
owner._model,
|
||||
new AlgoData(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(CustomOrderDefinition.CustomOneofCase.Iceberg)),
|
||||
editing_permissions: AlgoEditingPermissions.AllowEditValues)) { }
|
||||
AlgoData.create_unpersisted(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(CustomOrderDefinition.CustomOneofCase.Iceberg), CustomOrderDefinition.CustomOneofCase.Iceberg.display_name()),
|
||||
init_format: init_format,
|
||||
editing_permissions: init_permissions)) { }
|
||||
|
||||
public OrderParamsVM(OrderBookVM owner, AlgoDefinitionVM algo_vm) {
|
||||
OrderParamsVM(OrderBookVM owner, AlgoDefinitionVM algo_vm) {
|
||||
_owner = owner;
|
||||
this.algo_vm = algo_vm;
|
||||
_accounts = backing(
|
||||
@@ -789,7 +765,7 @@ namespace mbts.spark {
|
||||
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size) {
|
||||
permissions = _size_permissions & _param_permissions
|
||||
};
|
||||
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_comparer) {
|
||||
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_eq_comparer) {
|
||||
permissions = _price_permissions & _param_permissions
|
||||
};
|
||||
_price_param = price_param;
|
||||
@@ -904,8 +880,8 @@ namespace mbts.spark {
|
||||
this.column_filters = column_filters;
|
||||
}
|
||||
|
||||
public Func<OrderSecurity, bool> build_predicate(IMaskIndexInt32<string> dynamic_type_index) {
|
||||
var pred = column_filters.build_predicate(dynamic_type_index);
|
||||
public Func<OrderSecurityRootPair, bool> build_predicate() {
|
||||
var pred = column_filters.build_predicate();
|
||||
if (algo_tree_display_mode == AlgoTreeDisplayMode.RootAndLeafNodes) {
|
||||
if (pred == null) {
|
||||
pred = tree_disp_pred;
|
||||
@@ -913,10 +889,10 @@ namespace mbts.spark {
|
||||
var col_pred = pred;
|
||||
pred = o => tree_disp_pred(o) && col_pred(o);
|
||||
}
|
||||
bool tree_disp_pred(OrderSecurity o) =>
|
||||
!o.order.has_parent() ||
|
||||
o.order.is_exchange_order() ||
|
||||
o.order.order_type() == CustomOrderDefinition.CustomOneofCase.Spreader;
|
||||
bool tree_disp_pred(OrderSecurityRootPair o) =>
|
||||
!o.order.order.has_parent() ||
|
||||
o.order.order.is_exchange_order() ||
|
||||
o.order.order.order_type() == CustomOrderDefinition.CustomOneofCase.Spreader;
|
||||
}
|
||||
return pred;
|
||||
}
|
||||
@@ -994,11 +970,13 @@ namespace mbts.spark {
|
||||
var join = config.Configuration.get<bts.config.JoinAttachUIOptions>();
|
||||
var lean = config.Configuration.get<bts.config.LeanAttachUIOptions>();
|
||||
var top = config.Configuration.get<bts.config.TopAttachUIOptions>();
|
||||
var offset = config.Configuration.get<bts.config.OffsetAttachUIOptions>();
|
||||
_algo_type_conf = new Dictionary<CustomOrderDefinition.CustomOneofCase, (bool, bool)> {
|
||||
{ CustomOrderDefinition.CustomOneofCase.Cover, (cover.show_price, cover.show_quantity) },
|
||||
{ CustomOrderDefinition.CustomOneofCase.Join, (join.show_price, join.show_quantity) },
|
||||
{ CustomOrderDefinition.CustomOneofCase.Lean, (lean.show_price, lean.show_quantity) },
|
||||
{ CustomOrderDefinition.CustomOneofCase.Top, (top.show_price, top.show_quantity) }
|
||||
{ CustomOrderDefinition.CustomOneofCase.Top, (top.show_price, top.show_quantity) },
|
||||
{ CustomOrderDefinition.CustomOneofCase.Offset, (offset.show_price, offset.show_quantity) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -119,36 +119,36 @@ namespace bts.shell.orderbook {
|
||||
}
|
||||
|
||||
static class FilterStatesExtensions {
|
||||
public static Func<OrderSecurity, bool> build_predicate(this FilterStates fs, IMaskIndexInt32<string> dynamic_type_index) {
|
||||
var pb = new ColumnPredicateBuilder<OrderSecurity>();
|
||||
public static Func<OrderSecurityRootPair, bool> build_predicate(this FilterStates fs) {
|
||||
var pb = new ColumnPredicateBuilder<OrderSecurityRootPair>();
|
||||
// security
|
||||
pb.add_security_criteria(fs.security_criteria, o => o.security);
|
||||
pb.add_security_criteria(fs.security_criteria, o => o.order.security);
|
||||
// regex
|
||||
pb.add_regex(fs.order_key_regex, o => o.order.Key.ToString());
|
||||
pb.add_regex(fs.exchange_message_regex, o => o.order.State.LastMsg);
|
||||
pb.add_regex(fs.attached_target_order_key_regex, o => o.order.attached_target_order_key().ToString());
|
||||
pb.add_regex(fs.parent_key_regex, o => o.order.ParentKey.ToString());
|
||||
pb.add_regex(fs.order_key_regex, o => o.order.order.Key.ToString());
|
||||
pb.add_regex(fs.exchange_message_regex, o => o.order.order.State.LastMsg);
|
||||
pb.add_regex(fs.attached_target_order_key_regex, o => o.order.order.attached_target_order_key().ToString());
|
||||
pb.add_regex(fs.parent_key_regex, o => o.order.order.ParentKey.ToString());
|
||||
// time
|
||||
pb.add_time_filter(fs.time_created, o => o.order.created());
|
||||
pb.add_time_filter(fs.last_change, o => o.order.last_change());
|
||||
pb.add_time_filter(fs.time_created, o => o.order.order.created());
|
||||
pb.add_time_filter(fs.last_change, o => o.order.order.last_change());
|
||||
// discrete values
|
||||
pb.add_discrete_values(fs.users, o => o.order.user());
|
||||
pb.add_discrete_values(fs.accounts, o => o.order.account());
|
||||
pb.add_discrete_values(fs.exchanges, o => o.security?.Exchange ?? Venue.Unset);
|
||||
pb.add_discrete_values(fs.tifs, o => o.order.time_in_force());
|
||||
pb.add_discrete_values(fs.order_types, o => AlgoID.from_custom_def(o.order.Def.CustomDef));
|
||||
pb.add_discrete_values(fs.root_algo_types, o => AlgoID.from_algo_key(o.root_algo.key, dynamic_type_index));
|
||||
pb.add_discrete_values(fs.order_sub_types, o => o.order.sub_type());
|
||||
pb.add_discrete_values(fs.sides, o => o.order.side());
|
||||
pb.add_discrete_values(fs.statuses, o => o.order.status());
|
||||
pb.add_discrete_values(fs.symbols, o => o.security?.Symbol);
|
||||
pb.add_discrete_values(fs.users, o => o.order.order.user());
|
||||
pb.add_discrete_values(fs.accounts, o => o.order.order.account());
|
||||
pb.add_discrete_values(fs.exchanges, o => o.order.security?.Exchange ?? Venue.Unset);
|
||||
pb.add_discrete_values(fs.tifs, o => o.order.order.time_in_force());
|
||||
pb.add_discrete_values(fs.order_types, o => AlgoID.from_custom_def(o.order.order.Def.CustomDef));
|
||||
pb.add_discrete_values(fs.root_algo_types, o => AlgoID.from_algo_key(o.root.algo.key));
|
||||
pb.add_discrete_values(fs.order_sub_types, o => o.order.order.sub_type());
|
||||
pb.add_discrete_values(fs.sides, o => o.order.order.side());
|
||||
pb.add_discrete_values(fs.statuses, o => o.order.order.status());
|
||||
pb.add_discrete_values(fs.symbols, o => o.order.security?.Symbol);
|
||||
// numeric
|
||||
pb.add_numeric(fs.qty, o => o.order.quantity());
|
||||
pb.add_numeric(fs.price, o => o.order.price());
|
||||
pb.add_numeric(fs.qty_filled, o => o.order.filled());
|
||||
pb.add_numeric(fs.avg_price, o => o.order.avg_price());
|
||||
pb.add_numeric(fs.last_price, o => o.order.last_price());
|
||||
pb.add_numeric(fs.piq, o => o.piq ?? uint.MaxValue);
|
||||
pb.add_numeric(fs.qty, o => o.order.order.quantity());
|
||||
pb.add_price(fs.price, o => o.order.order.price(), o => o.order.security);
|
||||
pb.add_numeric(fs.qty_filled, o => o.order.order.filled());
|
||||
pb.add_numeric(fs.avg_price, o => o.order.order.avg_price());
|
||||
pb.add_numeric(fs.last_price, o => o.order.order.last_price());
|
||||
pb.add_numeric(fs.piq, o => o.order.piq ?? uint.MaxValue);
|
||||
return pb.to_predicate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace bts.shell.orderbook {
|
||||
pvm.selected_predicate_type = _corresponding_predicate_type(EnumHelper.parse_or_default<Columns>(col_id));
|
||||
return pvm;
|
||||
},
|
||||
(gen, anims) => new OrderBookGridSettings(gen, anims.Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
|
||||
(gen, anims) => new OrderBookGridSettings(gen, anims.empty_if_null().Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_data, a.cell_animations, a.row_animations, a.sound_notifications))) { algo_tree_display_mode = _algo_tree_display_mode },
|
||||
p => p.create_predicate(notification_service.data_cache),
|
||||
p => p.create_notification_predicate(notification_service.data_cache),
|
||||
notification_service == null ? new Func<IPredicate<OrderSecurity>, SoundNotification, IDisposable>((p, sn) => Disposable.empty) : notification_service.link_to_order,
|
||||
|
||||
@@ -1,168 +1,168 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.spark;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.spark.PAndLView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
|
||||
namespace bts.shell {
|
||||
internal class PAndLWindowFactory : ShellWindowFactory<PAndLWindowFactory.PAndLStartupData> {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
|
||||
public PAndLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, "SparkPAndL") {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_hub = settings_hub;
|
||||
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
public static string col_id(Columns col) => col.ToString();
|
||||
|
||||
public override Task<IShellWindow> load(PAndLStartupData state) {
|
||||
return Task.FromResult(_create(state));
|
||||
}
|
||||
|
||||
async void show(PAndLStartupData startup_data) {
|
||||
var w = await load(startup_data);
|
||||
_bus.post(SparkTags.ShowWindow, w);
|
||||
}
|
||||
|
||||
IShellWindow _create(PAndLStartupData state) {
|
||||
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
|
||||
state.grid,
|
||||
get: us => us.pnl_grid,
|
||||
set: (us, gs) => us.with(provided_pnl_gridsettings: gs));
|
||||
return new PAndLWindow(_orders_model, this, _bus, state, SparkCommands.ShowPAndL.icon, settings_manager);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class PAndLStartupData {
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for protobuf
|
||||
/// </summary>
|
||||
public PAndLStartupData() { }
|
||||
|
||||
public PAndLStartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, pnl_calc_type calc_type, GridSettings grid = default) {
|
||||
_sort_criteria = sorting_criteria;
|
||||
this.filters = filters;
|
||||
this.calc_type = calc_type;
|
||||
this.grid = grid;
|
||||
}
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion => _sort_criteria ?? (_sort_criteria = DefaultColumns.default_sorting_criterion.ToList());
|
||||
public static PAndLStartupData Default() =>
|
||||
new PAndLStartupData(null, new FilterStates(), pnl_calc_type.midpoint);
|
||||
|
||||
|
||||
[ProtoMember(5)]
|
||||
List<SortingCriteria> _sort_criteria;
|
||||
[ProtoMember(10)]
|
||||
public FilterStates filters;
|
||||
[ProtoMember(15)]
|
||||
public pnl_calc_type calc_type;
|
||||
//[ProtoMember(20)]
|
||||
// ProtoMember 20 cannot be used further
|
||||
[ProtoMember(25)]
|
||||
public GridSettings grid;
|
||||
|
||||
static class DefaultColumns {
|
||||
|
||||
public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: DefaultColumnWidths.Exchange);
|
||||
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
|
||||
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
|
||||
public static GridColumnSettings security_type => new GridColumnSettings(col_id(Columns.SecurityType), DefaultColumnHeaders.SecurityType, width: DefaultColumnWidths.SecurityType);
|
||||
public static GridColumnSettings start_net_position => new GridColumnSettings(col_id(Columns.StartNetPos), DefaultColumnHeaders.StartNetPos, width: DefaultColumnWidths.Start_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings start_pnl => new GridColumnSettings(col_id(Columns.StartPNL), DefaultColumnHeaders.StartPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_pnl => new GridColumnSettings(col_id(Columns.DayLong), DefaultColumnHeaders.DayLong, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_pnl => new GridColumnSettings(col_id(Columns.DayShort), DefaultColumnHeaders.DayShort, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_pnl => new GridColumnSettings(col_id(Columns.DayPNL), DefaultColumnHeaders.DayPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_net_pos => new GridColumnSettings(col_id(Columns.TotalNetPos), DefaultColumnHeaders.TotalNetPos, false, width: DefaultColumnWidths.Total_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_pnl => new GridColumnSettings(col_id(Columns.TotalPNL), DefaultColumnHeaders.TotalPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings pnl_price => new GridColumnSettings(col_id(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(account.id, true);
|
||||
yield return new SortingCriteria(exchange.id, true);
|
||||
yield return new SortingCriteria(symbol.id, true);
|
||||
yield return new SortingCriteria(security.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return account;
|
||||
yield return exchange;
|
||||
yield return symbol;
|
||||
yield return security;
|
||||
yield return security_type;
|
||||
yield return start_net_position;
|
||||
yield return start_pnl;
|
||||
yield return day_long_pnl;
|
||||
yield return day_short_pnl;
|
||||
yield return day_pnl;
|
||||
yield return total_net_pos;
|
||||
yield return total_pnl;
|
||||
yield return pnl_price;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class FilterStates {
|
||||
[ProtoMember(1)]
|
||||
public string account_regex;
|
||||
[ProtoMember(2)]
|
||||
public string exchange_regex;
|
||||
[ProtoMember(3)]
|
||||
public string symbol_regex;
|
||||
[ProtoMember(4)]
|
||||
public string security_regex;
|
||||
[ProtoMember(5)]
|
||||
public IntMinMaxState start_net_pos;
|
||||
[ProtoMember(6)]
|
||||
public DoubleMinMaxState start_pnl;
|
||||
[ProtoMember(7)]
|
||||
public UIntMinMaxState day_long_pos;
|
||||
[ProtoMember(8)]
|
||||
public UIntMinMaxState day_short_pos;
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState day_pnl;
|
||||
[ProtoMember(10)]
|
||||
public IntMinMaxState total_net_pos;
|
||||
[ProtoMember(11)]
|
||||
public DoubleMinMaxState total_pnl;
|
||||
[ProtoMember(12)]
|
||||
public DoubleMinMaxState pnl_price;
|
||||
|
||||
// [ProtoMember(13)]
|
||||
// ProtoMember 13 cannot be used further
|
||||
[ProtoMember(14)]
|
||||
public SecurityType[] security_types;
|
||||
[ProtoMember(15)]
|
||||
public bool[] has_day_trades;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.spark;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.spark.PAndLView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
|
||||
namespace bts.shell {
|
||||
internal class PAndLWindowFactory : ShellWindowFactory<PAndLWindowFactory.PAndLStartupData> {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
|
||||
public PAndLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, "SparkPAndL") {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_hub = settings_hub;
|
||||
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
public static string col_id(Columns col) => col.ToString();
|
||||
|
||||
public override Task<IShellWindow> load(PAndLStartupData state) {
|
||||
return Task.FromResult(_create(state));
|
||||
}
|
||||
|
||||
async void show(PAndLStartupData startup_data) {
|
||||
var w = await load(startup_data);
|
||||
_bus.post(SparkTags.ShowWindow, w);
|
||||
}
|
||||
|
||||
IShellWindow _create(PAndLStartupData state) {
|
||||
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
|
||||
state.grid,
|
||||
get: us => us.pnl_grid,
|
||||
set: (us, gs) => us.with(provided_pnl_gridsettings: gs));
|
||||
return new PAndLWindow(_orders_model, this, _bus, state, SparkCommands.ShowPAndL.icon, settings_manager);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class PAndLStartupData {
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for protobuf
|
||||
/// </summary>
|
||||
public PAndLStartupData() { }
|
||||
|
||||
public PAndLStartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, pnl_calc_type calc_type, GridSettings grid = default) {
|
||||
_sort_criteria = sorting_criteria;
|
||||
this.filters = filters;
|
||||
this.calc_type = calc_type;
|
||||
this.grid = grid;
|
||||
}
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion => _sort_criteria ?? (_sort_criteria = DefaultColumns.default_sorting_criterion.ToList());
|
||||
public static PAndLStartupData Default() =>
|
||||
new PAndLStartupData(null, new FilterStates(), pnl_calc_type.midpoint);
|
||||
|
||||
|
||||
[ProtoMember(5)]
|
||||
List<SortingCriteria> _sort_criteria;
|
||||
[ProtoMember(10)]
|
||||
public FilterStates filters;
|
||||
[ProtoMember(15)]
|
||||
public pnl_calc_type calc_type;
|
||||
//[ProtoMember(20)]
|
||||
// ProtoMember 20 cannot be used further
|
||||
[ProtoMember(25)]
|
||||
public GridSettings grid;
|
||||
|
||||
static class DefaultColumns {
|
||||
|
||||
public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: DefaultColumnWidths.Exchange);
|
||||
public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol);
|
||||
public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security);
|
||||
public static GridColumnSettings security_type => new GridColumnSettings(col_id(Columns.SecurityType), DefaultColumnHeaders.SecurityType, width: DefaultColumnWidths.SecurityType);
|
||||
public static GridColumnSettings start_net_position => new GridColumnSettings(col_id(Columns.StartNetPos), DefaultColumnHeaders.StartNetPos, width: DefaultColumnWidths.Start_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings start_pnl => new GridColumnSettings(col_id(Columns.StartPNL), DefaultColumnHeaders.StartPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_pnl => new GridColumnSettings(col_id(Columns.DayLong), DefaultColumnHeaders.DayLong, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_pnl => new GridColumnSettings(col_id(Columns.DayShort), DefaultColumnHeaders.DayShort, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_pnl => new GridColumnSettings(col_id(Columns.DayPNL), DefaultColumnHeaders.DayPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_net_pos => new GridColumnSettings(col_id(Columns.TotalNetPos), DefaultColumnHeaders.TotalNetPos, false, width: DefaultColumnWidths.Total_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_pnl => new GridColumnSettings(col_id(Columns.TotalPNL), DefaultColumnHeaders.TotalPnL, false, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
public static GridColumnSettings pnl_price => new GridColumnSettings(col_id(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(account.id, true);
|
||||
yield return new SortingCriteria(exchange.id, true);
|
||||
yield return new SortingCriteria(symbol.id, true);
|
||||
yield return new SortingCriteria(security.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return account;
|
||||
yield return exchange;
|
||||
yield return symbol;
|
||||
yield return security;
|
||||
yield return security_type;
|
||||
yield return start_net_position;
|
||||
yield return start_pnl;
|
||||
yield return day_long_pnl;
|
||||
yield return day_short_pnl;
|
||||
yield return day_pnl;
|
||||
yield return total_net_pos;
|
||||
yield return total_pnl;
|
||||
yield return pnl_price;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class FilterStates {
|
||||
[ProtoMember(1)]
|
||||
public string account_regex;
|
||||
[ProtoMember(2)]
|
||||
public string exchange_regex;
|
||||
[ProtoMember(3)]
|
||||
public string symbol_regex;
|
||||
[ProtoMember(4)]
|
||||
public string security_regex;
|
||||
[ProtoMember(5)]
|
||||
public IntMinMaxState start_net_pos;
|
||||
[ProtoMember(6)]
|
||||
public DoubleMinMaxState start_pnl;
|
||||
[ProtoMember(7)]
|
||||
public UIntMinMaxState day_long_pos;
|
||||
[ProtoMember(8)]
|
||||
public UIntMinMaxState day_short_pos;
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState day_pnl;
|
||||
[ProtoMember(10)]
|
||||
public IntMinMaxState total_net_pos;
|
||||
[ProtoMember(11)]
|
||||
public DoubleMinMaxState total_pnl;
|
||||
[ProtoMember(12)]
|
||||
public DoubleMinMaxState pnl_price;
|
||||
|
||||
// [ProtoMember(13)]
|
||||
// ProtoMember 13 cannot be used further
|
||||
[ProtoMember(14)]
|
||||
public SecurityType[] security_types;
|
||||
[ProtoMember(15)]
|
||||
public bool[] has_day_trades;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui.controls;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
public enum PNLCalculationType {
|
||||
None,
|
||||
Bid,
|
||||
Ask,
|
||||
Opposite,
|
||||
Same,
|
||||
Waterfall,
|
||||
Last,
|
||||
Mid,
|
||||
Settle
|
||||
}
|
||||
|
||||
readonly struct PositionPriceSize : IEquatable<PositionPriceSize> {
|
||||
public static readonly PositionPriceSize empty = new PositionPriceSize(0, 0, 0, 0);
|
||||
public readonly double long_pos, short_pos, long_avg, short_avg;
|
||||
public double net_pos => long_pos - short_pos;
|
||||
public bool has_position => long_pos > 0 || short_pos > 0;
|
||||
|
||||
public PositionPriceSize(double long_pos, double long_avg, double short_pos, double short_avg) {
|
||||
this.long_pos = long_pos;
|
||||
this.long_avg = long_avg;
|
||||
this.short_pos = short_pos;
|
||||
this.short_avg = short_avg;
|
||||
}
|
||||
|
||||
public bool Equals(PositionPriceSize other) =>
|
||||
MathUtils.epsilon_equals(long_pos, other.long_pos) &&
|
||||
MathUtils.epsilon_equals(long_avg, other.long_avg) &&
|
||||
MathUtils.epsilon_equals(short_pos, other.short_pos) &&
|
||||
MathUtils.epsilon_equals(short_avg, other.short_avg);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PositionPriceSize other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
long_pos.GetHashCode(),
|
||||
long_avg.GetHashCode(),
|
||||
short_pos.GetHashCode(),
|
||||
short_avg.GetHashCode());
|
||||
|
||||
public static bool operator ==(PositionPriceSize a, PositionPriceSize b) => a.Equals(b);
|
||||
public static bool operator !=(PositionPriceSize a, PositionPriceSize b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
readonly struct PNLRowKey : IEquatable<PNLRowKey>, IComparable<PNLRowKey>, IComparable {
|
||||
public static readonly PNLRowKey empty = new PNLRowKey();
|
||||
static readonly IComparer<PNLRowKey> _comparer_inst = new ComparerBuilder<PNLRowKey>(
|
||||
new[] {
|
||||
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.account ?? 0),
|
||||
Comparer<uint>.Default.wrap<uint, PNLRowKey>(r => r.security ?? 0)
|
||||
}).to_comparer();
|
||||
|
||||
public readonly Venue? exchange;
|
||||
public readonly Security.ContractOneofCase? security_type;
|
||||
public readonly string symbol;
|
||||
public readonly PNLKey data_key;
|
||||
public uint? security => data_key.security_id;
|
||||
public uint? account => data_key.account_id;
|
||||
|
||||
public PNLRowKey(
|
||||
Venue? exchange = null,
|
||||
Security.ContractOneofCase? security_type = null,
|
||||
string symbol = null,
|
||||
uint? security = null,
|
||||
uint? account = null) {
|
||||
//
|
||||
this.exchange = exchange;
|
||||
this.security_type = security_type;
|
||||
this.symbol = symbol;
|
||||
data_key = new PNLKey(account.Value, security.Value);
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLRowKey other ? CompareTo(other) : 1;
|
||||
|
||||
public int CompareTo(PNLRowKey other) =>
|
||||
_comparer_inst.Compare(this, other);
|
||||
|
||||
public bool Equals(PNLRowKey other) =>
|
||||
exchange == other.exchange &&
|
||||
security_type == other.security_type &&
|
||||
symbol == other.symbol &&
|
||||
security == other.security &&
|
||||
account == other.account;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLRowKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
exchange?.GetHashCode() ?? 0,
|
||||
security_type?.GetHashCode() ?? 0,
|
||||
symbol?.GetHashCode() ?? 0,
|
||||
security?.GetHashCode() ?? 0,
|
||||
account?.GetHashCode() ?? 0);
|
||||
|
||||
public static bool operator ==(PNLRowKey a, PNLRowKey b) => a.Equals(b);
|
||||
public static bool operator !=(PNLRowKey a, PNLRowKey b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
readonly struct WorkingOrderData {
|
||||
public readonly WorkingOrderSideData long_data, short_data;
|
||||
|
||||
WorkingOrderData(WorkingOrderSideData long_data, WorkingOrderSideData short_data) {
|
||||
this.long_data = long_data;
|
||||
this.short_data = short_data;
|
||||
}
|
||||
|
||||
public static WorkingOrderData Empty =>
|
||||
new WorkingOrderData(WorkingOrderSideData.EmptyLong, WorkingOrderSideData.EmptyShort);
|
||||
|
||||
public double top_bid =>
|
||||
long_data.data.Count > 0 ?
|
||||
long_data.data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public double top_ask =>
|
||||
short_data.data.Count > 0 ?
|
||||
short_data.data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public PositionPriceSize summary =>
|
||||
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
|
||||
|
||||
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders) =>
|
||||
orders.Aggregate(Empty, (seed, order) => seed.Add(order));
|
||||
|
||||
public WorkingOrderData Add(OrderRecordImmutable order) {
|
||||
if (order.working_quantity() < 0)
|
||||
return new WorkingOrderData(long_data, short_data);
|
||||
switch (order.side()) {
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Add(order), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
|
||||
}
|
||||
}
|
||||
|
||||
public WorkingOrderData Remove(OrderRecordImmutable order) {
|
||||
if (order.working_quantity() < 0)
|
||||
return new WorkingOrderData(long_data, short_data);
|
||||
switch (order.side()) {
|
||||
case Side.Buy: return new WorkingOrderData(long_data.Remove(order.Key, order.price()), short_data);
|
||||
case Side.Sell: return new WorkingOrderData(long_data, short_data.Remove(order.Key, order.price()));
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct WorkingOrderSideData {
|
||||
public readonly double avg_price;
|
||||
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data;
|
||||
public readonly int working_total;
|
||||
|
||||
WorkingOrderSideData(double avg_price, int working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data) {
|
||||
this.avg_price = avg_price;
|
||||
this.working_total = working_total;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
|
||||
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(OrderDataHelper.PriceComparer));
|
||||
|
||||
public double top_price =>
|
||||
data.Count > 0 ?
|
||||
data.First().Key :
|
||||
double.NaN;
|
||||
|
||||
public int top_volume =>
|
||||
data.Count > 0 ?
|
||||
data.Values.First().Values.Sum(o => o.working_quantity()) :
|
||||
0;
|
||||
|
||||
public WorkingOrderSideData Add(OrderRecordImmutable order_record) {
|
||||
uint order = order_record.Key;
|
||||
int working_qty = order_record.working_quantity();
|
||||
double price = order_record.price();
|
||||
if (working_qty <= 0)
|
||||
throw new ArgumentException(nameof(working_qty));
|
||||
if (working_total == 0) {
|
||||
return new WorkingOrderSideData(price, working_qty, data.Add(price, ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
} else return new WorkingOrderSideData(
|
||||
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
|
||||
working_total + working_qty,
|
||||
data.SetItem(price, data.TryGetValue(price, out var curr) ?
|
||||
curr.Add(order, order_record) :
|
||||
ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
|
||||
}
|
||||
|
||||
public WorkingOrderSideData Remove(uint order, double price) {
|
||||
var tmp_data = data;
|
||||
if (!tmp_data.TryGetValue(price, out var wdata))
|
||||
throw new KeyNotFoundException(nameof(price));
|
||||
if (!wdata.TryGetValue(order, out var worder))
|
||||
throw new KeyNotFoundException(nameof(order));
|
||||
var wqty = worder.working_quantity();
|
||||
wdata = wdata.Remove(order);
|
||||
if (wdata.Count == 0)
|
||||
tmp_data = tmp_data.Remove(price);
|
||||
if (tmp_data.Count == 0)
|
||||
return new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(data.KeyComparer));
|
||||
return new WorkingOrderSideData(
|
||||
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
|
||||
working_total - wqty,
|
||||
tmp_data);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
if (data.Count > 0) {
|
||||
var best_order = data.Values.First().Values.First();
|
||||
return OrderUtils.order_collection_summary(best_order.price(), avg_price, best_order.working_quantity(), working_total, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLRowData : IEquatable<PNLRowData> {
|
||||
public static readonly Func<PNLRowData, PNLKey> key_fn = d => d.key;
|
||||
public static IComparer<PNLRowData> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowData>(d => d.key);
|
||||
|
||||
static readonly WaterfallPriceFlags[] _waterfall_preferences = new[] {
|
||||
WaterfallPriceFlags.LastInsideSpread,
|
||||
WaterfallPriceFlags.Mid,
|
||||
WaterfallPriceFlags.LastGTEBid,
|
||||
WaterfallPriceFlags.Bid,
|
||||
WaterfallPriceFlags.LastLTEAsk,
|
||||
WaterfallPriceFlags.Ask,
|
||||
WaterfallPriceFlags.Last,
|
||||
WaterfallPriceFlags.Settle
|
||||
};
|
||||
|
||||
public readonly PNLKey key;
|
||||
public readonly SecurityImmutable? security;
|
||||
public readonly string account_name;
|
||||
public readonly PNLCalculationType calc_type;
|
||||
public readonly PositionPriceSize day, sod;
|
||||
public readonly WorkingOrderData working;
|
||||
|
||||
public readonly double
|
||||
cpp,
|
||||
bid_price,
|
||||
ask_price,
|
||||
last_price,
|
||||
settlement_price;
|
||||
|
||||
public PNLRowData(
|
||||
PNLKey key,
|
||||
double cpp = 0,
|
||||
double bid_price = 0,
|
||||
double ask_price = 0,
|
||||
double last_price = 0,
|
||||
double settlement_price = 0,
|
||||
PositionPriceSize? sod = null,
|
||||
PositionPriceSize? day = null,
|
||||
WorkingOrderData? working = null,
|
||||
SecurityImmutable? security = null,
|
||||
string account_name = null,
|
||||
PNLCalculationType calc_type = PNLCalculationType.Mid) {
|
||||
//
|
||||
this.key = key;
|
||||
this.cpp = cpp;
|
||||
this.bid_price = bid_price;
|
||||
this.ask_price = ask_price;
|
||||
this.last_price = last_price;
|
||||
this.settlement_price = settlement_price;
|
||||
this.sod = sod ?? PositionPriceSize.empty;
|
||||
this.day = day ?? PositionPriceSize.empty;
|
||||
this.working = working ?? WorkingOrderData.Empty;
|
||||
this.security = security;
|
||||
this.account_name = account_name;
|
||||
this.calc_type = calc_type;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
enum WaterfallPriceFlags {
|
||||
None = 0,
|
||||
Bid = 1,
|
||||
Ask = 1 << 1,
|
||||
Last = 1 << 2,
|
||||
Settle = 1 << 3,
|
||||
Mid = Bid | Ask,
|
||||
LastGTEBid = Bid | Last | 1 << 4,
|
||||
LastLTEAsk = Ask | Last | 1 << 5,
|
||||
LastInsideSpread = LastGTEBid | LastLTEAsk,
|
||||
BidGTESettle = Bid | Settle | 1 << 6,
|
||||
AskLTESettle = Ask | Settle | 1 << 7
|
||||
}
|
||||
|
||||
/// Market/Calc
|
||||
public double mid_price => (bid_price + ask_price) / 2;
|
||||
public double pnl_price {
|
||||
get {
|
||||
if (!day.has_position && !sod.has_position)
|
||||
return double.NaN;
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Bid: return bid_price;
|
||||
case PNLCalculationType.Ask: return ask_price;
|
||||
case PNLCalculationType.Mid: return mid_price;
|
||||
case PNLCalculationType.Last: return last_price;
|
||||
// waited averages must be used when the pnl price varies, depending on the side.
|
||||
case PNLCalculationType.Same: return MathUtils.weighted_average(bid_price, day_long_pos, ask_price, day_short_pos);
|
||||
case PNLCalculationType.Opposite: return MathUtils.weighted_average(ask_price, day_long_pos, bid_price, day_short_pos);
|
||||
case PNLCalculationType.Settle: return settlement_price;
|
||||
case PNLCalculationType.Waterfall:
|
||||
default: return _waterfall_price();
|
||||
}
|
||||
}
|
||||
}
|
||||
public double long_pnl_price {
|
||||
get {
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Same: return bid_price;
|
||||
case PNLCalculationType.Opposite: return ask_price;
|
||||
default: return pnl_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
public double short_pnl_price {
|
||||
get {
|
||||
switch (calc_type) {
|
||||
case PNLCalculationType.Same: return ask_price;
|
||||
case PNLCalculationType.Opposite: return bid_price;
|
||||
default: return pnl_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Sod
|
||||
public double sod_pnl => cpp *
|
||||
DoubleUtil.nan_to_zero_sum(
|
||||
sod_long_avg_price_delta_raw * sod_long_pos,
|
||||
sod_short_avg_price_delta_raw * sod_short_pos
|
||||
);
|
||||
// long
|
||||
public double sod_long_pos => sod.long_pos;
|
||||
public double sod_long_avg_price => cpp * sod.long_avg;
|
||||
public double sod_long_avg_price_raw => sod.long_avg;
|
||||
public double sod_long_avg_price_delta => cpp * sod_long_avg_price_delta_raw;
|
||||
public double sod_long_avg_price_delta_raw => sod.long_pos == 0 ? double.NaN : long_pnl_price - sod.long_avg;
|
||||
// short
|
||||
public double sod_short_pos => sod.short_pos;
|
||||
public double sod_short_avg_price => cpp * sod.short_avg;
|
||||
public double sod_short_avg_price_raw => sod.short_avg;
|
||||
public double sod_short_avg_price_delta => cpp * sod_short_avg_price_delta_raw;
|
||||
public double sod_short_avg_price_delta_raw => sod.short_pos == 0 ? double.NaN : sod.short_avg - short_pnl_price;
|
||||
// net
|
||||
public double sod_net_pos => sod.net_pos;
|
||||
public double sod_net_avg_price_delta => cpp * sod_net_avg_price_delta_raw;
|
||||
public double sod_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_long_avg_price_delta_raw, sod.long_pos,
|
||||
sod_short_avg_price_delta_raw, sod.short_pos);
|
||||
|
||||
/// Day
|
||||
public double day_pnl => cpp *
|
||||
DoubleUtil.nan_to_zero_sum(
|
||||
day_long_avg_price_delta_raw * day_long_pos,
|
||||
day_short_avg_price_delta_raw * day_short_pos
|
||||
);
|
||||
// long
|
||||
public double day_long_pos => day.long_pos;
|
||||
public double day_long_avg_price => cpp * day.long_avg;
|
||||
public double day_long_avg_price_raw => day.long_avg;
|
||||
public double day_long_avg_price_delta => cpp * day_long_avg_price_delta_raw;
|
||||
public double day_long_avg_price_delta_raw => day.long_pos == 0 ? double.NaN : long_pnl_price - day.long_avg;
|
||||
// short
|
||||
public double day_short_pos => day.short_pos;
|
||||
public double day_short_avg_price => cpp * day.short_avg;
|
||||
public double day_short_avg_price_raw => day.short_avg;
|
||||
public double day_short_avg_price_delta => cpp * day_short_avg_price_delta_raw;
|
||||
public double day_short_avg_price_delta_raw => day.short_pos == 0 ? double.NaN : day.short_avg - short_pnl_price;
|
||||
public bool has_day_trades => day.long_pos > 0 || day.short_pos > 0;
|
||||
// net
|
||||
public double day_net_pos => day.net_pos;
|
||||
public double day_net_avg_price_delta => cpp * day_net_avg_price_delta_raw;
|
||||
public double day_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
day_long_avg_price_delta_raw, day.long_pos,
|
||||
day_short_avg_price_delta_raw, day.short_pos);
|
||||
|
||||
/// Total
|
||||
public double total_pnl => DoubleUtil.nan_to_zero_sum(sod_pnl, day_pnl);
|
||||
// long
|
||||
public double total_long_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, day.long_pos);
|
||||
public double total_long_avg_price => cpp * total_long_avg_price_raw;
|
||||
public double total_long_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.long_avg, day.long_avg);
|
||||
public double total_long_avg_price_delta => cpp * total_long_avg_price_delta_raw;
|
||||
public double total_long_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_long_avg_price_delta_raw, sod.long_pos,
|
||||
day_long_avg_price_delta_raw, day.long_pos);
|
||||
// short
|
||||
public double total_short_pos => DoubleUtil.nan_to_zero_sum(sod.short_pos, day.short_pos);
|
||||
public double total_short_avg_price => cpp * total_short_avg_price_raw;
|
||||
public double total_short_avg_price_raw => DoubleUtil.nan_to_zero_sum(sod.short_avg, day.short_avg);
|
||||
public double total_short_avg_price_delta => cpp * total_short_avg_price_delta_raw;
|
||||
public double total_short_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
sod_short_avg_price_delta_raw, sod.short_pos,
|
||||
day_short_avg_price_delta_raw, day.short_pos);
|
||||
// net
|
||||
public double total_net_pos => DoubleUtil.nan_to_zero_sum(sod.long_pos, -sod.short_pos, day.long_pos, -day.short_pos);
|
||||
public double total_net_avg_price_delta => cpp * total_net_avg_price_delta_raw;
|
||||
public double total_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
total_long_avg_price_delta_raw, total_long_pos,
|
||||
total_short_avg_price_delta_raw, total_short_pos);
|
||||
|
||||
#region Working
|
||||
// long
|
||||
public double working_long_pos => working.long_data.working_total;
|
||||
public double working_long_avg_price => cpp * working_long_avg_price_raw;
|
||||
public double working_long_top_price => cpp * working_long_top_price_raw;
|
||||
public double working_long_top_price_raw => working.top_bid;
|
||||
public double working_long_avg_price_raw => working.long_data.avg_price;
|
||||
public double working_long_avg_price_delta => cpp * working_long_avg_price_delta_raw;
|
||||
public double working_long_avg_price_delta_raw => working_long_pos == 0 ? double.NaN : long_pnl_price - working_long_avg_price_raw;
|
||||
// short
|
||||
public double working_short_pos => working.short_data.working_total;
|
||||
public double working_short_avg_price => cpp * working_short_avg_price_raw;
|
||||
public double working_short_top_price => cpp * working_short_top_price_raw;
|
||||
public double working_short_top_price_raw => working.top_ask;
|
||||
public double working_short_avg_price_raw => working.short_data.avg_price;
|
||||
public double working_short_avg_price_delta => cpp * working_short_avg_price_delta_raw;
|
||||
public double working_short_avg_price_delta_raw => working_short_pos == 0 ? double.NaN : working_short_avg_price_raw - short_pnl_price;
|
||||
// net
|
||||
public double working_net_pos => working_long_pos - working_short_pos;
|
||||
public double working_net_avg_price_delta => cpp * working_net_avg_price_delta_raw;
|
||||
public double working_net_avg_price_delta_raw =>
|
||||
MathUtils.weighted_average(
|
||||
working_long_avg_price_delta_raw, working_long_pos,
|
||||
working_short_avg_price_delta_raw, working_short_pos);
|
||||
#endregion
|
||||
|
||||
WaterfallPriceFlags _waterfall_flags() {
|
||||
var flags = WaterfallPriceFlags.None;
|
||||
if (!double.IsNaN(bid_price)) flags |= WaterfallPriceFlags.Bid;
|
||||
if (!double.IsNaN(ask_price)) flags |= WaterfallPriceFlags.Ask;
|
||||
if (!double.IsNaN(last_price)) flags |= WaterfallPriceFlags.Last;
|
||||
if (last_price >= bid_price) flags |= WaterfallPriceFlags.LastGTEBid;
|
||||
if (last_price <= ask_price) flags |= WaterfallPriceFlags.LastLTEAsk;
|
||||
if (!double.IsNaN(settlement_price)) flags |= WaterfallPriceFlags.Settle;
|
||||
if (bid_price >= settlement_price) flags |= WaterfallPriceFlags.BidGTESettle;
|
||||
if (ask_price <= settlement_price) flags |= WaterfallPriceFlags.AskLTESettle;
|
||||
return flags;
|
||||
}
|
||||
|
||||
double _waterfall_price() {
|
||||
var flags = _waterfall_flags();
|
||||
// return price for the best match
|
||||
for (int i = 0; i < _waterfall_preferences.Length; i++) {
|
||||
if ((_waterfall_preferences[i] & flags) == _waterfall_preferences[i])
|
||||
return _waterfall_price(_waterfall_preferences[i]);
|
||||
}
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
double _waterfall_price(WaterfallPriceFlags ltp) {
|
||||
switch (ltp) {
|
||||
case WaterfallPriceFlags.Last:
|
||||
case WaterfallPriceFlags.LastInsideSpread:
|
||||
case WaterfallPriceFlags.LastGTEBid:
|
||||
case WaterfallPriceFlags.LastLTEAsk: return last_price;
|
||||
case WaterfallPriceFlags.Mid: return mid_price;
|
||||
case WaterfallPriceFlags.Bid:
|
||||
case WaterfallPriceFlags.BidGTESettle: return bid_price;
|
||||
case WaterfallPriceFlags.Ask:
|
||||
case WaterfallPriceFlags.AskLTESettle: return ask_price;
|
||||
case WaterfallPriceFlags.Settle: return settlement_price;
|
||||
default: return double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(PNLRowData other) =>
|
||||
key == other.key &&
|
||||
cpp == other.cpp &&
|
||||
security.HasValue == other.security.HasValue &&
|
||||
account_name == other.account_name &&
|
||||
sod == other.sod &&
|
||||
day == other.day &&
|
||||
working.summary == other.working.summary;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLRowData other && Equals(other);
|
||||
|
||||
// assuming callers will not combine different values with the same key,
|
||||
// the other components shouldn't be included in the hash calculation.
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public static bool operator ==(PNLRowData a, PNLRowData b) => a.Equals(b);
|
||||
public static bool operator !=(PNLRowData a, PNLRowData b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
static class PNLRowDataFactory {
|
||||
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value) t) =>
|
||||
new PNLRowData(new PNLKey(t.value.Pos.AccountId, t.value.Pos.SecurityId),
|
||||
t.value.CashValue,
|
||||
t.value.BidPrice,
|
||||
t.value.AskPrice,
|
||||
t.value.LastPrice,
|
||||
t.value.SettlementPrice,
|
||||
new PositionPriceSize(t.value.Pos.SodLongPos, t.value.Pos.SodLongAvgPrice, t.value.Pos.SodShortPos, t.value.Pos.SodShortAvgPrice),
|
||||
new PositionPriceSize(t.value.Pos.DayLongPos, t.value.Pos.DayLongAvgPrice, t.value.Pos.DayShortPos, t.value.Pos.DayShortAvgPrice),
|
||||
WorkingOrderData.Empty,
|
||||
t.sec,
|
||||
t.value.Pos.Account);
|
||||
}
|
||||
|
||||
static class PNLDataExtensions {
|
||||
// Note: PNLRowData is passed by ref because it is fairly large struct, and since it is a readonly struct, we know it cannot be modified (so might as well avoid excessive copying).
|
||||
public static PNLRowData with(this PNLRowData pnl_data, SecurityImmutable? security = null, string account_name = null, WorkingOrderData? working = null, PNLCalculationType? calc_type = null) =>
|
||||
new PNLRowData(
|
||||
pnl_data.key,
|
||||
pnl_data.cpp,
|
||||
pnl_data.bid_price,
|
||||
pnl_data.ask_price,
|
||||
pnl_data.last_price,
|
||||
pnl_data.settlement_price,
|
||||
pnl_data.sod,
|
||||
pnl_data.day,
|
||||
working ?? pnl_data.working,
|
||||
security ?? pnl_data.security,
|
||||
account_name ?? pnl_data.account_name,
|
||||
calc_type ?? pnl_data.calc_type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLKey : IEquatable<PNLKey>, IComparable<PNLKey>, IComparable {
|
||||
public readonly uint account_id, security_id;
|
||||
|
||||
public PNLKey(uint account_id, uint security_id) {
|
||||
this.account_id = account_id;
|
||||
this.security_id = security_id;
|
||||
}
|
||||
|
||||
public int CompareTo(PNLKey other) {
|
||||
int c = account_id.CompareTo(other.account_id);
|
||||
if (c == 0)
|
||||
c = security_id.CompareTo(other.security_id);
|
||||
return c;
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLKey other ? CompareTo(other) : 1;
|
||||
|
||||
public bool Equals(PNLKey other) =>
|
||||
security_id == other.security_id &&
|
||||
account_id == other.account_id;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
(security_id.GetHashCode() % MathUtils.Primes.P_65449) |
|
||||
((account_id.GetHashCode() % MathUtils.Primes.P_65449) << 16);
|
||||
|
||||
public override string ToString() =>
|
||||
$"Account: {account_id}, Security: {security_id}";
|
||||
|
||||
public static bool operator ==(PNLKey a, PNLKey b) => a.Equals(b);
|
||||
public static bool operator !=(PNLKey a, PNLKey b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLNormalizedData : IEquatable<PNLNormalizedData> {
|
||||
public static readonly PNLNormalizedData zero =
|
||||
new PNLNormalizedData(0, 0, 0, 0, 0, 0);
|
||||
|
||||
public static PNLNormalizedData create_nanless(
|
||||
decimal day_pnl,
|
||||
decimal day_long_pos,
|
||||
decimal day_short_pos,
|
||||
decimal sod_pnl,
|
||||
decimal sod_long_pos,
|
||||
decimal sod_short_pos,
|
||||
decimal working_long_pos,
|
||||
decimal working_short_pos) =>
|
||||
new PNLNormalizedData(
|
||||
day_pnl,
|
||||
day_long_pos,
|
||||
day_short_pos,
|
||||
sod_pnl,
|
||||
sod_long_pos,
|
||||
sod_short_pos,
|
||||
working_long_pos,
|
||||
working_short_pos);
|
||||
|
||||
public PNLNormalizedData(
|
||||
decimal day_pnl,
|
||||
decimal day_long_pos,
|
||||
decimal day_short_pos,
|
||||
decimal sod_pnl,
|
||||
decimal sod_long_pos,
|
||||
decimal sod_short_pos,
|
||||
decimal working_long_pos = 0,
|
||||
decimal working_short_pos = 0) {
|
||||
//
|
||||
this.day_pnl = day_pnl;
|
||||
this.day_long_pos = day_long_pos;
|
||||
this.day_short_pos = day_short_pos;
|
||||
this.sod_pnl = sod_pnl;
|
||||
this.sod_long_pos = sod_long_pos;
|
||||
this.sod_short_pos = sod_short_pos;
|
||||
this.working_long_pos = working_long_pos;
|
||||
this.working_short_pos = working_short_pos;
|
||||
}
|
||||
|
||||
// sod
|
||||
public decimal sod_pnl { get; }
|
||||
public decimal sod_long_pos { get; }
|
||||
public decimal sod_short_pos { get; }
|
||||
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
|
||||
// day
|
||||
public decimal day_pnl { get; }
|
||||
public decimal day_long_pos { get; }
|
||||
public decimal day_short_pos { get; }
|
||||
public decimal day_net_pos => day_long_pos - day_short_pos;
|
||||
// total
|
||||
public decimal total_pnl => sod_pnl + day_pnl;
|
||||
public decimal total_net_pos => total_long_pos - total_short_pos;
|
||||
public decimal total_long_pos => sod_long_pos + day_long_pos;
|
||||
public decimal total_short_pos => sod_short_pos + day_short_pos;
|
||||
// working
|
||||
public decimal working_long_pos { get; }
|
||||
public decimal working_net_pos => working_long_pos - working_short_pos;
|
||||
public decimal working_short_pos { get; }
|
||||
|
||||
public bool Equals(PNLNormalizedData other) =>
|
||||
day_pnl == other.day_pnl &&
|
||||
day_long_pos == other.day_long_pos &&
|
||||
day_short_pos == other.day_short_pos &&
|
||||
sod_pnl == other.sod_pnl &&
|
||||
sod_long_pos == other.sod_long_pos &&
|
||||
sod_short_pos == other.sod_short_pos &&
|
||||
working_long_pos == other.working_long_pos &&
|
||||
working_short_pos == other.working_short_pos;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLNormalizedData other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
day_pnl.GetHashCode(),
|
||||
sod_pnl.GetHashCode());
|
||||
|
||||
public override string ToString() =>
|
||||
string.Join(",", new[] {
|
||||
$"SOD PNL : {sod_pnl}",
|
||||
$"SOD Long : {sod_pnl}",
|
||||
$"SOD Short: {sod_pnl}",
|
||||
$"Day PNL : {day_pnl}",
|
||||
$"Day Long : {day_pnl}",
|
||||
$"Day Short: {day_pnl}",
|
||||
$"Work Long : {working_long_pos}",
|
||||
$"Work Short: {working_short_pos}",
|
||||
});
|
||||
|
||||
public static bool operator ==(PNLNormalizedData a, PNLNormalizedData b) => a.Equals(b);
|
||||
public static bool operator !=(PNLNormalizedData a, PNLNormalizedData b) => !a.Equals(b);
|
||||
|
||||
public static PNLNormalizedData operator +(PNLNormalizedData a, PNLNormalizedData b) =>
|
||||
new PNLNormalizedData(
|
||||
a.day_pnl + b.day_pnl,
|
||||
a.day_long_pos + b.day_long_pos,
|
||||
a.day_short_pos + b.day_short_pos,
|
||||
a.sod_pnl + b.sod_pnl,
|
||||
a.sod_long_pos + b.sod_long_pos,
|
||||
a.sod_short_pos + b.sod_short_pos,
|
||||
a.working_long_pos + b.working_long_pos,
|
||||
a.working_short_pos + b.working_short_pos);
|
||||
|
||||
public static PNLNormalizedData operator -(PNLNormalizedData a, PNLNormalizedData b) =>
|
||||
new PNLNormalizedData(
|
||||
a.day_pnl - b.day_pnl,
|
||||
a.day_long_pos - b.day_long_pos,
|
||||
a.day_short_pos - b.day_short_pos,
|
||||
a.sod_pnl - b.sod_pnl,
|
||||
a.sod_long_pos - b.sod_long_pos,
|
||||
a.sod_short_pos - b.sod_short_pos,
|
||||
a.working_long_pos - b.working_long_pos,
|
||||
a.working_short_pos - b.working_short_pos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
using bts.api;
|
||||
using bts.collections;
|
||||
using bts.shell.pnl;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell {
|
||||
class PNLWindowFactory : ShellWindowFactory<PNLSaveState> {
|
||||
const string _window_type = "PNL", _window_title = "PNL";
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly PNLConfig _pnl_conf;
|
||||
readonly IShellWindowThreadIDProvider _thread_id_provider;
|
||||
|
||||
public PNLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(owner, _window_type) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_hub = settings_hub;
|
||||
_pnl_conf = new PNLConfig(Configuration.get<bts.config.PNLOptions>());
|
||||
_thread_id_provider = ShellWindowThreadIDFactory.create(_window_type, Configuration.get<bts.config.MultiThreadedWindows>().pnl);
|
||||
_dispose = bus.subscribe(SparkTags.ShowPNLTag).OnDispatcher().Subscribe(_show);
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(PNLSaveState state) =>
|
||||
Task.FromResult(_create(state.settings));
|
||||
|
||||
void _show(PNLSettings settings) =>
|
||||
_bus.post(SparkTags.ShowWindow, _create(settings));
|
||||
|
||||
IShellWindow _create(PNLSettings settings) {
|
||||
if (settings != null) {
|
||||
var def_grid = PNLSettings.DefaultGridSettings;
|
||||
// add missing columns (i.e., if new cols were added after this workspace was saved)
|
||||
if (settings.grid.columns.Count != def_grid.columns.Count) {
|
||||
settings.grid.columns.difference_hard(def_grid.columns, out var added, out var removed, EqualityComparer.Create<GridColumnSettings, string>(c => c.id));
|
||||
var new_cols = settings.grid.columns.ToList();
|
||||
for (int i = 0; i < added.Count; i++) {
|
||||
var col = added[i];
|
||||
new_cols.Add(col);
|
||||
}
|
||||
settings.grid = settings.grid.with(columns: new_cols);
|
||||
}
|
||||
}
|
||||
var settings_manager = new WindowSettingsManager<PNLSettings>(_settings_hub,
|
||||
settings,
|
||||
get: us => us.pnl,
|
||||
set: (us, pnl) => us.with(pnl: pnl));
|
||||
// Need to keep a local copy of the columns, since they are defined in xaml (not the viewmodel), and get_save_state may be called on a non-dispatcher thread.
|
||||
// If that happens, we would be unable to access _grid.Columns, and thus unable to get the current column settings.
|
||||
// Use an ImmutableList to avoid any threading issues when responding to CollectionChanged events on the dispatcher thread.
|
||||
ImmutableList<DIYGrid.ColumnBase> cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty;
|
||||
return SparkWindow<PNLSaveState>.create(_window_type, _window_title, _bus, this, Icons.Accounting, _thread_id_provider.next_id(), () => {
|
||||
IEnumerable<SortingCriteria> last_sorting_criterion = null;
|
||||
var vm = new PNLVM(_bus, _orders_model, settings ?? PNLSettings.Default);
|
||||
var content = new PNLView {
|
||||
DataContext = vm
|
||||
};
|
||||
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
|
||||
|
||||
Func<PNLSaveState> get_save_state = () => {
|
||||
if (last_sorting_criterion == null)
|
||||
last_sorting_criterion = PNLSettings.Default.sorting_criterion;
|
||||
var cols = cached_columns.ToArray(c => c.create());
|
||||
var filters = vm.get_filters_states();
|
||||
var calc_type = vm.calculation_type;
|
||||
var state = settings_manager.instance_settings;
|
||||
if (state == null) {
|
||||
// don't use defaults if the user manipulated the window state outside of the settings dialog (e.g., resized column(s) via drag, or changed sort/filter)
|
||||
var defaults = settings_manager.default_settings;
|
||||
if (calc_type == defaults.calculation_type &&
|
||||
defaults.sorting_criterion.SequenceEqual(last_sorting_criterion) &&
|
||||
defaults.grid.columns.SequenceEqual(cols) && defaults.filters.safe_equals(filters)) {
|
||||
// the user's current state still matches the defaults (i.e., no outside manipulation was performed); return null so this window will use the latest default settings when restored
|
||||
return new PNLSaveState { use_defaults = true, settings = null };
|
||||
}
|
||||
state = defaults;
|
||||
}
|
||||
// assign latest values, as they may have be modified outside of the settings dialog (in which case the settings manager would be out of date)
|
||||
state = state.with(
|
||||
last_sorting_criterion,
|
||||
vm.anchors,
|
||||
filters,
|
||||
state.grid.with(columns: cols),
|
||||
calc_type,
|
||||
vm.header_row_display);
|
||||
return new PNLSaveState { use_defaults = false, settings = state };
|
||||
};
|
||||
IEnumerable<SparkCommand> commands = new[] {
|
||||
new SparkCommandCommand("Collapse All", Icons.RightArrow, new DelegateCommand(() => vm.set_all_expanded(false))),
|
||||
new SparkCommandCommand("Expand All", Icons.DownArrow, new DelegateCommand(() => vm.set_all_expanded(true))),
|
||||
new SparkCommandCommand("Play", Icons.Play, new DelegateCommand(() => vm.is_paused = false)) {
|
||||
show_in_context_menu = _pnl_conf.show_pause_resume,
|
||||
show_in_toolbar = _pnl_conf.show_pause_resume
|
||||
},
|
||||
new SparkCommandCommand("Pause", Icons.Pause, new DelegateCommand(() => vm.is_paused = true)) {
|
||||
show_in_context_menu = _pnl_conf.show_pause_resume,
|
||||
show_in_toolbar = _pnl_conf.show_pause_resume
|
||||
},
|
||||
SparkCommands.window_settings_command(new DialogCommand(() => {
|
||||
settings_manager.instance_settings = get_save_state().settings;
|
||||
var dialog_vm = new ShellSettingsDialogVM<PNLSettings, PNLSettingsVM>(
|
||||
settings_manager,
|
||||
us => us.pnl,
|
||||
UserSettings.FileExtensions.PNL,
|
||||
s => new PNLSettingsVM(s));
|
||||
var dialog = new ShellSettingsDialog { DataContext = dialog_vm };
|
||||
dialog_vm.RequestClose += on_request_close;
|
||||
return dialog;
|
||||
void on_request_close(object sender, EventArgs e) =>
|
||||
WindowUtilities.force_close(dialog);
|
||||
}))
|
||||
};
|
||||
IDisposable dispose = Disposable.Create(
|
||||
settings_manager.subscribe_current().OnDispatcher().Subscribe(s => {
|
||||
vm.load_from(s);
|
||||
var col_setting_map = s.grid.columns.ToDictionary(c => c.id);
|
||||
var fallback_family = new FontFamily(s.grid.font_family);
|
||||
// group
|
||||
var group_col_setting_map = s.group_columns.ToDictionary(c => c.id);
|
||||
var group_appearance = s.group_cell_appearance.to_appearance();
|
||||
// header
|
||||
var header_col_setting_map = s.header_columns.ToDictionary(c => c.id);
|
||||
var header_appearance = s.header_cell_appearance.to_appearance();
|
||||
//
|
||||
foreach (var c in content._grid.Columns.OfType<PNLColumn>()) {
|
||||
if (col_setting_map.TryGetValue(c.id, out var cs)) {
|
||||
c.load_from(cs);
|
||||
}
|
||||
if (group_col_setting_map.TryGetValue(c.id, out var gcs)) {
|
||||
c.load_group_params(
|
||||
group_appearance.font_size,
|
||||
gcs.font_weight_override ?? group_appearance.font_weight,
|
||||
gcs.font_color_override ?? group_appearance.foreground,
|
||||
group_appearance.font_family);
|
||||
}
|
||||
if (header_col_setting_map.TryGetValue(c.id, out var hcs)) {
|
||||
c.load_header_params(
|
||||
header_appearance.font_size,
|
||||
hcs.font_weight_override ?? header_appearance.font_weight,
|
||||
hcs.font_color_override ?? header_appearance.foreground,
|
||||
header_appearance.font_family);
|
||||
}
|
||||
}
|
||||
vm.rows_view.set_sort_descriptions(s.sorting_criterion, content._grid.Columns);
|
||||
CollectionHelper.sync_with_never_duplicate(content._grid.Columns, s.grid.columns, cs => {
|
||||
throw new InvalidOperationException();
|
||||
}, c => c.id.ToLower(), cs => cs.id.ToLower());
|
||||
}),
|
||||
vm.rows_view
|
||||
.subscribe_sorting_criterion(sm => content._grid.Columns.TryGetFirst(dc => dc.sort_member_path == sm, out var match) ? match.id : null)
|
||||
.Subscribe(sc => last_sorting_criterion = sc),
|
||||
content._grid.Columns.Subscribe((s, e) => {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add: add_new(); break;
|
||||
case NotifyCollectionChangedAction.Remove: remove_old(); break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
remove_old();
|
||||
add_new();
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(content._grid.Columns);
|
||||
break;
|
||||
}
|
||||
void add_new() =>
|
||||
cached_columns = e.NewStartingIndex >= 0 ?
|
||||
cached_columns.InsertRange(e.NewStartingIndex, e.NewItems.OfType<DIYGrid.ColumnBase>()) :
|
||||
cached_columns.AddRange(e.NewItems.OfType<DIYGrid.ColumnBase>());
|
||||
|
||||
void remove_old() =>
|
||||
cached_columns = e.OldStartingIndex >= 0 ?
|
||||
cached_columns.RemoveRange(e.OldStartingIndex, e.OldItems.Count) :
|
||||
cached_columns.RemoveRange(e.OldItems.OfType<DIYGrid.ColumnBase>());
|
||||
}),
|
||||
Disposable.wrap_collection(commands.OfType<IDisposable>()),
|
||||
vm);
|
||||
return (content, commands, get_save_state, dispose);
|
||||
},
|
||||
min_width_offset: SystemParameters.HorizontalScrollBarButtonWidth + 8,
|
||||
min_height_offset: 60);
|
||||
}
|
||||
|
||||
readonly struct PNLConfig {
|
||||
public readonly bool
|
||||
enable_animations,
|
||||
show_pause_resume;
|
||||
|
||||
public PNLConfig(bts.config.PNLOptions options) {
|
||||
enable_animations = options.enable_animations;
|
||||
show_pause_resume = options.show_pause_resume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using bts.utils;
|
||||
using mbts.expression;
|
||||
using mbts.ui;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using static bts.shell.pnl.PNLView;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface IPNLAnimationTrigger<T> {
|
||||
IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value);
|
||||
}
|
||||
|
||||
readonly struct PNLAnimationTrigger<T> : IPNLAnimationTrigger<T> {
|
||||
public readonly Func<T, bool> predicate;
|
||||
public readonly IReadOnlyDictionary<Columns, PNLCellAppearance> animations;
|
||||
|
||||
public PNLAnimationTrigger(Func<T, bool> predicate, IReadOnlyDictionary<Columns, PNLCellAppearance> animations) {
|
||||
this.predicate = predicate;
|
||||
this.animations = animations;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<Columns, PNLCellAppearance> trigger(T value) =>
|
||||
predicate(value) ? animations : DictionaryHelper.empty<Columns, PNLCellAppearance>();
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLAnimationTriggerData : IEquatable<PNLAnimationTriggerData> {
|
||||
static readonly int _type_hash = typeof(PNLAnimationTriggerData).GetHashCode();
|
||||
|
||||
[ProtoMember(1)]
|
||||
public string expression { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public PNLAnimation[] animations { get; set; }
|
||||
|
||||
public IPNLAnimationTrigger<T> to_trigger<T>() =>
|
||||
new PNLAnimationTrigger<T>(
|
||||
CoreExpressionEvaluator<T>.try_create(expression, out var eval) ?
|
||||
eval.apply : new Func<T, bool>(_ => true),
|
||||
animations.ToDictionary(a => a.column, a => a.to_appearance()));
|
||||
|
||||
public bool Equals(PNLAnimationTriggerData other) =>
|
||||
other != null &&
|
||||
expression == other.expression &&
|
||||
EqualityComparer.safe_sequence_equals(animations, other.animations);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLAnimationTriggerData);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLAnimation : IEquatable<PNLAnimation> {
|
||||
static readonly int _type_hash = typeof(PNLAnimation).GetHashCode();
|
||||
|
||||
[ProtoMember(1)]
|
||||
public Columns column { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public int? font_weight { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public uint? foreground { get; set; }
|
||||
[ProtoMember(4)]
|
||||
public uint? background { get; set; }
|
||||
|
||||
public PNLCellAppearance to_appearance() =>
|
||||
new PNLCellAppearance(
|
||||
foreground?.argb_color(),
|
||||
background?.argb_color(),
|
||||
font_weight.HasValue ? FontWeight.FromOpenTypeWeight(font_weight.Value) : (FontWeight?)null);
|
||||
|
||||
public bool Equals(PNLAnimation other) =>
|
||||
other != null &&
|
||||
column == other.column &&
|
||||
font_weight == other.font_weight &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLAnimation);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using bts.proto;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility.text;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLCellAppearance : IEquatable<PNLCellAppearance> {
|
||||
public readonly FontWeight? font_weight;
|
||||
public readonly Color? foreground, background;
|
||||
public readonly double font_size;
|
||||
public readonly FontFamily font_family;
|
||||
|
||||
public PNLCellAppearance(Color? foreground = null, Color? background = null, FontWeight? font_weight = null, FontFamily font_family = null, double font_size = double.NaN) {
|
||||
this.foreground = foreground;
|
||||
this.background = background;
|
||||
this.font_weight = font_weight;
|
||||
this.font_family = font_family;
|
||||
this.font_size = font_size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the specified appearance to replace null/absent values.
|
||||
/// </summary>
|
||||
public PNLCellAppearance merge(PNLCellAppearance other) =>
|
||||
new PNLCellAppearance(
|
||||
foreground ?? other.foreground,
|
||||
background ?? other.background,
|
||||
font_weight ?? other.font_weight,
|
||||
font_family ?? other.font_family,
|
||||
double.IsNaN(font_size) || font_size == 0 ? other.font_size : font_size);
|
||||
|
||||
public bool Equals(PNLCellAppearance other) =>
|
||||
font_weight == other.font_weight &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background &&
|
||||
font_size.Equals(other.font_size) &&
|
||||
Equals(font_family, other.font_family);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is PNLCellAppearance other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(
|
||||
font_weight?.GetHashCode() ?? 0,
|
||||
foreground?.GetHashCode() ?? 0,
|
||||
background?.GetHashCode() ?? 0,
|
||||
font_size.GetHashCode(),
|
||||
font_family?.GetHashCode() ?? 0);
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLCellAppearanceProto : IEquatable<PNLCellAppearanceProto> {
|
||||
static readonly int _type_hash = typeof(PNLCellAppearanceProto).GetHashCode();
|
||||
|
||||
[ProtoMember(1)]
|
||||
public FontParamsProto font_params { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public uint? foreground { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public uint? background { get; set; }
|
||||
|
||||
public static PNLCellAppearanceProto from_appearance(PNLCellAppearance cell_appearance) =>
|
||||
new PNLCellAppearanceProto {
|
||||
font_params = new FontParamsProto {
|
||||
font_family = cell_appearance.font_family?.name(),
|
||||
font_color = cell_appearance.foreground?.rgb_color() ?? 0,
|
||||
font_size = cell_appearance.font_size,
|
||||
font_weight = (cell_appearance.font_weight ?? FontWeights.Normal).ToOpenTypeWeight()
|
||||
},
|
||||
background = cell_appearance.background?.rgb_color()
|
||||
};
|
||||
|
||||
public PNLCellAppearance to_appearance() {
|
||||
var font = font_params?.to_wpf_params();
|
||||
return new PNLCellAppearance(
|
||||
foreground?.rgb_color(),
|
||||
background?.argb_color(),
|
||||
font?.weight,
|
||||
font?.family,
|
||||
font?.size ?? double.NaN);
|
||||
}
|
||||
|
||||
public bool Equals(PNLCellAppearanceProto other) =>
|
||||
other != null &&
|
||||
foreground == other.foreground &&
|
||||
background == other.background &&
|
||||
font_params.safe_equals(other.font_params);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLCellAppearanceProto);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using bts.collections;
|
||||
using bts.proto;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility.text;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellAppearanceParamsVM : ViewModelBase {
|
||||
FontWeight _font_weight;
|
||||
FontFamily _font_family;
|
||||
double _font_size = double.NaN;
|
||||
|
||||
public ColorPickerPopupVM background { get; } = new ColorPickerPopupVM { bind_opacity_to_alpha = true, opacity = 1 };
|
||||
public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM();
|
||||
public FontWeight font_weight { get => _font_weight; set => Set(ref _font_weight, value); }
|
||||
public FontFamily font_family { get => _font_family; set => Set(ref _font_family, value); }
|
||||
public double font_size { get => _font_size; set => Set(ref _font_size, value); }
|
||||
|
||||
public PNLCellAppearance to_appearance() =>
|
||||
new PNLCellAppearance(font_color.color, background.color, font_weight, font_family, font_size);
|
||||
|
||||
public void load(PNLCellAppearance appearance) {
|
||||
if (appearance.background.HasValue)
|
||||
background.color = appearance.background.Value;
|
||||
if (appearance.font_family != null)
|
||||
font_family = appearance.font_family;
|
||||
if (appearance.font_weight.HasValue)
|
||||
font_weight = appearance.font_weight.Value;
|
||||
if (appearance.font_size > 0)
|
||||
font_size = appearance.font_size;
|
||||
}
|
||||
}
|
||||
|
||||
class PNLColumnParamsVM : ViewModelBase {
|
||||
bool
|
||||
_is_background_overridden,
|
||||
_is_font_color_overridden,
|
||||
_is_font_weight_overridden,
|
||||
_is_font_family_overridden,
|
||||
_is_font_size_overridden;
|
||||
|
||||
FontWeight _font_weight_override;
|
||||
FontFamily _font_family_override;
|
||||
double _font_size_override = double.NaN;
|
||||
|
||||
// background
|
||||
public bool is_background_overridden { get => _is_background_overridden; set => Set(ref _is_background_overridden, value); }
|
||||
public ColorPickerPopupVM background_override { get; } = new ColorPickerPopupVM {
|
||||
bind_opacity_to_alpha = true,
|
||||
opacity = 1
|
||||
};
|
||||
// font color
|
||||
public bool is_font_color_overridden { get => _is_font_color_overridden; set => Set(ref _is_font_color_overridden, value); }
|
||||
public ColorPickerPopupVM font_color_override { get; } = new ColorPickerPopupVM();
|
||||
// font weight
|
||||
public bool is_font_weight_overridden { get => _is_font_weight_overridden; set => Set(ref _is_font_weight_overridden, value); }
|
||||
public FontWeight font_weight_override { get => _font_weight_override; set => Set(ref _font_weight_override, value); }
|
||||
// font family
|
||||
public bool is_font_family_overriden { get => _is_font_family_overridden; set => Set(ref _is_font_family_overridden, value); }
|
||||
public FontFamily font_family_override { get => _font_family_override; set => Set(ref _font_family_override, value); }
|
||||
// font size
|
||||
public bool is_font_size_overriden { get => _is_font_size_overridden; set => Set(ref _is_font_size_overridden, value); }
|
||||
public double font_size_override { get => _font_size_override; set => Set(ref _font_size_override, value); }
|
||||
|
||||
public GridColumnSettings apply_to(GridColumnSettings target) =>
|
||||
target.with(
|
||||
font_weight_override: new NullableFontWeightProto(is_font_weight_overridden ? font_weight_override : (FontWeight?)null),
|
||||
font_color_override: new NullableColorProto(is_font_color_overridden ? font_color_override.color : (Color?)null),
|
||||
background_override: new NullableColorProto(is_background_overridden ? background_override.color : (Color?)null));
|
||||
|
||||
public void load(GridColumnSettings settings) {
|
||||
if (settings != null)
|
||||
load(settings.font_weight_override, settings.font_color_override, settings.background_override);
|
||||
}
|
||||
|
||||
public void load(FontParamsProto font_params) {
|
||||
if (font_params == null)
|
||||
return;
|
||||
// font weight
|
||||
is_font_weight_overridden = font_params.font_weight > 0;
|
||||
if (is_font_weight_overridden)
|
||||
font_weight_override = FontWeight.FromOpenTypeWeight(font_params.font_weight);
|
||||
// font color
|
||||
is_font_color_overridden = font_params.font_color > 0;
|
||||
if (is_font_color_overridden)
|
||||
font_color_override.color = font_params.font_color.rgb_color();
|
||||
// font family
|
||||
if (!string.IsNullOrEmpty(font_params.font_family)) {
|
||||
font_family_override = new FontFamily(font_params.font_family);
|
||||
}
|
||||
if (!double.IsNaN(font_params.font_size) && font_params.font_size > 0) {
|
||||
font_size_override = font_params.font_size;
|
||||
} else {
|
||||
font_size_override = double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
public void load(PNLAnimation animation) {
|
||||
if (animation != null)
|
||||
load(
|
||||
animation.font_weight.HasValue ? FontWeight.FromOpenTypeWeight(animation.font_weight.Value) : (FontWeight?)null,
|
||||
animation.foreground?.argb_color(),
|
||||
animation.background?.argb_color());
|
||||
}
|
||||
|
||||
public void load(FontWeight? font_weight, Color? font_color, Color? background) {
|
||||
// font weight
|
||||
is_font_weight_overridden = font_weight.HasValue;
|
||||
if (is_font_weight_overridden)
|
||||
font_weight_override = font_weight.Value;
|
||||
// font color
|
||||
is_font_color_overridden = font_color.HasValue;
|
||||
if (is_font_color_overridden)
|
||||
font_color_override.color = font_color.Value;
|
||||
// background
|
||||
is_background_overridden = background.HasValue;
|
||||
if (background.HasValue)
|
||||
background_override.color = background.Value;
|
||||
}
|
||||
|
||||
public PNLAnimation to_animation(PNLView.Columns key) =>
|
||||
new PNLAnimation {
|
||||
column = key,
|
||||
foreground = is_font_color_overridden ? font_color_override.color.rgb_color() : (uint?)null,
|
||||
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
|
||||
font_weight = is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : (int?)null
|
||||
};
|
||||
|
||||
public PNLCellAppearanceProto to_appearance_proto() =>
|
||||
new PNLCellAppearanceProto {
|
||||
background = is_background_overridden ? background_override.color.rgb_color() : (uint?)null,
|
||||
foreground = is_font_color_overridden ? font_color_override.color.rgb_color() : (uint?)null,
|
||||
font_params = to_font_params()
|
||||
};
|
||||
|
||||
public FontParamsProto to_font_params() =>
|
||||
new FontParamsProto {
|
||||
font_color = is_font_color_overridden ? font_color_override.color.rgb_color() : 0,
|
||||
font_weight = is_font_weight_overridden ? font_weight_override.ToOpenTypeWeight() : 0,
|
||||
font_family = font_family_override?.name(),
|
||||
font_size = font_size_override
|
||||
};
|
||||
}
|
||||
|
||||
class PNLColumnVM : ViewModelBase, ISimpleColumnVM {
|
||||
readonly SimpleGridSettingsVM _owner;
|
||||
readonly PNLView.Columns _pnl_id;
|
||||
object _selected_item;
|
||||
string _display_name;
|
||||
bool _is_visible;
|
||||
double _width;
|
||||
HorizontalAlignment _horizontal_alignment;
|
||||
|
||||
public PNLColumnVM(SimpleGridSettingsVM owner, PNLView.Columns id) {
|
||||
_owner = owner;
|
||||
_pnl_id = id;
|
||||
default_column_vm.PropertyChanged += echo;
|
||||
void echo(object sender, PropertyChangedEventArgs e) {
|
||||
switch (e.PropertyName) {
|
||||
case nameof(is_font_weight_overridden):
|
||||
case nameof(is_font_color_overridden):
|
||||
RaisePropertyChanged(e.PropertyName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PNLColumnParamsVM default_column_vm { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM group_column_vm { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM header_column_vm { get; } = new PNLColumnParamsVM();
|
||||
//
|
||||
public ObservableCollection<PNLRuleColumnVM> rule_columns { get; } = new ObservableCollection<PNLRuleColumnVM>();
|
||||
|
||||
public object selected_item {
|
||||
get => _selected_item;
|
||||
set {
|
||||
if (value != null)
|
||||
Set(ref _selected_item, value);
|
||||
}
|
||||
}
|
||||
|
||||
public string id { get => _pnl_id.ToString(); set => Log.assert(value == _pnl_id.ToString()); }
|
||||
public string display_name { get => _display_name; set => Set(ref _display_name, value); }
|
||||
public double width { get => _width; set => Set(ref _width, value); }
|
||||
public bool is_visible { get => _is_visible; set => Set(ref _is_visible, value); }
|
||||
public bool is_font_color_overridden {
|
||||
get => default_column_vm.is_font_color_overridden;
|
||||
set => default_column_vm.is_font_color_overridden = value;
|
||||
}
|
||||
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => Set(ref _horizontal_alignment, value); }
|
||||
|
||||
public ColorPickerPopupVM font_color_override => default_column_vm.font_color_override;
|
||||
|
||||
public bool is_font_weight_overridden { get => default_column_vm.is_font_weight_overridden; set => default_column_vm.is_font_weight_overridden = value; }
|
||||
public FontWeight font_weight_override { get => default_column_vm.font_weight_override; set => default_column_vm.font_weight_override = value; }
|
||||
|
||||
public void apply_flags(GridColumnSettingsFeatureFlags flags) { }
|
||||
|
||||
public GridColumnSettings create() {
|
||||
var result = new GridColumnSettings(id: id, display_name: display_name, is_visible: is_visible, horizontal_alignment: horizontal_alignment, width: width);
|
||||
result = default_column_vm.apply_to(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void load_from(GridColumnSettings value) {
|
||||
_assert_id(value.id);
|
||||
default_column_vm.load(value);
|
||||
display_name = value.display_name;
|
||||
width = value.width;
|
||||
is_visible = value.is_visible;
|
||||
horizontal_alignment = value.horizontal_alignment;
|
||||
}
|
||||
|
||||
public void load_from(GridColumnSettings value, IReadOnlyDictionary<string, PNLAnimation> triggers, GridColumnSettings group, IReadOnlyDictionary<string, PNLAnimation> group_triggers, IReadOnlyDictionary<string, PNLAnimation> header_triggers, GridColumnSettings header) {
|
||||
_assert_id(value.id);
|
||||
load_from(value);
|
||||
if (group != null)
|
||||
group_column_vm.load(group);
|
||||
if (header != null)
|
||||
header_column_vm.load(header);
|
||||
rule_columns.SetItems(triggers.Select(kv => {
|
||||
var rule_col = new PNLRuleColumnVM(kv.Key);
|
||||
rule_col.load_from(kv.Value, group_triggers.GetValueOrDefault(kv.Key), header_triggers.GetValueOrDefault(kv.Key));
|
||||
return rule_col;
|
||||
}));
|
||||
}
|
||||
|
||||
public IEnumerable<(string expression, PNLAnimation row, PNLAnimation group, PNLAnimation header)> get_triggers() {
|
||||
foreach (var col in rule_columns)
|
||||
yield return (col.rule_text, col.row_params.to_animation(_pnl_id), col.group_params.to_animation(_pnl_id), col.header_params.to_animation(_pnl_id));
|
||||
}
|
||||
|
||||
void _assert_id(string id) =>
|
||||
Log.assert(id == _pnl_id.ToString(), $"PNLColumn id string '{id}' does not match the expected {typeof(PNLView.Columns)} key '{_pnl_id}.'");
|
||||
|
||||
public override string ToString() => "Default";
|
||||
}
|
||||
|
||||
class PNLRuleColumnVM : ViewModelBaseWithValidation, IEquatable<PNLRuleColumnVM> {
|
||||
public PNLRuleColumnVM(string rule_text) =>
|
||||
this.rule_text = rule_text;
|
||||
|
||||
public string rule_text { get; }
|
||||
|
||||
public PNLColumnParamsVM row_params { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM group_params { get; } = new PNLColumnParamsVM();
|
||||
public PNLColumnParamsVM header_params { get; } = new PNLColumnParamsVM();
|
||||
|
||||
public void load_from(PNLAnimation row, PNLAnimation group, PNLAnimation header) {
|
||||
row_params.load(row);
|
||||
group_params.load(group);
|
||||
header_params.load(header);
|
||||
}
|
||||
|
||||
public bool Equals(PNLRuleColumnVM other) =>
|
||||
other != null && rule_text == other.rule_text;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as PNLRuleColumnVM);
|
||||
public override int GetHashCode() => rule_text.GetHashCode();
|
||||
public override string ToString() => rule_text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
using bts.api;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
[ProtoContract]
|
||||
class FilterStates : IEquatable<FilterStates> {
|
||||
static readonly int _type_hash = typeof(FilterStates).GetHashCode();
|
||||
|
||||
// account
|
||||
[ProtoMember(1)]
|
||||
uint[] _account;
|
||||
public uint[] account {
|
||||
get => _account ?? Array.Empty<uint>();
|
||||
set => _account = value;
|
||||
}
|
||||
// exchange
|
||||
[ProtoMember(2)]
|
||||
Venue[] _exchange;
|
||||
public Venue[] exchange {
|
||||
get => _exchange ?? Array.Empty<Venue>();
|
||||
set => _exchange = value;
|
||||
}
|
||||
// symbol
|
||||
[ProtoMember(3)]
|
||||
string[] _symbol;
|
||||
public string[] symbol {
|
||||
get => _symbol ?? Array.Empty<string>();
|
||||
set => _symbol = value;
|
||||
}
|
||||
// security
|
||||
[ProtoMember(4)]
|
||||
public string security { get; set; }
|
||||
// security type
|
||||
[ProtoMember(5)]
|
||||
Security.ContractOneofCase[] _security_type;
|
||||
public Security.ContractOneofCase[] security_type {
|
||||
get => _security_type ?? Array.Empty<Security.ContractOneofCase>();
|
||||
set => _security_type = value;
|
||||
}
|
||||
|
||||
/// sod
|
||||
[ProtoMember(6)]
|
||||
public DoubleMinMaxState sod_pnl { get; set; }
|
||||
// sod long
|
||||
[ProtoMember(7)]
|
||||
public DoubleMinMaxState sod_long_pos { get; set; }
|
||||
[ProtoMember(8)]
|
||||
public DoubleMinMaxState sod_long_avg_price { get; set; }
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState sod_long_avg_price_delta { get; set; }
|
||||
// sod short
|
||||
[ProtoMember(10)]
|
||||
public DoubleMinMaxState sod_short_pos { get; set; }
|
||||
[ProtoMember(11)]
|
||||
public DoubleMinMaxState sod_short_avg_price { get; set; }
|
||||
[ProtoMember(12)]
|
||||
public DoubleMinMaxState sod_short_avg_price_delta { get; set; }
|
||||
// sod net
|
||||
[ProtoMember(13)]
|
||||
public DoubleMinMaxState sod_net_pos { get; set; }
|
||||
[ProtoMember(14)]
|
||||
public DoubleMinMaxState sod_net_avg_price_delta { get; set; }
|
||||
|
||||
/// day
|
||||
[ProtoMember(15)]
|
||||
public DoubleMinMaxState day_pnl { get; set; }
|
||||
// day long
|
||||
[ProtoMember(16)]
|
||||
public DoubleMinMaxState day_long_pos { get; set; }
|
||||
[ProtoMember(17)]
|
||||
public DoubleMinMaxState day_long_avg_price { get; set; }
|
||||
[ProtoMember(18)]
|
||||
public DoubleMinMaxState day_long_avg_price_delta { get; set; }
|
||||
// day short
|
||||
[ProtoMember(19)]
|
||||
public DoubleMinMaxState day_short_pos { get; set; }
|
||||
[ProtoMember(20)]
|
||||
public DoubleMinMaxState day_short_avg_price { get; set; }
|
||||
[ProtoMember(21)]
|
||||
public DoubleMinMaxState day_short_avg_price_delta { get; set; }
|
||||
// day net
|
||||
[ProtoMember(22)]
|
||||
public DoubleMinMaxState day_net_pos { get; set; }
|
||||
[ProtoMember(23)]
|
||||
public DoubleMinMaxState day_net_avg_price_delta { get; set; }
|
||||
|
||||
/// total
|
||||
[ProtoMember(24)]
|
||||
public DoubleMinMaxState total_pnl { get; set; }
|
||||
// total long
|
||||
[ProtoMember(25)]
|
||||
public DoubleMinMaxState total_long_pos { get; set; }
|
||||
[ProtoMember(26)]
|
||||
public DoubleMinMaxState total_long_avg_price { get; set; }
|
||||
[ProtoMember(27)]
|
||||
public DoubleMinMaxState total_long_avg_price_delta { get; set; }
|
||||
// total short
|
||||
[ProtoMember(28)]
|
||||
public DoubleMinMaxState total_short_pos { get; set; }
|
||||
[ProtoMember(29)]
|
||||
public DoubleMinMaxState total_short_avg_price { get; set; }
|
||||
[ProtoMember(30)]
|
||||
public DoubleMinMaxState total_short_avg_price_delta { get; set; }
|
||||
// total net
|
||||
[ProtoMember(31)]
|
||||
public DoubleMinMaxState total_net_pos { get; set; }
|
||||
[ProtoMember(32)]
|
||||
public DoubleMinMaxState total_net_avg_price_delta { get; set; }
|
||||
|
||||
/// market
|
||||
[ProtoMember(33)]
|
||||
public DoubleMinMaxState bid_price { get; set; }
|
||||
[ProtoMember(34)]
|
||||
public DoubleMinMaxState mid_price { get; set; }
|
||||
[ProtoMember(35)]
|
||||
public DoubleMinMaxState ask_price { get; set; }
|
||||
[ProtoMember(36)]
|
||||
public DoubleMinMaxState last_price { get; set; }
|
||||
[ProtoMember(37)]
|
||||
public DoubleMinMaxState settlement_price { get; set; }
|
||||
[ProtoMember(38)]
|
||||
public DoubleMinMaxState pnl_price { get; set; }
|
||||
|
||||
/// working (long)
|
||||
[ProtoMember(39)]
|
||||
public DoubleMinMaxState working_long_pos { get; set; }
|
||||
[ProtoMember(40)]
|
||||
public DoubleMinMaxState working_long_avg_price { get; set; }
|
||||
[ProtoMember(41)]
|
||||
public DoubleMinMaxState working_long_avg_price_delta { get; set; }
|
||||
[ProtoMember(42)]
|
||||
// working short
|
||||
public DoubleMinMaxState working_short_pos { get; set; }
|
||||
[ProtoMember(43)]
|
||||
public DoubleMinMaxState working_short_avg_price { get; set; }
|
||||
[ProtoMember(44)]
|
||||
public DoubleMinMaxState working_short_avg_price_delta { get; set; }
|
||||
// working net
|
||||
[ProtoMember(45)]
|
||||
public DoubleMinMaxState working_net_pos { get; set; }
|
||||
[ProtoMember(46)]
|
||||
public DoubleMinMaxState working_net_avg_price_delta { get; set; }
|
||||
|
||||
// has day trades
|
||||
[ProtoMember(47)]
|
||||
bool[] _has_day_trades { get; set; }
|
||||
public bool[] has_day_trades {
|
||||
get => _has_day_trades ?? Array.Empty<bool>();
|
||||
set => _has_day_trades = value;
|
||||
}
|
||||
// cpp
|
||||
[ProtoMember(48)]
|
||||
public DoubleMinMaxState cpp { get; set; }
|
||||
|
||||
public bool Equals(FilterStates other) =>
|
||||
other != null &&
|
||||
account.SequenceEqual(other.account) &&
|
||||
symbol.SequenceEqual(other.symbol) &&
|
||||
exchange.SequenceEqual(other.exchange) &&
|
||||
security == other.security &&
|
||||
security_type.SequenceEqual(other.security_type) &&
|
||||
/// sod
|
||||
sod_pnl.safe_equals(other.sod_pnl) &&
|
||||
sod_long_pos.safe_equals(other.sod_long_pos) &&
|
||||
sod_long_avg_price.safe_equals(other.sod_long_avg_price) &&
|
||||
sod_long_avg_price_delta.safe_equals(other.sod_long_avg_price_delta) &&
|
||||
sod_short_pos.safe_equals(other.sod_short_pos) &&
|
||||
sod_short_avg_price.safe_equals(other.sod_short_avg_price) &&
|
||||
sod_short_avg_price_delta.safe_equals(other.sod_short_avg_price_delta) &&
|
||||
sod_net_pos.safe_equals(other.sod_net_pos) &&
|
||||
sod_net_avg_price_delta.safe_equals(other.sod_net_avg_price_delta) &&
|
||||
/// day
|
||||
day_pnl.safe_equals(other.day_pnl) &&
|
||||
day_long_pos.safe_equals(other.day_long_pos) &&
|
||||
day_long_avg_price.safe_equals(other.day_long_avg_price) &&
|
||||
day_long_avg_price_delta.safe_equals(other.day_long_avg_price_delta) &&
|
||||
day_short_pos.safe_equals(other.day_short_pos) &&
|
||||
day_short_avg_price.safe_equals(other.day_short_avg_price) &&
|
||||
day_short_avg_price_delta.safe_equals(other.day_short_avg_price_delta) &&
|
||||
day_net_pos.safe_equals(other.day_net_pos) &&
|
||||
day_net_avg_price_delta.safe_equals(other.day_net_avg_price_delta) &&
|
||||
/// market
|
||||
bid_price.safe_equals(other.bid_price) &&
|
||||
ask_price.safe_equals(other.ask_price) &&
|
||||
mid_price.safe_equals(other.mid_price) &&
|
||||
last_price.safe_equals(other.last_price) &&
|
||||
settlement_price.safe_equals(other.settlement_price) &&
|
||||
pnl_price.safe_equals(other.pnl_price) &&
|
||||
/// total
|
||||
total_pnl.safe_equals(other.total_pnl) &&
|
||||
total_long_pos.safe_equals(other.total_long_pos) &&
|
||||
total_long_avg_price.safe_equals(other.total_long_avg_price) &&
|
||||
total_long_avg_price_delta.safe_equals(other.total_long_avg_price_delta) &&
|
||||
total_short_pos.safe_equals(other.total_short_pos) &&
|
||||
total_short_avg_price.safe_equals(other.total_short_avg_price) &&
|
||||
total_short_avg_price_delta.safe_equals(other.total_short_avg_price_delta) &&
|
||||
total_net_pos.safe_equals(other.total_net_pos) &&
|
||||
total_net_avg_price_delta.safe_equals(other.total_net_avg_price_delta) &&
|
||||
has_day_trades.SequenceEqual(other.has_day_trades) &&
|
||||
cpp.safe_equals(other.cpp);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as FilterStates);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_type_hash;
|
||||
}
|
||||
|
||||
static class PNLFilterStatesExtensions {
|
||||
public static Func<PNLRowData, bool> build_predicate(this FilterStates fs) {
|
||||
var bd = new ColumnPredicateBuilder<PNLRowData>();
|
||||
bd.add_discrete_values(fs.account, d => d.key.account_id);
|
||||
bd.add_discrete_values(fs.exchange, d => d.security.exchange_or_default());
|
||||
bd.add_discrete_values(fs.symbol, d => d.security.symbol_or_default());
|
||||
bd.add_regex(fs.security, d => d.security.ToString());
|
||||
bd.add_discrete_values(fs.security_type, d => d.security.HasValue ? d.security.Value.ContractCase : Security.ContractOneofCase.None);
|
||||
/// sod
|
||||
bd.add_numeric(fs.sod_pnl, d => d.sod_pnl);
|
||||
// sod long
|
||||
bd.add_numeric(fs.sod_long_pos, d => d.sod_long_pos);
|
||||
bd.add_numeric(fs.sod_long_avg_price, d => d.sod_long_avg_price_raw);
|
||||
bd.add_numeric(fs.sod_long_avg_price_delta, d => d.sod_long_avg_price_delta_raw);
|
||||
// sod short
|
||||
bd.add_numeric(fs.sod_short_pos, d => d.sod_short_pos);
|
||||
bd.add_numeric(fs.sod_short_avg_price, d => d.sod_short_avg_price_raw);
|
||||
bd.add_numeric(fs.sod_short_avg_price_delta, d => d.sod_short_avg_price_delta_raw);
|
||||
// sod net
|
||||
bd.add_numeric(fs.sod_net_pos, d => d.sod_net_pos);
|
||||
bd.add_numeric(fs.sod_net_avg_price_delta, d => d.sod_net_avg_price_delta_raw);
|
||||
/// day
|
||||
bd.add_numeric(fs.day_pnl, d => d.day_pnl);
|
||||
// day long
|
||||
bd.add_numeric(fs.day_long_pos, d => d.day_long_pos);
|
||||
bd.add_numeric(fs.day_long_avg_price, d => d.day_long_avg_price_raw);
|
||||
bd.add_numeric(fs.day_long_avg_price_delta, d => d.day_long_avg_price_delta_raw);
|
||||
// day short
|
||||
bd.add_numeric(fs.day_short_pos, d => d.day_short_pos);
|
||||
bd.add_numeric(fs.day_short_avg_price, d => d.day_short_avg_price_raw);
|
||||
bd.add_numeric(fs.day_short_avg_price_delta, d => d.day_short_avg_price_delta_raw);
|
||||
// day net
|
||||
bd.add_numeric(fs.day_net_pos, d => d.day_net_pos);
|
||||
bd.add_numeric(fs.day_net_avg_price_delta, d => d.day_net_avg_price_delta_raw);
|
||||
/// total
|
||||
bd.add_numeric(fs.total_pnl, d => d.total_pnl);
|
||||
// total long
|
||||
bd.add_numeric(fs.total_long_pos, d => d.total_long_pos);
|
||||
bd.add_numeric(fs.total_long_avg_price, d => d.total_long_avg_price_raw);
|
||||
bd.add_numeric(fs.total_long_avg_price_delta, d => d.total_long_avg_price_delta_raw);
|
||||
// total short
|
||||
bd.add_numeric(fs.total_short_pos, d => d.total_short_pos);
|
||||
bd.add_numeric(fs.total_short_avg_price, d => d.total_short_avg_price_raw);
|
||||
bd.add_numeric(fs.total_short_avg_price_delta, d => d.total_short_avg_price_delta_raw);
|
||||
// total net
|
||||
bd.add_numeric(fs.total_net_pos, d => d.total_net_pos);
|
||||
bd.add_numeric(fs.total_net_avg_price_delta, d => d.total_net_avg_price_delta_raw);
|
||||
/// market
|
||||
bd.add_numeric(fs.bid_price, d => d.bid_price);
|
||||
bd.add_numeric(fs.ask_price, d => d.ask_price);
|
||||
bd.add_numeric(fs.mid_price, d => d.mid_price);
|
||||
bd.add_numeric(fs.last_price, d => d.last_price);
|
||||
bd.add_numeric(fs.settlement_price, d => d.settlement_price);
|
||||
bd.add_numeric(fs.pnl_price, d => d.pnl_price);
|
||||
/// working (long)
|
||||
bd.add_numeric(fs.working_long_pos, d => d.working_long_pos);
|
||||
bd.add_numeric(fs.working_long_avg_price, d => d.working_long_avg_price_raw);
|
||||
bd.add_numeric(fs.working_long_avg_price_delta, d => d.working_long_avg_price_delta_raw);
|
||||
// working short
|
||||
bd.add_numeric(fs.working_short_pos, d => d.working_short_pos);
|
||||
bd.add_numeric(fs.working_short_avg_price, d => d.working_short_avg_price_raw);
|
||||
bd.add_numeric(fs.working_short_avg_price_delta, d => d.working_short_avg_price_delta_raw);
|
||||
// working net
|
||||
bd.add_numeric(fs.working_net_pos, d => d.working_net_pos);
|
||||
bd.add_numeric(fs.working_net_avg_price_delta, d => d.working_net_avg_price_delta_raw);
|
||||
// misc
|
||||
bd.add_discrete_values(fs.has_day_trades, d => d.has_day_trades);
|
||||
bd.add_numeric(fs.cpp, d => d.cpp);
|
||||
return bd.to_predicate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
using bts.proto;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using static bts.shell.pnl.PNLView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
using Headers = mbts.ui.controls.DIYGrid.DefaultColumnHeaders.PNLHeaders;
|
||||
using Widths = mbts.ui.controls.DIYGrid.DefaultColumnWidths.PNL;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
[ProtoContract]
|
||||
class PNLSaveState : IEquatable<PNLSaveState> {
|
||||
[ProtoMember(1)]
|
||||
public bool use_defaults { get; set; }
|
||||
[ProtoMember(2)]
|
||||
public PNLSettings settings { get; set; }
|
||||
|
||||
public bool Equals(PNLSaveState other) =>
|
||||
other != null &&
|
||||
use_defaults == other.use_defaults &&
|
||||
(use_defaults || settings.safe_equals(other.settings));
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class PNLSettings : IEquatable<PNLSettings> {
|
||||
const double _default_group_font_size = double.NaN;
|
||||
// bold
|
||||
const int _default_group_font_weight = 700;
|
||||
|
||||
public static readonly IReadOnlyList<PNLHeaderRowDisplay> header_row_display_options =
|
||||
new[] {
|
||||
PNLHeaderRowDisplay.Total,
|
||||
PNLHeaderRowDisplay.SelectedTotal,
|
||||
PNLHeaderRowDisplay.Total | PNLHeaderRowDisplay.SelectedTotal
|
||||
};
|
||||
|
||||
public static readonly IReadOnlyList<Columns> anchorable_columns =
|
||||
new[] {
|
||||
Columns.Account,
|
||||
Columns.Exchange,
|
||||
Columns.Symbol,
|
||||
Columns.Security
|
||||
};
|
||||
|
||||
[ProtoMember(1)]
|
||||
SortingCriteria[] _sorting_criterion;
|
||||
[ProtoMember(5)]
|
||||
Columns[] _anchors;
|
||||
[ProtoMember(7)]
|
||||
PNLCellAppearanceProto _group_cell_appearance;
|
||||
[ProtoMember(8)]
|
||||
PNLCellAppearanceProto _header_cell_appearance;
|
||||
// don't store in proto yet (this type may change)
|
||||
[ProtoMember(9)]
|
||||
PNLAnimationTriggerData[] _animations;
|
||||
[ProtoMember(10)]
|
||||
PNLAnimationTriggerData[] _group_header_animations;
|
||||
[ProtoMember(11)]
|
||||
GridColumnSettings[] _group_columns;
|
||||
[ProtoMember(12)]
|
||||
GridColumnSettings[] _header_columns;
|
||||
[ProtoMember(13)]
|
||||
PNLAnimationTriggerData[] _header_row_animations;
|
||||
|
||||
PNLCellAppearanceProto _grid_cell_appearance_backing;
|
||||
|
||||
public static PNLSettings Default =>
|
||||
new PNLSettings {
|
||||
_sorting_criterion = new[] {
|
||||
new SortingCriteria(nameof(Columns.Account), true),
|
||||
new SortingCriteria(nameof(Columns.Security), true)
|
||||
},
|
||||
grid = DefaultGridSettings,
|
||||
calculation_type = PNLCalculationType.Waterfall,
|
||||
header_row_display = PNLHeaderRowDisplay.Total | PNLHeaderRowDisplay.SelectedTotal,
|
||||
_anchors = new[] { Columns.Account, Columns.Symbol },
|
||||
_group_cell_appearance = new PNLCellAppearanceProto {
|
||||
foreground = Colors.Black.rgb_color(),
|
||||
font_params = new FontParamsProto {
|
||||
font_family = GridSettings.Default.font_family,
|
||||
font_weight = FontWeights.Bold.ToOpenTypeWeight(),
|
||||
font_size = 11
|
||||
},
|
||||
background = Colors.Transparent.rgb_color()
|
||||
},
|
||||
_header_cell_appearance = new PNLCellAppearanceProto {
|
||||
foreground = Colors.Black.rgb_color(),
|
||||
font_params = new FontParamsProto {
|
||||
font_family = GridSettings.Default.font_family,
|
||||
font_weight = FontWeights.Bold.ToOpenTypeWeight(),
|
||||
font_size = 12
|
||||
},
|
||||
background = Colors.Transparent.rgb_color()
|
||||
}
|
||||
};
|
||||
|
||||
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion);
|
||||
|
||||
public static fills.FillsSettings DefaultAuxiliaryFills {
|
||||
get {
|
||||
var grid = fills.FillsSettings.MiniGridSettings;
|
||||
if (grid.columns.TryGetFirst(c => c.id == nameof(mbts.ui.spark.FillsView.Columns.Side), out var side_col, out int side_col_pos)) {
|
||||
var cols = grid.columns.ToList();
|
||||
cols[side_col_pos] = side_col.with(is_visible: false);
|
||||
grid = grid.with(cols);
|
||||
}
|
||||
var rv = fills.FillsSettings.Default();
|
||||
rv.grid = grid;
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLRowData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.DayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLRowData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss_color, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid_color, column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask_color, column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_group_header_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<PNLAnimationTriggerData> default_header_row_animations {
|
||||
get {
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.SODPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.SODNetPos));
|
||||
// day
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.DayPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.DayNetPos));
|
||||
// total
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(background: BTSColors.PNL.profit_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(background: BTSColors.PNL.loss_color.with(OpacityLevels.Light), column: Columns.TotalPNL));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.TotalNetPos));
|
||||
// working
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} > 0", _animation(background: BTSColors.MarketData.bid_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
yield return _trigger($"{nameof(PNLNormalizedData.working_net_pos)} < 0", _animation(background: BTSColors.MarketData.ask_color.with(OpacityLevels.Medium), column: Columns.WorkingNetPos));
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoMember(2)]
|
||||
public FilterStates filters { get; set; }
|
||||
[ProtoMember(3)]
|
||||
public PNLCalculationType calculation_type { get; set; }
|
||||
[ProtoMember(4)]
|
||||
public GridSettings grid { get; set; }
|
||||
[ProtoMember(6)]
|
||||
public PNLHeaderRowDisplay header_row_display { get; set; }
|
||||
|
||||
public PNLCellAppearance grid_defaults =>
|
||||
new PNLCellAppearance(
|
||||
font_family: new FontFamily(grid.font_family),
|
||||
font_size: grid.font_size,
|
||||
font_weight: grid.font_weight,
|
||||
background: grid.background,
|
||||
foreground: grid.font_color.rgb_color());
|
||||
|
||||
public PNLCellAppearanceProto group_cell_appearance {
|
||||
get => _group_cell_appearance ?? _grid_cell_appearance;
|
||||
set => _group_cell_appearance = value;
|
||||
}
|
||||
public PNLCellAppearanceProto header_cell_appearance {
|
||||
get => _header_cell_appearance ?? _grid_cell_appearance;
|
||||
set => _header_cell_appearance = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<SortingCriteria> sorting_criterion => _sorting_criterion ?? (_sorting_criterion = DefaultColumns.default_sorting_criterion.ToArray());
|
||||
public IReadOnlyList<Columns> anchors => _anchors ?? (_anchors = Array.Empty<Columns>());
|
||||
public IReadOnlyList<PNLAnimationTriggerData> animations => _animations ?? (_animations = default_animations.ToArray());
|
||||
public IReadOnlyList<PNLAnimationTriggerData> group_header_animations => _group_header_animations ?? (_group_header_animations = default_group_header_animations.ToArray());
|
||||
public IReadOnlyList<PNLAnimationTriggerData> header_row_animations => _header_row_animations ?? (_header_row_animations = default_header_row_animations.ToArray());
|
||||
public IReadOnlyList<GridColumnSettings> group_columns => _group_columns ?? (_group_columns = DefaultColumns.group_columns.ToArray());
|
||||
public IReadOnlyList<GridColumnSettings> header_columns => _header_columns ?? (_header_columns = DefaultColumns.header_columns.ToArray());
|
||||
|
||||
PNLCellAppearanceProto _grid_cell_appearance => _grid_cell_appearance_backing ?? (
|
||||
_grid_cell_appearance_backing = new PNLCellAppearanceProto {
|
||||
background = grid.background.rgb_color(),
|
||||
foreground = grid.font_color,
|
||||
font_params = new FontParamsProto {
|
||||
//font_color = grid.font_color,
|
||||
font_family = grid.font_family,
|
||||
font_weight = grid.font_weight.ToOpenTypeWeight(),
|
||||
font_size = grid.font_size
|
||||
}
|
||||
});
|
||||
|
||||
public PNLSettings with(
|
||||
IEnumerable<SortingCriteria> sorting_criterion = null,
|
||||
IEnumerable<Columns> anchors = null,
|
||||
FilterStates filters = null,
|
||||
GridSettings grid = null,
|
||||
PNLCalculationType? calculation_type = null,
|
||||
PNLHeaderRowDisplay? header_row_display = null,
|
||||
IEnumerable<GridColumnSettings> group_columns = null,
|
||||
IEnumerable<PNLAnimationTriggerData> animations = null,
|
||||
IEnumerable<PNLAnimationTriggerData> group_header_animations = null,
|
||||
IEnumerable<PNLAnimationTriggerData> header_row_animations = null,
|
||||
IEnumerable<GridColumnSettings> header_columns = null) =>
|
||||
new PNLSettings {
|
||||
_sorting_criterion = (sorting_criterion ?? _sorting_criterion.empty_if_null()).ToArray(),
|
||||
_anchors = (anchors ?? _anchors.empty_if_null()).ToArray(),
|
||||
filters = filters ?? this.filters,
|
||||
grid = grid ?? this.grid,
|
||||
calculation_type = calculation_type ?? this.calculation_type,
|
||||
header_row_display = header_row_display ?? this.header_row_display,
|
||||
_group_columns = group_columns?.ToArray() ?? _group_columns,
|
||||
_header_columns = header_columns?.ToArray() ?? _header_columns,
|
||||
_animations = animations?.ToArray() ?? _animations,
|
||||
_group_header_animations = group_header_animations?.ToArray() ?? _group_header_animations,
|
||||
_header_row_animations = header_row_animations?.ToArray() ?? _header_row_animations,
|
||||
_group_cell_appearance = _group_cell_appearance,
|
||||
_header_cell_appearance = _header_cell_appearance
|
||||
};
|
||||
|
||||
public bool Equals(PNLSettings other) =>
|
||||
other != null &&
|
||||
calculation_type == other.calculation_type &&
|
||||
filters.safe_equals(other.filters) &&
|
||||
header_row_display == other.header_row_display &&
|
||||
header_cell_appearance.safe_equals(other.header_cell_appearance) &&
|
||||
group_cell_appearance.safe_equals(other.group_cell_appearance) &&
|
||||
anchors.SequenceEqual(other.anchors) &&
|
||||
group_columns.SequenceEqual(other.group_columns) &&
|
||||
animations.SequenceEqual(other.animations) &&
|
||||
group_header_animations.SequenceEqual(other.group_header_animations) &&
|
||||
grid.safe_equals(other.grid);
|
||||
|
||||
static PNLAnimationTriggerData _trigger(string expression, params PNLAnimation[] animations) =>
|
||||
new PNLAnimationTriggerData {
|
||||
expression = expression,
|
||||
animations = animations
|
||||
};
|
||||
|
||||
static PNLAnimation _animation(
|
||||
Columns column,
|
||||
Color? foreground = null,
|
||||
Color? background = null,
|
||||
FontWeight? font_weight = null) =>
|
||||
new PNLAnimation {
|
||||
column = column,
|
||||
foreground = foreground?.rgb_color(),
|
||||
background = background?.rgb_color(),
|
||||
font_weight = font_weight?.ToOpenTypeWeight()
|
||||
};
|
||||
|
||||
static class DefaultColumns {
|
||||
public static GridColumnSettings account => new GridColumnSettings(nameof(Columns.Account), DefaultColumnHeaders.Account, width: Widths.Account);
|
||||
public static GridColumnSettings exchange => new GridColumnSettings(nameof(Columns.Exchange), DefaultColumnHeaders.Exchange, false, width: Widths.Exchange);
|
||||
public static GridColumnSettings symbol => new GridColumnSettings(nameof(Columns.Symbol), DefaultColumnHeaders.Symbol, width: Widths.Symbol);
|
||||
public static GridColumnSettings security => new GridColumnSettings(nameof(Columns.Security), DefaultColumnHeaders.Security, width: Widths.Security);
|
||||
public static GridColumnSettings security_type => new GridColumnSettings(nameof(Columns.SecurityType), Headers.SecurityType, width: Widths.SecurityType);
|
||||
// sod
|
||||
public static GridColumnSettings sod_pnl => new GridColumnSettings(nameof(Columns.SODPNL), Headers.SOD.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_long_pos => new GridColumnSettings(nameof(Columns.SODLongPos), Headers.SOD.Long, false, width: Widths.Qty);
|
||||
public static GridColumnSettings sod_long_avg_price => new GridColumnSettings(nameof(Columns.SODLongAvgPrice), Headers.SOD.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_long_avg_price_delta => new GridColumnSettings(nameof(Columns.SODLongAvgPriceDelta), Headers.SOD.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_short_pos => new GridColumnSettings(nameof(Columns.SODShortPos), Headers.SOD.Short, false, width: Widths.Qty);
|
||||
public static GridColumnSettings sod_short_avg_price => new GridColumnSettings(nameof(Columns.SODShortAvgPrice), Headers.SOD.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_short_avg_price_delta => new GridColumnSettings(nameof(Columns.SODShortAvgPriceDelta), Headers.SOD.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings sod_net_pos => new GridColumnSettings(nameof(Columns.SODNetPos), Headers.SOD.Net, false, width: Widths.Qty);
|
||||
public static GridColumnSettings sod_net_avg_price_delta => new GridColumnSettings(nameof(Columns.SODNetAvgPriceDelta), Headers.SOD.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// day
|
||||
public static GridColumnSettings day_pnl => new GridColumnSettings(nameof(Columns.DayPNL), Headers.Day.PNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_pos => new GridColumnSettings(nameof(Columns.DayLongPos), Headers.Day.Long, false, width: Widths.Qty);
|
||||
public static GridColumnSettings day_long_avg_price => new GridColumnSettings(nameof(Columns.DayLongAvgPrice), Headers.Day.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_long_avg_price_delta => new GridColumnSettings(nameof(Columns.DayLongAvgPriceDelta), Headers.Day.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_pos => new GridColumnSettings(nameof(Columns.DayShortPos), Headers.Day.Short, false, width: Widths.Qty);
|
||||
public static GridColumnSettings day_short_avg_price => new GridColumnSettings(nameof(Columns.DayShortAvgPrice), Headers.Day.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_short_avg_price_delta => new GridColumnSettings(nameof(Columns.DayShortAvgPriceDelta), Headers.Day.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings day_net_pos => new GridColumnSettings(nameof(Columns.DayNetPos), Headers.Day.Net, width: Widths.Qty);
|
||||
public static GridColumnSettings day_net_avg_price_delta => new GridColumnSettings(nameof(Columns.DayNetAvgPriceDelta), Headers.Day.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// working
|
||||
public static GridColumnSettings working_long_pos => new GridColumnSettings(nameof(Columns.WorkingLongPos), Headers.Working.Long, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.bid);
|
||||
public static GridColumnSettings working_long_avg_price => new GridColumnSettings(nameof(Columns.WorkingLongAvgPrice), Headers.Working.LongAvg, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_long_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingLongAvgPriceDelta), Headers.Working.LongAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_short_pos => new GridColumnSettings(nameof(Columns.WorkingShortPos), Headers.Working.Short, false, width: Widths.WorkingQty, font_color_override: BTSColors.MarketData.ask);
|
||||
public static GridColumnSettings working_short_avg_price => new GridColumnSettings(nameof(Columns.WorkingShortAvgPrice), Headers.Working.ShortAvg, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_short_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingShortAvgPriceDelta), Headers.Working.ShortAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings working_net_pos => new GridColumnSettings(nameof(Columns.WorkingNetPos), Headers.Working.Net, width: Widths.WorkingQty);
|
||||
public static GridColumnSettings working_net_avg_price_delta => new GridColumnSettings(nameof(Columns.WorkingNetAvgPriceDelta), Headers.Working.NetAvgDelta, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// total
|
||||
public static GridColumnSettings total_pnl => new GridColumnSettings(nameof(Columns.TotalPNL), Headers.Total.PNL, false, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_long_pos => new GridColumnSettings(nameof(Columns.TotalLongPos), Headers.Total.Long, false, width: Widths.Qty);
|
||||
public static GridColumnSettings total_long_avg_price => new GridColumnSettings(nameof(Columns.TotalLongAvgPrice), Headers.Total.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_long_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalLongAvgPriceDelta), Headers.Total.LongAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_short_pos => new GridColumnSettings(nameof(Columns.TotalShortPos), Headers.Total.Short, false, width: Widths.Qty);
|
||||
public static GridColumnSettings total_short_avg_price => new GridColumnSettings(nameof(Columns.TotalShortAvgPrice), Headers.Total.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_short_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalShortAvgPriceDelta), Headers.Total.ShortAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings total_net_pos => new GridColumnSettings(nameof(Columns.TotalNetPos), Headers.Total.Net, false, width: Widths.Qty);
|
||||
public static GridColumnSettings total_net_avg_price_delta => new GridColumnSettings(nameof(Columns.TotalNetAvgPriceDelta), Headers.Total.NetAvgDelta, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// market
|
||||
public static GridColumnSettings bid_price => new GridColumnSettings(nameof(Columns.BidPrice), DefaultColumnHeaders.Bid, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.bid, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings ask_price => new GridColumnSettings(nameof(Columns.AskPrice), DefaultColumnHeaders.Ask, false, width: DefaultColumnWidths.Price, font_color_override: BTSColors.MarketData.ask, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings mid_price => new GridColumnSettings(nameof(Columns.MidPrice), DefaultColumnHeaders.Mid, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings last_price => new GridColumnSettings(nameof(Columns.LastPrice), DefaultColumnHeaders.Last, false, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings settlement_price => new GridColumnSettings(nameof(Columns.SettlementPrice), DefaultColumnHeaders.Settlement, false, width: DefaultColumnWidths.Settlement, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings pnl_price => new GridColumnSettings(nameof(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings cpp => new GridColumnSettings(nameof(Columns.CPP), "CPP", false, width: 50, horizontal_alignment: HorizontalAlignment.Right);
|
||||
// misc
|
||||
public static GridColumnSettings has_day_trades => new GridColumnSettings(nameof(Columns.HasDayTrades), Headers.HasDayTrades, false, width: Widths.HasDayTrades);
|
||||
|
||||
public static IEnumerable<SortingCriteria> default_sorting_criterion {
|
||||
get {
|
||||
yield return new SortingCriteria(account.id, true);
|
||||
yield return new SortingCriteria(security.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return account;
|
||||
yield return exchange;
|
||||
yield return symbol;
|
||||
yield return security;
|
||||
yield return security_type;
|
||||
|
||||
yield return total_pnl; // hidden
|
||||
yield return sod_pnl;
|
||||
yield return day_pnl;
|
||||
/// total
|
||||
yield return total_net_pos;
|
||||
yield return total_net_avg_price_delta; // hidden
|
||||
// total long
|
||||
yield return total_long_pos; // hidden
|
||||
yield return total_long_avg_price; // hidden
|
||||
yield return total_long_avg_price_delta; // hidden
|
||||
// total short
|
||||
yield return total_short_pos; // hidden
|
||||
yield return total_short_avg_price; // hidden
|
||||
yield return total_short_avg_price_delta; // hidden
|
||||
/// sod
|
||||
yield return sod_net_pos;
|
||||
yield return sod_net_avg_price_delta; // hidden
|
||||
// sod long
|
||||
yield return sod_long_pos; // hidden
|
||||
yield return sod_long_avg_price; // hidden
|
||||
yield return sod_long_avg_price_delta; // hidden
|
||||
// sod short
|
||||
yield return sod_short_pos; // hidden
|
||||
yield return sod_short_avg_price; // hidden
|
||||
yield return sod_short_avg_price_delta; // hidden
|
||||
/// day
|
||||
yield return day_net_pos;
|
||||
yield return day_net_avg_price_delta; // hidden
|
||||
// day long
|
||||
yield return day_long_pos; // hidden
|
||||
yield return day_long_avg_price; // hidden
|
||||
yield return day_long_avg_price_delta; // hidden
|
||||
// day short
|
||||
yield return day_short_pos; // hidden
|
||||
yield return day_short_avg_price; // hidden
|
||||
yield return day_short_avg_price_delta; // hidden
|
||||
|
||||
yield return has_day_trades;
|
||||
/// working
|
||||
// working net
|
||||
yield return working_net_pos;
|
||||
yield return working_net_avg_price_delta;
|
||||
// working long
|
||||
yield return working_long_pos;
|
||||
yield return working_long_avg_price;
|
||||
yield return working_long_avg_price_delta;
|
||||
// working short
|
||||
yield return working_short_pos;
|
||||
yield return working_short_avg_price;
|
||||
yield return working_short_avg_price_delta;
|
||||
/// market
|
||||
yield return bid_price;
|
||||
yield return ask_price;
|
||||
yield return mid_price;
|
||||
yield return last_price;
|
||||
yield return settlement_price;
|
||||
yield return pnl_price;
|
||||
yield return cpp;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> group_columns {
|
||||
get {
|
||||
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GridColumnSettings> header_columns {
|
||||
get {
|
||||
var rv = all.Select(cs => cs.with(font_weight_override: new NullableFontWeightProto(FontWeights.Bold))).ToList();
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.SODPNL, _override_color);
|
||||
GridSettingsHelper.try_replace_column(rv, Columns.DayPNL, _override_color);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
static GridColumnSettings _override_color(GridColumnSettings col_settings) =>
|
||||
_override_color(col_settings, 0);
|
||||
|
||||
static GridColumnSettings _override_color(GridColumnSettings col_settings, uint color) =>
|
||||
col_settings.with(font_color_override: new NullableColorProto(color));
|
||||
}
|
||||
|
||||
static class OpacityLevels {
|
||||
public const byte Light = 0x55, Medium = 0x6A;
|
||||
}
|
||||
}
|
||||
|
||||
static class PNLSettingsExtensions {
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers(this PNLSettings settings) => settings.animations.Select(a => a.to_trigger<PNLRowData>()).ToList();
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> group_triggers(this PNLSettings settings) => settings.group_header_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
|
||||
public static IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> header_triggers(this PNLSettings settings) => settings.header_row_animations.Select(a => a.to_trigger<PNLNormalizedData>()).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.expression;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLSettingsVM : DisposableViewModelBase, IWindowSettingsVM<PNLSettings>, IValueFactory<PNLSettings>, IObservable<PNLSettings>, ITemplateSelectorKey {
|
||||
readonly IReadOnlyDictionary<ColumnKeys, AnchorItemVM> _anchor_cache;
|
||||
readonly DeferrableActionWrapper _sync_anchors;
|
||||
|
||||
PNLCalculationType _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display;
|
||||
PNLSettings _last_loaded;
|
||||
|
||||
public PNLSettingsVM(PNLSettings settings) {
|
||||
// init anchor cache, deferrable resync
|
||||
IDictionary<string, object> raw_anchor_cache = new ExpandoObject();
|
||||
raw_anchor_cache.AddRange(PNLSettings.anchorable_columns
|
||||
.Select(k => new KeyValuePair<string, object>(k.ToString(), new AnchorItemVM(this, k))));
|
||||
_anchor_cache =
|
||||
MappedDictionary.create(
|
||||
new ReadOnlyDictionary<string, object>(anchor_vm = raw_anchor_cache),
|
||||
ConvertUtils.CastDelegate<object, AnchorItemVM>.value,
|
||||
EnumHelper.parse<ColumnKeys>,
|
||||
k => k.ToString());
|
||||
_sync_anchors = new DeferrableActionWrapper(() => {
|
||||
for (int i = 0; i < anchor_items.Count; i++) {
|
||||
anchor_items[i].anchor_index = i + 1;
|
||||
anchor_items[i].refresh();
|
||||
}
|
||||
});
|
||||
core_settings_vm = new SimpleGridSettingsVM(column_factory: col_settings => {
|
||||
var col_vm = new PNLColumnVM(core_settings_vm, EnumHelper.parse<ColumnKeys>(col_settings.id));
|
||||
col_vm.load_from(col_settings);
|
||||
return col_vm;
|
||||
}) {
|
||||
can_edit_background = true
|
||||
};
|
||||
core_settings_vm.selection_highlight.bind_opacity_to_alpha = true;
|
||||
_dispose = Disposable.Create(
|
||||
anchor_items.Subscribe((s, e) => _sync_anchors.execute()));
|
||||
load(settings);
|
||||
}
|
||||
|
||||
public dynamic anchor_vm { get; }
|
||||
public ObservableCollection<AnchorItemVM> anchor_items { get; } = new ObservableCollection<AnchorItemVM>();
|
||||
public SimpleGridSettingsVM core_settings_vm { get; }
|
||||
public PNLCellAppearanceParamsVM group_row_defaults { get; } = new PNLCellAppearanceParamsVM();
|
||||
public PNLCellAppearanceParamsVM header_row_defaults { get; } = new PNLCellAppearanceParamsVM();
|
||||
public PNLCalculationType calculation_type { get => _calculation_type; set => Set(ref _calculation_type, value); }
|
||||
public IEnumerable<PNLCalculationType> calculation_types { get; } = EnumHelper.get_values_except(PNLCalculationType.None);
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public IEnumerable<PNLHeaderRowDisplay> header_row_display_options => PNLSettings.header_row_display_options;
|
||||
|
||||
string ITemplateSelectorKey.key => GridSettingsTemplateKeys.PNL;
|
||||
|
||||
public PNLSettings get_result() {
|
||||
var s = _last_loaded == null ? new PNLSettings() : SparkUtils.clone_proto(_last_loaded);
|
||||
s.calculation_type = _calculation_type;
|
||||
s.grid = core_settings_vm.create();
|
||||
var trigger_anims = core_settings_vm.columns
|
||||
.OfType<PNLColumnVM>()
|
||||
.SelectMany(c => c.get_triggers())
|
||||
.GroupBy(tup => tup.expression)
|
||||
.Where(g => CoreExpressionEvaluator<PNLRowData>.validate(g.Key))
|
||||
.Select(g => (
|
||||
expression: g.Key,
|
||||
animations: g.ToArray()))
|
||||
.ToList();
|
||||
|
||||
var normalized_anims = trigger_anims
|
||||
.Where(tup => CoreExpressionEvaluator<PNLNormalizedData>.validate(tup.expression))
|
||||
.ToList();
|
||||
|
||||
s = s.with(anchors: anchor_items.Select(vm => vm.key),
|
||||
group_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.group_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
header_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.header_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
animations: trigger_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.row)
|
||||
}),
|
||||
group_header_animations: normalized_anims
|
||||
.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.group)
|
||||
}),
|
||||
header_row_animations: normalized_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.header)
|
||||
}));
|
||||
s.header_row_display = header_row_display;
|
||||
s.header_cell_appearance = PNLCellAppearanceProto.from_appearance(header_row_defaults.to_appearance());
|
||||
s.group_cell_appearance = PNLCellAppearanceProto.from_appearance(group_row_defaults.to_appearance());
|
||||
return s;
|
||||
}
|
||||
|
||||
public void load(PNLSettings value) {
|
||||
_last_loaded = value;
|
||||
header_row_defaults.load(value.header_cell_appearance.to_appearance());
|
||||
group_row_defaults.load(value.group_cell_appearance.to_appearance());
|
||||
var column_map = _map_column_data(value.grid.columns, value.animations);
|
||||
var group_column_map = _map_column_data(value.group_columns, value.group_header_animations);
|
||||
var header_column_map = _map_column_data(value.header_columns, value.header_row_animations);
|
||||
|
||||
core_settings_vm.load_from(value.grid);
|
||||
|
||||
for (int i = 0; i < core_settings_vm.columns.Count; i++) {
|
||||
var col_vm = core_settings_vm.columns[i] as PNLColumnVM ?? throw new InvalidOperationException($"In {nameof(PNLSettingsVM)}, all columns should be of type {typeof(PNLColumnVM)}.");
|
||||
var key = EnumHelper.parse<ColumnKeys>(col_vm.id);
|
||||
var (col_settings, trigger_animations) = column_map[key];
|
||||
var group_data = group_column_map.GetNullable(key);
|
||||
var header_data = header_column_map.GetNullable(key);
|
||||
col_vm.load_from(col_settings, trigger_animations, group_data?.settings, group_data?.triggers, header_data?.triggers, header_data?.settings);
|
||||
}
|
||||
using (_sync_anchors.defer_execution())
|
||||
anchor_items.WrapSelector(a => a.key, k => _anchor_cache[k]).sync_with(value.anchors.Intersect(PNLSettings.anchorable_columns));
|
||||
header_row_display = value.header_row_display;
|
||||
calculation_type = value.calculation_type;
|
||||
}
|
||||
|
||||
PNLSettings IValueFactory<PNLSettings>.create() => get_result();
|
||||
void IValueFactory<PNLSettings>.load_from(PNLSettings value) => load(value);
|
||||
IDisposable IObservable<PNLSettings>.Subscribe(IObserver<PNLSettings> observer) => throw new NotImplementedException();
|
||||
|
||||
static Dictionary<ColumnKeys, (GridColumnSettings settings, Dictionary<string, PNLAnimation> triggers)> _map_column_data(IEnumerable<GridColumnSettings> columns, IEnumerable<PNLAnimationTriggerData> triggers) {
|
||||
var map = columns.ToDictionary(
|
||||
c => c.strong_id<ColumnKeys>(),
|
||||
c => (settings: c, triggers: new Dictionary<string, PNLAnimation>()));
|
||||
foreach (var trig in triggers) {
|
||||
foreach (var anim in trig.animations) {
|
||||
if (map.TryGetValue(anim.column, out var col_data))
|
||||
col_data.triggers[trig.expression] = anim;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
void _append_or_remove_anchor(ColumnKeys key, bool include) {
|
||||
if (!_anchor_cache.TryGetValue(key, out var vm))
|
||||
throw new KeyNotFoundException($"Column key '{key}' is not anchorable (expected one of: {string.Join(", ", PNLSettings.anchorable_columns)}).");
|
||||
if (vm.include == include)
|
||||
return;
|
||||
if (include) {
|
||||
if (anchor_items.Any(a => a.key == key))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} already contains an element with key '{key}.'");
|
||||
anchor_items.Add(vm);
|
||||
vm.anchor_index = anchor_items.Count;
|
||||
} else {
|
||||
if (!anchor_items.Remove(vm))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} does not contain an element with key '{key}.'");
|
||||
vm.anchor_index = null;
|
||||
}
|
||||
_sync_anchors.execute();
|
||||
}
|
||||
|
||||
public class AnchorItemVM : ViewModelBase, IEquatable<AnchorItemVM> {
|
||||
readonly PNLSettingsVM _owner;
|
||||
int? _anchor_index;
|
||||
|
||||
public AnchorItemVM(PNLSettingsVM owner, ColumnKeys key) {
|
||||
_owner = owner;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public ColumnKeys key { get; }
|
||||
public bool include { get => anchor_index.HasValue; set => _owner._append_or_remove_anchor(key, value); }
|
||||
public int? anchor_index { get => _anchor_index; set => Set(ref _anchor_index, value); }
|
||||
|
||||
public void refresh() =>
|
||||
RaisePropertyChanged(nameof(include), nameof(anchor_index));
|
||||
|
||||
public bool Equals(AnchorItemVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as AnchorItemVM);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
EnumHelper.get_display_name(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellEmbeddedAnimationModel<TRow> {
|
||||
IReadOnlyList<IPNLAnimationTrigger<TRow>> _triggers;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults;
|
||||
TRow _row;
|
||||
|
||||
public PNLCellEmbeddedAnimationModel(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
|
||||
_defaults = defaults;
|
||||
_triggers = triggers;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<ColumnKeys, PNLCellAppearance> cell_appearances { get; private set; }
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<TRow>> triggers) {
|
||||
_defaults = defaults;
|
||||
_triggers = triggers;
|
||||
on_row(_row);
|
||||
}
|
||||
|
||||
public void on_row(TRow data) {
|
||||
_row = data;
|
||||
var bd = _defaults.ToBuilder();
|
||||
foreach (var t in _triggers) {
|
||||
foreach (var kv in t.trigger(data)) {
|
||||
if (!bd.TryGetValue(kv.Key, out var app)) {
|
||||
app = _defaults[kv.Key];
|
||||
}
|
||||
app = kv.Value.merge(app);
|
||||
bd[kv.Key] = app;
|
||||
}
|
||||
}
|
||||
cell_appearances = bd.ToImmutable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using mbts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.spark;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLCellIndexBuilder<TRow> {
|
||||
public readonly DIYGridAnimationModel<PNLView.Columns, TRow> animation_model = new DIYGridAnimationModel<PNLView.Columns, TRow>(TimeSpan.FromSeconds(0.3));
|
||||
|
||||
static readonly Func<WorkingOrderSideData, string> _fmt_working_order_data =
|
||||
d => OrderUtils.order_collection_summary(d.top_price, d.top_volume, d.working_total);
|
||||
|
||||
readonly Dictionary<PNLView.Columns, IPNLCellVM<TRow>> _cell_index = new Dictionary<PNLView.Columns, IPNLCellVM<TRow>>();
|
||||
|
||||
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) =>
|
||||
add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
|
||||
|
||||
public PNLCellVM<TRow, T> add_pnl<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
|
||||
|
||||
public PNLCellVM<TRow, decimal> add_pnl_decimal(PNLView.Columns key, Func<TRow, decimal> get) =>
|
||||
add_delta<decimal>(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparerDecimal, get, FormattingDelegates.Price<decimal>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_pnl_double(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_net_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_net_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, double> add_long_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.bid, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_long_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, double> add_short_price(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.ask, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, T> add_short_pos<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, T> add_price<T>(PNLView.Columns key, Func<TRow, T> get) where T : struct, IComparable<T>, IFormattable =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, get, FormattingDelegates.Price<T>.Value);
|
||||
|
||||
public PNLCellVM<TRow, double> add_price_double(PNLView.Columns key, Func<TRow, double> get) =>
|
||||
add_delta(key, BTSColors.PNL.profit_color, BTSColors.PNL.loss_color, OrderDataHelper.PriceComparer, get, FormattingDelegates.Price<double>.Value);
|
||||
|
||||
public PNLCellVM<TRow, WorkingOrderSideData> add_working_long(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
|
||||
_add_working(key, BTSColors.MarketData.bid, BTSColors.MarketData.ask, get, get_market, OrderDataHelper.PriceComparer);
|
||||
|
||||
public PNLCellVM<TRow, WorkingOrderSideData> add_working_short(PNLView.Columns key, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market) =>
|
||||
_add_working(key, BTSColors.MarketData.ask, BTSColors.MarketData.bid, get, get_market, new ReverseComparer<double>(OrderDataHelper.PriceComparer));
|
||||
|
||||
PNLCellVM<TRow, WorkingOrderSideData> _add_working(PNLView.Columns key, Color increase, Color decrease, Func<TRow, WorkingOrderSideData> get, Func<TRow, double> get_market, IComparer<double> price_comparer) {
|
||||
var delta_trig = DIYGridTriggerAnimations.delta_trigger<int, PNLView.Columns>(key, increase, decrease)
|
||||
.wrap<int, TRow, PNLView.Columns>(r => get(r).working_total);
|
||||
var top_of_book_trig = new TopOfBookTrigger<int, double>(key, increase,
|
||||
r => get(r).working_total,
|
||||
r => get(r).top_price,
|
||||
get_market,
|
||||
price_comparer);
|
||||
var zipped_trig = delta_trig.zip_indefinite_frame(r => {
|
||||
var frames = top_of_book_trig.on_data(r);
|
||||
return frames.TryGetFirst(out var f) ? f.animation : null;
|
||||
});
|
||||
var cell = add(key, get, _fmt_working_order_data);
|
||||
animation_model.add_trigger(delta_trig);
|
||||
animation_model.add_trigger(top_of_book_trig);
|
||||
return cell;
|
||||
}
|
||||
|
||||
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct, IComparable<T> =>
|
||||
add_delta(key, increase, decrease, Comparer<T>.Default, get, fmt);
|
||||
|
||||
public PNLCellVM<TRow, T> add_delta<T>(PNLView.Columns key, Color increase, Color decrease, IComparer<T> comparer, Func<TRow, T> get, Func<T, string> fmt = null) where T : struct {
|
||||
var cell = add(key, get, fmt, EqualityComparer<T>.Default, Comparer<T>.Default);
|
||||
animation_model.add_trigger(
|
||||
DIYGridTriggerAnimations.delta_trigger(key, increase, decrease, comparer)
|
||||
.wrap(get));
|
||||
return cell;
|
||||
}
|
||||
|
||||
public PNLCellVM<TRow, T> add<T>(PNLView.Columns key, Func<TRow, T> get, Func<T, string> fmt, IEqualityComparer<T> eq_comparer, IComparer<T> comparer) {
|
||||
var cell = new PNLCellVM<TRow, T>(get, fmt, eq_comparer, comparer);
|
||||
_cell_index[key] = cell;
|
||||
return cell;
|
||||
}
|
||||
|
||||
public void on_row(TRow row) {
|
||||
foreach (var cell in _cell_index.Values)
|
||||
cell.on_row(row);
|
||||
animation_model.on_row(row);
|
||||
}
|
||||
|
||||
public void set_appearances(IReadOnlyDictionary<PNLView.Columns, PNLCellAppearance> appearances) {
|
||||
foreach (var kv in appearances) {
|
||||
if (_cell_index.TryGetValue(kv.Key, out var c))
|
||||
c.apply_appearance(kv.Value);
|
||||
}
|
||||
}
|
||||
|
||||
class TopOfBookTrigger<TPosition, TPrice> : ITriggerAnimation<TRow, PNLView.Columns> where TPosition : struct, IComparable<TPosition> where TPrice : struct, IComparable<TPrice> {
|
||||
readonly PNLView.Columns _column;
|
||||
readonly Func<TRow, TPosition> _get_pos;
|
||||
readonly Func<TRow, TPrice> _get_price, _get_market;
|
||||
readonly IComparer<TPrice> _comparer;
|
||||
readonly Color _border_color;
|
||||
|
||||
public TopOfBookTrigger(
|
||||
PNLView.Columns column,
|
||||
Color border_color,
|
||||
Func<TRow, TPosition> get_pos,
|
||||
Func<TRow, TPrice> get_price,
|
||||
Func<TRow, TPrice> get_market,
|
||||
IComparer<TPrice> comparer) {
|
||||
_column = column;
|
||||
_border_color = border_color;
|
||||
_get_pos = get_pos;
|
||||
_get_price = get_price;
|
||||
_get_market = get_market;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
public IEnumerable<AnimationFrame<PNLView.Columns>> on_data(TRow value) {
|
||||
var pos = _get_pos(value);
|
||||
if (pos.CompareTo(default) > 0) {
|
||||
var price = _get_price(value);
|
||||
var market = _get_market(value);
|
||||
if (_comparer.Compare(price, market) >= 0)
|
||||
yield return new AnimationFrame<PNLView.Columns>(
|
||||
TimeSpan.Zero,
|
||||
ImmutableDictionary<SymbolKey<PNLView.Columns>, Animation>.Empty.Add(
|
||||
new SymbolKey<PNLView.Columns>(_column, mbts.ui.spark.CellAnimation.Type.Border),
|
||||
new Animation(mbts.ui.spark.CellAnimation.Type.Border, _border_color)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
interface IPNLCellVM {
|
||||
object value { get; }
|
||||
FontWeight font_weight { get; set; }
|
||||
Color foreground { get; set; }
|
||||
Color background { get; set; }
|
||||
Color border { get; set; }
|
||||
}
|
||||
|
||||
interface IPNLCellVM<TRow> : IPNLCellVM {
|
||||
void on_row(TRow row);
|
||||
}
|
||||
|
||||
class PNLCellVM<TRow, TValue> : ViewModelBase, IObserver<PNLAnimation>, IPNLCellVM<TRow>, IComparable<PNLCellVM<TRow, TValue>>, IComparable {
|
||||
readonly Func<TValue, string> _fmt;
|
||||
readonly Func<TRow, TValue> _get;
|
||||
readonly IEqualityComparer<TValue> _eq_comparer;
|
||||
readonly IComparer<TValue> _comparer;
|
||||
Color
|
||||
_foreground = Colors.Black,
|
||||
_background = Colors.Transparent,
|
||||
_border = Colors.Transparent;
|
||||
string _text;
|
||||
TValue _value;
|
||||
|
||||
public PNLCellVM(Func<TRow, TValue> get)
|
||||
: this(get, null, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
|
||||
|
||||
public PNLCellVM(Func<TRow, TValue> get, Func<TValue, string> fmt)
|
||||
: this(get, fmt, EqualityComparer<TValue>.Default, Comparer<TValue>.Default) { }
|
||||
|
||||
public PNLCellVM(Func<TRow, TValue> get, Func<TValue, string> fmt, IEqualityComparer<TValue> eq_comparer, IComparer<TValue> comparer) {
|
||||
_get = get;
|
||||
_fmt = fmt;
|
||||
_eq_comparer = eq_comparer;
|
||||
_comparer = comparer;
|
||||
}
|
||||
|
||||
object IPNLCellVM.value => value;
|
||||
public TValue value {
|
||||
get => _value;
|
||||
set {
|
||||
if (Set(ref _value, value)) {
|
||||
_text = null;
|
||||
RaisePropertyChanged(nameof(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
public string text {
|
||||
get => _text ?? (_text = _fmt?.Invoke(value) ?? value?.ToString());
|
||||
set => Set(ref _text, value);
|
||||
}
|
||||
|
||||
public Color foreground {
|
||||
get => _foreground;
|
||||
set => Set(ref _foreground, value);
|
||||
}
|
||||
|
||||
public Color background {
|
||||
get => _background;
|
||||
set => Set(ref _background, value);
|
||||
}
|
||||
|
||||
public Color border {
|
||||
get => _border;
|
||||
set => Set(ref _border, value);
|
||||
}
|
||||
|
||||
FontWeight _font_weight;
|
||||
public FontWeight font_weight {
|
||||
get => _font_weight;
|
||||
set => Set(ref _font_weight, value);
|
||||
}
|
||||
|
||||
public void on_row(TRow row) =>
|
||||
value = _get(row);
|
||||
|
||||
public void OnNext(PNLAnimation animation) {
|
||||
if (animation.foreground.HasValue)
|
||||
foreground = animation.foreground.Value.argb_color();
|
||||
if (animation.background.HasValue)
|
||||
background = animation.background.Value.argb_color();
|
||||
if (animation.font_weight.HasValue)
|
||||
font_weight = FontWeight.FromOpenTypeWeight(animation.font_weight.Value);
|
||||
}
|
||||
public void OnError(Exception error) { }
|
||||
public void OnCompleted() { }
|
||||
|
||||
public override string ToString() =>
|
||||
text;
|
||||
|
||||
int IComparable<PNLCellVM<TRow, TValue>>.CompareTo(PNLCellVM<TRow, TValue> other) =>
|
||||
other == null ? throw new ArgumentNullException(nameof(other)) : _comparer.Compare(value, other.value);
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is PNLCellVM<TRow, TValue> other ?
|
||||
_comparer.Compare(value, other.value) :
|
||||
throw new ArgumentException(nameof(obj), $"The specified object is not of type {typeof(PNLCellVM<TRow, TValue>)} (actually {obj?.GetType()}).");
|
||||
}
|
||||
|
||||
static class PNLCellVMExtensions {
|
||||
public static void apply_appearance(this IPNLCellVM cell, PNLCellAppearance animation) {
|
||||
if (animation.foreground.HasValue)
|
||||
cell.foreground = animation.foreground.Value;
|
||||
if (animation.background.HasValue)
|
||||
cell.background = animation.background.Value;
|
||||
if (animation.font_weight.HasValue)
|
||||
cell.font_weight = animation.font_weight.Value;
|
||||
// TODO: font family, size
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLNormalizedDataVM : IRaiseNotifyPropertyChanged, INotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLNormalizedDataVM> _change_detector =
|
||||
ObjectSnapshot.build_detector_except<PNLNormalizedDataVM>(nameof(data));
|
||||
|
||||
PNLNormalizedData _data;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event Action data_changed;
|
||||
|
||||
public PNLNormalizedData data {
|
||||
get => _data;
|
||||
set {
|
||||
if (_data != value) {
|
||||
using (_change_detector.detect_changes(this))
|
||||
_data = value;
|
||||
data_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// total aggregate pnl
|
||||
public decimal day_pnl => _data.day_pnl;
|
||||
public decimal sod_pnl => _data.sod_pnl;
|
||||
public decimal total_pnl => _data.day_pnl + _data.sod_pnl;
|
||||
// day pos
|
||||
public decimal day_long_pos => _data.day_long_pos;
|
||||
public decimal day_short_pos => _data.day_short_pos;
|
||||
public decimal day_net_pos => _data.day_long_pos - _data.day_short_pos;
|
||||
// sod pos
|
||||
public decimal sod_long_pos => _data.sod_long_pos;
|
||||
public decimal sod_short_pos => _data.sod_short_pos;
|
||||
public decimal sod_net_pos => _data.sod_long_pos - _data.sod_short_pos;
|
||||
// total pos
|
||||
public decimal total_net_pos => _data.total_net_pos;
|
||||
public decimal total_long_pos => _data.day_long_pos + _data.sod_long_pos;
|
||||
public decimal total_short_pos => _data.day_short_pos + _data.sod_short_pos;
|
||||
// working
|
||||
public decimal working_long_pos => _data.working_long_pos;
|
||||
public decimal working_short_pos => _data.working_short_pos;
|
||||
public decimal working_net_pos => working_long_pos - working_short_pos;
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
|
||||
class PNLHeaderRowVM : IRaiseNotifyPropertyChanged, INotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLHeaderRowVM> _change_detector =
|
||||
ObjectSnapshot.build_detector_except<PNLHeaderRowVM>(nameof(data));
|
||||
|
||||
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _default_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
PNLNormalizedData _data = PNLNormalizedData.zero;
|
||||
|
||||
public PNLHeaderRowVM(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_default_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
|
||||
_cell_map.set_appearances(default_appearances);
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl_decimal(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add_long_pos(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
|
||||
working_short_pos = _cell_map.add_short_pos(ColumnKeys.WorkingShortPos, r => r.working_short_pos);
|
||||
// total
|
||||
total_pnl = _cell_map.add_pnl_decimal(ColumnKeys.TotalPNL, d => d.total_pnl);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event Action data_changed;
|
||||
|
||||
public PNLNormalizedData data {
|
||||
get => _data;
|
||||
set {
|
||||
if (value != _data) {
|
||||
_data = value;
|
||||
_cell_map.on_row(value);
|
||||
_animator.on_row(value);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
data_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
|
||||
// sod
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_default_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator.on_settings(_default_appearances, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using bts.api;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLRowVM : DisposableViewModelBase, IEquatable<PNLRowVM> {
|
||||
public static readonly IComparer<PNLRowVM> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowVM>(r => r.key);
|
||||
static readonly Func<bool, string> _fmt_has_day_trades = hdt => hdt ? "Yes" : "No";
|
||||
//
|
||||
readonly PNLCellIndexBuilder<PNLRowData> _cell_map = new PNLCellIndexBuilder<PNLRowData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLRowData> _animator;
|
||||
bool _refresh_normalized_data = true, _is_selected;
|
||||
PNLNormalizedData _normalized_data;
|
||||
|
||||
public PNLRowVM(PNLRowData data, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
|
||||
this.data = data;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLRowData>(default_appearances, triggers);
|
||||
_animator.on_row(data);
|
||||
pnl_price = _cell_map.add_price_double(ColumnKeys.PNLPrice, d => d.pnl_price);
|
||||
account_name_cell = _cell_map.add(ColumnKeys.Account, d => d.account_name);
|
||||
security_cell = _cell_map.add(ColumnKeys.Security, d => d.security, null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
|
||||
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
|
||||
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
|
||||
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.security?.Symbol ?? string.Empty);
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.SODNetAvgPriceDelta, d => d.sod_net_avg_price_delta_raw);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price_raw);
|
||||
sod_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.SODLongAvgPriceDelta, d => d.sod_long_avg_price_delta_raw);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price_raw);
|
||||
sod_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.SODShortAvgPriceDelta, d => d.sod_short_avg_price_delta_raw);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.DayNetAvgPriceDelta, d => d.day_net_avg_price_delta_raw);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price_raw);
|
||||
day_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.DayLongAvgPriceDelta, d => d.day_long_avg_price_delta_raw);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price_raw);
|
||||
day_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.DayShortAvgPriceDelta, d => d.day_short_avg_price_delta_raw);
|
||||
// working
|
||||
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.WorkingNetAvgPriceDelta, d => d.working_net_avg_price_delta_raw);
|
||||
working_long_pos = _cell_map.add_working_long(ColumnKeys.WorkingLongPos, d => d.working.long_data, d => d.bid_price);
|
||||
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price_raw);
|
||||
working_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPriceDelta, d => d.working_long_avg_price_delta_raw);
|
||||
working_short_pos = _cell_map.add_working_short(ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price);
|
||||
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price_raw);
|
||||
working_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPriceDelta, d => d.working_short_avg_price_delta_raw);
|
||||
// total
|
||||
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
|
||||
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_net_avg_price_delta = _cell_map.add_net_price(ColumnKeys.TotalNetAvgPriceDelta, d => d.total_net_avg_price_delta_raw);
|
||||
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price_raw);
|
||||
total_long_avg_price_delta = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPriceDelta, d => d.total_long_avg_price_delta_raw);
|
||||
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price_raw);
|
||||
total_short_avg_price_delta = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPriceDelta, d => d.total_short_avg_price_delta_raw);
|
||||
// market
|
||||
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
|
||||
ask_price = _cell_map.add_price_double(ColumnKeys.AskPrice, d => d.ask_price);
|
||||
mid_price = _cell_map.add_price_double(ColumnKeys.MidPrice, d => d.mid_price);
|
||||
last_price = _cell_map.add_price_double(ColumnKeys.LastPrice, d => d.last_price);
|
||||
settlement_price = _cell_map.add_price_double(ColumnKeys.SettlementPrice, d => d.settlement_price);
|
||||
// misc
|
||||
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
|
||||
cpp = _cell_map.add(ColumnKeys.CPP, d => d.cpp);
|
||||
|
||||
_cell_map.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
|
||||
animations = new_animations.Select(kv => new mbts.ui.spark.CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
|
||||
RaisePropertyChanged(nameof(animations));
|
||||
});
|
||||
}
|
||||
|
||||
// events
|
||||
public event Action<PNLNormalizedData> updated;
|
||||
public event Action<bool> is_selected_changed;
|
||||
|
||||
// data
|
||||
public PNLKey key => data.key;
|
||||
public PNLRowData data { get; private set; }
|
||||
public PNLNormalizedData normalized_data {
|
||||
get {
|
||||
if (_refresh_normalized_data) {
|
||||
_refresh_normalized_data = false;
|
||||
var nd = new PNLNormalizedData(
|
||||
(decimal)DoubleUtil.nan_to_zero(day_pnl.value),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.day.long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.day.short_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(sod_pnl.value),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.sod.long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.sod.short_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_short_pos));
|
||||
if (_normalized_data != nd) {
|
||||
_normalized_data = nd;
|
||||
updated?.Invoke(nd);
|
||||
}
|
||||
}
|
||||
return _normalized_data;
|
||||
}
|
||||
}
|
||||
// security
|
||||
public string account_name => data.account_name;
|
||||
public SecurityImmutable? security => data.security;
|
||||
public Security.ContractOneofCase security_type => security?.ContractCase ?? Security.ContractOneofCase.None;
|
||||
public Venue exchange => security?.Exchange ?? Venue.Unset;
|
||||
public string symbol => security.symbol_or_default();
|
||||
// cells
|
||||
public PNLCellVM<PNLRowData, string> account_name_cell { get; }
|
||||
public PNLCellVM<PNLRowData, SecurityImmutable?> security_cell { get; }
|
||||
public PNLCellVM<PNLRowData, Security.ContractOneofCase> security_type_cell { get; }
|
||||
public PNLCellVM<PNLRowData, Venue> exchange_cell { get; }
|
||||
public PNLCellVM<PNLRowData, string> symbol_cell { get; }
|
||||
// sod
|
||||
public PNLCellVM<PNLRowData, double> sod_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> sod_short_avg_price_delta { get; }
|
||||
// (market)
|
||||
public PNLCellVM<PNLRowData, double> bid_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> mid_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> ask_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> last_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> settlement_price { get; }
|
||||
// pnl price
|
||||
public PNLCellVM<PNLRowData, double> pnl_price { get; }
|
||||
// day
|
||||
public PNLCellVM<PNLRowData, double> day_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> day_short_avg_price_delta { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLRowData, double> working_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> working_short_avg_price_delta { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLRowData, double> total_pnl { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_net_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_net_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_long_avg_price_delta { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_pos { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_avg_price { get; }
|
||||
public PNLCellVM<PNLRowData, double> total_short_avg_price_delta { get; }
|
||||
|
||||
public PNLCellVM<PNLRowData, bool> has_day_trades { get; }
|
||||
public PNLCellVM<PNLRowData, double> cpp { get; }
|
||||
|
||||
public IList<mbts.ui.spark.CellAnimation> animations { get; private set; }
|
||||
public bool is_selected {
|
||||
get => _is_selected;
|
||||
set {
|
||||
if (Set(ref _is_selected, value))
|
||||
is_selected_changed?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(PNLRowData data) {
|
||||
_refresh_normalized_data = true;
|
||||
this.data = data;
|
||||
_cell_map.on_row(data);
|
||||
_animator.on_row(data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers) {
|
||||
_animator.on_settings(defaults, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
}
|
||||
|
||||
|
||||
public bool Equals(PNLRowVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
using SecurityType = bts.api.Security.ContractOneofCase;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLVM : DisposableViewModelBase, IHandleRowSelection {
|
||||
static readonly IReadOnlyList<Func<PNLVM, IDataGridColumnFilter<PNLRowData>>> _filter_getters =
|
||||
ObjectSnapshot.property_delegates_of_type<PNLVM, IDataGridColumnFilter<PNLRowData>>().ToReadOnlyList();
|
||||
static readonly TimeSpan _auxiliary_grid_update_throttle = TimeSpan.FromSeconds(0.2);
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
readonly IObservableCollectionAddRemoveOnly<PNLRowVM> _selected_rows = new ObservableSortedCollection<PNLRowVM>(PNLRowVM.key_comparer);
|
||||
readonly IList<PNLRowData> _rows_data_wrapper;
|
||||
readonly IList<PNLKey> _rows_key_wrapper;
|
||||
readonly DeferrableActionWrapper _refresh_filter;
|
||||
//
|
||||
readonly Property<PNLCalculationType> _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display = PNLHeaderRowDisplay.Total;
|
||||
PNLRowCollectionView _rows_view;
|
||||
Brush _selection_highlight;
|
||||
bool _is_paused;
|
||||
// default appearances
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance>
|
||||
_default_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
|
||||
_default_group_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty,
|
||||
_default_header_appearances = ImmutableDictionary<ColumnKeys, PNLCellAppearance>.Empty;
|
||||
// triggers
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLRowData>>
|
||||
_triggers = Array.Empty<IPNLAnimationTrigger<PNLRowData>>();
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>>
|
||||
_group_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>(),
|
||||
_header_triggers = Array.Empty<IPNLAnimationTrigger<PNLNormalizedData>>();
|
||||
// pause/resume
|
||||
(TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw)? _resume;
|
||||
|
||||
public PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings)
|
||||
: this(bus, orders_model, settings, orders_model.security_account_pnl) { }
|
||||
|
||||
PNLVM(IBus bus, OrdersModel orders_model, PNLSettings settings, IObservable<TableUpdate<PNLRowData, PNLKey>> pnl_source) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_rows_data_wrapper = rows.WrapSelector(r => r.data, d => new PNLRowVM(d, _default_appearances, _triggers));
|
||||
_rows_key_wrapper = rows.WrapSelector(r => r.key, null);
|
||||
_calculation_type = backing(nameof(calculation_type), PNLCalculationType.Waterfall);
|
||||
anchors.AddRange(settings.anchors);
|
||||
// commands
|
||||
set_anchor_cmd = new DelegateCommand<ColumnKeys>(key => anchors.SetItems(new[] { key }));
|
||||
append_anchor_cmd = new DelegateCommand<ColumnKeys>(key => {
|
||||
if (!anchors.Remove(key))
|
||||
anchors.Add(key);
|
||||
});
|
||||
// filters
|
||||
account_filter = DiscreteValuesFilterVM.Common.account<PNLRowData>(orders_model, r => new AccountTuple(r.key.account_id, r.account_name), nameof(PNLRowVM.account_name));
|
||||
_refresh_filter = new DeferrableActionWrapper(() => _filter_changed?.Invoke(get_filters_states().build_predicate()));
|
||||
var filter_src = Observable.Create<Func<PNLRowData, bool>>(o => {
|
||||
o.OnNext(get_filters_states().build_predicate());
|
||||
_filter_changed += o.OnNext;
|
||||
return Disposable.Create(() => _filter_changed -= o.OnNext);
|
||||
});
|
||||
foreach (var f in _all_filters)
|
||||
f.filter_changed += _refresh_filter.execute;
|
||||
// totals/selected headers
|
||||
total_aggregate = new PNLHeaderRowVM(_default_header_appearances, _header_triggers);
|
||||
selected_aggregate = new PNLHeaderRowVM(_default_header_appearances, _header_triggers);
|
||||
// load/subscribe to data
|
||||
load_from(settings);
|
||||
_rows_view = new PNLRowCollectionView(rows, _default_group_appearances, _group_triggers);
|
||||
_dispose = Disposable.Create(
|
||||
pnl_source.Operate(
|
||||
_calculation_type.observe_current(),
|
||||
(data, calc) => data.with(calc_type: calc)
|
||||
).FilterWithRaw(filter_src, PNLRowData.key_fn, null)
|
||||
.OnDispatcher()
|
||||
.Subscribe(_update),
|
||||
Disposable.Create(() => {
|
||||
foreach (var f in _all_filters)
|
||||
f.filter_changed -= _refresh_filter.execute;
|
||||
rows.DisposeAll();
|
||||
_rows_view.Groups.OfType<IDisposable>().DisposeAll();
|
||||
}));
|
||||
}
|
||||
|
||||
event Action<SelectionChangedEventArgs> _selection_changed;
|
||||
event Action<TableUpdate<PNLRowData, PNLKey>> _updated;
|
||||
event Action<Func<PNLRowData, bool>> _filter_changed;
|
||||
|
||||
// rows/selected rows
|
||||
public ObservableCollection<PNLRowVM> rows { get; } = new ObservableCollection<PNLRowVM>();
|
||||
public ICollectionView rows_view => _rows_view;
|
||||
public int filtered_row_count => rows.Count;
|
||||
public int total_row_count { get; private set; }
|
||||
public IReadOnlyObservableCollection<PNLRowVM> selected_rows => _selected_rows;
|
||||
// display/grouping
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public ObservableCollection<ColumnKeys> anchors { get; } = new ObservableCollection<ColumnKeys>();
|
||||
// calc
|
||||
public PNLCalculationType calculation_type { get => _calculation_type.Value; set => _calculation_type.set(value); }
|
||||
public PNLHeaderRowVM total_aggregate { get; }
|
||||
public PNLHeaderRowVM selected_aggregate { get; }
|
||||
public bool is_paused {
|
||||
get => _is_paused;
|
||||
set {
|
||||
if (Set(ref _is_paused, value) && !value && _resume.HasValue) {
|
||||
var curr_state = ImmutableDictionary<PNLKey, PNLRowData>.Empty.AddRange(rows.Select(r => new KeyValuePair<PNLKey, PNLRowData>(r.key, r.data)));
|
||||
var reset = TableUpdate<PNLRowData, PNLKey>.update(
|
||||
new TableUpdate<PNLRowData, PNLKey>(
|
||||
curr_state,
|
||||
curr_state.Values,
|
||||
Enumerable.Empty<Change<PNLRowData>>(),
|
||||
Enumerable.Empty<PNLRowData>(),
|
||||
PNLRowData.key_fn),
|
||||
_resume.Value.filtered.state,
|
||||
EqualityComparer<PNLRowData>.Default);
|
||||
_update((reset, _resume.Value.raw));
|
||||
_resume = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Brush selection_highlight { get => _selection_highlight; set => Set(ref _selection_highlight, value); }
|
||||
|
||||
// commands
|
||||
public ICommand set_anchor_cmd { get; }
|
||||
public ICommand append_anchor_cmd { get; }
|
||||
public ICommand show_working_orders_cmd { get; }
|
||||
|
||||
public void set_all_expanded(bool is_expanded) =>
|
||||
_rows_view.set_all_expanded(is_expanded);
|
||||
|
||||
#region Filters
|
||||
/// security
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, uint[]> account_filter { get; }
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, Venue[]> exchange_filter { get; } = DiscreteValuesFilterVM.Common.exchange<PNLRowData>(r => r.security.exchange_or_default(), nameof(PNLRowVM.exchange));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, string[]> symbol_filter { get; } = DiscreteValuesFilterVM.Common.symbol<PNLRowData>(r => r.security.symbol_or_default(), nameof(PNLRowVM.symbol));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, string> security_filter { get; } = new RegexFilterVM<PNLRowData>(r => r.security?.ToString(), nameof(PNLRowData.security));
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, SecurityType[]> security_type_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, SecurityType>(
|
||||
EnumHelper.get_values_except(SecurityType.Option, SecurityType.Synthetic).ToArray(),
|
||||
r => r.security?.ContractCase ?? SecurityType.None,
|
||||
nameof(PNLRowData.security));
|
||||
/// sod
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_avg);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_long_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_avg);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_short_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_net_avg_price_delta);
|
||||
/// day
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price_delta_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_net_avg_price_delta_raw);
|
||||
/// working (long)
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_long_avg_price_delta_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_avg_price_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_short_avg_price_delta_raw);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_avg_price_delta_raw);
|
||||
/// total
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_pnl);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price_delta);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_pos);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_avg_price_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_avg_price_delta);
|
||||
/// market
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.bid_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> ask_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.ask_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> mid_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.mid_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> last_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.last_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> settlement_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.settlement_price);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> pnl_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.pnl_price);
|
||||
/// misc
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, bool[]> has_day_trades_filter { get; } = DiscreteValuesFilterVM.Create<PNLRowData, bool>(new[] { true, false }, r => r.has_day_trades);
|
||||
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> cpp_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.cpp);
|
||||
|
||||
IEnumerable<IDataGridColumnFilter<PNLRowData>> _all_filters =>
|
||||
_filter_getters.Select(get => get(this));
|
||||
|
||||
public FilterStates get_filters_states() => new FilterStates {
|
||||
account = account_filter.get_state(),
|
||||
exchange = exchange_filter.get_state(),
|
||||
symbol = symbol_filter.get_state(),
|
||||
security = security_filter.get_state(),
|
||||
security_type = security_type_filter.get_state(),
|
||||
/// sod
|
||||
sod_pnl = sod_pnl_filter.get_state(),
|
||||
sod_long_pos = sod_long_pos_filter.get_state(),
|
||||
sod_long_avg_price = sod_long_avg_price_filter.get_state(),
|
||||
sod_long_avg_price_delta = sod_long_avg_price_delta_filter.get_state(),
|
||||
sod_short_pos = sod_short_pos_filter.get_state(),
|
||||
sod_short_avg_price = sod_short_avg_price_filter.get_state(),
|
||||
sod_short_avg_price_delta = sod_short_avg_price_delta_filter.get_state(),
|
||||
sod_net_pos = sod_net_pos_filter.get_state(),
|
||||
sod_net_avg_price_delta = sod_net_avg_price_delta_filter.get_state(),
|
||||
/// day
|
||||
day_pnl = day_pnl_filter.get_state(),
|
||||
day_long_pos = day_long_pos_filter.get_state(),
|
||||
day_long_avg_price = day_long_avg_price_filter.get_state(),
|
||||
day_long_avg_price_delta = day_long_avg_price_delta_filter.get_state(),
|
||||
day_short_pos = day_short_pos_filter.get_state(),
|
||||
day_short_avg_price = day_short_avg_price_filter.get_state(),
|
||||
day_short_avg_price_delta = day_short_avg_price_delta_filter.get_state(),
|
||||
day_net_pos = day_net_pos_filter.get_state(),
|
||||
day_net_avg_price_delta = day_net_avg_price_delta_filter.get_state(),
|
||||
/// total
|
||||
total_pnl = total_pnl_filter.get_state(),
|
||||
total_long_pos = total_long_pos_filter.get_state(),
|
||||
total_long_avg_price = total_long_avg_price_filter.get_state(),
|
||||
total_long_avg_price_delta = total_long_avg_price_delta_filter.get_state(),
|
||||
total_short_pos = total_short_pos_filter.get_state(),
|
||||
total_short_avg_price = total_short_avg_price_filter.get_state(),
|
||||
total_short_avg_price_delta = total_short_avg_price_delta_filter.get_state(),
|
||||
total_net_pos = total_net_pos_filter.get_state(),
|
||||
total_net_avg_price_delta = total_net_avg_price_delta_filter.get_state(),
|
||||
/// working
|
||||
working_long_pos = working_long_pos_filter.get_state(),
|
||||
working_long_avg_price = working_long_avg_price_filter.get_state(),
|
||||
working_long_avg_price_delta = working_long_avg_price_delta_filter.get_state(),
|
||||
working_short_pos = working_short_pos_filter.get_state(),
|
||||
working_short_avg_price = working_short_avg_price_filter.get_state(),
|
||||
working_short_avg_price_delta = working_short_avg_price_delta_filter.get_state(),
|
||||
working_net_pos = working_net_pos_filter.get_state(),
|
||||
working_net_avg_price_delta = working_net_avg_price_delta_filter.get_state(),
|
||||
/// market
|
||||
bid_price = bid_price_filter.get_state(),
|
||||
ask_price = ask_price_filter.get_state(),
|
||||
mid_price = mid_price_filter.get_state(),
|
||||
last_price = last_price_filter.get_state(),
|
||||
settlement_price = settlement_price_filter.get_state(),
|
||||
pnl_price = pnl_price_filter.get_state(),
|
||||
/// misc
|
||||
has_day_trades = has_day_trades_filter.get_state()
|
||||
};
|
||||
#endregion
|
||||
|
||||
public void load_from(PNLSettings settings) {
|
||||
selection_highlight = settings.grid.selection_highlight.to_brush();
|
||||
calculation_type = settings.calculation_type;
|
||||
if (settings.filters != null) {
|
||||
using (_refresh_filter.defer_execution()) {
|
||||
var f = settings.filters;
|
||||
/// security
|
||||
account_filter.load_state(f.account);
|
||||
exchange_filter.load_state(f.exchange);
|
||||
symbol_filter.load_state(f.symbol);
|
||||
security_filter.load_state(f.security);
|
||||
security_type_filter.load_state(f.security_type);
|
||||
/// sod
|
||||
sod_pnl_filter.load_state(f.sod_pnl);
|
||||
sod_long_pos_filter.load_state(f.sod_long_pos);
|
||||
sod_long_avg_price_filter.load_state(f.sod_long_avg_price);
|
||||
sod_long_avg_price_delta_filter.load_state(f.sod_long_avg_price_delta);
|
||||
sod_short_pos_filter.load_state(f.sod_short_pos);
|
||||
sod_short_avg_price_filter.load_state(f.sod_short_avg_price);
|
||||
sod_short_avg_price_delta_filter.load_state(f.sod_short_avg_price_delta);
|
||||
sod_net_pos_filter.load_state(f.sod_net_pos);
|
||||
sod_net_avg_price_delta_filter.load_state(f.sod_net_avg_price_delta);
|
||||
/// day
|
||||
day_pnl_filter.load_state(f.day_pnl);
|
||||
day_long_pos_filter.load_state(f.day_long_pos);
|
||||
day_long_avg_price_filter.load_state(f.day_long_avg_price);
|
||||
day_long_avg_price_delta_filter.load_state(f.day_long_avg_price_delta);
|
||||
day_short_pos_filter.load_state(f.day_short_pos);
|
||||
day_short_avg_price_filter.load_state(f.day_short_avg_price);
|
||||
day_short_avg_price_delta_filter.load_state(f.day_short_avg_price_delta);
|
||||
day_net_pos_filter.load_state(f.day_net_pos);
|
||||
day_net_avg_price_delta_filter.load_state(f.day_net_avg_price_delta);
|
||||
/// total
|
||||
total_pnl_filter.load_state(f.total_pnl);
|
||||
total_long_pos_filter.load_state(f.total_long_pos);
|
||||
total_long_avg_price_filter.load_state(f.total_long_avg_price);
|
||||
total_long_avg_price_delta_filter.load_state(f.total_long_avg_price_delta);
|
||||
total_short_pos_filter.load_state(f.total_short_pos);
|
||||
total_short_avg_price_filter.load_state(f.total_short_avg_price);
|
||||
total_short_avg_price_delta_filter.load_state(f.total_short_avg_price_delta);
|
||||
total_net_pos_filter.load_state(f.total_net_pos);
|
||||
/// working
|
||||
working_long_pos_filter.load_state(f.working_long_pos);
|
||||
working_long_avg_price_filter.load_state(f.working_long_avg_price);
|
||||
working_long_avg_price_delta_filter.load_state(f.working_long_avg_price_delta);
|
||||
working_short_pos_filter.load_state(f.working_short_pos);
|
||||
working_short_avg_price_filter.load_state(f.working_short_avg_price);
|
||||
working_short_avg_price_delta_filter.load_state(f.working_short_avg_price_delta);
|
||||
working_net_pos_filter.load_state(f.working_net_pos);
|
||||
working_net_avg_price_delta_filter.load_state(f.working_net_avg_price_delta);
|
||||
/// market
|
||||
bid_price_filter.load_state(f.bid_price);
|
||||
mid_price_filter.load_state(f.mid_price);
|
||||
ask_price_filter.load_state(f.ask_price);
|
||||
last_price_filter.load_state(f.last_price);
|
||||
settlement_price_filter.load_state(f.settlement_price);
|
||||
pnl_price_filter.load_state(f.pnl_price);
|
||||
/// misc
|
||||
has_day_trades_filter.load_state(f.has_day_trades);
|
||||
cpp_filter.load_state(f.cpp);
|
||||
}
|
||||
}
|
||||
anchors.sync_with(settings.anchors);
|
||||
header_row_display = settings.header_row_display;
|
||||
//
|
||||
map_appearances(ref _default_appearances, settings.grid.columns, settings.grid_defaults);
|
||||
map_appearances(ref _default_group_appearances, settings.group_columns, settings.group_cell_appearance.to_appearance().merge(settings.grid_defaults));
|
||||
map_appearances(ref _default_header_appearances, settings.header_columns, settings.header_cell_appearance.to_appearance().merge(settings.grid_defaults));
|
||||
_triggers = settings.triggers();
|
||||
_group_triggers = settings.group_triggers();
|
||||
_header_triggers = settings.header_triggers();
|
||||
foreach (var r in rows)
|
||||
r.on_settings(_default_appearances, _triggers);
|
||||
total_aggregate.on_settings(_default_header_appearances, _header_triggers);
|
||||
selected_aggregate.on_settings(_default_header_appearances, _header_triggers);
|
||||
void map_appearances(ref ImmutableDictionary<ColumnKeys, PNLCellAppearance> from, IEnumerable<GridColumnSettings> col_settings, PNLCellAppearance fallback_appearance) {
|
||||
var dabd = from.ToBuilder();
|
||||
foreach (var k in EnumHelper.get_values<ColumnKeys>()) {
|
||||
if (GridSettingsHelper.try_find_column(col_settings, k, out var match))
|
||||
dabd[k] = new PNLCellAppearance(match.font_color_override, match.background_override, match.font_weight_override).merge(fallback_appearance);
|
||||
}
|
||||
from = dabd.ToImmutable();
|
||||
}
|
||||
if (_rows_view == null)
|
||||
_rows_view = new PNLRowCollectionView(rows, _default_group_appearances, _group_triggers);
|
||||
else _rows_view.on_settings(_default_group_appearances, _group_triggers);
|
||||
}
|
||||
|
||||
#region FillsVM Factory
|
||||
public FillsVM mk_fills_vm(PNLRowVM row, ColumnKeys key) {
|
||||
var state = PNLSettings.DefaultAuxiliaryFills;
|
||||
_append_key_filters(state.filters, key);
|
||||
_append_row_filters(state.filters, row);
|
||||
state.grid = _hide_cols(state.grid, anchors);
|
||||
var src = _orders_model.on_fill_tuples.Filter(ft => ft.fill.SecurityKey == row.key.security_id && ft.account_id == row.key.account_id);
|
||||
return new FillsVM(_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
|
||||
}
|
||||
|
||||
public FillsVM mk_fills_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
var state = PNLSettings.DefaultAuxiliaryFills;
|
||||
_append_key_filters(state.filters, key);
|
||||
_append_group_filters(state.filters, group, anchors);
|
||||
// hide all columns with implicit data (i.e., the anchors)
|
||||
state.grid = _hide_cols(state.grid, anchors.WrapSubRange(0, group.level));
|
||||
return new FillsVM(_bus, _orders_model, Observable.Return(state), new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
|
||||
}
|
||||
|
||||
public FillsVM mk_fills_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
|
||||
var state = fills.FillsSettings.Default();
|
||||
state.grid = orderbook.OrderBookStartupData.DefaultFillsGrid.general;
|
||||
state.filters = new fills.FillsFilterStates();
|
||||
state.grid = _hide_cols(state.grid, anchors);
|
||||
_append_key_filters(state.filters, key);
|
||||
var src = _orders_model.on_fill_tuples;
|
||||
switch (header) {
|
||||
case PNLHeaderRowDisplay.SelectedTotal: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<FillTuple, bool>>(
|
||||
cb => _selection_changed += cb,
|
||||
cb => _selection_changed -= cb,
|
||||
_ => predicate_factory(_selected_rows.Select(r => r.key)),
|
||||
_auxiliary_grid_update_throttle)
|
||||
.StartWith(() => predicate_factory(_selected_rows.Select(r => r.key)));
|
||||
src = src.Filter(filter_src, FillTuple.key_fn);
|
||||
}
|
||||
break;
|
||||
case PNLHeaderRowDisplay.Total: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<TableUpdate<PNLRowData, PNLKey>, Func<FillTuple, bool>>(
|
||||
cb => _updated += cb,
|
||||
cb => _updated -= cb,
|
||||
t => predicate_factory(t.state.Keys),
|
||||
_auxiliary_grid_update_throttle)
|
||||
.StartWith(() => predicate_factory(rows.Select(r => r.key)));
|
||||
src = src.Filter(filter_src, FillTuple.key_fn);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new FillsVM(_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<mbts.ui.settings.GridSettings>.DisconnectedImpl(state.grid));
|
||||
//
|
||||
Func<FillTuple, bool> predicate_factory(IEnumerable<PNLKey> keys) {
|
||||
var key_set = new HashSet<PNLKey>(keys);
|
||||
return new Func<FillTuple, bool>(ft => key_set.Contains(new PNLKey(ft.account_id, ft.fill.SecurityKey)));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OrderBookVM Factory
|
||||
public OrderBookVM mk_orderbook_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
|
||||
var state = new orderbook.OrderBookStartupData(
|
||||
new orderbook.FilterStates(),
|
||||
orderbook.OrderBookStartupData.default_mini_grid,
|
||||
use_defaults: false);
|
||||
_append_key_filters(state.filters, key);
|
||||
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, anchors);
|
||||
var src = _orders_model.on_order_tuples_with_root_and_piq;
|
||||
switch (header) {
|
||||
case PNLHeaderRowDisplay.SelectedTotal: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<SelectionChangedEventArgs, Func<OrderSecurityRootPair, bool>>(
|
||||
cb => _selection_changed += cb,
|
||||
cb => _selection_changed -= cb,
|
||||
_ => predicate_factory(_selected_rows.Select(r => r.key)),
|
||||
_auxiliary_grid_update_throttle);
|
||||
src = src.Filter(filter_src, OrderSecurityHelper.root_pair_key_fn);
|
||||
}
|
||||
break;
|
||||
case PNLHeaderRowDisplay.Total: {
|
||||
var filter_src = System.Reactive.Linq.ObservableExtensions.FromEventThrottle<TableUpdate<PNLRowData, PNLKey>, Func<OrderSecurityRootPair, bool>>(
|
||||
cb => _updated += cb,
|
||||
cb => _updated -= cb,
|
||||
t => predicate_factory(t.state.Keys),
|
||||
_auxiliary_grid_update_throttle)
|
||||
.StartWith(() => predicate_factory(rows.Select(r => r.key)));
|
||||
src = src.Filter(filter_src, OrderSecurityHelper.root_pair_key_fn);
|
||||
}
|
||||
break;
|
||||
}
|
||||
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
|
||||
var obvm = new OrderBookVM(_bus, null, _orders_model, src, settings_man, true);
|
||||
obvm.columns.InsertRange(0, OrderBookVM.OrderBookColumnCollection.editable_columns);
|
||||
return obvm;
|
||||
Func<OrderSecurityRootPair, bool> predicate_factory(IEnumerable<PNLKey> keys) {
|
||||
var key_set = new HashSet<PNLKey>(keys);
|
||||
return new Func<OrderSecurityRootPair, bool>(o => key_set.Contains(new PNLKey(o.order.order.account_id(), o.order.order.security_key())));
|
||||
}
|
||||
}
|
||||
|
||||
public OrderBookVM mk_orderbook_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
var filters = new orderbook.FilterStates();
|
||||
_append_group_filters(filters, group, anchors);
|
||||
return _mk_orderbook_vm_common(filters, anchors.WrapSubRange(0, group.level));
|
||||
}
|
||||
|
||||
public OrderBookVM mk_orderbook_vm(PNLRowVM row, ColumnKeys key) {
|
||||
var filters = new orderbook.FilterStates();
|
||||
_append_key_filters(filters, key);
|
||||
_append_row_filters(filters, row);
|
||||
return _mk_orderbook_vm_common(filters, anchors.Concat(new[] { ColumnKeys.Account, ColumnKeys.Symbol, ColumnKeys.Security }).Distinct());
|
||||
}
|
||||
|
||||
OrderBookVM _mk_orderbook_vm_common(orderbook.FilterStates filters, IEnumerable<ColumnKeys> hide_counterparts) {
|
||||
var state = new orderbook.OrderBookStartupData(filters, orderbook.OrderBookStartupData.default_mini_grid, use_defaults: false);
|
||||
state.orderbook_grid.general = _hide_cols(state.orderbook_grid.general, hide_counterparts);
|
||||
var settings_man = new WindowSettingsManager<orderbook.OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
|
||||
var obvm = new OrderBookVM(_bus, null, _orders_model, settings_man, true);
|
||||
foreach (var c in OrderBookVM.OrderBookColumnCollection.editable_columns)
|
||||
obvm.columns.Insert(0, c);
|
||||
obvm.load_from(state);
|
||||
return obvm;
|
||||
}
|
||||
#endregion
|
||||
|
||||
static GridSettings _hide_cols(GridSettings grid, IEnumerable<ColumnKeys> columns) =>
|
||||
grid.with(_hide_cols(grid.columns, columns));
|
||||
|
||||
static IEnumerable<GridColumnSettings> _hide_cols(IEnumerable<GridColumnSettings> cols, IEnumerable<ColumnKeys> columns) {
|
||||
// hide columns who's data is implied by the grouping
|
||||
var col_hash = new HashSet<string>(columns.Select(k => k.ToString()));
|
||||
foreach (var c in cols)
|
||||
yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
|
||||
}
|
||||
|
||||
#region Fills Popup Utils
|
||||
static void _append_key_filters(fills.FillsFilterStates f, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.SODNetPos:
|
||||
case ColumnKeys.SODLongPos:
|
||||
case ColumnKeys.SODShortPos:
|
||||
f.fill_kind = new[] { Fill.Types.FillKind.Sod };
|
||||
break;
|
||||
case ColumnKeys.DayNetPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
f.fill_kind = new[] { Fill.Types.FillKind.Standard, Fill.Types.FillKind.Leg };
|
||||
break;
|
||||
}
|
||||
switch (key) {
|
||||
case ColumnKeys.SODLongPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.TotalLongPos:
|
||||
f.side = new[] { Side.Buy };
|
||||
break;
|
||||
case ColumnKeys.SODShortPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
case ColumnKeys.TotalShortPos:
|
||||
f.side = new[] { Side.Sell };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_group_filters(fills.FillsFilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
|
||||
if (group != null) {
|
||||
int anchor_pos = group.level - 1;
|
||||
if (anchor_pos < 0)
|
||||
throw new InvalidOperationException("Group level is less than 1.");
|
||||
if (anchor_pos >= anchors.Count)
|
||||
throw new InvalidOperationException("Group level is greater than the number of anchors.");
|
||||
_append_fills_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
while (anchor_pos-- > 0 && group != null) {
|
||||
_append_fills_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_fills_filter(fills.FillsFilterStates filter, PNLRowCollectionViewGroup group, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.Account: filter.account = new[] { group.Name.ToString() }; break;
|
||||
case ColumnKeys.Symbol: filter.symbol = new[] { group.Name.ToString() }; break;
|
||||
case ColumnKeys.Exchange: filter.exchange = new[] { (Venue)group.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (group.Name is SecurityImmutable sec)
|
||||
filter.security = new SecurityCriteria(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_row_filters(fills.FillsFilterStates f, PNLRowVM row) {
|
||||
f.account = new[] { row.account_name };
|
||||
f.security = SecurityCriteria.from_keys(new[] { row.key.security_id });
|
||||
}
|
||||
|
||||
static void _append_key_filters(orderbook.FilterStates f, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.WorkingLongPos:
|
||||
case ColumnKeys.DayLongPos:
|
||||
case ColumnKeys.TotalLongPos:
|
||||
f.sides = new[] { Side.Buy };
|
||||
break;
|
||||
case ColumnKeys.WorkingShortPos:
|
||||
case ColumnKeys.DayShortPos:
|
||||
case ColumnKeys.TotalShortPos:
|
||||
f.sides = new[] { Side.Sell };
|
||||
break;
|
||||
}
|
||||
switch (key) {
|
||||
case ColumnKeys.WorkingLongPos:
|
||||
case ColumnKeys.WorkingShortPos:
|
||||
case ColumnKeys.WorkingNetPos:
|
||||
f.statuses = new[] { OrderStatus.PendingWorking, OrderStatus.Working };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_group_filters(orderbook.FilterStates f, PNLRowCollectionViewGroup group, IReadOnlyList<ColumnKeys> anchors) {
|
||||
if (group != null) {
|
||||
int anchor_pos = group.level - 1;
|
||||
if (anchor_pos < 0)
|
||||
throw new InvalidOperationException("Group level is less than 1.");
|
||||
if (anchor_pos >= anchors.Count)
|
||||
throw new InvalidOperationException("Group level is greater than the number of anchors.");
|
||||
_append_orderbook_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
while (anchor_pos-- > 0 && group != null) {
|
||||
_append_orderbook_filter(f, group, anchors[anchor_pos]);
|
||||
group = group.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _append_row_filters(orderbook.FilterStates f, PNLRowVM row) {
|
||||
f.accounts = new[] { row.account_name };
|
||||
f.security_criteria = SecurityCriteria.from_keys(new[] { row.key.security_id });
|
||||
}
|
||||
|
||||
static void _append_orderbook_filter(orderbook.FilterStates filter, PNLRowCollectionViewGroup g, ColumnKeys key) {
|
||||
switch (key) {
|
||||
case ColumnKeys.Account: filter.accounts = new[] { g.Name.ToString() }; break;
|
||||
case ColumnKeys.Symbol: filter.symbols = new[] { g.Name.ToString() }; break;
|
||||
case ColumnKeys.Exchange: filter.exchanges = new[] { (Venue)g.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (g.Name is SecurityImmutable sec)
|
||||
filter.security_criteria = new SecurityCriteria(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _update((TableUpdate<PNLRowData, PNLKey> filtered, TableUpdate<PNLRowData, PNLKey> raw) tup) {
|
||||
if (_is_paused) {
|
||||
_resume = tup;
|
||||
return;
|
||||
}
|
||||
var (filtered, raw) = tup;
|
||||
int old_total_row_count = total_row_count;
|
||||
int old_filtered_row_count = filtered_row_count;
|
||||
var total_pnl = total_aggregate.data;
|
||||
// don't adjust selected_pnl incrementally - it may be possible for the selection to change throughout the update, making incremental calculations tricky
|
||||
foreach (var v in filtered.removed) {
|
||||
int i = CollectionHelper.binary_search(_rows_data_wrapper, v, PNLRowData.key_comparer);
|
||||
Log.assert(i >= 0, $"{typeof(PNLVM)} received an update with a removed item that was not found via binary search.");
|
||||
if (i >= 0) {
|
||||
if (rows[i].security_type == SecurityType.Future)
|
||||
total_pnl -= rows[i].normalized_data;
|
||||
rows.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
foreach (var v in filtered.added) {
|
||||
var r = new PNLRowVM(v.with(calc_type: calculation_type), _default_appearances, _triggers);
|
||||
rows.InsertSorted(r, PNLRowVM.key_comparer);
|
||||
if (r.security_type == SecurityType.Future)
|
||||
total_pnl += r.normalized_data;
|
||||
}
|
||||
foreach (var d in filtered.changed) {
|
||||
int i = CollectionHelper.binary_search(_rows_data_wrapper, d, PNLRowData.key_comparer);
|
||||
if (i >= 0) {
|
||||
var r = rows[i];
|
||||
if (r.security_type == SecurityType.Future)
|
||||
total_pnl -= r.normalized_data;
|
||||
r.update(d);
|
||||
if (r.security_type == SecurityType.Future)
|
||||
total_pnl += r.normalized_data;
|
||||
}
|
||||
}
|
||||
if (_selected_rows.Count > 0) {
|
||||
selected_aggregate.data = _selected_rows
|
||||
.Where(r => r.security_type == SecurityType.Future)
|
||||
.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
|
||||
}
|
||||
//
|
||||
total_aggregate.data = total_pnl;
|
||||
total_row_count = tup.raw.state.Count;
|
||||
if (total_row_count != old_total_row_count)
|
||||
RaisePropertyChanged(nameof(total_row_count));
|
||||
if (filtered_row_count != old_filtered_row_count)
|
||||
RaisePropertyChanged(nameof(filtered_row_count));
|
||||
_updated?.Invoke(filtered);
|
||||
}
|
||||
|
||||
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
|
||||
RowSelectionHelper.apply_changes(_selected_rows, e);
|
||||
// adding/subtracting pnl incrementally can lose precision over time, due to rounding errors.
|
||||
// do the full calculation on each selection change, as long as it doesn't hurt perf (which seems unlikely).
|
||||
selected_aggregate.data = _selected_rows.Aggregate(PNLNormalizedData.zero, (d, r) => d + r.normalized_data);
|
||||
_selection_changed?.Invoke(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using bts.collections;
|
||||
using mbts.ui.utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class AnchorIndex {
|
||||
readonly IDictionary<PNLView.Columns, bool> _wrapped_anchors;
|
||||
|
||||
public AnchorIndex() {
|
||||
IDictionary<string, object> anchors_dict = anchors = new ExpandoObject();
|
||||
_wrapped_anchors = MappedDictionary.create_mutable(
|
||||
anchors_dict,
|
||||
EnumHelper.parse<PNLView.Columns>,
|
||||
key => key.ToString(),
|
||||
obj => (bool)obj,
|
||||
_ => _);
|
||||
// initialize the dictionary with every key (the keys should never change)
|
||||
foreach (var k in _keys)
|
||||
_wrapped_anchors[k] = false;
|
||||
}
|
||||
|
||||
public dynamic anchors { get; }
|
||||
|
||||
IReadOnlyList<PNLView.Columns> _keys => EnumHelper.get_values<PNLView.Columns>();
|
||||
|
||||
public void reset(IEnumerable<PNLView.Columns> anchor_list) {
|
||||
var anchor_set = new HashSet<PNLView.Columns>(anchor_list.empty_if_null());
|
||||
foreach (var key in _keys)
|
||||
_wrapped_anchors[key] = anchor_set.Contains(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace bts.shell.pnl {
|
||||
readonly struct PNLCell {
|
||||
public readonly object item;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLCell(object item, PNLView.Columns column) {
|
||||
this.item = item;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"Item: {item}, Column: {column}";
|
||||
}
|
||||
|
||||
readonly struct PNLRowCell {
|
||||
public readonly PNLRowVM row;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLRowCell(PNLRowVM row, PNLView.Columns column) {
|
||||
this.row = row;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLGroupCell {
|
||||
public readonly PNLRowCollectionViewGroup group;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLGroupCell(PNLRowCollectionViewGroup group, PNLView.Columns column) {
|
||||
this.group = group;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct PNLHeaderRowCell {
|
||||
public readonly PNLHeaderRowDisplay header_row;
|
||||
public readonly PNLView.Columns column;
|
||||
|
||||
public PNLHeaderRowCell(PNLHeaderRowDisplay header_row, PNLView.Columns column) {
|
||||
this.header_row = header_row;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
static class PNLCellExtensions {
|
||||
public static OneOf<PNLRowCell, PNLGroupCell, PNLHeaderRowCell> oneof(this PNLCell cell) =>
|
||||
OneOf<PNLRowVM, PNLRowCollectionViewGroup, PNLHeaderRowDisplay>.infer(cell.item).Extend(
|
||||
r => new PNLRowCell(r, cell.column),
|
||||
g => new PNLGroupCell(g, cell.column),
|
||||
h => new PNLHeaderRowCell(h, cell.column));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using bts.utils;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility.text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLColumn : DIYGrid.ColumnBase<PNLView.Columns>, IRaiseNotifyPropertyChanged {
|
||||
static readonly IPropertyChangedDetector<PNLColumn>
|
||||
_group_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
|
||||
nameof(group_font_weight_override),
|
||||
nameof(group_font_weight),
|
||||
nameof(group_font_color_override),
|
||||
nameof(group_font_color),
|
||||
nameof(group_font_family),
|
||||
nameof(group_font_size)),
|
||||
_header_settings_change_detector = ObjectSnapshot.build_detector<PNLColumn>(
|
||||
nameof(header_font_weight_override),
|
||||
nameof(header_font_weight),
|
||||
nameof(header_font_color_override),
|
||||
nameof(header_font_color),
|
||||
nameof(header_font_family),
|
||||
nameof(header_font_size));
|
||||
|
||||
double _group_font_size = 11, _header_font_size = 12;
|
||||
int? _anchor_index;
|
||||
FontFamily
|
||||
_group_font_family = TextUtils.StandardFontFamily,
|
||||
_header_font_family = TextUtils.StandardFontFamily;
|
||||
|
||||
public PNLColumn() {
|
||||
// default to be hidden, so columns are not rendered until their settings are loaded,
|
||||
// in which case, they will only be rendered if 'settings.is_visible' is true.
|
||||
is_visible = false;
|
||||
}
|
||||
|
||||
public int? anchor_index {
|
||||
get => _anchor_index;
|
||||
set => set(ref _anchor_index, value);
|
||||
}
|
||||
// group
|
||||
public FontWeight? group_font_weight_override { get; private set; }
|
||||
public Color? group_font_color_override { get; private set; }
|
||||
public FontFamily group_font_family => _group_font_family ?? font_family;
|
||||
public double group_font_size => double.IsNaN(_group_font_size) || _group_font_size == 0 ? font_size : _group_font_size;
|
||||
public FontWeight group_font_weight => group_font_weight_override ?? font_weight;
|
||||
public Color group_font_color => group_font_color_override ?? font_color;
|
||||
|
||||
public FontWeight? header_font_weight_override { get; private set; }
|
||||
public Color? header_font_color_override { get; private set; }
|
||||
public FontFamily header_font_family => _header_font_family ?? font_family;
|
||||
public double header_font_size => double.IsNaN(_header_font_size) || _header_font_size == 0 ? font_size : _header_font_size;
|
||||
public FontWeight header_font_weight => header_font_weight_override ?? font_weight;
|
||||
public Color header_font_color => header_font_color_override ?? font_color;
|
||||
|
||||
public void load_group_params(
|
||||
double group_font_size = double.NaN, FontWeight? group_font_weight = null, Color? group_font_color = null, FontFamily group_font_family = null) {
|
||||
using (_group_settings_change_detector.detect_changes(this)) {
|
||||
_group_font_size = group_font_size;
|
||||
_group_font_family = group_font_family;
|
||||
group_font_weight_override = group_font_weight;
|
||||
group_font_color_override = group_font_color;
|
||||
}
|
||||
}
|
||||
|
||||
public void load_header_params(
|
||||
double header_font_size = double.NaN, FontWeight? header_font_weight = null, Color? header_font_color = null, FontFamily header_font_family = null) {
|
||||
using (_header_settings_change_detector.detect_changes(this)) {
|
||||
_header_font_size = header_font_size;
|
||||
_header_font_family = header_font_family;
|
||||
header_font_weight_override = header_font_weight;
|
||||
header_font_color_override = header_font_color;
|
||||
}
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
raise_prop_changed(prop);
|
||||
|
||||
protected override void on_key_initialized(PNLView.Columns key) =>
|
||||
id = key.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Data;
|
||||
using CellAnimation = mbts.ui.spark.CellAnimation;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLRowCollectionView : ListCollectionView {
|
||||
ReadOnlyObservableCollection<object> _base_groups, _groups_wrapper;
|
||||
ObservableAutoWrapper<object, object> _groups;
|
||||
// appearance/triggers
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
|
||||
public PNLRowCollectionView(IList list, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(list) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
}
|
||||
|
||||
public override bool CanFilter => true;
|
||||
public override bool CanGroup => true;
|
||||
|
||||
public override ReadOnlyObservableCollection<object> Groups {
|
||||
get {
|
||||
if (_groups_wrapper == null) {
|
||||
_base_groups = base.Groups;
|
||||
if (_base_groups == null)
|
||||
return null;
|
||||
_groups = new ObservableAutoWrapper<object, object>(_base_groups, obj => new PNLRowCollectionViewGroup((CollectionViewGroup)obj, this, _defaults_appearances, _triggers));
|
||||
_groups_wrapper = new ReadOnlyObservableCollection<object>(_groups);
|
||||
}
|
||||
return _groups_wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
foreach (var g in _groups_wrapper.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.on_settings(default_appearances, triggers);
|
||||
}
|
||||
|
||||
public void set_all_expanded(bool is_expanded) {
|
||||
foreach (var g in Groups.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.set_all_expanded(is_expanded);
|
||||
}
|
||||
}
|
||||
|
||||
class PNLRowCollectionViewGroup : CollectionViewGroup, IRaiseNotifyPropertyChanged, IEquatable<PNLRowCollectionViewGroup> {
|
||||
readonly PNLRowCollectionView _root;
|
||||
readonly CollectionViewGroup _from;
|
||||
readonly Dictionary<object, InnerGroupSubscription> _inner_groups = new Dictionary<object, InnerGroupSubscription>();
|
||||
readonly Dictionary<PNLKey, IDisposable> _rows = new Dictionary<PNLKey, IDisposable>();
|
||||
readonly VersionContainer<bool?> _is_all_selected;
|
||||
readonly DeferrableActionWrapper _refresh_is_all_selected;
|
||||
readonly HashSet<PNLRowVM> _selected_rows = new HashSet<PNLRowVM>();
|
||||
readonly PNLCellIndexBuilder<PNLNormalizedData> _cell_map = new PNLCellIndexBuilder<PNLNormalizedData>();
|
||||
readonly PNLCellEmbeddedAnimationModel<PNLNormalizedData> _animator;
|
||||
ImmutableDictionary<ColumnKeys, PNLCellAppearance> _defaults_appearances;
|
||||
IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> _triggers;
|
||||
IDisposable _dispose;
|
||||
|
||||
bool _is_expanded = true;
|
||||
int _selected_item_count_backing;
|
||||
|
||||
public PNLRowCollectionViewGroup(CollectionViewGroup from, PNLRowCollectionView root, ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) : base(from.Name) {
|
||||
_root = root;
|
||||
_from = from;
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
_animator = new PNLCellEmbeddedAnimationModel<PNLNormalizedData>(default_appearances, triggers);
|
||||
_animator.on_row(normalized_data.data);
|
||||
normalized_data.data_changed += () => {
|
||||
_cell_map.on_row(normalized_data.data);
|
||||
_animator.on_row(normalized_data.data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
};
|
||||
//sod_net_pos
|
||||
_is_all_selected = new VersionContainer<bool?>(() => {
|
||||
if (_selected_item_count == 0)
|
||||
return false;
|
||||
if (_selected_item_count == ProtectedItemCount)
|
||||
return true;
|
||||
return null;
|
||||
});
|
||||
bool? last_published_all_selected = _is_all_selected.value;
|
||||
_refresh_is_all_selected = new DeferrableActionWrapper(() => {
|
||||
if (_is_all_selected.value != last_published_all_selected) {
|
||||
last_published_all_selected = is_all_selected;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
|
||||
}
|
||||
});
|
||||
// sod
|
||||
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
|
||||
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
|
||||
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
|
||||
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
|
||||
// day
|
||||
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
|
||||
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
|
||||
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
|
||||
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
|
||||
// total
|
||||
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
|
||||
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.total_net_pos);
|
||||
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.total_long_pos);
|
||||
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.total_short_pos);
|
||||
// working
|
||||
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
|
||||
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
|
||||
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.working_short_pos);
|
||||
|
||||
_cell_map.on_row(normalized_data.data);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
((INotifyCollectionChanged)_from.Items).CollectionChanged += _from_items_CollectionChanged;
|
||||
_from_items_CollectionChanged(_from.Items, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _from.Items, 0));
|
||||
|
||||
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
|
||||
//stopwatch.Start();
|
||||
animations = new_animations.Select(kv => new CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(animations)));
|
||||
});
|
||||
}
|
||||
|
||||
public IList<CellAnimation> animations { get; private set; }
|
||||
|
||||
public event Action ProtectedItemCountChanged, selected_item_count_changed;
|
||||
|
||||
public PNLNormalizedDataVM normalized_data { get; } = new PNLNormalizedDataVM();
|
||||
|
||||
// sod
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
|
||||
// day
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
|
||||
// total
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
|
||||
// working
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_net_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_long_pos { get; }
|
||||
public PNLCellVM<PNLNormalizedData, decimal> working_short_pos { get; }
|
||||
//
|
||||
public PNLRowCollectionViewGroup parent { get; private set; }
|
||||
public int level => parent == null ? 1 : parent.level + 1;
|
||||
public override bool IsBottomLevel => _from.IsBottomLevel;
|
||||
|
||||
public IEnumerable<PNLRowCollectionViewGroup> ancestors {
|
||||
get {
|
||||
if (parent != null) {
|
||||
var p = parent;
|
||||
do {
|
||||
yield return p;
|
||||
p = p.parent;
|
||||
} while (p != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<object> GroupNamePath {
|
||||
get {
|
||||
var path = new List<object> { Name };
|
||||
foreach (var a in ancestors)
|
||||
path.Add(a.Name);
|
||||
return path.WrapReverseRO();
|
||||
}
|
||||
}
|
||||
|
||||
public bool is_expanded { get => _is_expanded; set => _set(ref _is_expanded, value); }
|
||||
public bool? is_all_selected {
|
||||
get => _is_all_selected.value;
|
||||
set {
|
||||
// The setter should not be called while is_all_selected deferred.
|
||||
// However, it is good to be defensive, so let's add a conditional check anyways:
|
||||
// if 'is_all_selected' is deferred, the value must be blindly propagated,
|
||||
// bc the equality comparison is erroneous, as the current state is unknown.
|
||||
if (value.HasValue && value != is_all_selected) {
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
foreach (var i in Items) {
|
||||
if (i is PNLRowCollectionViewGroup g)
|
||||
g.is_all_selected = value;
|
||||
else if (i is PNLRowVM r)
|
||||
r.is_selected = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new int ProtectedItemCount {
|
||||
get => base.ProtectedItemCount;
|
||||
set {
|
||||
if (base.ProtectedItemCount != value) {
|
||||
base.ProtectedItemCount = value;
|
||||
_is_all_selected.increment_version();
|
||||
ProtectedItemCountChanged?.Invoke();
|
||||
_refresh_is_all_selected.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _selected_item_count {
|
||||
get => _selected_item_count_backing;
|
||||
set {
|
||||
Log.assert(value >= 0, $"{nameof(_selected_item_count)} cannot be negative.");
|
||||
_selected_item_count_backing = value;
|
||||
_is_all_selected.increment_version();
|
||||
_refresh_is_all_selected.execute();
|
||||
selected_item_count_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances, IReadOnlyList<IPNLAnimationTrigger<PNLNormalizedData>> triggers) {
|
||||
_defaults_appearances = default_appearances;
|
||||
_triggers = triggers;
|
||||
if (Name.Equals("GC")) {
|
||||
|
||||
}
|
||||
_animator.on_settings(default_appearances, triggers);
|
||||
_cell_map.set_appearances(_animator.cell_appearances);
|
||||
foreach (var g in ProtectedItems.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.on_settings(default_appearances, triggers);
|
||||
}
|
||||
|
||||
public void set_all_expanded(bool is_expanded) {
|
||||
this.is_expanded = is_expanded;
|
||||
foreach (var g in Items.OfTypeSafe<PNLRowCollectionViewGroup>())
|
||||
g.set_all_expanded(is_expanded);
|
||||
}
|
||||
|
||||
public bool Equals(PNLRowCollectionViewGroup other) =>
|
||||
other != null &&
|
||||
level == other.level &&
|
||||
Equals(Name, other.Name);
|
||||
|
||||
void _select_all() {
|
||||
foreach (var i in Items) {
|
||||
if (i is PNLRowCollectionViewGroup cvg) {
|
||||
cvg._select_all();
|
||||
} else if (i is PNLRowVM row) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable _detect_is_all_selected_changed() {
|
||||
bool? was_all_selected = is_all_selected;
|
||||
return Disposable.Create(() => {
|
||||
if (is_all_selected != was_all_selected)
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(is_all_selected)));
|
||||
});
|
||||
}
|
||||
|
||||
void _add_item(int index, object item) {
|
||||
Log.assert(index >= 0, "Index is less than 0.");
|
||||
if (item is CollectionViewGroup group) {
|
||||
var inner_group = new PNLRowCollectionViewGroup(group, _root, _defaults_appearances, _triggers);
|
||||
_inner_groups.Add(group.Name, new InnerGroupSubscription(this, inner_group));
|
||||
ProtectedItems.Insert(index, inner_group);
|
||||
} else {
|
||||
var row = (PNLRowVM)item;
|
||||
ProtectedItems.Insert(index, item);
|
||||
_rows.Add(row.key, _connect_row(row));
|
||||
}
|
||||
}
|
||||
|
||||
void _remove_item(int index, object item) {
|
||||
if (item is PNLRowCollectionViewGroup inner_group) {
|
||||
if (!_inner_groups.TryPopValue(inner_group.Name, out var ig_sub))
|
||||
throw new InvalidOperationException($"Inner groups cache does not have an entry for the specified name '{inner_group.Name}' (entire tree structure may be compromised).");
|
||||
ig_sub.Dispose();
|
||||
Disposable.dispose(ref inner_group._dispose);
|
||||
} else {
|
||||
var row = (PNLRowVM)item;
|
||||
if (!_rows.TryPopValue(row.key, out var row_sub))
|
||||
throw new InvalidOperationException($"Inner rows cache does not have an entry for the specified irowstem key '{row.key}' (entire tree structure may be compromised).");
|
||||
row_sub.Dispose();
|
||||
}
|
||||
ProtectedItems.RemoveAt(index);
|
||||
}
|
||||
|
||||
void _from_items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
for (int i = 0; i < e.OldItems.Count; i++)
|
||||
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
for (int i = 0; i < e.OldItems.Count; i++)
|
||||
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
while (ProtectedItems.Count > 0)
|
||||
_remove_item(0, ProtectedItems[0]);
|
||||
for (int i = 0; i < _from.Items.Count; i++)
|
||||
_add_item(i, _from.Items[i]);
|
||||
break;
|
||||
}
|
||||
_is_all_selected.increment_version();
|
||||
_refresh_is_all_selected.execute();
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable _connect_row(PNLRowVM row) {
|
||||
using (_detect_is_all_selected_changed()) {
|
||||
if (Name.Equals("GC")) {
|
||||
|
||||
}
|
||||
var last_data = PNLNormalizedData.zero;
|
||||
on_updated(row.normalized_data);
|
||||
row.updated += on_updated;
|
||||
row.is_selected_changed += on_is_selected_changed;
|
||||
if (row.is_selected) {
|
||||
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained selected row that was just added to group ({row}).");
|
||||
_selected_item_count++;
|
||||
}
|
||||
ProtectedItemCount++;
|
||||
return Disposable.Create(() => {
|
||||
row.updated -= on_updated;
|
||||
row.is_selected_changed -= on_is_selected_changed;
|
||||
using (_refresh_is_all_selected.defer_execution()) {
|
||||
if (row.is_selected) {
|
||||
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain selected row that was just removed from group ({row}).");
|
||||
_selected_item_count--;
|
||||
}
|
||||
var newval = normalized_data.data - row.normalized_data;
|
||||
normalized_data.data = newval;
|
||||
ProtectedItemCount--;
|
||||
}
|
||||
});
|
||||
void on_updated(PNLNormalizedData new_data) {
|
||||
if (Name.Equals("GC")) {
|
||||
|
||||
}
|
||||
var less_last = normalized_data.data - last_data;
|
||||
var plus_new = less_last + new_data;
|
||||
normalized_data.data += new_data - last_data;
|
||||
if (plus_new != normalized_data.data) {
|
||||
|
||||
}
|
||||
Log.assert(plus_new == normalized_data.data);
|
||||
last_data = new_data;
|
||||
}
|
||||
void on_is_selected_changed(bool is_selected) {
|
||||
if (is_selected) {
|
||||
Log.assert(_selected_rows.Add(row), $"Selected Rows out of sync ({Name}): hash set already contained row that was just selected ({row}).");
|
||||
_selected_item_count++;
|
||||
} else {
|
||||
Log.assert(_selected_rows.Remove(row), $"Selected Rows out of sync ({Name}): hash set did not contain row that was just unselected ({row}).");
|
||||
_selected_item_count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
|
||||
_set(ref backing, new_value, EqualityComparer<T>.Default, prop);
|
||||
|
||||
void _set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
|
||||
if (!comparer.Equals(backing, new_value)) {
|
||||
backing = new_value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
|
||||
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(prop));
|
||||
|
||||
class InnerGroupSubscription : IDisposable {
|
||||
public readonly PNLRowCollectionViewGroup _inner_group;
|
||||
readonly PNLRowCollectionViewGroup _parent;
|
||||
PNLNormalizedData _last_data = PNLNormalizedData.zero;
|
||||
int _last_count, _last_selected_item_count;
|
||||
|
||||
public InnerGroupSubscription(PNLRowCollectionViewGroup parent, PNLRowCollectionViewGroup inner_group) {
|
||||
_parent = parent;
|
||||
_inner_group = inner_group;
|
||||
_inner_group.parent = parent;
|
||||
// init item count
|
||||
_on_item_count_changed();
|
||||
// listen to item count changes
|
||||
_inner_group.ProtectedItemCountChanged += _on_item_count_changed;
|
||||
// init selected item count
|
||||
_on_selected_item_count_changed();
|
||||
// listen to selected item count changes
|
||||
_inner_group.selected_item_count_changed += _on_selected_item_count_changed;
|
||||
// init data
|
||||
_on_normalized_data_changed();
|
||||
// listen to data changes
|
||||
_inner_group.normalized_data.data_changed += _on_normalized_data_changed;
|
||||
_parent._selected_item_count += _inner_group._selected_item_count;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_inner_group.ProtectedItemCountChanged -= _on_item_count_changed;
|
||||
_inner_group.selected_item_count_changed -= _on_selected_item_count_changed;
|
||||
_inner_group.normalized_data.data_changed -= _on_normalized_data_changed;
|
||||
_inner_group.parent = null;
|
||||
// make sure the state is valid (if a bad state goes by undetected here, nefarious/difficult-to-track bugs could arise)
|
||||
if (_last_count != _inner_group.ProtectedItemCount) {
|
||||
Log.Ui.warn("Cached inner count is not equal to the inner group's ItemCount (entire tree structure may be compromised).");
|
||||
}
|
||||
if (_last_data != _inner_group.normalized_data.data) {
|
||||
Log.Ui.warn($"Cached normalized data is not equal to the inner group's normalized data (aggregated pnl data may be compromised).");
|
||||
}
|
||||
_parent.ProtectedItemCount -= _last_count;
|
||||
_parent.normalized_data.data -= _last_data;
|
||||
_parent._selected_item_count -= _last_selected_item_count;
|
||||
}
|
||||
|
||||
void _on_item_count_changed() {
|
||||
// add the delta
|
||||
_parent.ProtectedItemCount += (_inner_group.ProtectedItemCount - _last_count);
|
||||
// reset the local cache
|
||||
_last_count = _inner_group.ProtectedItemCount;
|
||||
}
|
||||
|
||||
void _on_selected_item_count_changed() {
|
||||
_parent._selected_item_count += (_inner_group._selected_item_count - _last_selected_item_count);
|
||||
_last_selected_item_count = _inner_group._selected_item_count;
|
||||
}
|
||||
|
||||
void _on_normalized_data_changed() {
|
||||
var new_data = _inner_group.normalized_data.data;
|
||||
_parent.normalized_data.data += new_data - _last_data;
|
||||
_last_data = new_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,525 @@
|
||||
using bts.collections;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.controls.adorners;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
enum PNLHeaderRowDisplay {
|
||||
None,
|
||||
Total,
|
||||
SelectedTotal
|
||||
}
|
||||
|
||||
partial class PNLView : UserControl {
|
||||
public enum Columns {
|
||||
Account = 1,
|
||||
Exchange,
|
||||
Symbol,
|
||||
Security,
|
||||
SecurityType,
|
||||
// SOD
|
||||
SODPNL,
|
||||
SODLongPos,
|
||||
SODLongAvgPrice,
|
||||
SODLongAvgPriceDelta,
|
||||
SODShortPos,
|
||||
SODShortAvgPrice,
|
||||
SODShortAvgPriceDelta,
|
||||
SODNetPos,
|
||||
SODNetAvgPriceDelta,
|
||||
// Day
|
||||
DayPNL,
|
||||
DayLongPos,
|
||||
DayLongAvgPrice,
|
||||
DayLongAvgPriceDelta,
|
||||
DayShortAvgPriceDelta,
|
||||
DayShortAvgPrice,
|
||||
DayShortPos,
|
||||
DayCalcType,
|
||||
DayNetPos,
|
||||
DayNetAvgPriceDelta,
|
||||
// Working
|
||||
WorkingLongPos,
|
||||
WorkingLongAvgPrice,
|
||||
WorkingLongAvgPriceDelta,
|
||||
WorkingShortPos,
|
||||
WorkingShortAvgPrice,
|
||||
WorkingShortAvgPriceDelta,
|
||||
WorkingNetPos,
|
||||
WorkingNetAvgPriceDelta,
|
||||
// Total
|
||||
TotalPNL,
|
||||
TotalLongPos,
|
||||
TotalLongAvgPrice,
|
||||
TotalLongAvgPriceDelta,
|
||||
TotalShortPos,
|
||||
TotalShortAvgPrice,
|
||||
TotalShortAvgPriceDelta,
|
||||
TotalNetPos,
|
||||
TotalNetAvgPriceDelta,
|
||||
// Market
|
||||
BidPrice,
|
||||
MidPrice,
|
||||
AskPrice,
|
||||
LastPrice,
|
||||
SettlementPrice,
|
||||
PNLPrice,
|
||||
// Misc
|
||||
HasDayTrades,
|
||||
CPP
|
||||
}
|
||||
|
||||
public static readonly IValueConverter
|
||||
IncrementalGroupLevelConverter = ValueConverter.create<int, int>(level => level + 1),
|
||||
GroupLevelMarginConverter = ValueConverter.create<int, Thickness>(level => new Thickness(24 * level, 0, 0, 0)),
|
||||
ExpandedVisualStateConverter = ValueConverter.create<bool, string>(is_expanded => is_expanded ? "Expanded" : "Collapsed");
|
||||
|
||||
public static readonly IMultiValueConverter
|
||||
TotalHeaderRowDisplayTextConverter = ValueConverter.create<int, int, string>((filtered, total) => $"Total ({(filtered == total ? total.ToString() : $"{filtered}/{total}")})"),
|
||||
CellParamsConverter = ValueConverter.create<object, Columns, PNLCell>((item, col) => new PNLCell(item, col));
|
||||
|
||||
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
|
||||
|
||||
const double _auxiliary_popup_height = 300;
|
||||
static readonly string _node_path_separator = $" {SpecialCharacters.Dot} ";
|
||||
|
||||
IDisposable _anchors_hook, _animations_hook;
|
||||
int _anchor_version;
|
||||
|
||||
public PNLView() {
|
||||
SetCurrentValue(AnchorIndexProperty, new AnchorIndex());
|
||||
InitializeComponent();
|
||||
SetBinding(AnchorsProperty, new Binding(nameof(PNLVM.anchors)));
|
||||
_grid.ConnectOnLoaded(() => {
|
||||
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter)) {
|
||||
presenter.GroupItemSelectionPivot += on_group_item_selection_pivot;
|
||||
return Disposable.Create(() => presenter.GroupItemSelectionPivot -= on_group_item_selection_pivot);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
void on_group_item_selection_pivot(object sender, GroupItemSelectionPivotEventArgs e) =>
|
||||
e.Handled = e.group_item is PNLRowCollectionViewGroup pnl_group && pnl_group.is_expanded;
|
||||
}
|
||||
|
||||
public new PNLVM DataContext {
|
||||
get => base.DataContext as PNLVM;
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
|
||||
public bool EnableAnimations {
|
||||
get => _animations_hook != null;
|
||||
set {
|
||||
if (EnableAnimations != value) {
|
||||
if (_animations_hook == null) {
|
||||
_animations_hook = AdornerHelper.attach_continuously_on_loaded(
|
||||
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
|
||||
_grid);
|
||||
} else {
|
||||
_animations_hook.Dispose();
|
||||
_animations_hook = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region AnchorIndex
|
||||
public static readonly DependencyProperty AnchorIndexProperty =
|
||||
WPFHelper.create_dp<AnchorIndex, PNLView>(
|
||||
nameof(AnchorIndex),
|
||||
(o, v) => v?.reset(o?.Anchors));
|
||||
|
||||
public AnchorIndex AnchorIndex {
|
||||
get => GetValue(AnchorIndexProperty) as AnchorIndex;
|
||||
set => SetValue(AnchorIndexProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GroupLevel
|
||||
public static readonly DependencyProperty GroupLevelProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"GroupLevel",
|
||||
typeof(int),
|
||||
typeof(PNLView),
|
||||
new PropertyMetadata(0, (d, e) => {
|
||||
if (d is GroupItem group && e.NewValue is int level)
|
||||
group.SetBinding(DIYGrid.ColumnKeyProperty, new Binding($"{nameof(Anchors)}[{level}]") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(PNLView), 1) });
|
||||
}));
|
||||
|
||||
public static int GetGroupLevel(DependencyObject d) => (int)d.GetValue(GroupLevelProperty);
|
||||
public static void SetGroupLevel(DependencyObject d, int value) => d.SetValue(GroupLevelProperty, value);
|
||||
|
||||
void _grid_loaded(object sender, RoutedEventArgs e) {
|
||||
// this is a hack to make room for the 'totals row' between the column headers and the regular rows.
|
||||
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter rp)) {
|
||||
rp.SetBinding(MarginProperty, new Binding(nameof(PNLVM.header_row_display)) {
|
||||
Converter = ValueConverter.create<PNLHeaderRowDisplay, Thickness>(hrd => {
|
||||
double top = 0;
|
||||
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
|
||||
top += 24;
|
||||
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
|
||||
top += 24;
|
||||
return new Thickness(0, top, 0, 0);
|
||||
}),
|
||||
Source = DataContext
|
||||
});
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Anchors
|
||||
public static readonly DependencyProperty AnchorsProperty =
|
||||
WPFHelper.create_dp<IEnumerable<Columns>, PNLView>(
|
||||
nameof(Anchors),
|
||||
(o, v) => {
|
||||
var cols = o?._grid?.Columns;
|
||||
if (cols == null)
|
||||
return;
|
||||
if (!(o._grid.Rows is ICollectionView rows_view))
|
||||
return;
|
||||
if (!(v is INotifyCollectionChanged anchor_notifier && v is IReadOnlyList<Columns> anchor_keys))
|
||||
return;
|
||||
int version = ++o._anchor_version;
|
||||
bool busy = false;
|
||||
var pnl_cols = cols.CastObservable<DIYGrid.ColumnBase, PNLColumn>();
|
||||
Disposable.replace(ref o._anchors_hook, Disposable.Create(
|
||||
anchor_notifier.Subscribe((_, __) => {
|
||||
if (busy)
|
||||
return;
|
||||
// invoke async to avoid noisy consecutive resets due to batch changes
|
||||
busy = true;
|
||||
o.Dispatcher.InvokeAsync(() => {
|
||||
sync_anchor_cols();
|
||||
busy = false;
|
||||
});
|
||||
})));
|
||||
o.AnchorIndex?.reset(anchor_keys);
|
||||
sync_anchor_cols();
|
||||
void sync_anchor_cols() {
|
||||
// since reset is invoked asynchronously, need to check we are still the latest version.
|
||||
if (o._anchor_version != version)
|
||||
return;
|
||||
o.AnchorIndex?.reset(anchor_keys);
|
||||
// move all the anchor columns to the left, ordered by anchor position (grouping hierarchy).
|
||||
for (int i = 0; i < anchor_keys.Count; i++) {
|
||||
if (pnl_cols.TryGetFirst(c => c.key == anchor_keys[i], out var match, out int curr_pos)) {
|
||||
match.is_visible = true;
|
||||
if (curr_pos != i) {
|
||||
cols.Move(curr_pos, i);
|
||||
}
|
||||
match.anchor_index = i + 1;
|
||||
}
|
||||
}
|
||||
for (int i = anchor_keys.Count; i < pnl_cols.Count; i++) {
|
||||
pnl_cols[i].anchor_index = null;
|
||||
}
|
||||
using (rows_view.DeferRefresh()) {
|
||||
rows_view.GroupDescriptions.Clear();
|
||||
var anchor_cols = from key in anchor_keys
|
||||
join col in pnl_cols on key equals col.key
|
||||
select new PNLGroupDescription(col);
|
||||
rows_view.GroupDescriptions.AddRange(anchor_cols);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public IEnumerable<Columns> Anchors {
|
||||
get => GetValue(AnchorsProperty) as IEnumerable<Columns>;
|
||||
set => SetValue(AnchorsProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _cell_click(PNLCell cell) {
|
||||
Log.Ui.info($"PNLCell Click: {cell}.");
|
||||
try {
|
||||
switch (cell.column) {
|
||||
case Columns.SODNetPos:
|
||||
case Columns.SODLongPos:
|
||||
case Columns.SODShortPos:
|
||||
case Columns.DayNetPos:
|
||||
case Columns.DayLongPos:
|
||||
case Columns.DayShortPos:
|
||||
case Columns.TotalNetPos:
|
||||
case Columns.TotalLongPos:
|
||||
case Columns.TotalShortPos: {
|
||||
var oneof = cell.oneof();
|
||||
if (oneof == null)
|
||||
return;
|
||||
var (vm, context, title, fg) = cell.oneof().Match(_row_fills, _group_fills, _header_fills);
|
||||
_show_fills_grid(vm, context, title, fg);
|
||||
}
|
||||
break;
|
||||
case Columns.WorkingNetPos:
|
||||
case Columns.WorkingLongPos:
|
||||
case Columns.WorkingShortPos: {
|
||||
var (vm, context, title, fg) = cell.oneof().Match(_orderbook_params, _orderbook_params, _orderbook_params);
|
||||
_show_orderbook(vm, context, title, fg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.Ui.warn($"{nameof(PNLView)} error on cell click: {ex}.");
|
||||
}
|
||||
}
|
||||
|
||||
#region Fills
|
||||
(FillsVM vm, string context, string title, Color fg) _row_fills(PNLRowCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
|
||||
}
|
||||
|
||||
(FillsVM vm, string context, string title, Color fg) _group_fills(PNLGroupCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
|
||||
}
|
||||
|
||||
(FillsVM vm, string context, string title, Color fg) _header_fills(PNLHeaderRowCell cell) {
|
||||
var (title_desc, fg) = _fills_title(cell.column);
|
||||
return (DataContext.mk_fills_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
|
||||
}
|
||||
|
||||
|
||||
static (string desc, Color fg) _fills_title(Columns key) {
|
||||
const uint net_fg = 0x333333u;
|
||||
switch (key) {
|
||||
case Columns.SODLongPos: return ("Open Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.SODShortPos: return ("Open Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.SODNetPos: return ("Open Fills", net_fg.rgb_color());
|
||||
case Columns.DayLongPos: return ("Day Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.DayShortPos: return ("Day Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.DayNetPos: return ("Day Fills", net_fg.rgb_color());
|
||||
case Columns.TotalLongPos: return ("All Buy Fills", BTSColors.MarketData.bid);
|
||||
case Columns.TotalShortPos: return ("All Sell Fills", BTSColors.MarketData.ask);
|
||||
case Columns.TotalNetPos: return ("All Fills", net_fg.rgb_color());
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
|
||||
}
|
||||
}
|
||||
|
||||
async void _show_fills_grid(FillsVM fills_vm, string context_desc, string title_desc, Color fg) {
|
||||
var fills_view = new FillsView { DataContext = fills_vm, MaxHeight = _auxiliary_popup_height };
|
||||
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
|
||||
fills_view.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
// banner
|
||||
var group_header = new TextBlock {
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Text = context_desc
|
||||
};
|
||||
var title = new TextBlock {
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = fg.to_brush(),
|
||||
Text = title_desc
|
||||
};
|
||||
var keep_open_checkbox = new CheckBox {
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Content = "Keep Open",
|
||||
Focusable = false
|
||||
};
|
||||
var close_bn = new Button {
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(8, 4, 4, 4),
|
||||
Command = ApplicationCommands.Close,
|
||||
Style = TryFindResource("RoundCloseButton") as Style
|
||||
};
|
||||
var banner_panel_rt = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
|
||||
banner_panel_rt.Children.Add(keep_open_checkbox);
|
||||
banner_panel_rt.Children.Add(close_bn);
|
||||
// grid panel
|
||||
var panel = new Grid();
|
||||
panel.Children.Add(group_header);
|
||||
panel.Children.Add(title);
|
||||
panel.Children.Add(banner_panel_rt);
|
||||
panel.Children.Add(fills_view);
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
Grid.SetRow(fills_view, 1);
|
||||
// popup adorner
|
||||
var popup_adorner = new PopupAdorner(
|
||||
_grid, _grid,
|
||||
new Border {
|
||||
Child = panel,
|
||||
Background = BTSColors.common_background_brush,
|
||||
BorderBrush = Brushes.Black,
|
||||
BorderThickness = new Thickness(1),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top
|
||||
},
|
||||
HorizontalPosition.Middle,
|
||||
VerticalPosition.Bottom,
|
||||
0,
|
||||
0) {
|
||||
OpenRelativeToMouse = true,
|
||||
EnableDragMove = true,
|
||||
ClipToBounds = true
|
||||
};
|
||||
close_bn.CommandTarget = popup_adorner.PopupElement;
|
||||
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding(nameof(CheckBox.IsChecked)) { Source = keep_open_checkbox });
|
||||
popup_adorner.PopupElement.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
|
||||
popup_adorner.PopupElement.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => {
|
||||
Log.assert(popup_adorner != null);
|
||||
popup_adorner.detach();
|
||||
popup_adorner = null;
|
||||
}));
|
||||
//
|
||||
popup_adorner.attach();
|
||||
popup_adorner.IsOpen = true;
|
||||
FocusManager.SetIsFocusScope(popup_adorner.PopupElement, true);
|
||||
fills_view.Focus();
|
||||
popup_adorner.AdornedElement.Focus();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OrderBook
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLRowCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
|
||||
}
|
||||
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLGroupCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
|
||||
}
|
||||
|
||||
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLHeaderRowCell cell) {
|
||||
var (title_desc, fg) = _orderbook_title(cell.column);
|
||||
return (DataContext.mk_orderbook_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
|
||||
}
|
||||
|
||||
static (string desc, Color fg) _orderbook_title(Columns key) {
|
||||
switch (key) {
|
||||
case Columns.WorkingLongPos: return ("Working Buy Orders", BTSColors.MarketData.bid);
|
||||
case Columns.WorkingShortPos: return ("Working Sell Orders", BTSColors.MarketData.ask);
|
||||
case Columns.WorkingNetPos: return ("Working Orders", 0x333333u.rgb_color());
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
|
||||
}
|
||||
}
|
||||
|
||||
async void _show_orderbook(OrderBookVM obvm, string context_desc, string title_desc, Color fg) {
|
||||
var mini_ob = new MiniOrderBookView(obvm) { MaxHeight = _auxiliary_popup_height };
|
||||
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
|
||||
mini_ob.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
// banner
|
||||
var group_header = new TextBlock {
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Text = context_desc
|
||||
};
|
||||
var title = new TextBlock {
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = fg.to_brush(),
|
||||
Text = title_desc
|
||||
};
|
||||
var keep_open_checkbox = new CheckBox {
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Content = "Keep Open",
|
||||
Focusable = false
|
||||
};
|
||||
var close_bn = new Button {
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(8, 4, 4, 4),
|
||||
Command = ApplicationCommands.Close,
|
||||
//CommandTarget = mini_ob,
|
||||
Style = TryFindResource("RoundCloseButton") as Style
|
||||
};
|
||||
var banner_panel_rt = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
|
||||
banner_panel_rt.Children.Add(keep_open_checkbox);
|
||||
banner_panel_rt.Children.Add(close_bn);
|
||||
// grid panel
|
||||
var panel = new Grid();
|
||||
panel.Children.Add(group_header);
|
||||
panel.Children.Add(title);
|
||||
panel.Children.Add(banner_panel_rt);
|
||||
panel.Children.Add(mini_ob);
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
Grid.SetRow(mini_ob, 1);
|
||||
// popup adorner
|
||||
var popup_adorner = new PopupAdorner(
|
||||
_grid, _grid,
|
||||
new Border {
|
||||
Child = panel,
|
||||
Background = BTSColors.common_background_brush,
|
||||
BorderBrush = Brushes.Black,
|
||||
BorderThickness = new Thickness(1),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top
|
||||
},
|
||||
HorizontalPosition.Middle,
|
||||
VerticalPosition.Bottom,
|
||||
0,
|
||||
0) {
|
||||
OpenRelativeToMouse = true,
|
||||
EnableDragMove = true,
|
||||
ClipToBounds = true
|
||||
};
|
||||
close_bn.CommandTarget = popup_adorner.PopupElement;
|
||||
popup_adorner.SetBinding(PopupAdorner.StaysOpenProperty, new Binding(nameof(CheckBox.IsChecked)) { Source = keep_open_checkbox });
|
||||
popup_adorner.PopupElement.InputBindings.Add(new KeyBinding(ApplicationCommands.Close, Key.Escape, ModifierKeys.None));
|
||||
popup_adorner.PopupElement.CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, (s, e) => {
|
||||
Log.assert(popup_adorner != null);
|
||||
popup_adorner.detach();
|
||||
popup_adorner = null;
|
||||
}));
|
||||
//
|
||||
popup_adorner.attach();
|
||||
popup_adorner.IsOpen = true;
|
||||
FocusManager.SetIsFocusScope(popup_adorner.PopupElement, true);
|
||||
mini_ob.Focus();
|
||||
popup_adorner.AdornedElement.Focus();
|
||||
}
|
||||
#endregion
|
||||
|
||||
void _cell_click_executed(object sender, ExecutedRoutedEventArgs e) {
|
||||
if (e.Parameter is PNLCell cell) {
|
||||
_cell_click(cell);
|
||||
} else Log.Ui.warn($"{nameof(PNLView)} '{nameof(_cell_click_executed)}' parameter should be a {typeof(PNLCell)} (actually '{e.Parameter}').");
|
||||
}
|
||||
|
||||
void _on_group_item_mouse_down(object sender, MouseButtonEventArgs e) {
|
||||
if (!e.Handled && sender is FrameworkElement el && el.DataContext is PNLRowCollectionViewGroup group) {
|
||||
group.is_all_selected = !group.is_all_selected.GetValueOrDefault();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
|
||||
public PNLGroupDescription(PNLColumn column) : base(column.data_member_path.Path) =>
|
||||
key = column.key;
|
||||
|
||||
public Columns key { get; }
|
||||
|
||||
public bool Equals(PNLGroupDescription other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as PNLGroupDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ namespace bts.shell {
|
||||
LadderWindowFactory _lwf;
|
||||
OrderBookWindowFactory _obwf;
|
||||
FillsWindowFactory _fwf;
|
||||
PAndLWindowFactory _plwf;
|
||||
PAndLWindowFactory _old_plwf;
|
||||
PNLWindowFactory _plwf;
|
||||
AlgoStudioWindowFactory _aswf;
|
||||
ManualFillsWindowFactory _mfwf;
|
||||
|
||||
@@ -36,7 +37,11 @@ namespace bts.shell {
|
||||
_lwf = new LadderWindowFactory(this, bus, wm, settings_hub, orders_model, mbo);
|
||||
_obwf = new OrderBookWindowFactory(this, bus, notification_service, notification_service_data_cache, orders_model, settings_hub);
|
||||
_fwf = new FillsWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
var pnl_conf = mbts.config.Configuration.get<bts.config.PNLOptions>();
|
||||
if (pnl_conf.show_new_pnl)
|
||||
_plwf = new PNLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
if (pnl_conf.show_old_pnl)
|
||||
_old_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_aswf = new AlgoStudioWindowFactory(this, bus, orders_model, error_reporter);
|
||||
_mfwf = new ManualFillsWindowFactory(this, bus, orders_model);
|
||||
}
|
||||
@@ -50,6 +55,7 @@ namespace bts.shell {
|
||||
public IEnumerable<SparkCommand> actions => new [] {
|
||||
SparkCommands.ShowOrders.complete(() => bts.shell.orderbook.OrderBookStartupData.Default),
|
||||
SparkCommands.ShowFills(null).complete(() => new FillsSettings()),
|
||||
SparkCommands.ShowPNL.complete(() => null),
|
||||
SparkCommands.ShowPAndL.complete(() => new PAndLWindowFactory.PAndLStartupData()),
|
||||
SparkCommands.PickLadder,
|
||||
SparkCommands.PickOrderTicket,
|
||||
@@ -61,7 +67,10 @@ namespace bts.shell {
|
||||
yield return _lwf;
|
||||
yield return _obwf;
|
||||
yield return _fwf;
|
||||
yield return _plwf;
|
||||
if (_plwf != null)
|
||||
yield return _plwf;
|
||||
if (_old_plwf != null)
|
||||
yield return _old_plwf;
|
||||
yield return _aswf;
|
||||
yield return _mfwf;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public uint account_id =>
|
||||
fill.FillKind != FillKind.Sod && order.HasValue ?
|
||||
order.Value.account_id() :
|
||||
fill.InternalAccountId;
|
||||
|
||||
public bool Equals(FillTuple other) => other != null
|
||||
&& other.order.HasValue == order.HasValue
|
||||
&& other.security.HasValue == security.HasValue
|
||||
|
||||
@@ -26,7 +26,11 @@ namespace mbts.spark.utilities {
|
||||
public readonly Func<CustomOrderDefinition, CustomOrderDefinition> get_child;
|
||||
public readonly Action<CustomOrderDefinition, CustomOrderDefinition> assign_child;
|
||||
|
||||
public ChildAlgoData(string name, Func<IMessage, CustomOrderDefinition> get_child_from_impl, Action<IMessage, CustomOrderDefinition> assign_child_to_impl, Func<CustomOrderDefinition, CustomOrderDefinition> get_child, Action<CustomOrderDefinition, CustomOrderDefinition> assign_child) {
|
||||
public ChildAlgoData(string name, Func<IMessage, CustomOrderDefinition> get_child_from_impl,
|
||||
Action<IMessage, CustomOrderDefinition> assign_child_to_impl,
|
||||
Func<CustomOrderDefinition, CustomOrderDefinition> get_child,
|
||||
Action<CustomOrderDefinition, CustomOrderDefinition> assign_child)
|
||||
{
|
||||
this.name = name;
|
||||
this.get_child = get_child;
|
||||
this.assign_child = assign_child;
|
||||
@@ -67,29 +71,19 @@ namespace mbts.spark.utilities {
|
||||
public IObservable<IReadOnlyList<AccountTuple>> subscribe_account_tuples() => _admin_model.on_account_tuples;
|
||||
|
||||
#region Defaults
|
||||
public CustomOrderDefinition.CustomOneofCase default_order_type() =>
|
||||
CustomOrderDefinition.CustomOneofCase.Limit;
|
||||
|
||||
public CustomOrderDefinition.CustomOneofCase default_order_type(SecurityImmutable security, bool is_child = false) {
|
||||
if (is_child) return CustomOrderDefinition.CustomOneofCase.Limit;
|
||||
public CustomOrderDefinition.CustomOneofCase default_order_type(SecurityImmutable security) {
|
||||
switch (security.ContractCase) {
|
||||
case Security.ContractOneofCase.Synthetic: return CustomOrderDefinition.CustomOneofCase.Spreader;
|
||||
default: return CustomOrderDefinition.CustomOneofCase.Limit;
|
||||
}
|
||||
}
|
||||
|
||||
public CustomOrderDefinition.CustomOneofCase default_order_type(SecurityImmutable? security, bool is_child = false) =>
|
||||
security.HasValue ? default_order_type(security.Value, is_child) : default_order_type();
|
||||
|
||||
public TimeInForce default_time_in_force() =>
|
||||
TimeInForce.Day;
|
||||
|
||||
public TimeInForce default_time_in_force(SecurityImmutable security) =>
|
||||
default_time_in_force();
|
||||
|
||||
public TimeInForce default_time_in_force(SecurityImmutable? security) =>
|
||||
security.HasValue ? default_time_in_force(security.Value) : default_time_in_force();
|
||||
|
||||
public IEnumerable<CustomOrderDefinition.CustomOneofCase> default_order_types() =>
|
||||
OrderDataHelper.exchange_order_types;
|
||||
|
||||
@@ -141,15 +135,15 @@ namespace mbts.spark.utilities {
|
||||
|
||||
public Dictionary<AlgoKey, AlgoData> default_algos(IEnumerable<CustomOrderDefinition.CustomOneofCase> types) {
|
||||
var def_map = new Dictionary<AlgoKey, AlgoData>();
|
||||
foreach (var ot in types.Except(new[] { CustomOrderDefinition.CustomOneofCase.Dynamic })) {
|
||||
var key = new AlgoKey(new AlgoNameStructureKey(ot));
|
||||
def_map[key] = new AlgoData(custom_orderi(ot), ot.display_name(), key);
|
||||
foreach (var ot in types.Except(new[] { CustomOrderDefinition.CustomOneofCase.Dynamic, CustomOrderDefinition.CustomOneofCase.Composite })) {
|
||||
var data = AlgoData.from_custom_def(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(ot));
|
||||
def_map[data.key] = data;
|
||||
}
|
||||
var dynamic_defs = new List<CustomOrderDefinitionImmutable>();
|
||||
_dynamic_helper.add_dynamic_order_defs(dynamic_defs, custom_order);
|
||||
foreach (var dd in dynamic_defs) {
|
||||
var key = new AlgoKey(new AlgoNameStructureKey(dd.Dynamic.DynamicOrderType, new AlgoStructureKey(CustomOrderDefinition.CustomOneofCase.Dynamic)));
|
||||
def_map[key] = new AlgoData(dd, dd.Dynamic.DynamicOrderType, key);
|
||||
var data = AlgoData.from_custom_def(dd);
|
||||
def_map[data.key] = data;
|
||||
}
|
||||
return def_map;
|
||||
}
|
||||
@@ -157,6 +151,7 @@ namespace mbts.spark.utilities {
|
||||
|
||||
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,
|
||||
@@ -168,17 +163,14 @@ namespace mbts.spark.utilities {
|
||||
// this will be the tag 50 on CME orders
|
||||
// current user might be okay, but we might also want to get it
|
||||
// from the store that serves the risk admin gui
|
||||
ManualOrderIndicator = ManualOrderIndicator.Yes
|
||||
ManualOrderIndicator = ManualOrderIndicator.Yes,
|
||||
User = _admin_model.user.name,
|
||||
UserId = _admin_model.user.id
|
||||
};
|
||||
if (account.HasValue) {
|
||||
rv.Account = account.Value.name;
|
||||
rv.AccountId = account.Value.id;
|
||||
}
|
||||
var user = _admin_model.get_current_user();
|
||||
if (user.HasValue) {
|
||||
rv.User = user.Value.name;
|
||||
rv.UserId = user.Value.id;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
@@ -215,6 +207,7 @@ namespace mbts.spark.utilities {
|
||||
public TriggerOrderDefinition trigger_order() => OrderDataHelper.Defaults.TriggerOrderDefinition();
|
||||
public DynamicOrderDefinition dynamic_order() => OrderDataHelper.Defaults.DynamicOrderDefinition();
|
||||
public SpreaderOrderDefinition spreader_order() => OrderDataHelper.Defaults.SpreaderOrderDefinition();
|
||||
public SplitterOrderDefinition splitter_order() => OrderDataHelper.Defaults.SplitterOrderDefinition();
|
||||
public SpreaderOrderDefinition spreader_order(SecurityImmutable security, ExecutionStrategyImmutable strategy) =>
|
||||
new SpreaderOrderDefinition {
|
||||
Security = security.CloneToMutable(),
|
||||
@@ -245,8 +238,12 @@ namespace mbts.spark.utilities {
|
||||
case CustomOrderDefinition.CustomOneofCase.Offset: rv.Offset = offset_order(); break;
|
||||
case CustomOrderDefinition.CustomOneofCase.Trigger: rv.Trigger = trigger_order(); break;
|
||||
case CustomOrderDefinition.CustomOneofCase.Spreader: rv.Spreader = spreader_order(); break;
|
||||
case CustomOrderDefinition.CustomOneofCase.Splitter: rv.Splitter = splitter_order(); break;
|
||||
case CustomOrderDefinition.CustomOneofCase.Dynamic: rv.Dynamic = dynamic_order(); break;
|
||||
//case CustomOrderDefinition.CustomOneofCase.QueueHolder: rv.QueueHolder = queue_holder_order(); break;
|
||||
default:
|
||||
OrderTypeVMFactory.GenericOrderFactory.setup_generic_custom_order(rv, order_type);
|
||||
break;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
@@ -272,6 +269,7 @@ namespace mbts.spark.utilities {
|
||||
case CustomOrderDefinition.CustomOneofCase.Offset: return new OrderMetadata { KeyBinding = "O", Color = BTSColors.Orders.off_order };
|
||||
case CustomOrderDefinition.CustomOneofCase.Trigger: return new OrderMetadata { KeyBinding = "T", Color = BTSColors.Orders.trigger_order };
|
||||
case CustomOrderDefinition.CustomOneofCase.Spreader: return new OrderMetadata { KeyBinding = "", Color = BTSColors.Orders.spreader_order };
|
||||
// case CustomOrderDefinition.CustomOneofCase.Splitter: return new OrderMetadata { KeyBinding = "", Color = BTSColors.orange.rgb_color() };
|
||||
case CustomOrderDefinition.CustomOneofCase.Dynamic: return new OrderMetadata { KeyBinding = "", Color = BTSColors.Orders.top_order };
|
||||
case CustomOrderDefinition.CustomOneofCase.None: goto default;
|
||||
//case CustomOrderDefinition.CustomOneofCase.QueueHolder: return new OrderMetadata { KeyBinding = "Q", Color = BTSColors.purple.rgb_color() };
|
||||
@@ -306,6 +304,30 @@ namespace mbts.spark.utilities {
|
||||
|
||||
public static OneofDescriptor oneof_descriptor => _oneof_desc;
|
||||
|
||||
const double _price_error_margin = 0.0000001;
|
||||
const decimal _price_error_margin_decimal = 0.0000001m;
|
||||
|
||||
public static readonly IComparer<double> PriceComparer = Comparer<double>.Create((a, b) => {
|
||||
if (double.IsNaN(a)) {
|
||||
return double.IsNaN(b) ? 0 : -1;
|
||||
} else if (double.IsNaN(b)) {
|
||||
return 1;
|
||||
} else {
|
||||
double diff = a - b;
|
||||
if (diff <= _price_error_margin && diff >= -_price_error_margin)
|
||||
return 0;
|
||||
return diff > 0 ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
public static readonly IComparer<decimal> PriceComparerDecimal = Comparer<decimal>.Create((a, b) => {
|
||||
decimal diff = a - b;
|
||||
if (diff <= _price_error_margin_decimal && diff >= -_price_error_margin_decimal)
|
||||
return 0;
|
||||
return diff > 0 ? 1 : -1;
|
||||
});
|
||||
|
||||
const string _primary_child_field = "child";
|
||||
// algo-type-case maps
|
||||
// Note: obv this will break if we change the name of the 'CustomOneofCase' field
|
||||
static readonly OneofDescriptor _oneof_desc = CustomOrderDefinition.Descriptor.Oneofs.FirstOrDefault(fd => fd.Name == "custom");
|
||||
@@ -349,35 +371,69 @@ namespace mbts.spark.utilities {
|
||||
f.FieldType == FieldType.Message && f.MessageType.ClrType == typeof(CustomOrderDefinition);
|
||||
}
|
||||
|
||||
public static IEnumerable<CustomOrderDefinition> enumerate_algo_children(CustomOrderDefinition algo) {
|
||||
switch (algo.CustomCase) {
|
||||
#region Algo Structure/Hierarchy
|
||||
public static IEnumerable<(CustomOrderDefinition.CustomOneofCase type, string dyn, uint studio, uint spreader_sec)> enumerate_algo_structure(CustomOrderDefinitionImmutable custom_defi) {
|
||||
string dyn = null;
|
||||
uint studio = 0, spreader_sec = 0;
|
||||
foreach (var cd in enumerate_algo_hierarchy(custom_defi.CloneToMutable())) {
|
||||
switch (cd.CustomCase) {
|
||||
case CustomOrderDefinition.CustomOneofCase.Composite: studio = cd.Composite.AlgoTemplateKey; break;
|
||||
case CustomOrderDefinition.CustomOneofCase.Dynamic: dyn = cd.Dynamic.DynamicOrderType; break;
|
||||
case CustomOrderDefinition.CustomOneofCase.Spreader: spreader_sec = cd.Spreader.Security.Id; break;
|
||||
}
|
||||
yield return (cd.CustomCase, dyn, studio, spreader_sec);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates through every algo member immediately defined <paramref name="custom_def"/> (i.e., no recursion, just the first level of children).
|
||||
/// </summary>
|
||||
/// <param name="custom_defi">The root algo, from which enumeration begins.</param>
|
||||
/// <returns>An IEnumerable of each algo member contained in <paramref name="custom_def"/></returns>
|
||||
public static IEnumerable<CustomOrderDefinition> enumerate_algo_children(CustomOrderDefinition custom_def) {
|
||||
switch (custom_def.CustomCase) {
|
||||
case CustomOrderDefinition.CustomOneofCase.Dynamic:
|
||||
foreach (var child in enumerate_dynamic_algo_children(algo.Dynamic))
|
||||
yield return child;
|
||||
foreach (var kv in custom_def.Dynamic.OrderParams) {
|
||||
if (kv.Value.ValueCase == DynamicOrderParam.ValueOneofCase.AsOrderDef)
|
||||
yield return kv.Value.AsOrderDef ?? new CustomOrderDefinition();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
foreach (var cad in get_child_algo_data(algo)) {
|
||||
if (cad.get_child(algo) is CustomOrderDefinition child)
|
||||
yield return child;
|
||||
var child_algo_data = get_child_algo_data(custom_def);
|
||||
if (child_algo_data.Count > 0) {
|
||||
// need to convert to underlying proto type to use the [reflection-based]generated child selectors
|
||||
for (int i = 0; i < child_algo_data.Count; i++) {
|
||||
// don't skip undefined children (this normalizes the structure).
|
||||
// each algo type should always yield the same number of children.
|
||||
yield return child_algo_data[i].get_child(custom_def) ?? new CustomOrderDefinition();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<CustomOrderDefinition> enumerate_dynamic_algo_children(DynamicOrderDefinition dynamic_algo) {
|
||||
foreach (var kv in dynamic_algo.OrderParams) {
|
||||
if (kv.Value.ValueCase == DynamicOrderParam.ValueOneofCase.AsOrderDef)
|
||||
yield return kv.Value.AsOrderDef;
|
||||
/// <summary>
|
||||
/// Enumerates through every algo member contained in <paramref name="custom_def"/>, excluding <paramref name="custom_def"/> itself.
|
||||
/// </summary>
|
||||
/// <param name="custom_defi">The root algo, from which enumeration begins.</param>
|
||||
/// <returns>An IEnumerable of each algo member contained in <paramref name="custom_def"/></returns>
|
||||
public static IEnumerable<CustomOrderDefinition> enumerate_algo_descendents(CustomOrderDefinition custom_def) {
|
||||
foreach (var child in enumerate_algo_children(custom_def)) {
|
||||
yield return child;
|
||||
foreach (var grand_child in enumerate_algo_descendents(child))
|
||||
yield return grand_child;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<CustomOrderDefinition> enumerate_algo_hierarchy(CustomOrderDefinition algo) {
|
||||
yield return algo;
|
||||
foreach (var child in enumerate_algo_children(algo)) {
|
||||
yield return child;
|
||||
foreach (var grand_child in enumerate_algo_hierarchy(child))
|
||||
yield return grand_child;
|
||||
}
|
||||
/// <summary>
|
||||
/// Enumerates through every algo member contained in <paramref name="custom_def"/>, including <paramref name="custom_def"/> itself.
|
||||
/// </summary>
|
||||
/// <param name="custom_def">The root algo, from which enumeration begins.</param>
|
||||
/// <returns>An IEnumerable of each algo member contained in <paramref name="custom_def"/></returns>
|
||||
public static IEnumerable<CustomOrderDefinition> enumerate_algo_hierarchy(CustomOrderDefinition custom_def) {
|
||||
yield return custom_def;
|
||||
foreach (var d in enumerate_algo_descendents(custom_def))
|
||||
yield return d;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ChildAlgoData> get_child_algo_data(CustomOrderDefinitionImmutable defi) =>
|
||||
@@ -405,6 +461,12 @@ namespace mbts.spark.utilities {
|
||||
}
|
||||
}
|
||||
|
||||
public static ChildAlgoData? get_primary_child_algo_data(CustomOrderDefinition algo) {
|
||||
var child_data = get_child_algo_data(algo);
|
||||
return child_data.TryGetFirst(cad => cad.name == _primary_child_field, out ChildAlgoData best_match) ? best_match :
|
||||
child_data.TryGetFirst(out best_match) ? best_match : (ChildAlgoData?)null;
|
||||
}
|
||||
|
||||
public static CustomOrderDefinition create_algo(CustomOrderDefinition.CustomOneofCase algo_type) {
|
||||
var algo = new CustomOrderDefinition();
|
||||
if (_algo_type_case_descriptor_data.TryGetValue(algo_type, out AlgoTypeCaseDescriptorData desc))
|
||||
@@ -412,8 +474,18 @@ namespace mbts.spark.utilities {
|
||||
return algo;
|
||||
}
|
||||
|
||||
public static bool append_algo_node(CustomOrderDefinition target, CustomOrderDefinition.CustomOneofCase algo_type) {
|
||||
if (_algo_type_case_descriptor_data.TryGetValue(algo_type, out var desc)) {
|
||||
var node = ProtoHelper.mutable_factory(ProtoHelper.get_field_type(desc.type_case_descriptor));
|
||||
desc.type_case_descriptor.Accessor.SetValue(target, node);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ChildAlgoData> get_child_algo_data(CustomOrderDefinition.CustomOneofCase algo_type) =>
|
||||
_algo_type_case_descriptor_data.TryGetValue(algo_type, out AlgoTypeCaseDescriptorData d) ? d.child_algo_data : Array.Empty<ChildAlgoData>();
|
||||
#endregion
|
||||
|
||||
public static OrderMetadata get_metadata(CustomOrderDefinition.CustomOneofCase order_type) {
|
||||
switch (order_type) {
|
||||
@@ -459,6 +531,7 @@ namespace mbts.spark.utilities {
|
||||
case CustomOrderDefinition.CustomOneofCase.Trigger: return "TG";
|
||||
case CustomOrderDefinition.CustomOneofCase.Join: return "JN";
|
||||
case CustomOrderDefinition.CustomOneofCase.Spreader: return "SR";
|
||||
case CustomOrderDefinition.CustomOneofCase.Composite: return "COMP";
|
||||
default: return algo_type.ToString();
|
||||
}
|
||||
}
|
||||
@@ -496,7 +569,7 @@ namespace mbts.spark.utilities {
|
||||
|
||||
public static IReadOnlyDictionary<AlgoKey, AlgoData> get_format_algo_data(AlgoFormats f) {
|
||||
var algo_types = get_format_algo_types(f);
|
||||
var algo_dict = algo_types.Select(at => new AlgoData(Defaults.CustomOrderDefinitionImmutable(at), at.display_name(), new AlgoKey(at))).ToDictionaryCrushed(a => a.key);
|
||||
var algo_dict = algo_types.Select(at => AlgoData.from_custom_def(Defaults.CustomOrderDefinitionImmutable(at))).ToDictionaryCrushed(a => a.key);
|
||||
return algo_dict;
|
||||
}
|
||||
|
||||
@@ -582,19 +655,9 @@ namespace mbts.spark.utilities {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a key that is uniquely identified by <see cref="CustomOrderDefinition.CustomOneofCase"/>,
|
||||
/// and potentially <see cref="DynamicOrderDefinition.DynamicOrderType"/> (if it is a <see cref="DynamicOrderDefinition"/>).
|
||||
/// </summary>
|
||||
public static AlgoKey get_shallow_factory_key(CustomOrderDefinitionImmutable defi) =>
|
||||
new AlgoKey(new AlgoNameStructureKey(
|
||||
defi.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic ? defi.Dynamic.DynamicOrderType : defi.CustomCase.type_id(),
|
||||
new AlgoStructureKey(defi.CustomCase)));
|
||||
|
||||
public static readonly CustomOrderDefinition.CustomOneofCase[]
|
||||
// The following standard types are not yet ready for production use:
|
||||
// CustomOrderDefinition.CustomOneofCase.Tick,
|
||||
// CustomOrderDefinition.CustomOneofCase.Dynamic,
|
||||
|
||||
// exchange_order_types are the types which can be submitted on an exchange security
|
||||
exchange_order_types = new[] {
|
||||
CustomOrderDefinition.CustomOneofCase.Market,
|
||||
CustomOrderDefinition.CustomOneofCase.Limit,
|
||||
@@ -605,24 +668,32 @@ namespace mbts.spark.utilities {
|
||||
CustomOrderDefinition.CustomOneofCase.Lean,
|
||||
CustomOrderDefinition.CustomOneofCase.Join,
|
||||
CustomOrderDefinition.CustomOneofCase.Cover,
|
||||
CustomOrderDefinition.CustomOneofCase.Top
|
||||
CustomOrderDefinition.CustomOneofCase.Top,
|
||||
CustomOrderDefinition.CustomOneofCase.Offset,
|
||||
CustomOrderDefinition.CustomOneofCase.Splitter,
|
||||
CustomOrderDefinition.CustomOneofCase.Scratch,
|
||||
CustomOrderDefinition.CustomOneofCase.Prorata,
|
||||
},
|
||||
// The following spreader types are not yet ready for production use:
|
||||
// CustomOrderDefinition.CustomOneofCase.Dynamic,
|
||||
spreader_order_types = new[] {
|
||||
CustomOrderDefinition.CustomOneofCase.Spreader,
|
||||
CustomOrderDefinition.CustomOneofCase.Iceberg,
|
||||
CustomOrderDefinition.CustomOneofCase.Lean,
|
||||
CustomOrderDefinition.CustomOneofCase.Join,
|
||||
CustomOrderDefinition.CustomOneofCase.Cover,
|
||||
CustomOrderDefinition.CustomOneofCase.Top
|
||||
CustomOrderDefinition.CustomOneofCase.Cover,
|
||||
CustomOrderDefinition.CustomOneofCase.Top,
|
||||
CustomOrderDefinition.CustomOneofCase.Splitter,
|
||||
CustomOrderDefinition.CustomOneofCase.Scratch,
|
||||
CustomOrderDefinition.CustomOneofCase.Prorata,
|
||||
},
|
||||
attachable_order_types = new[] {
|
||||
CustomOrderDefinition.CustomOneofCase.Iceberg,
|
||||
CustomOrderDefinition.CustomOneofCase.Lean,
|
||||
CustomOrderDefinition.CustomOneofCase.Join,
|
||||
CustomOrderDefinition.CustomOneofCase.Cover,
|
||||
CustomOrderDefinition.CustomOneofCase.Top
|
||||
CustomOrderDefinition.CustomOneofCase.Top,
|
||||
CustomOrderDefinition.CustomOneofCase.Offset,
|
||||
CustomOrderDefinition.CustomOneofCase.Scratch,
|
||||
CustomOrderDefinition.CustomOneofCase.Prorata,
|
||||
},
|
||||
outright_order_types = new[] {
|
||||
CustomOrderDefinition.CustomOneofCase.Market,
|
||||
@@ -688,17 +759,11 @@ namespace mbts.spark.utilities {
|
||||
// That causes problems in some usages (e.g., AlgoStudio, and the Ladder/Ticket algo popup), bc double.NaN != double.NaN.
|
||||
// These equality comparers should be user in such cases, as they use the actual double.Equals method, not the == operator.
|
||||
// https://github.com/protocolbuffers/protobuf/issues/3725
|
||||
public static readonly IEqualityComparer<CustomOrderDefinitionImmutable> custom_order_def_compareri;
|
||||
public static readonly IEqualityComparer<CompositeAlgoDefinitionImmutable> algo_def_compareri;
|
||||
public static readonly IEqualityComparer<CustomOrderDefinitionImmutable>
|
||||
custom_order_defi_comparer = new CustomDefinitionComparer(),
|
||||
custom_order_defi_structure_comparer = new CustomDefinitionStructureComparer();
|
||||
|
||||
static EqualsHelpers() {
|
||||
int mod = MathUtils.Primes.P_1087;
|
||||
custom_order_def_compareri = new _Compareri(mod);
|
||||
algo_def_compareri = new CompositeAlgoDefinitionImmutableComparer(mod, custom_order_def_compareri);
|
||||
}
|
||||
|
||||
public static bool equals(CustomOrderDefinitionImmutable a, CustomOrderDefinitionImmutable b) => custom_order_def_compareri.Equals(a, b);
|
||||
public static bool equals(CompositeAlgoDefinitionImmutable a, CompositeAlgoDefinitionImmutable b) => algo_def_compareri.Equals(a, b);
|
||||
public static bool equals(CustomOrderDefinitionImmutable a, CustomOrderDefinitionImmutable b) => custom_order_defi_comparer.Equals(a, b);
|
||||
|
||||
public static bool structure_equals(CustomOrderDefinitionImmutable a, CustomOrderDefinitionImmutable b) {
|
||||
return impl(a.CloneToMutable(), b.CloneToMutable());
|
||||
@@ -718,7 +783,7 @@ namespace mbts.spark.utilities {
|
||||
!b.HasChild);
|
||||
|
||||
public static bool top_equals(TopOrderDefinitionImmutable a, TopOrderDefinitionImmutable b) =>
|
||||
a.WatchLevelTickOffset == b.WatchLevelTickOffset &&
|
||||
a.TicksOff == b.TicksOff &&
|
||||
(a.HasTopThreshold ?
|
||||
b.HasTopThreshold && quantity_measure_equals(a.TopThreshold, b.TopThreshold) :
|
||||
!b.HasTopThreshold) &&
|
||||
@@ -843,11 +908,7 @@ namespace mbts.spark.utilities {
|
||||
}
|
||||
}
|
||||
|
||||
class _Compareri : IEqualityComparer<CustomOrderDefinitionImmutable> {
|
||||
readonly int _mod;
|
||||
|
||||
public _Compareri(int mod) => _mod = mod;
|
||||
|
||||
class CustomDefinitionComparer : IEqualityComparer<CustomOrderDefinitionImmutable> {
|
||||
public bool Equals(CustomOrderDefinitionImmutable a, CustomOrderDefinitionImmutable b) {
|
||||
if (a.CustomCase != b.CustomCase) return false;
|
||||
switch (a.CustomCase) {
|
||||
@@ -909,7 +970,7 @@ namespace mbts.spark.utilities {
|
||||
add_component(_get_hash(_get_tick_child(tick)));
|
||||
break;
|
||||
case CustomOrderDefinition.CustomOneofCase.Top:
|
||||
add_component(def.Top.WatchLevelTickOffset.GetHashCode());
|
||||
add_component(def.Top.TicksOff.GetHashCode());
|
||||
add_component(def.Top.HasTopThreshold ? def.Top.TopThreshold.GetHashCode() : 0);
|
||||
add_component(_get_top_child(def.Top).GetHashCode());
|
||||
break;
|
||||
@@ -931,29 +992,17 @@ namespace mbts.spark.utilities {
|
||||
}
|
||||
|
||||
void _add_component(ref int hash, ref int phase_mult, int component_hash) =>
|
||||
hash += EqualityComparer.phase_hash(component_hash, phase_mult++, _mod);
|
||||
hash += EqualityComparer.phase_hash(component_hash, phase_mult++, MathUtils.Primes.P_1087);
|
||||
|
||||
int _get_hash(CustomOrderDefinitionImmutable? a) => a.HasValue ? GetHashCode(a.Value) : 0;
|
||||
}
|
||||
|
||||
class CompositeAlgoDefinitionImmutableComparer : IEqualityComparer<CompositeAlgoDefinitionImmutable> {
|
||||
readonly int _mod;
|
||||
readonly IEqualityComparer<CustomOrderDefinitionImmutable> _def_comparer;
|
||||
class CustomDefinitionStructureComparer : IEqualityComparer<CustomOrderDefinitionImmutable> {
|
||||
public bool Equals(CustomOrderDefinitionImmutable def1, CustomOrderDefinitionImmutable def2) =>
|
||||
enumerate_algo_structure(def1).SequenceEqual(enumerate_algo_structure(def2));
|
||||
|
||||
public CompositeAlgoDefinitionImmutableComparer(int mod, IEqualityComparer<CustomOrderDefinitionImmutable> def_comparer) {
|
||||
_mod = mod;
|
||||
_def_comparer = def_comparer;
|
||||
}
|
||||
|
||||
public bool Equals(CompositeAlgoDefinitionImmutable x, CompositeAlgoDefinitionImmutable y) =>
|
||||
x.Name == y.Name &&
|
||||
x.HasAlgo ?
|
||||
y.HasAlgo && _def_comparer.Equals(x.Algo, y.Algo) :
|
||||
!y.HasAlgo;
|
||||
|
||||
public int GetHashCode(CompositeAlgoDefinitionImmutable algo_def) =>
|
||||
EqualityComparer.safe_get_hash_code(algo_def.Name) % _mod +
|
||||
EqualityComparer.phase_hash(algo_def.HasAlgo ? _def_comparer.GetHashCode(algo_def.Algo) : 0, 1, _mod);
|
||||
public int GetHashCode(CustomOrderDefinitionImmutable def) =>
|
||||
EqualityComparer.auto_phase(enumerate_algo_structure(def));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,8 +1021,8 @@ namespace mbts.spark.utilities {
|
||||
// param types
|
||||
public static QuantityMeasure QuantityMeasure => new QuantityMeasure { Absolute = 1 };
|
||||
// core data types
|
||||
public static AlgoData AlgoData() => new AlgoData(CustomOrderDefinitionImmutable());
|
||||
public static AlgoData AlgoData(CustomOrderDefinition.CustomOneofCase algo_type) => new AlgoData(CustomOrderDefinitionImmutable(algo_type));
|
||||
public static AlgoData AlgoData() => spark.AlgoData.from_custom_def(CustomOrderDefinitionImmutable());
|
||||
public static AlgoData AlgoData(CustomOrderDefinition.CustomOneofCase algo_type) => spark.AlgoData.from_custom_def(CustomOrderDefinitionImmutable(algo_type));
|
||||
|
||||
public static OrderDefinition OrderDefinition => new OrderDefinition { CustomDef = new CustomOrderDefinition() };
|
||||
public static OrderRecord OrderRecord => new OrderRecord {
|
||||
@@ -1008,6 +1057,9 @@ namespace mbts.spark.utilities {
|
||||
case bts.api.orderman.CustomOrderDefinition.CustomOneofCase.Trigger: rv.Trigger = TriggerOrderDefinition(); break;
|
||||
case bts.api.orderman.CustomOrderDefinition.CustomOneofCase.Spreader: rv.Spreader = SpreaderOrderDefinition(); break;
|
||||
case bts.api.orderman.CustomOrderDefinition.CustomOneofCase.Dynamic: rv.Dynamic = DynamicOrderDefinition(); break;
|
||||
default:
|
||||
OrderTypeVMFactory.GenericOrderFactory.setup_generic_custom_order(rv, order_type);
|
||||
break;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
@@ -1033,6 +1085,9 @@ namespace mbts.spark.utilities {
|
||||
public static TriggerOrderDefinition TriggerOrderDefinition() => new TriggerOrderDefinition { Child = CustomOrderDefinition(), TriggerCondition = new TriggerCondition(), TriggerAction = new TriggerAction(), };
|
||||
public static DynamicOrderDefinition DynamicOrderDefinition() => new DynamicOrderDefinition();
|
||||
public static SpreaderOrderDefinition SpreaderOrderDefinition() => new SpreaderOrderDefinition { ExecutionStrategy = new ExecutionStrategy() };
|
||||
|
||||
public static SplitterOrderDefinition SplitterOrderDefinition() => new SplitterOrderDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.utils;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -16,24 +14,26 @@ namespace mbts.spark {
|
||||
/// </summary>
|
||||
class OrderParamsAggregator {
|
||||
readonly TimeSpan _throttle;
|
||||
readonly CommonValueGroupMonitor<AlgoSecurityKey, AlgoSecurityKey> _algo_def_mon =
|
||||
new CommonValueGroupMonitor<AlgoSecurityKey, AlgoSecurityKey>(
|
||||
readonly CommonValueGroupMonitor<CustomOrderDefinitionImmutable, CustomOrderDefinitionImmutable> _algo_def_mon =
|
||||
new CommonValueGroupMonitor<CustomOrderDefinitionImmutable, CustomOrderDefinitionImmutable>(
|
||||
ask => ask,
|
||||
EqualityComparer<AlgoSecurityKey>.Default,
|
||||
EqualityComparer.Create<AlgoSecurityKey, CustomOrderDefinitionImmutable>(ask => ask.algo, OrderDataHelper.EqualsHelpers.custom_order_def_compareri));
|
||||
OrderDataHelper.EqualsHelpers.custom_order_defi_structure_comparer,
|
||||
OrderDataHelper.EqualsHelpers.custom_order_defi_comparer);
|
||||
readonly CommonValueMonitor<SecurityImmutable?> _spreader_security_mon = new CommonValueMonitor<SecurityImmutable?>();
|
||||
readonly CommonValueMonitor<SecurityImmutable?> _security_mon = new CommonValueMonitor<SecurityImmutable?>();
|
||||
readonly CommonValueMonitor<double> _price_mon = new CommonValueMonitor<double>(double.NaN);
|
||||
readonly CommonValueMonitor<uint> _size_mon = new CommonValueMonitor<uint>();
|
||||
readonly CommonValueMonitor<AccountTuple> _account_mon = new CommonValueMonitor<AccountTuple>();
|
||||
readonly CommonValueMonitor<bool> _all_active_mon = new CommonValueMonitor<bool>();
|
||||
readonly CommonValueMonitorMultiplexor<OrderSecurity> _group_mon = new CommonValueMonitorMultiplexor<OrderSecurity>();
|
||||
readonly CommonValueMonitorMultiplexor<OrderSecurityAlgo> _group_mon = new CommonValueMonitorMultiplexor<OrderSecurityAlgo>();
|
||||
readonly DynamicOrderHelper _dyn_helper;
|
||||
readonly Func<uint, OrderSecurityAlgo?> _lookup_order;
|
||||
|
||||
public OrderParamsAggregator(DynamicOrderHelper dyn_helper, TimeSpan throttle) {
|
||||
public OrderParamsAggregator(DynamicOrderHelper dyn_helper, TimeSpan throttle, Func<uint, OrderSecurityAlgo?> lookup_order) {
|
||||
_dyn_helper = dyn_helper;
|
||||
_throttle = throttle;
|
||||
_group_mon.add_member_mon(_algo_def_mon, _algo_key);
|
||||
_lookup_order = lookup_order;
|
||||
_group_mon.add_member_mon(_algo_def_mon, _build_algo_tree);
|
||||
_group_mon.add_member_mon(_spreader_security_mon, _get_spreader_sec, s => s.HasValue);
|
||||
_group_mon.add_member_mon(_security_mon, osam => osam.security, s => s.HasValue);
|
||||
_group_mon.add_member_mon(_price_mon, osam => osam.order.price());
|
||||
@@ -49,7 +49,7 @@ namespace mbts.spark {
|
||||
public AccountTuple? account => _account_mon.common_value_nullable();
|
||||
public bool all_active => _all_active_mon.common_value;
|
||||
|
||||
public IObservable<IEnumerable<CustomOrderDefinitionImmutable>> subscribe_algos() => _algo_def_mon.SubscribeThrottled(_throttle, (ask, vals) => vals.Select(v => v.algo), Enumerable.Empty<CustomOrderDefinitionImmutable>());
|
||||
public IObservable<IEnumerable<CustomOrderDefinitionImmutable>> subscribe_algos() => _algo_def_mon.SubscribeThrottled(_throttle, (ask, vals) => vals, Enumerable.Empty<CustomOrderDefinitionImmutable>());
|
||||
public IObservable<SecurityImmutable?> subscribe_spreader_security() => _spreader_security_mon.SubscribeThrottled(_throttle);
|
||||
public IObservable<SecurityImmutable?> subscribe_security() => _security_mon.SubscribeThrottled(_throttle);
|
||||
public IObservable<double> subscribe_price() => _price_mon.SubscribeThrottled(_throttle);
|
||||
@@ -61,45 +61,75 @@ namespace mbts.spark {
|
||||
public IDisposable defer_updates() =>
|
||||
_group_mon.defer_updates();
|
||||
|
||||
event Action<TableUpdate<OrderSecurity, uint>> _updated;
|
||||
public void Update(TableUpdate<OrderSecurity, uint> table) {
|
||||
_updated?.Invoke(table);
|
||||
}
|
||||
|
||||
public void Add(OrderSecurity order_tuple) =>
|
||||
public void Add(OrderSecurityAlgo order_tuple) =>
|
||||
_group_mon.add(order_tuple);
|
||||
|
||||
public void Remove(OrderSecurity order_tuple) =>
|
||||
public void Remove(OrderSecurityAlgo order_tuple) =>
|
||||
_group_mon.remove(order_tuple);
|
||||
|
||||
static SecurityImmutable? _get_spreader_sec(OrderSecurity osam) =>
|
||||
public void Clear() =>
|
||||
_group_mon.clear();
|
||||
|
||||
static SecurityImmutable? _get_spreader_sec(OrderSecurityAlgo osam) =>
|
||||
OrderDataHelper.enumerate_algo_hierarchy(osam.order.Def.CustomDef.CloneToMutable())
|
||||
.Any(algo => algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Spreader) ?
|
||||
osam.security : null;
|
||||
|
||||
AlgoSecurityKey _algo_key(OrderSecurity osam) =>
|
||||
new AlgoSecurityKey(osam.order.Def.CustomDef, _get_spreader_sec(osam), AlgoStructureSerializer.structure_key(osam.order.Def.CustomDef, _dyn_helper.dynamic_type_index));
|
||||
CustomOrderDefinitionImmutable _build_algo_tree(OrderSecurityAlgo root) =>
|
||||
root.order.Def.AttachTargetOrderKey == 0 ?
|
||||
root.order.Def.CustomDef :
|
||||
new CustomOrderDefinitionImmutable(_add_attached_targets(root.order.Def.CustomDef.CloneToMutable(), root.order.Key, root.order.Def.AttachTargetOrderKey));
|
||||
|
||||
struct AlgoSecurityKey {
|
||||
public readonly CustomOrderDefinitionImmutable algo;
|
||||
public readonly SecurityImmutable? security;
|
||||
public readonly AlgoStructureKey key;
|
||||
|
||||
public AlgoSecurityKey(CustomOrderDefinitionImmutable algo, SecurityImmutable? security, AlgoStructureKey key) {
|
||||
this.algo = algo;
|
||||
this.security = security;
|
||||
this.key = key;
|
||||
CustomOrderDefinition _add_attached_targets(CustomOrderDefinition mroot, uint root_id, uint attached_target) {
|
||||
var lin = _lineage(root_id, attached_target);
|
||||
if (lin != null && lin.Count > 0) {
|
||||
var mparent = mroot;
|
||||
var pcad = OrderDataHelper.get_primary_child_algo_data(mparent);
|
||||
if (pcad.HasValue) {
|
||||
do {
|
||||
var mchild = lin.Pop().order.Def.CustomDef.CloneToMutable();
|
||||
pcad.Value.assign_child(mparent, mchild);
|
||||
mparent = mchild;
|
||||
pcad = OrderDataHelper.get_primary_child_algo_data(mparent);
|
||||
} while (lin.Count > 0 && pcad.HasValue);
|
||||
}
|
||||
}
|
||||
return mroot;
|
||||
}
|
||||
|
||||
public bool Equals(AlgoSecurityKey other) =>
|
||||
security == other.security &&
|
||||
key == other.key;
|
||||
|
||||
public override bool Equals(object obj) => obj is AlgoSecurityKey other && Equals(other);
|
||||
public override int GetHashCode() => key.GetHashCode();
|
||||
|
||||
public static bool operator ==(AlgoSecurityKey a, AlgoSecurityKey b) => a.Equals(b);
|
||||
public static bool operator !=(AlgoSecurityKey a, AlgoSecurityKey b) => !a.Equals(b);
|
||||
Stack<OrderSecurityAlgo> _lineage(uint root, uint target) {
|
||||
// check args/preconditions
|
||||
if (root == 0) throw new ArgumentException("Cannot be zero.", nameof(root));
|
||||
if (target == 0) throw new ArgumentException("Cannot be zero.", nameof(target));
|
||||
if (target == root) return null;
|
||||
var inner = _lookup_order(target);
|
||||
if (!inner.HasValue)
|
||||
return null;
|
||||
// if 'target' has an attach target iteself (nested), push the nested lineage onto the stack first,
|
||||
// then push the original target onto the stack, and try to hop up each algo-generation
|
||||
// via parent-key lookups until we reach the root.
|
||||
// **If the lineage is disjointed, and we never reach the root, return null**
|
||||
var ival = inner.Value;
|
||||
var lin_stack = ival.order.attached_target_order_key() != 0 ?
|
||||
_lineage(ival.order.Key, ival.order.attached_target_order_key()) ?? new Stack<OrderSecurityAlgo>() :
|
||||
new Stack<OrderSecurityAlgo>();
|
||||
lin_stack.Push(ival);
|
||||
target = ival.order.ParentKey;
|
||||
while (target != root) {
|
||||
if (target == 0) {
|
||||
// **disjointed lineage**
|
||||
return null;
|
||||
}
|
||||
inner = _lookup_order(target);
|
||||
if (!inner.HasValue) {
|
||||
// **disjointed lineage**
|
||||
return null;
|
||||
}
|
||||
ival = inner.Value;
|
||||
lin_stack.Push(ival);
|
||||
target = ival.order.ParentKey;
|
||||
}
|
||||
return lin_stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,6 @@ namespace mbts.spark {
|
||||
class OrderPropertiesEditorVM : DisposableViewModelBase {
|
||||
readonly OrdersModel _model;
|
||||
readonly Property<SecurityImmutable> _security;
|
||||
readonly Property<AlgoDefinitionTemplateVM> _selected_algo_template, _selected_favorite_algo_template;
|
||||
readonly SelectorProperty<AccountTuple, AccountTuple?> _accounts;
|
||||
readonly SelectorProperty<TimeInForce> _time_in_force;
|
||||
readonly SelectionModel<AlgoDefinitionVM> _selected_algo_definition;
|
||||
@@ -60,9 +59,8 @@ namespace mbts.spark {
|
||||
readonly ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey> _algo_definitions = new ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey>(d => d.key);
|
||||
readonly ListCollectionView<AlgoDefinitionVM> _algo_definitions_view, _favorite_algo_definitions_view;
|
||||
readonly TableUpdateBuffer<AlgoData, AlgoKey> _definitions_buffer = new TableUpdateBuffer<AlgoData, AlgoKey>(d => d.key);
|
||||
readonly EditableCollectionModel<AlgoDefinitionTemplateVM, AlgoDefinitionVM> _algo_template_collection_model;
|
||||
readonly ObservableKeyedCollection<AlgoDefinitionTemplateVM, AlgoKey> _favorite_templates = new ObservableKeyedCollection<AlgoDefinitionTemplateVM, AlgoKey>(tvm => tvm.key);
|
||||
readonly ObservableAggregateCollection<AlgoParamRowVM> _algo_param_rows = new ObservableAggregateCollection<AlgoParamRowVM>();
|
||||
readonly AlgoDefinitionVM the_anchor_algo_def_vm;
|
||||
IReadOnlyDictionary<AlgoKey, AlgoDefinitionMetadata> _algo_metadata = DictionaryHelper.empty<AlgoKey, AlgoDefinitionMetadata>();
|
||||
IReadOnlyDictionary<AlgoKey, int> _favorite_algos_index = DictionaryHelper.empty<AlgoKey, int>();
|
||||
IDisposable _algo_param_rows_hook;
|
||||
@@ -71,10 +69,10 @@ namespace mbts.spark {
|
||||
|
||||
public OrderPropertiesEditorVM(OrdersModel model, IObservable<SecurityMD> security_md_source, IObservable<InlineOrderProperties> order_properties) {
|
||||
_model = model;
|
||||
the_anchor_algo_def_vm = AlgoDefinitionVM.create(_model, OrderDataHelper.Defaults.AlgoData(CustomOrderDefinition.CustomOneofCase.Limit));
|
||||
var security_source = security_md_source.Select(smd => smd.security);
|
||||
_security = backing(nameof(security), security_source, SecurityDataHelper.Defaults.SecurityImmutable);
|
||||
security_source = _security.observe_current(true);
|
||||
_selected_algo_template = backing<AlgoDefinitionTemplateVM>(nameof(selected_algo_template));
|
||||
_selected_algo_definition = new SelectionModel<AlgoDefinitionVM>(algo_definitions,
|
||||
() => {
|
||||
var t = _model.factory.default_order_type(security);
|
||||
@@ -105,13 +103,18 @@ namespace mbts.spark {
|
||||
var inline_account_src = order_properties.AggregateLatest(
|
||||
model.factory.subscribe_accounts(),
|
||||
(last, change, cache) => convert_inline_account(change.New.account, cache),
|
||||
(last, props, change) => convert_inline_account(props.account, change.New),
|
||||
(last, props, change) => convert_inline_account(props.account, change.New, false),
|
||||
() => new InlineUserProperty<AccountTuple>(),
|
||||
new InlineOrderProperties(),
|
||||
DictionaryHelper.empty<uint, string>());
|
||||
// Note that with the .Where on the account_tuples, we will ignore any
|
||||
// updates for which all accounts have been removed.
|
||||
// This was a safer change than changing SelectorProperty's IEnumberable.OnNext
|
||||
// so that it would not reset the selected item on server reboots; i.e. this
|
||||
// change only affects this IObservable.
|
||||
_accounts = backing(
|
||||
nameof(selected_account),
|
||||
model.factory.subscribe_account_tuples(),
|
||||
model.factory.subscribe_account_tuples().Where(items => items.Any()),
|
||||
inline_account_src,
|
||||
a => new AccountTuple?(a),
|
||||
() => accounts.Cast<AccountTuple?>().FirstOrDefault(),
|
||||
@@ -126,29 +129,8 @@ namespace mbts.spark {
|
||||
order_properties.Select(ops => ops.time_in_force),
|
||||
() => model.factory.default_time_in_force(security),
|
||||
tif => time_in_forces.Contains(tif));
|
||||
_selected_favorite_algo_template = backing<AlgoDefinitionTemplateVM>(nameof(selected_favorite_algo_template));
|
||||
// items source views
|
||||
algo_templates_view = CollectionViewSource.GetDefaultView(algo_templates);
|
||||
var favorite_algo_templates_view = new ListCollectionView(ObservableAggregateCollection.create(_algo_definitions.AsCollection(), d => d.templates));
|
||||
favorite_algo_templates_view.LiveGroupingProperties.Add(nameof(AlgoDefinitionTemplateVM.owner_key));
|
||||
favorite_algo_templates_view.LiveFilteringProperties.Add(nameof(AlgoDefinitionTemplateVM.is_favorite));
|
||||
favorite_algo_templates_view.Filter = item => (item as AlgoDefinitionTemplateVM)?.is_favorite ?? false;
|
||||
favorite_algo_templates_view.IsLiveFiltering = true;
|
||||
this.favorite_algo_templates_view = favorite_algo_templates_view;
|
||||
// commands
|
||||
save_definition_cmd = new AsyncCommand<AlgoDefinitionVM>(save_definition_async);
|
||||
save_template_cmd = new AsyncCommand<AlgoDefinitionTemplateVM>(save_template_async);
|
||||
remove_template_cmd = new AsyncCommand<AlgoDefinitionTemplateVM>(tvm => _model.algo_model.remove_template(tvm.key));
|
||||
toggle_is_favorite_cmd = new DelegateCommand<AlgoDefinitionTemplateVM>(tvm => _favorite_templates.Add(tvm));
|
||||
_algo_template_collection_model = new EditableCollectionModel<AlgoDefinitionTemplateVM, AlgoDefinitionVM>(d => selected_algo_definition == null ? default : selected_algo_definition.template_factory(), d => d != null);
|
||||
redo_cmd = ObservantCommand.create(
|
||||
t => t?.redo(),
|
||||
t => t == null ? Observable.Return(false) : System.Reactive.Linq.ObservableExtensions.FromEvent(cb => t.can_redo_changed += cb, cb => t.can_redo_changed -= cb, () => t.can_redo).OnDispatcher(),
|
||||
out IObserver<AlgoDefinitionTemplateVM> redo_observer);
|
||||
undo_cmd = ObservantCommand.create(
|
||||
t => t?.undo(),
|
||||
t => t == null ? Observable.Return(false) : System.Reactive.Linq.ObservableExtensions.FromEvent(cb => t.can_undo_changed += cb, cb => t.can_undo_changed -= cb, () => t.can_undo).OnDispatcher(),
|
||||
out IObserver<AlgoDefinitionTemplateVM> undo_observer);
|
||||
undo_definition_cmd = ObservantCommand.create(
|
||||
d => d?.undo(),
|
||||
d => d == null ? Observable.Return(false) : System.Reactive.Linq.ObservableExtensions.FromEvent(cb => d.can_undo_changed += cb, cb => d.can_undo_changed -= cb, () => d.can_undo).OnDispatcher(),
|
||||
@@ -172,7 +154,7 @@ namespace mbts.spark {
|
||||
}, out IObserver<bool> revert_algo_params_observer);
|
||||
var algo_params_dirty_mon = new ReactiveThresholdMonitor<AlgoParamRowVM>(r => r.param.subscribe_is_dirty());
|
||||
var nullable_security_md_source = security_md_source.Select(smd => new SecurityMD?(smd)).RefCountReplay(1);
|
||||
var inline_algo_relay = new SelectionModelInlinePropRelay<AlgoDefinitionVM, AlgoDefinitionVM, AlgoID>(_selected_algo_definition, vm => AlgoID.from_algo_key(vm.key, _model.dynamic_helper.dynamic_type_index));
|
||||
var inline_algo_relay = new SelectionModelInlinePropRelay<AlgoDefinitionVM, AlgoDefinitionVM, AlgoID>(_selected_algo_definition, vm => AlgoID.from_algo_key(vm.key));
|
||||
//
|
||||
_dispose = new CompositeDisposable(
|
||||
order_properties.OnDispatcher().Subscribe(props => {
|
||||
@@ -181,31 +163,10 @@ namespace mbts.spark {
|
||||
algo_params_dirty_mon.Subscribe(set_default_algo_params_observer, revert_algo_params_observer),
|
||||
ObservableAutoWrapper.connect_item_hooks(_algo_param_rows, algo_params_dirty_mon.join),
|
||||
_selected_algo_definition.subscribe_changed(_selected_algo_definition_Changed),
|
||||
as_demuxer().subscribe<AlgoDefinitionTemplateVM>(nameof(selected_algo_template)).Subscribe(
|
||||
undo_observer,
|
||||
redo_observer),
|
||||
as_demuxer().subscribe<AlgoDefinitionVM>(nameof(selected_algo_definition)).Subscribe(
|
||||
undo_definition_observer,
|
||||
redo_definition_observer,
|
||||
revert_definition_observer),
|
||||
algo_definitions.Subscribe((s, e) => {
|
||||
if (e.OldItems != null)
|
||||
e.OldItems.OfType<IDisposable>().DisposeAll();
|
||||
if (e.NewItems != null) {
|
||||
foreach (AlgoDefinitionVM d in e.NewItems) {
|
||||
switch (d.key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
if (_model.algo_model.ui_configuration.include_custom_templates && _model.algo_model.templates_enabled())
|
||||
d.prepend_custom_template();
|
||||
break;
|
||||
case AlgoKey.Mode.Factory:
|
||||
if (_model.algo_model.ui_configuration.include_legacy_custom_templates && _model.algo_model.templates_enabled())
|
||||
d.prepend_custom_template();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
security_md_source.Select(smd => model.factory.default_algos(smd.security)).Table(s => s.key).Subscribe(factory_defs => {
|
||||
using (_selected_algo_definition.begin_update())
|
||||
AlgoCollectionHelper.update_persisted_definitions(_algo_definitions, factory_defs, fd => {
|
||||
@@ -217,43 +178,41 @@ namespace mbts.spark {
|
||||
model.algo_model.definitions.Subscribe(_definitions_buffer),
|
||||
_definitions_buffer.OnDispatcher().Subscribe(u => {
|
||||
AlgoCollectionHelper.update_persisted_definitions(_algo_definitions, u, def_data => {
|
||||
var def_vm = AlgoDefinitionVM.create(model, def_data, true, OrderDataHelper.get_algo_format(security.ContractCase), AlgoEditingPermissions.AllowEditValues);
|
||||
var def_vm = AlgoDefinitionVM.create(model, def_data, OrderDataHelper.get_algo_format(security.ContractCase), AlgoEditingPermissions.AllowEditValues);
|
||||
def_vm.selected_format = OrderDataHelper.get_algo_format(security.ContractCase);
|
||||
def_vm.favorite_index = _get_favorite_algo_index(def_vm.key);
|
||||
return def_vm;
|
||||
});
|
||||
}),
|
||||
}),
|
||||
model.algo_model.metadata_service.subscribe_metadata().OnDispatcher().Subscribe(_on_template_metadata),
|
||||
ObservableAutoWrapper.connect_item_hooks(algo_definitions, vm =>
|
||||
Disposable.Create(
|
||||
vm.connect_data(nullable_security_md_source),
|
||||
security_source.Subscribe(s => {
|
||||
var fmt = OrderDataHelper.get_algo_format(s.ContractCase);
|
||||
foreach (var d in algo_definitions)
|
||||
d.selected_format = fmt;
|
||||
}),
|
||||
vm), true)) {
|
||||
() => {
|
||||
Disposable.dispose(ref _template_collection_sync);
|
||||
Disposable.dispose(ref _algo_param_rows_hook);
|
||||
}
|
||||
Disposable.Create(
|
||||
vm.connect_data(nullable_security_md_source),
|
||||
security_source.Subscribe(s => {
|
||||
var fmt = OrderDataHelper.get_algo_format(s.ContractCase);
|
||||
foreach (var d in algo_definitions)
|
||||
d.selected_format = fmt;
|
||||
}),
|
||||
vm),
|
||||
true)) {
|
||||
() => Disposable.dispose(ref _algo_param_rows_hook)
|
||||
};
|
||||
// local methods
|
||||
async Task save_template_async(AlgoDefinitionTemplateVM template) {
|
||||
using (_preserve_selection_by_index())
|
||||
await AlgoPersistenceHelper.persist_template(model, algo_definitions, template);
|
||||
}
|
||||
async Task save_definition_async(AlgoDefinitionVM definition) {
|
||||
switch (definition.key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
await AlgoPersistenceHelper.persist_definition(model, algo_definitions, definition);
|
||||
using (_definitions_buffer.buffer_updates())
|
||||
await _model.algo_model.save_definition(definition.create_algo_def());
|
||||
break;
|
||||
case AlgoKey.Mode.Factory:
|
||||
case AlgoKey.Mode.Unpersisted:
|
||||
using (_definitions_buffer.buffer_updates()) {
|
||||
var persisted_key_lookup = await AlgoPersistenceHelper.persist_definition(model, algo_definitions, definition);
|
||||
if (persisted_key_lookup.TryGetValue(definition.key, out AlgoKey pkey) && _algo_definitions.TryGetValue(pkey, out AlgoDefinitionVM pdef))
|
||||
selected_algo_definition = pdef;
|
||||
var persisted_def = await model.algo_model.save_definition(definition.create_algo_def());
|
||||
if (persisted_def.HasValue) {
|
||||
int pos = algo_definitions.IndexOf(definition);
|
||||
if (pos >= 0)
|
||||
selected_algo_definition = algo_definitions[pos] = definition.to_persisted(persisted_def.Value);
|
||||
}
|
||||
if (definition.key.mode == AlgoKey.Mode.Factory) {
|
||||
/// Revert the factory definition to its original state.
|
||||
/// Note: this won't revert the parameters in the algo popup since the newly persisted definition (<see cref="pdef"/>) is selected now.
|
||||
@@ -263,38 +222,38 @@ namespace mbts.spark {
|
||||
break;
|
||||
}
|
||||
}
|
||||
InlineUserProperty<AccountTuple> convert_inline_account(InlineUserProperty<uint> account_id, IReadOnlyDictionary<uint, string> account_map) =>
|
||||
//
|
||||
// Regarding the 3rd parameter, force_override_from_inline_property:
|
||||
// When using an InlineUserProperty, in the context of assigning a default vs selected item,
|
||||
// there is somewhat of a conflict between the IObserver<InlineUserProperty<TItem>> and the
|
||||
// IObserver<IEnumerable<TItem>> OnNext methods. This 3rd paramter provides yet another override
|
||||
// to say if the force_override should come from the inline property or if the force_override should
|
||||
// be disbled, by way of the immediate return in ViewModelBase._try_auto_select when force_override
|
||||
// is false. Our intent is then accomplished because we are no longer attempting to set a default/
|
||||
// selected item when the account_tuples potentially changes
|
||||
InlineUserProperty<AccountTuple> convert_inline_account(InlineUserProperty<uint> account_id, IReadOnlyDictionary<uint, string> account_map, bool force_override_from_inline_property = true) =>
|
||||
account_id.has_value && account_map.TryGetValue(account_id.value, out string name) ?
|
||||
new InlineUserProperty<AccountTuple>(new AccountTuple(account_id.value, name), account_id.force_override) :
|
||||
new InlineUserProperty<AccountTuple>(new AccountTuple(account_id.value, name), force_override_from_inline_property ? account_id.force_override : false) :
|
||||
new InlineUserProperty<AccountTuple>();
|
||||
}
|
||||
|
||||
IDisposable _template_collection_sync;
|
||||
void _selected_algo_definition_Changed(AlgoDefinitionVM old_value, AlgoDefinitionVM new_value, SelectionReason reason) {
|
||||
Disposable.dispose(ref _template_collection_sync);
|
||||
Disposable.dispose(ref _algo_param_rows_hook);
|
||||
if (old_value != null)
|
||||
// Taha feels that we should always revert to a Limit order either when the user explicity unchecks a sticky button
|
||||
// or when a non-sticky button is pressed. Rather than completely remove the stack at this time (b/c it may be come
|
||||
// back in the future), let's just always push a specfic def_vm.key onto it [Limit] ;)
|
||||
//_selected_algo_stack.Push(old_value.key);
|
||||
_selected_algo_stack.Push(the_anchor_algo_def_vm.key);
|
||||
if (new_value != null) {
|
||||
_template_collection_sync = ObservableAutoWrapper.connect_two_way(new_value.templates_with_custom, algo_templates);
|
||||
if (use_algo_param_grid)
|
||||
_algo_param_rows_hook = _connect_algo_param_rows(new_value, _algo_param_rows);
|
||||
if (reason != SelectionReason.Explicit)
|
||||
_selected_algo_stack.Push(new_value.key);
|
||||
} else {
|
||||
algo_templates.Clear();
|
||||
}
|
||||
if (open_algo_popup_on_select) {
|
||||
if (reason == SelectionReason.UserSelectedNewValue && new_value?.algo_root is IAlgoVM ar && ar.can_expand)
|
||||
ar.is_expanded = true;
|
||||
}
|
||||
if (selected_algo_template == null && _model.algo_model.templates_enabled()) {
|
||||
if (new_value != null)
|
||||
selected_algo_template = new_value.templates.FirstOrDefault(tvm => tvm.is_default, tvm => _favorite_templates.ContainsKey(tvm.key), tvm => true) ?? new_value.templates_with_custom.FirstOrDefault();
|
||||
} else {
|
||||
RaisePropertyChanged(nameof(selected_algo_template));
|
||||
}
|
||||
RaisePropertyChanged(nameof(selected_algo_definition), nameof(selected_favorite_algo_definition), nameof(can_expand), nameof(is_expanded));
|
||||
_selected_favorite_algo_template.Value = selected_algo_template is AlgoDefinitionTemplateVM t && t.is_favorite ? t : null;
|
||||
_algo_template_collection_model.OnNext(new_value);
|
||||
}
|
||||
|
||||
public SecurityImmutable security => _security.Value;
|
||||
@@ -302,7 +261,6 @@ namespace mbts.spark {
|
||||
public AccountTuple? selected_account { get => _accounts.selected_item; set => _accounts.selected_item = value; }
|
||||
public ReadOnlyObservableCollection<TimeInForce> time_in_forces => _time_in_force.items;
|
||||
public TimeInForce selected_time_in_force { get => _time_in_force.selected_item; set => _time_in_force.selected_item = value; }
|
||||
public CustomOrderDefinitionImmutable? definition => selected_algo_template?.algo_root?.definition;
|
||||
public int favorite_algos_column_count { get => _favorite_algos_column_count; private set => Set(ref _favorite_algos_column_count, value); }
|
||||
public bool use_algo_param_grid {
|
||||
get => _use_algo_param_grid;
|
||||
@@ -317,47 +275,26 @@ namespace mbts.spark {
|
||||
}
|
||||
// items sources
|
||||
public ObservableCollection<AlgoDefinitionVM> algo_definitions => _algo_definitions;
|
||||
public ObservableCollection<AlgoDefinitionTemplateVM> algo_templates { get; } = new ObservableCollection<AlgoDefinitionTemplateVM>();
|
||||
public IReadOnlyObservableCollection<AlgoParamRowVM> algo_param_rows => _algo_param_rows;
|
||||
public ICollectionView<AlgoDefinitionVM> algo_definitions_view => _algo_definitions_view;
|
||||
public ICollectionView<AlgoDefinitionVM> favorite_algo_definitions_view => _favorite_algo_definitions_view;
|
||||
public ICollectionView algo_templates_view { get; }
|
||||
public ICollectionView favorite_algo_templates_view { get; }
|
||||
// selected items
|
||||
public AlgoDefinitionVM selected_algo_definition {
|
||||
get => _selected_algo_definition.selected_item;
|
||||
set => _selected_algo_definition.selected_item = value;
|
||||
}
|
||||
public AlgoDefinitionTemplateVM selected_algo_template {
|
||||
get => selected_algo_definition?.selected_template;
|
||||
set {
|
||||
if (value != selected_algo_template) {
|
||||
if (value == null) {
|
||||
if (selected_algo_definition != null)
|
||||
selected_algo_definition.selected_template = null;
|
||||
_selected_favorite_algo_template.set(null);
|
||||
} else {
|
||||
value.owner.selected_template = value;
|
||||
_selected_algo_definition.select_explicit(value.owner); // idempotent
|
||||
_selected_favorite_algo_template.Value = value.is_favorite ? value : null; // idempotent
|
||||
}
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
public AlgoDefinitionTemplateVM selected_favorite_algo_template {
|
||||
get => _selected_favorite_algo_template.Value;
|
||||
set {
|
||||
if (_selected_favorite_algo_template.set_new(value)) {
|
||||
if (value != null) {
|
||||
value.owner.selected_template = value;
|
||||
if (value.owner != selected_algo_definition)
|
||||
_selected_algo_definition.select_explicit(value.owner);
|
||||
RaisePropertyChanged(nameof(selected_algo_template));
|
||||
}
|
||||
}
|
||||
|
||||
public bool try_get_algo_definition(AlgoKey a, out AlgoDefinitionVM result) =>
|
||||
_algo_definitions.TryGetValue(a, out result);
|
||||
|
||||
public bool try_pop_selected_algo_definition() {
|
||||
if (_selected_algo_stack.TryPop(out AlgoKey k) && _algo_definitions.TryGetValue(k, out AlgoDefinitionVM vm)) {
|
||||
_selected_algo_definition.select_explicit(vm);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public AlgoDefinitionVM selected_favorite_algo_definition {
|
||||
get => selected_algo_definition?.is_favorite ?? false ? selected_algo_definition : null;
|
||||
set {
|
||||
@@ -389,13 +326,10 @@ namespace mbts.spark {
|
||||
public ICommand revert_definition_cmd { get; }
|
||||
public ICommand set_default_algo_params_cmd { get; }
|
||||
public ICommand revert_algo_params_cmd { get; }
|
||||
public ICommand undo_cmd { get; }
|
||||
public ICommand redo_cmd { get; }
|
||||
public ICommand undo_definition_cmd { get; }
|
||||
public ICommand redo_definition_cmd { get; }
|
||||
public ICommand remove_template_cmd { get; }
|
||||
public ICommand toggle_is_favorite_cmd { get; }
|
||||
public IEditableCollectionModel<AlgoDefinitionTemplateVM> algo_template_collection_model => _algo_template_collection_model;
|
||||
|
||||
public IObservable<AccountTuple?> observe_account(bool start_immediately = false) =>
|
||||
_accounts.observe_current(start_immediately);
|
||||
@@ -404,9 +338,7 @@ namespace mbts.spark {
|
||||
_time_in_force.observe_current(start_immediately);
|
||||
|
||||
public IObservable<IAlgoVM> observe_order_type(bool start_immediately = false) =>
|
||||
_model.algo_model.templates_enabled() ?
|
||||
_selected_algo_template.observe_current(start_immediately).Select(t => t?.algo_root) :
|
||||
_selected_algo_definition.observe_selected_item(start_immediately).Select(d => d?.algo_root);
|
||||
_selected_algo_definition.observe_selected_item(start_immediately).Select(d => d?.algo_root);
|
||||
|
||||
public OrderDefinition create(Side side) {
|
||||
if (HasErrors)
|
||||
@@ -414,42 +346,32 @@ namespace mbts.spark {
|
||||
var order = _model.factory.order(security, selected_account);
|
||||
order.TimeInForce = selected_time_in_force;
|
||||
order.Side = side;
|
||||
if (_model.algo_model.templates_enabled()) {
|
||||
selected_algo_template.algo_root.assign_to(order.CustomDef);
|
||||
} else {
|
||||
var algo = new CustomOrderDefinition();
|
||||
selected_algo_definition.algo_root.assign_to(algo);
|
||||
switch (selected_algo_definition.key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
order.CustomDef = new CustomOrderDefinition {
|
||||
Composite = new CompositeOrderDefinition {
|
||||
AlgoTemplateKey = selected_algo_definition.key.persisted.id_uint,
|
||||
Definition = algo
|
||||
}
|
||||
};
|
||||
selected_algo_definition.algo_root.assign_to(order.CustomDef.Composite.Definition);
|
||||
break;
|
||||
default:
|
||||
order.CustomDef = algo;
|
||||
break;
|
||||
}
|
||||
var algo = new CustomOrderDefinition();
|
||||
selected_algo_definition.algo_root.assign_to(algo);
|
||||
switch (selected_algo_definition.key.mode) {
|
||||
case AlgoKey.Mode.Persisted:
|
||||
order.CustomDef = new CustomOrderDefinition {
|
||||
Composite = new CompositeOrderDefinition {
|
||||
AlgoTemplateKey = selected_algo_definition.key.persisted,
|
||||
Definition = algo
|
||||
}
|
||||
};
|
||||
selected_algo_definition.algo_root.assign_to(order.CustomDef.Composite.Definition);
|
||||
break;
|
||||
default:
|
||||
order.CustomDef = algo;
|
||||
break;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
public OrderMetadata get_metadata(CustomOrderDefinition.CustomOneofCase order_type) =>
|
||||
_model.factory.get_metadata(order_type);
|
||||
|
||||
public void on_favorite_algos(LadderFavoriteAlgosSettings value) {
|
||||
_favorite_algos_index = value.favorite_algos.Select((v, i) => (v, i)).ToDictionaryCrushed(t => t.v, t => t.i);
|
||||
foreach (var d in algo_definitions) {
|
||||
d.favorite_index = _get_favorite_algo_index(d.key);
|
||||
foreach (var t in d.templates) {
|
||||
if (t.key.mode == AlgoKey.Mode.Persisted)
|
||||
t.is_favorite = _favorite_algos_index.ContainsKey(t.key);
|
||||
}
|
||||
}
|
||||
favorite_algos_column_count = value.column_count;
|
||||
RaisePropertyChanged(nameof(selected_favorite_algo_definition));
|
||||
}
|
||||
|
||||
static IDisposable _connect_algo_param_rows(AlgoDefinitionVM def, ObservableAggregateCollection<AlgoParamRowVM> rows) =>
|
||||
@@ -465,29 +387,7 @@ namespace mbts.spark {
|
||||
|
||||
void _on_template_metadata(IReadOnlyDictionary<AlgoKey, AlgoDefinitionMetadata> meta) {
|
||||
_algo_metadata = meta;
|
||||
var curr_selected_template = selected_algo_template;
|
||||
if (curr_selected_template?.is_favorite ?? false)
|
||||
selected_favorite_algo_template = curr_selected_template;
|
||||
algo_definitions_view.Refresh();
|
||||
}
|
||||
|
||||
// This preserves the selected items through a save op.
|
||||
// When a definition/template is persisted for the first time, its unpersisted-placeholder item is removed and replaced with a new persisted one.
|
||||
// If an unpersisted item is selected when it is removed, the view will automatically select null - this method is a workaround for that.
|
||||
IDisposable _preserve_selection_by_index() {
|
||||
(int? selected_def_index, int? selected_tmp_index) = AlgoPersistenceHelper.selection_snapshot(algo_definitions, selected_algo_definition, selected_algo_template);
|
||||
return new CompositeDisposable(_definitions_buffer.buffer_updates()) {
|
||||
() => {
|
||||
if (selected_def_index is int di && algo_definitions.check_index(di)) {
|
||||
var def = algo_definitions[di];
|
||||
if (selected_tmp_index is int ti && def.templates.check_index(ti)) {
|
||||
selected_algo_template = def.templates[ti];
|
||||
} else {
|
||||
selected_algo_definition = def;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,55 +4,28 @@ using System;
|
||||
|
||||
namespace bts.api.orderman {
|
||||
readonly struct OrderSecurity : IEquatable<OrderSecurity> {
|
||||
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order) {
|
||||
this.security = sec;
|
||||
this.order = order;
|
||||
piq = null;
|
||||
algo = null;
|
||||
meta = null;
|
||||
root_algo = new AlgoData(order.Def.CustomDef);
|
||||
root_meta = null;
|
||||
}
|
||||
public OrderSecurity((SecurityImmutable? sec, OrderRecordImmutable order) t) {
|
||||
security = t.sec;
|
||||
order = t.order;
|
||||
piq = null;
|
||||
algo = null;
|
||||
meta = null;
|
||||
root_algo = new AlgoData(order.Def.CustomDef);
|
||||
root_meta = null;
|
||||
}
|
||||
|
||||
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order, uint? piq) {
|
||||
this.security = sec;
|
||||
this.order = order;
|
||||
this.piq = piq;
|
||||
algo = null;
|
||||
meta = null;
|
||||
root_algo = new AlgoData(order.Def.CustomDef);
|
||||
root_meta = null;
|
||||
}
|
||||
|
||||
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order, uint? piq, AlgoData? algo, AlgoDefinitionMetadata? meta) {
|
||||
this.security = sec;
|
||||
this.order = order;
|
||||
this.piq = piq;
|
||||
this.algo = algo;
|
||||
this.meta = meta;
|
||||
root_algo = algo ?? new AlgoData(order.Def.CustomDef);
|
||||
root_meta = meta;
|
||||
}
|
||||
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order, uint? piq, AlgoData? algo, AlgoDefinitionMetadata? meta, AlgoData? rootalgo = null, AlgoDefinitionMetadata? rootmeta = null) {
|
||||
this.security = sec;
|
||||
this.order = order;
|
||||
this.piq = piq;
|
||||
this.algo = algo;
|
||||
this.meta = meta;
|
||||
if (rootalgo.HasValue) {
|
||||
this.root_algo = rootalgo.Value;
|
||||
this.root_meta = rootmeta;
|
||||
} else {
|
||||
this.root_algo = algo ?? new AlgoData(order.Def.CustomDef);
|
||||
this.root_meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator OrderSecurity(ValueTuple<SecurityImmutable?, OrderRecordImmutable> x) => new OrderSecurity(x);
|
||||
@@ -61,20 +34,13 @@ namespace bts.api.orderman {
|
||||
public uint? piq { get; }
|
||||
public AlgoData? algo { get; }
|
||||
public AlgoDefinitionMetadata? meta { get; }
|
||||
public AlgoData root_algo { get; }
|
||||
public AlgoDefinitionMetadata? root_meta { get; }
|
||||
|
||||
|
||||
public OrderSecurity with(AlgoData? root_algo, AlgoDefinitionMetadata? root_meta = null) =>
|
||||
new OrderSecurity(security, order, piq, algo, meta, root_algo, root_meta);
|
||||
|
||||
public bool Equals(OrderSecurity other) =>
|
||||
order == other.order &&
|
||||
security == other.security &&
|
||||
piq == other.piq &&
|
||||
algo == other.algo &&
|
||||
meta == other.meta &&
|
||||
root_algo == other.root_algo;
|
||||
meta == other.meta;
|
||||
|
||||
public override bool Equals(object obj) => EqualityComparer.equals_obj(this, obj);
|
||||
|
||||
@@ -84,62 +50,104 @@ namespace bts.api.orderman {
|
||||
security?.GetHashCode() ?? 0,
|
||||
piq?.GetHashCode() ?? 0,
|
||||
algo?.GetHashCode() ?? 0,
|
||||
meta?.GetHashCode() ?? 0,
|
||||
root_algo.GetHashCode());
|
||||
meta?.GetHashCode() ?? 0);
|
||||
|
||||
public static bool operator == (OrderSecurity left, OrderSecurity right) => left.Equals(right);
|
||||
public static bool operator != (OrderSecurity left, OrderSecurity right) => !left.Equals(right);
|
||||
|
||||
}
|
||||
|
||||
struct OrderSecurityAlgoMeta : IEquatable<OrderSecurityAlgoMeta> {
|
||||
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
|
||||
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
|
||||
readonly struct OrderSecurityAlgo : IEquatable<OrderSecurityAlgo> {
|
||||
public readonly OrderRecordImmutable order;
|
||||
public readonly SecurityImmutable? security;
|
||||
/// TODO: refactor <see cref="algo"/> to 'studio_algo' or something that indicates it does not bear the same relation to this order, that <see cref="root_algo"/> does to the root order.
|
||||
/// The former is meant to hold studio algo information, and nothing else. Naturally, it indicates whether or not this is a studio order by being non-null (<see cref="is_studio_algo"/>).
|
||||
/// The latter, however, can be any kind of algo (studio, exchange, etc.); it holds information about the root-most algo in this order's branch.
|
||||
public readonly AlgoData? algo;
|
||||
public readonly AlgoData algo;
|
||||
public readonly AlgoDefinitionMetadata? meta;
|
||||
public readonly AlgoData root_algo;
|
||||
public readonly AlgoDefinitionMetadata? root_meta;
|
||||
public readonly uint? piq;
|
||||
|
||||
public OrderSecurityAlgoMeta(OrderRecordImmutable order, SecurityImmutable? security, AlgoData? algo, AlgoDefinitionMetadata? meta, AlgoData? root_algo = null, AlgoDefinitionMetadata? root_meta = null) {
|
||||
OrderSecurityAlgo(OrderRecordImmutable order, SecurityImmutable? security, AlgoData algo, AlgoDefinitionMetadata? meta, uint? piq) {
|
||||
this.order = order;
|
||||
this.security = security;
|
||||
this.algo = algo;
|
||||
this.meta = meta;
|
||||
if (root_algo.HasValue) {
|
||||
this.root_algo = root_algo.Value;
|
||||
this.root_meta = root_meta;
|
||||
} else {
|
||||
this.root_algo = algo ?? new AlgoData(order.Def.CustomDef);
|
||||
this.root_meta = meta;
|
||||
}
|
||||
this.piq = piq;
|
||||
}
|
||||
|
||||
public bool is_studio_algo => algo.HasValue;
|
||||
public static OrderSecurityAlgo from_order_security(OrderSecurity os) =>
|
||||
new OrderSecurityAlgo(
|
||||
os.order,
|
||||
os.security,
|
||||
os.algo ?? AlgoData.from_custom_def(os.order.Def.CustomDef),
|
||||
os.meta,
|
||||
os.piq);
|
||||
|
||||
public OrderSecurityAlgoMeta with(AlgoData? root_algo, AlgoDefinitionMetadata? root_meta = null) =>
|
||||
new OrderSecurityAlgoMeta(order, security, algo, meta, root_algo, root_meta);
|
||||
|
||||
public bool Equals(OrderSecurityAlgoMeta other) =>
|
||||
public bool Equals(OrderSecurityAlgo other) =>
|
||||
order == other.order &&
|
||||
security == other.security &&
|
||||
algo == other.algo &&
|
||||
meta == other.meta &&
|
||||
root_algo == other.root_algo;
|
||||
algo == other.algo;
|
||||
|
||||
public override bool Equals(object obj) => EqualityComparer.equals_obj(this, obj);
|
||||
public override bool Equals(object obj) =>
|
||||
obj is OrderSecurityAlgo other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
order.GetHashCode(),
|
||||
security?.GetHashCode() ?? 0,
|
||||
algo?.GetHashCode() ?? 0,
|
||||
meta?.GetHashCode() ?? 0,
|
||||
root_algo.GetHashCode());
|
||||
EqualityComparer.auto_phase(
|
||||
order.Key.GetHashCode(),
|
||||
security.HasValue ? security.Value.Id.GetHashCode() : 0,
|
||||
algo.GetHashCode());
|
||||
|
||||
public static bool operator ==(OrderSecurityAlgoMeta a, OrderSecurityAlgoMeta b) => a.Equals(b);
|
||||
public static bool operator !=(OrderSecurityAlgoMeta a, OrderSecurityAlgoMeta b) => !a.Equals(b);
|
||||
public static bool operator ==(OrderSecurityAlgo a, OrderSecurityAlgo b) => a.Equals(b);
|
||||
public static bool operator !=(OrderSecurityAlgo a, OrderSecurityAlgo b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
|
||||
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
|
||||
readonly struct OrderSecurityRootPair : IEquatable<OrderSecurityRootPair> {
|
||||
public readonly OrderSecurityAlgo order;
|
||||
readonly OrderSecurityAlgo? _root;
|
||||
|
||||
OrderSecurityRootPair(OrderSecurityAlgo order) {
|
||||
this.order = order;
|
||||
_root = null;
|
||||
}
|
||||
|
||||
OrderSecurityRootPair(OrderSecurityAlgo order, OrderSecurityAlgo? root) {
|
||||
this.order = order;
|
||||
_root = root;
|
||||
}
|
||||
|
||||
public OrderSecurityAlgo root => _root ?? order;
|
||||
|
||||
public static OrderSecurityRootPair from_order_security(OrderSecurity order) =>
|
||||
new OrderSecurityRootPair(OrderSecurityAlgo.from_order_security(order));
|
||||
|
||||
public static OrderSecurityRootPair from_order_security(OrderSecurity order, OrderSecurity root) =>
|
||||
new OrderSecurityRootPair(
|
||||
OrderSecurityAlgo.from_order_security(order),
|
||||
OrderSecurityAlgo.from_order_security(root));
|
||||
|
||||
public bool Equals(OrderSecurityRootPair other) =>
|
||||
order == other.order &&
|
||||
_root == other._root;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is OrderSecurityRootPair other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_root.HasValue ?
|
||||
(order.GetHashCode() % (1 << 15)) | (_root.Value.GetHashCode() % (1 << 15) << 16) :
|
||||
order.GetHashCode() % (1 << 15);
|
||||
|
||||
public static bool operator ==(OrderSecurityRootPair a, OrderSecurityRootPair b) => a.Equals(b);
|
||||
public static bool operator !=(OrderSecurityRootPair a, OrderSecurityRootPair b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
static class OrderSecurityHelper {
|
||||
public static readonly Func<OrderSecurity, uint>
|
||||
key_fn = o => o.order.Key,
|
||||
parent_key_fn = o => o.order.ParentKey;
|
||||
|
||||
public static readonly Func<OrderSecurityRootPair, uint>
|
||||
root_pair_key_fn = o => o.order.order.Key,
|
||||
root_pair_parent_key_fn = o => o.order.order.ParentKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using bts;
|
||||
using bts.spark;
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AlgoDef = bts.api.orderman.CustomOrderDefinition;
|
||||
using AlgoDefi = bts.api.orderman.CustomOrderDefinitionImmutable;
|
||||
using CompositeDef = bts.api.orderman.CompositeOrderDefinition;
|
||||
using CompositeDefi = bts.api.orderman.CompositeOrderDefinitionImmutable;
|
||||
|
||||
namespace mbts.spark {
|
||||
static partial class OrderTypeVMFactory {
|
||||
class CompositeAlgoVM : OrderTypeVMBase<CompositeDef, CompositeDefi> {
|
||||
IDisposable _studio_def_hooks;
|
||||
bool _is_disposed;
|
||||
AlgoDefinitionMetadata? _studio_meta;
|
||||
|
||||
public CompositeAlgoVM(AlgoKey id, string display_name, AlgoEditingPermissions editing_permissions)
|
||||
: base(id, display_name, editing_permissions) { }
|
||||
|
||||
public override string abbreviation => _studio_meta?.abbreviation ?? base.abbreviation;
|
||||
public override OrderMetadata meta {
|
||||
get {
|
||||
var m = new OrderMetadata { KeyBinding = string.Empty, NoProps = false };
|
||||
if (_studio_meta.HasValue && _studio_meta.Value.badge_color.HasValue)
|
||||
m.Color = _studio_meta.Value.badge_color.Value.rgb_color();
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
public override void assign_to(AlgoDef value) {
|
||||
if (value.Composite == null)
|
||||
value.Composite = new CompositeDef();
|
||||
value.Composite.AlgoTemplateKey = id.persisted;
|
||||
base.assign_to(value);
|
||||
}
|
||||
protected override void Dispose(bool disposing) {
|
||||
_is_disposed = true;
|
||||
Disposable.dispose(ref _studio_def_hooks);
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
protected override void on_model_internal() {
|
||||
Log.assert(!_is_disposed);
|
||||
_studio_def_hooks = Disposable.Create(model.algo_model.definitions.OnDispatcher().Subscribe(studio_def_map => {
|
||||
if (studio_def_map.try_get(id, out var data)) {
|
||||
display_name = data.name;
|
||||
load_from(data.value);
|
||||
}
|
||||
}),
|
||||
model.algo_model.metadata_service.subscribe_metadata().OnDispatcher().Subscribe(studio_meta_map => {
|
||||
if (studio_meta_map.TryGetValue(id, out var m)) {
|
||||
var old_abb = abbreviation;
|
||||
var old_meta = meta;
|
||||
_studio_meta = m;
|
||||
if (abbreviation != old_abb)
|
||||
RaisePropertyChanged(nameof(abbreviation));
|
||||
if (meta != old_meta)
|
||||
RaisePropertyChanged(nameof(meta));
|
||||
}
|
||||
}));
|
||||
}
|
||||
protected override void on_security_internal() => base.on_security_internal();
|
||||
protected override void on_security_md_internal() => base.on_security_md_internal();
|
||||
protected override void validate() => base.validate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ namespace mbts.spark {
|
||||
|
||||
public event Action definition_changed;
|
||||
public IAlgoVM impl { get; private set; }
|
||||
public AlgoKey id { get; } = new AlgoKey(AlgoType.None);
|
||||
public AlgoKey id { get; } = AlgoKey.from_known_type(AlgoType.None);
|
||||
public AlgoType order_type => _definition.value.CustomCase;
|
||||
public OrderMetadata meta => OrderDataHelper.get_metadata(order_type);
|
||||
public bool can_expand => impl?.can_expand ?? false;
|
||||
@@ -58,8 +58,6 @@ namespace mbts.spark {
|
||||
public bool key_match(KeyEventArgs e) => false;
|
||||
public void load_from(AlgoDefi value) => impl?.load_from(value);
|
||||
public void load_many(IEnumerable<AlgoDefi> values) => impl?.load_many(values);
|
||||
public Task<AlgoDefi?> load_from_async(AlgoDefi definition) =>
|
||||
impl?.load_from_async(definition) ?? Task.FromResult<AlgoDefi?>(definition);
|
||||
void IAlgoVM.ensure_init() => impl?.ensure_init();
|
||||
|
||||
protected override void Dispose(bool disposing) =>
|
||||
@@ -96,7 +94,7 @@ namespace mbts.spark {
|
||||
if (impl != null && impl.order_type == fmt_type)
|
||||
break;
|
||||
_clear_impl();
|
||||
impl = OrderTypeVMFactory.create(new AlgoKey(fmt_type), fmt_type.display_name(), fmt_type, is_expanded: true);
|
||||
impl = OrderTypeVMFactory.create(AlgoKey.from_known_type(fmt_type), fmt_type.display_name(), fmt_type, is_expanded: true);
|
||||
_algo_params.add_collection(impl.algo_params);
|
||||
(impl as IObserver<OrdersModel>)?.OnNext(_model);
|
||||
break;
|
||||
|
||||
Loaded 100 of 399 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user