Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e0a92fd65 | ||
|
|
a9c4173b02 | ||
|
|
7c99fe90de | ||
|
|
feff24bf10 |
No files matched your search
Vendored
+2
-2
@@ -213,7 +213,7 @@
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "",
|
||||
"preLaunchTask": "Build Spark Sdk (Debug)",
|
||||
"preLaunchTask": "Build Api Server (Debug)",
|
||||
"windows": {
|
||||
"program": "Bts.Spark.Sdk.Demo.exe",
|
||||
},
|
||||
@@ -285,7 +285,7 @@
|
||||
"windows": {
|
||||
"program": "Spark.exe",
|
||||
"args": [
|
||||
"--Spark:ConfigServer",
|
||||
"--sparkd",
|
||||
"${input:sparkConfigServer}"
|
||||
],
|
||||
"cwd": "build/Debug/bin/net8.0-windows",
|
||||
|
||||
+2
-35
@@ -81,8 +81,6 @@ services:
|
||||
limits:
|
||||
memory: "${SPARKD__MEMORY:-4gb}"
|
||||
sparkd-multicast:
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- ${SPARKD__DATA_DIR:-sparkd_data}:/home/bts/.local/share/bts/sparkd/data
|
||||
- ${SPARKD__MD_RECORDING_DIR:-md_recording}:/md_recording
|
||||
@@ -95,7 +93,7 @@ services:
|
||||
- CONFIG_DIR=${SPARKD__CONFIG_DIR}
|
||||
- PYTHON_REQUIREMENTS=${SPARKD__PYTHON_REQUIREMENTS}
|
||||
- SPARKD_ENTRYPOINT=${SPARKD__SPARKD_ENTRYPOINT}
|
||||
shm_size: "${SPARKD__SHM_SIZE:-8192mb}"
|
||||
shm_size: "${SPARKD__SHM_SIZE:-2048mb}"
|
||||
env_file:
|
||||
- .env
|
||||
- ${SPARKD__CONFIG_DIR}/.env
|
||||
@@ -107,38 +105,7 @@ services:
|
||||
delay: 3s
|
||||
resources:
|
||||
limits:
|
||||
memory: "${SPARKD__MEMORY:-16gb}"
|
||||
|
||||
|
||||
sparkd-multicast-cert:
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- ${SPARKD__DATA_DIR:-sparkd_data}:/home/bts/.local/share/bts/sparkd/data
|
||||
- ${SPARKD__MD_RECORDING_DIR:-md_recording}:/md_recording
|
||||
- ${SPARKD__RUNTIME_CONF_DIR:-sparkd_conf}:/home/bts/.local/share/bts/sparkd/conf
|
||||
build:
|
||||
dockerfile: src/app_sparkd/Dockerfile
|
||||
args:
|
||||
- IMAGE=${SPARKD__IMAGE}
|
||||
- SOURCE_DIR=${SPARKD__SOURCE_DIR}
|
||||
- CONFIG_DIR=${SPARKD__CONFIG_DIR}
|
||||
- PYTHON_REQUIREMENTS=${SPARKD__PYTHON_REQUIREMENTS}
|
||||
- SPARKD_ENTRYPOINT=${SPARKD__SPARKD_ENTRYPOINT}
|
||||
shm_size: "${SPARKD__SHM_SIZE:-8192mb}"
|
||||
env_file:
|
||||
- .env
|
||||
- ${SPARKD__CONFIG_DIR}/.env
|
||||
network_mode: "host"
|
||||
entrypoint: ${SPARKD__MAIN_ENTRYPOINT}
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: any
|
||||
delay: 3s
|
||||
resources:
|
||||
limits:
|
||||
memory: "${SPARKD__MEMORY:-16gb}"
|
||||
|
||||
memory: "${SPARKD__MEMORY:-4gb}"
|
||||
sparkd-tail:
|
||||
image: bluetradingsystems/linux_dotnet8
|
||||
volumes:
|
||||
|
||||
@@ -19,11 +19,6 @@ RUN add-apt-repository ppa:ubuntu-toolchain-r/test \
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y dotnet8
|
||||
|
||||
RUN apt-get update && apt-get install -y gdb
|
||||
RUN apt-get update && apt-get install -y fish
|
||||
RUN apt-get update && apt-get install -y vim
|
||||
RUN apt-get update && apt-get install -y less
|
||||
|
||||
RUN locale-gen en_US.UTF-8
|
||||
|
||||
ARG docker_user="bts"
|
||||
|
||||
Submodule
+1
Submodule docker/linux added at 6537812262.
Submodule
+1
Submodule docker/windows added at de7d40c09d.
@@ -0,0 +1 @@
|
||||
r=TimeSeriesStream(ReplayFutureLiveBBOStream("ES"))
|
||||
@@ -0,0 +1,3 @@
|
||||
x,y,z,id
|
||||
123,1.001,abc,1
|
||||
123,1.001,abc,0
|
||||
|
@@ -0,0 +1,3 @@
|
||||
x,y,z,id
|
||||
123,1.001,abc,1
|
||||
123,1.001,abc,0
|
||||
|
@@ -0,0 +1,78 @@
|
||||
id,ts_event,contract,bid_price,bid_size,ask_price,ask_size,timestamp
|
||||
76,2026-03-13T05:57:25.177965500Z,ESU5,6363.75,51,6364,63,3/13/2026 5:57:25 AM
|
||||
75,2026-03-13T05:57:25.068062900Z,ESU5,6363.75,52,6364,63,3/13/2026 5:57:25 AM
|
||||
74,2026-03-13T05:57:24.963428500Z,ESU5,6363.75,56,6364,63,3/13/2026 5:57:24 AM
|
||||
73,2026-03-13T05:57:24.781215700Z,ESU5,6363.75,57,6364,63,3/13/2026 5:57:24 AM
|
||||
72,2026-03-13T05:57:24.676290100Z,ESU5,6363.75,57,6364,63,3/13/2026 5:57:24 AM
|
||||
71,2026-03-13T05:57:24.566898400Z,ESU5,6363.75,56,6364,63,3/13/2026 5:57:24 AM
|
||||
70,2026-03-13T05:57:24.430800400Z,ESU5,6363.75,57,6364,63,3/13/2026 5:57:24 AM
|
||||
69,2026-03-13T05:57:24.273683400Z,ESU5,6363.75,56,6364,63,3/13/2026 5:57:24 AM
|
||||
68,2026-03-13T05:57:24.167656100Z,ESU5,6363.75,56,6364,64,3/13/2026 5:57:24 AM
|
||||
67,2026-03-13T05:57:23.995720100Z,ESU5,6363.75,56,6364,65,3/13/2026 5:57:23 AM
|
||||
66,2026-03-13T05:57:23.894029100Z,ESU5,6363.75,56,6364,68,3/13/2026 5:57:23 AM
|
||||
65,2026-03-13T05:57:23.728897300Z,ESU5,6363.75,56,6364,68,3/13/2026 5:57:23 AM
|
||||
64,2026-03-13T05:57:23.614316800Z,ESU5,6363.75,52,6364,68,3/13/2026 5:57:23 AM
|
||||
63,2026-03-13T05:57:23.503752700Z,ESU5,6363.75,51,6364,68,3/13/2026 5:57:23 AM
|
||||
62,2026-03-13T05:57:23.289108200Z,ESU5,6363.75,50,6364,68,3/13/2026 5:57:23 AM
|
||||
61,2026-03-13T05:57:23.155673400Z,ESU5,6363.75,49,6364,68,3/13/2026 5:57:23 AM
|
||||
60,2026-03-13T05:57:23.028898400Z,ESU5,6363.75,48,6364,68,3/13/2026 5:57:23 AM
|
||||
59,2026-03-13T05:57:22.887132700Z,ESU5,6363.75,46,6364,68,3/13/2026 5:57:22 AM
|
||||
58,2026-03-13T05:57:22.676916300Z,ESU5,6363.75,44,6364,68,3/13/2026 5:57:22 AM
|
||||
57,2026-03-13T05:57:22.520328000Z,ESU5,6363.75,43,6364,68,3/13/2026 5:57:22 AM
|
||||
56,2026-03-13T05:57:22.405244600Z,ESU5,6363.75,43,6364,69,3/13/2026 5:57:22 AM
|
||||
55,2026-03-13T05:57:22.294674100Z,ESU5,6363.75,41,6364,69,3/13/2026 5:57:22 AM
|
||||
54,2026-03-13T05:57:22.163467900Z,ESU5,6363.75,38,6364,69,3/13/2026 5:57:22 AM
|
||||
53,2026-03-13T05:57:21.992443000Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:21 AM
|
||||
52,2026-03-13T05:57:21.872338400Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:21 AM
|
||||
51,2026-03-13T05:57:21.742801800Z,ESU5,6363.75,37,6364,71,3/13/2026 5:57:21 AM
|
||||
50,2026-03-13T05:57:21.557711100Z,ESU5,6363.75,37,6364,72,3/13/2026 5:57:21 AM
|
||||
49,2026-03-13T05:57:21.434977000Z,ESU5,6363.75,37,6364,73,3/13/2026 5:57:21 AM
|
||||
48,2026-03-13T05:57:21.285751600Z,ESU5,6363.75,37,6364,72,3/13/2026 5:57:21 AM
|
||||
47,2026-03-13T05:57:21.184392000Z,ESU5,6363.75,37,6364,73,3/13/2026 5:57:21 AM
|
||||
46,2026-03-13T05:57:21.076843800Z,ESU5,6363.75,34,6364,73,3/13/2026 5:57:21 AM
|
||||
45,2026-03-13T05:57:20.894131800Z,ESU5,6363.75,32,6364,73,3/13/2026 5:57:20 AM
|
||||
44,2026-03-13T05:57:20.759550000Z,ESU5,6363.75,30,6364,73,3/13/2026 5:57:20 AM
|
||||
43,2026-03-13T05:57:20.641547900Z,ESU5,6363.75,28,6364,73,3/13/2026 5:57:20 AM
|
||||
42,2026-03-13T05:57:20.540293200Z,ESU5,6363.75,27,6364,73,3/13/2026 5:57:20 AM
|
||||
41,2026-03-13T05:57:20.439034600Z,ESU5,6363.75,27,6364,74,3/13/2026 5:57:20 AM
|
||||
40,2026-03-13T05:57:20.337795000Z,ESU5,6363.75,27,6364,73,3/13/2026 5:57:20 AM
|
||||
39,2026-03-13T05:57:20.236606600Z,ESU5,6363.75,27,6364,72,3/13/2026 5:57:20 AM
|
||||
38,2026-03-13T05:57:20.135417700Z,ESU5,6363.75,30,6364,72,3/13/2026 5:57:20 AM
|
||||
37,2026-03-13T05:57:20.034272700Z,ESU5,6363.75,32,6364,72,3/13/2026 5:57:20 AM
|
||||
36,2026-03-13T05:57:19.933096800Z,ESU5,6363.75,34,6364,72,3/13/2026 5:57:19 AM
|
||||
35,2026-03-13T05:57:19.832012600Z,ESU5,6363.75,36,6364,72,3/13/2026 5:57:19 AM
|
||||
34,2026-03-13T05:57:19.730878500Z,ESU5,6363.75,38,6364,72,3/13/2026 5:57:19 AM
|
||||
33,2026-03-13T05:57:19.629787400Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:19 AM
|
||||
32,2026-03-13T05:57:19.528697000Z,ESU5,6363.75,38,6364,70,3/13/2026 5:57:19 AM
|
||||
31,2026-03-13T05:57:19.427077000Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:19 AM
|
||||
30,2026-03-13T05:57:19.326450300Z,ESU5,6363.75,39,6364,71,3/13/2026 5:57:19 AM
|
||||
29,2026-03-13T05:57:19.225427900Z,ESU5,6363.75,39,6364,70,3/13/2026 5:57:19 AM
|
||||
28,2026-03-13T05:57:19.123339800Z,ESU5,6363.75,40,6364,70,3/13/2026 5:57:19 AM
|
||||
27,2026-03-13T05:57:19.021936500Z,ESU5,6363.75,41,6364,70,3/13/2026 5:57:19 AM
|
||||
26,2026-03-13T05:57:18.920859200Z,ESU5,6363.75,45,6364,70,3/13/2026 5:57:18 AM
|
||||
25,2026-03-13T05:57:18.820148400Z,ESU5,6363.75,46,6364,70,3/13/2026 5:57:18 AM
|
||||
24,2026-03-13T05:57:18.718988100Z,ESU5,6363.75,46,6364,69,3/13/2026 5:57:18 AM
|
||||
23,2026-03-13T05:57:18.618299700Z,ESU5,6363.75,49,6364,69,3/13/2026 5:57:18 AM
|
||||
22,2026-03-13T05:57:18.517138100Z,ESU5,6363.75,52,6364,69,3/13/2026 5:57:18 AM
|
||||
21,2026-03-13T05:57:18.416335200Z,ESU5,6363.75,53,6364,69,3/13/2026 5:57:18 AM
|
||||
20,2026-03-13T05:57:18.315335900Z,ESU5,6363.75,55,6364,69,3/13/2026 5:57:18 AM
|
||||
19,2026-03-13T05:57:18.214368600Z,ESU5,6363.75,57,6364,69,3/13/2026 5:57:18 AM
|
||||
18,2026-03-13T05:57:18.113754700Z,ESU5,6363.75,57,6364,65,3/13/2026 5:57:18 AM
|
||||
17,2026-03-13T05:57:18.012706500Z,ESU5,6363.75,56,6364,65,3/13/2026 5:57:18 AM
|
||||
16,2026-03-13T05:57:17.911672600Z,ESU5,6363.75,54,6364,65,3/13/2026 5:57:17 AM
|
||||
15,2026-03-13T05:57:17.810879900Z,ESU5,6363.75,52,6364,65,3/13/2026 5:57:17 AM
|
||||
14,2026-03-13T05:57:17.710089700Z,ESU5,6363.75,49,6364,65,3/13/2026 5:57:17 AM
|
||||
13,2026-03-13T05:57:17.609388300Z,ESU5,6363.75,49,6364,67,3/13/2026 5:57:17 AM
|
||||
12,2026-03-13T05:57:17.508697400Z,ESU5,6363.75,49,6364,67,3/13/2026 5:57:17 AM
|
||||
11,2026-03-13T05:57:17.408027000Z,ESU5,6363.75,53,6364,67,3/13/2026 5:57:17 AM
|
||||
10,2026-03-13T05:57:17.307406200Z,ESU5,6363.75,49,6364,67,3/13/2026 5:57:17 AM
|
||||
9,2026-03-13T05:57:17.206714300Z,ESU5,6363.75,49,6364,71,3/13/2026 5:57:17 AM
|
||||
8,2026-03-13T05:57:17.105530300Z,ESU5,6363.75,48,6364,71,3/13/2026 5:57:17 AM
|
||||
7,2026-03-13T05:57:17.004796800Z,ESU5,6363.75,47,6364,71,3/13/2026 5:57:17 AM
|
||||
6,2026-03-13T05:57:16.904113900Z,ESU5,6363.75,46,6364,71,3/13/2026 5:57:16 AM
|
||||
5,2026-03-13T05:57:16.803381900Z,ESU5,6363.75,43,6364,71,3/13/2026 5:57:16 AM
|
||||
4,2026-03-13T05:57:16.701969600Z,ESU5,6363.75,43,6364,72,3/13/2026 5:57:16 AM
|
||||
3,2026-03-13T05:57:16.600765600Z,ESU5,6363.75,43,6364,73,3/13/2026 5:57:16 AM
|
||||
2,2026-03-13T05:57:16.499986300Z,ESU5,6363.75,43,6364,74,3/13/2026 5:57:16 AM
|
||||
1,2026-03-13T05:57:16.399241700Z,ESU5,6363.75,43,6364,75,3/13/2026 5:57:16 AM
|
||||
0,2026-03-13T05:57:16.264038000Z,ESU5,6363.75,43,6364,76,3/13/2026 5:57:16 AM
|
||||
|
@@ -0,0 +1,91 @@
|
||||
id,ts_event,contract,bid_price,bid_size,ask_price,ask_size,timestamp
|
||||
89,2026-03-13T05:57:26.904401600Z,ESU5,6363.75,43,6364,67,3/13/2026 5:57:26 AM
|
||||
88,2026-03-13T05:57:26.778037200Z,ESU5,6363.75,43,6364,69,3/13/2026 5:57:26 AM
|
||||
87,2026-03-13T05:57:26.597038500Z,ESU5,6363.75,43,6364,69,3/13/2026 5:57:26 AM
|
||||
86,2026-03-13T05:57:26.438768300Z,ESU5,6363.75,43,6364,68,3/13/2026 5:57:26 AM
|
||||
85,2026-03-13T05:57:26.328259100Z,ESU5,6363.75,43,6364,67,3/13/2026 5:57:26 AM
|
||||
84,2026-03-13T05:57:26.216211200Z,ESU5,6363.75,44,6364,67,3/13/2026 5:57:26 AM
|
||||
83,2026-03-13T05:57:26.099281200Z,ESU5,6363.75,46,6364,67,3/13/2026 5:57:26 AM
|
||||
82,2026-03-13T05:57:25.979814900Z,ESU5,6363.75,48,6364,67,3/13/2026 5:57:25 AM
|
||||
81,2026-03-13T05:57:25.852317700Z,ESU5,6363.75,48,6364,66,3/13/2026 5:57:25 AM
|
||||
80,2026-03-13T05:57:25.684323100Z,ESU5,6363.75,48,6364,65,3/13/2026 5:57:25 AM
|
||||
79,2026-03-13T05:57:25.539436900Z,ESU5,6363.75,48,6364,64,3/13/2026 5:57:25 AM
|
||||
78,2026-03-13T05:57:25.425439200Z,ESU5,6363.75,49,6364,64,3/13/2026 5:57:25 AM
|
||||
77,2026-03-13T05:57:25.298054100Z,ESU5,6363.75,51,6364,64,3/13/2026 5:57:25 AM
|
||||
76,2026-03-13T05:57:25.177965500Z,ESU5,6363.75,51,6364,63,3/13/2026 5:57:25 AM
|
||||
75,2026-03-13T05:57:25.068062900Z,ESU5,6363.75,52,6364,63,3/13/2026 5:57:25 AM
|
||||
74,2026-03-13T05:57:24.963428500Z,ESU5,6363.75,56,6364,63,3/13/2026 5:57:24 AM
|
||||
73,2026-03-13T05:57:24.781215700Z,ESU5,6363.75,57,6364,63,3/13/2026 5:57:24 AM
|
||||
72,2026-03-13T05:57:24.676290100Z,ESU5,6363.75,57,6364,63,3/13/2026 5:57:24 AM
|
||||
71,2026-03-13T05:57:24.566898400Z,ESU5,6363.75,56,6364,63,3/13/2026 5:57:24 AM
|
||||
70,2026-03-13T05:57:24.430800400Z,ESU5,6363.75,57,6364,63,3/13/2026 5:57:24 AM
|
||||
69,2026-03-13T05:57:24.273683400Z,ESU5,6363.75,56,6364,63,3/13/2026 5:57:24 AM
|
||||
68,2026-03-13T05:57:24.167656100Z,ESU5,6363.75,56,6364,64,3/13/2026 5:57:24 AM
|
||||
67,2026-03-13T05:57:23.995720100Z,ESU5,6363.75,56,6364,65,3/13/2026 5:57:23 AM
|
||||
66,2026-03-13T05:57:23.894029100Z,ESU5,6363.75,56,6364,68,3/13/2026 5:57:23 AM
|
||||
65,2026-03-13T05:57:23.728897300Z,ESU5,6363.75,56,6364,68,3/13/2026 5:57:23 AM
|
||||
64,2026-03-13T05:57:23.614316800Z,ESU5,6363.75,52,6364,68,3/13/2026 5:57:23 AM
|
||||
63,2026-03-13T05:57:23.503752700Z,ESU5,6363.75,51,6364,68,3/13/2026 5:57:23 AM
|
||||
62,2026-03-13T05:57:23.289108200Z,ESU5,6363.75,50,6364,68,3/13/2026 5:57:23 AM
|
||||
61,2026-03-13T05:57:23.155673400Z,ESU5,6363.75,49,6364,68,3/13/2026 5:57:23 AM
|
||||
60,2026-03-13T05:57:23.028898400Z,ESU5,6363.75,48,6364,68,3/13/2026 5:57:23 AM
|
||||
59,2026-03-13T05:57:22.887132700Z,ESU5,6363.75,46,6364,68,3/13/2026 5:57:22 AM
|
||||
58,2026-03-13T05:57:22.676916300Z,ESU5,6363.75,44,6364,68,3/13/2026 5:57:22 AM
|
||||
57,2026-03-13T05:57:22.520328000Z,ESU5,6363.75,43,6364,68,3/13/2026 5:57:22 AM
|
||||
56,2026-03-13T05:57:22.405244600Z,ESU5,6363.75,43,6364,69,3/13/2026 5:57:22 AM
|
||||
55,2026-03-13T05:57:22.294674100Z,ESU5,6363.75,41,6364,69,3/13/2026 5:57:22 AM
|
||||
54,2026-03-13T05:57:22.163467900Z,ESU5,6363.75,38,6364,69,3/13/2026 5:57:22 AM
|
||||
53,2026-03-13T05:57:21.992443000Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:21 AM
|
||||
52,2026-03-13T05:57:21.872338400Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:21 AM
|
||||
51,2026-03-13T05:57:21.742801800Z,ESU5,6363.75,37,6364,71,3/13/2026 5:57:21 AM
|
||||
50,2026-03-13T05:57:21.557711100Z,ESU5,6363.75,37,6364,72,3/13/2026 5:57:21 AM
|
||||
49,2026-03-13T05:57:21.434977000Z,ESU5,6363.75,37,6364,73,3/13/2026 5:57:21 AM
|
||||
48,2026-03-13T05:57:21.285751600Z,ESU5,6363.75,37,6364,72,3/13/2026 5:57:21 AM
|
||||
47,2026-03-13T05:57:21.184392000Z,ESU5,6363.75,37,6364,73,3/13/2026 5:57:21 AM
|
||||
46,2026-03-13T05:57:21.076843800Z,ESU5,6363.75,34,6364,73,3/13/2026 5:57:21 AM
|
||||
45,2026-03-13T05:57:20.894131800Z,ESU5,6363.75,32,6364,73,3/13/2026 5:57:20 AM
|
||||
44,2026-03-13T05:57:20.759550000Z,ESU5,6363.75,30,6364,73,3/13/2026 5:57:20 AM
|
||||
43,2026-03-13T05:57:20.641547900Z,ESU5,6363.75,28,6364,73,3/13/2026 5:57:20 AM
|
||||
42,2026-03-13T05:57:20.540293200Z,ESU5,6363.75,27,6364,73,3/13/2026 5:57:20 AM
|
||||
41,2026-03-13T05:57:20.439034600Z,ESU5,6363.75,27,6364,74,3/13/2026 5:57:20 AM
|
||||
40,2026-03-13T05:57:20.337795000Z,ESU5,6363.75,27,6364,73,3/13/2026 5:57:20 AM
|
||||
39,2026-03-13T05:57:20.236606600Z,ESU5,6363.75,27,6364,72,3/13/2026 5:57:20 AM
|
||||
38,2026-03-13T05:57:20.135417700Z,ESU5,6363.75,30,6364,72,3/13/2026 5:57:20 AM
|
||||
37,2026-03-13T05:57:20.034272700Z,ESU5,6363.75,32,6364,72,3/13/2026 5:57:20 AM
|
||||
36,2026-03-13T05:57:19.933096800Z,ESU5,6363.75,34,6364,72,3/13/2026 5:57:19 AM
|
||||
35,2026-03-13T05:57:19.832012600Z,ESU5,6363.75,36,6364,72,3/13/2026 5:57:19 AM
|
||||
34,2026-03-13T05:57:19.730878500Z,ESU5,6363.75,38,6364,72,3/13/2026 5:57:19 AM
|
||||
33,2026-03-13T05:57:19.629787400Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:19 AM
|
||||
32,2026-03-13T05:57:19.528697000Z,ESU5,6363.75,38,6364,70,3/13/2026 5:57:19 AM
|
||||
31,2026-03-13T05:57:19.427077000Z,ESU5,6363.75,38,6364,71,3/13/2026 5:57:19 AM
|
||||
30,2026-03-13T05:57:19.326450300Z,ESU5,6363.75,39,6364,71,3/13/2026 5:57:19 AM
|
||||
29,2026-03-13T05:57:19.225427900Z,ESU5,6363.75,39,6364,70,3/13/2026 5:57:19 AM
|
||||
28,2026-03-13T05:57:19.123339800Z,ESU5,6363.75,40,6364,70,3/13/2026 5:57:19 AM
|
||||
27,2026-03-13T05:57:19.021936500Z,ESU5,6363.75,41,6364,70,3/13/2026 5:57:19 AM
|
||||
26,2026-03-13T05:57:18.920859200Z,ESU5,6363.75,45,6364,70,3/13/2026 5:57:18 AM
|
||||
25,2026-03-13T05:57:18.820148400Z,ESU5,6363.75,46,6364,70,3/13/2026 5:57:18 AM
|
||||
24,2026-03-13T05:57:18.718988100Z,ESU5,6363.75,46,6364,69,3/13/2026 5:57:18 AM
|
||||
23,2026-03-13T05:57:18.618299700Z,ESU5,6363.75,49,6364,69,3/13/2026 5:57:18 AM
|
||||
22,2026-03-13T05:57:18.517138100Z,ESU5,6363.75,52,6364,69,3/13/2026 5:57:18 AM
|
||||
21,2026-03-13T05:57:18.416335200Z,ESU5,6363.75,53,6364,69,3/13/2026 5:57:18 AM
|
||||
20,2026-03-13T05:57:18.315335900Z,ESU5,6363.75,55,6364,69,3/13/2026 5:57:18 AM
|
||||
19,2026-03-13T05:57:18.214368600Z,ESU5,6363.75,57,6364,69,3/13/2026 5:57:18 AM
|
||||
18,2026-03-13T05:57:18.113754700Z,ESU5,6363.75,57,6364,65,3/13/2026 5:57:18 AM
|
||||
17,2026-03-13T05:57:18.012706500Z,ESU5,6363.75,56,6364,65,3/13/2026 5:57:18 AM
|
||||
16,2026-03-13T05:57:17.911672600Z,ESU5,6363.75,54,6364,65,3/13/2026 5:57:17 AM
|
||||
15,2026-03-13T05:57:17.810879900Z,ESU5,6363.75,52,6364,65,3/13/2026 5:57:17 AM
|
||||
14,2026-03-13T05:57:17.710089700Z,ESU5,6363.75,49,6364,65,3/13/2026 5:57:17 AM
|
||||
13,2026-03-13T05:57:17.609388300Z,ESU5,6363.75,49,6364,67,3/13/2026 5:57:17 AM
|
||||
12,2026-03-13T05:57:17.508697400Z,ESU5,6363.75,49,6364,67,3/13/2026 5:57:17 AM
|
||||
11,2026-03-13T05:57:17.408027000Z,ESU5,6363.75,53,6364,67,3/13/2026 5:57:17 AM
|
||||
10,2026-03-13T05:57:17.307406200Z,ESU5,6363.75,49,6364,67,3/13/2026 5:57:17 AM
|
||||
9,2026-03-13T05:57:17.206714300Z,ESU5,6363.75,49,6364,71,3/13/2026 5:57:17 AM
|
||||
8,2026-03-13T05:57:17.105530300Z,ESU5,6363.75,48,6364,71,3/13/2026 5:57:17 AM
|
||||
7,2026-03-13T05:57:17.004796800Z,ESU5,6363.75,47,6364,71,3/13/2026 5:57:17 AM
|
||||
6,2026-03-13T05:57:16.904113900Z,ESU5,6363.75,46,6364,71,3/13/2026 5:57:16 AM
|
||||
5,2026-03-13T05:57:16.803381900Z,ESU5,6363.75,43,6364,71,3/13/2026 5:57:16 AM
|
||||
4,2026-03-13T05:57:16.701969600Z,ESU5,6363.75,43,6364,72,3/13/2026 5:57:16 AM
|
||||
3,2026-03-13T05:57:16.600765600Z,ESU5,6363.75,43,6364,73,3/13/2026 5:57:16 AM
|
||||
2,2026-03-13T05:57:16.499986300Z,ESU5,6363.75,43,6364,74,3/13/2026 5:57:16 AM
|
||||
1,2026-03-13T05:57:16.399241700Z,ESU5,6363.75,43,6364,75,3/13/2026 5:57:16 AM
|
||||
0,2026-03-13T05:57:16.264038000Z,ESU5,6363.75,43,6364,76,3/13/2026 5:57:16 AM
|
||||
|
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataPacket:
|
||||
state: List[Any]
|
||||
deleted_ids: List[int]
|
||||
added_ids: List[int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DslResponse:
|
||||
data_packet: DataPacket | None
|
||||
req_id: str | None
|
||||
error: str | None
|
||||
|
||||
|
||||
def _resolve_interop_exe() -> str:
|
||||
env_path = os.environ.get("BLUEORB_DSL_INTEROP")
|
||||
if env_path and os.path.isfile(env_path):
|
||||
return env_path
|
||||
candidates = [
|
||||
"/workspaces/spark/build/Debug/bin/net8.0/BlueOrbDSLInteropServer",
|
||||
"/workspaces/spark/build/Release/bin/net8.0/BlueOrbDSLInteropServer",
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
raise FileNotFoundError(
|
||||
"BlueOrbDSLInterop executable not found. "
|
||||
"Set BLUEORB_DSL_INTEROP to the full path."
|
||||
)
|
||||
|
||||
_interop_proc: Optional[subprocess.Popen] = None
|
||||
_reader_thread: Optional[threading.Thread] = None
|
||||
_callbacks: Dict[str, Callable[[DslResponse], None]] = {}
|
||||
_first_response_events: Dict[str, threading.Event] = {}
|
||||
_first_responses: Dict[str, DslResponse] = {}
|
||||
_state_lock = threading.Lock()
|
||||
|
||||
|
||||
def start_interop() -> None:
|
||||
"""
|
||||
Start the BlueOrbDSLInteropServer process if it's not already running.
|
||||
"""
|
||||
global _interop_proc
|
||||
if _interop_proc is not None and _interop_proc.poll() is None:
|
||||
return
|
||||
exe = _resolve_interop_exe()
|
||||
_interop_proc = subprocess.Popen(
|
||||
[exe],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
_start_reader_thread()
|
||||
|
||||
|
||||
def stop_interop() -> None:
|
||||
"""
|
||||
Stop the BlueOrbDSLInteropServer process if it's running.
|
||||
"""
|
||||
global _interop_proc
|
||||
if _interop_proc is None:
|
||||
return
|
||||
if _interop_proc.poll() is None and _interop_proc.stdin is not None:
|
||||
_interop_proc.stdin.write(":exit\n")
|
||||
_interop_proc.stdin.flush()
|
||||
_interop_proc.stdin.close()
|
||||
_interop_proc.wait(timeout=5)
|
||||
global _reader_thread
|
||||
if _reader_thread is not None:
|
||||
_reader_thread.join(timeout=1)
|
||||
_reader_thread = None
|
||||
with _state_lock:
|
||||
_callbacks.clear()
|
||||
_first_response_events.clear()
|
||||
_first_responses.clear()
|
||||
_interop_proc = None
|
||||
|
||||
|
||||
def get_interop_status() -> tuple[bool, int | None]:
|
||||
"""
|
||||
Returns current interop process status as (is_running, exit_code).
|
||||
exit_code is None when process was never started or is still running.
|
||||
"""
|
||||
proc = _interop_proc
|
||||
if proc is None:
|
||||
return (False, None)
|
||||
rc = proc.poll()
|
||||
return (rc is None, rc)
|
||||
|
||||
|
||||
def get_dsl(dsl: str, req_id: str, callback: Callable[[DslResponse], None]) -> DslResponse:
|
||||
"""
|
||||
Sends the DSL to BlueOrbDSLInteropServer and returns the first response for this req_id.
|
||||
Subsequent streaming updates for the same req_id are delivered via callback.
|
||||
Requires start_interop() to be called first.
|
||||
"""
|
||||
assert _interop_proc is not None and _interop_proc.poll() is None, (
|
||||
"Interop process is not running. Call start_interop() first."
|
||||
)
|
||||
assert _interop_proc.stdin is not None
|
||||
|
||||
payload = {"dsl": dsl, "req_id": req_id}
|
||||
first_event = threading.Event()
|
||||
with _state_lock:
|
||||
_callbacks[req_id] = callback
|
||||
_first_response_events[req_id] = first_event
|
||||
_first_responses.pop(req_id, None)
|
||||
|
||||
_interop_proc.stdin.write(json.dumps(payload) + "\n")
|
||||
_interop_proc.stdin.flush()
|
||||
|
||||
if not first_event.wait(timeout=10):
|
||||
with _state_lock:
|
||||
_first_response_events.pop(req_id, None)
|
||||
_callbacks.pop(req_id, None)
|
||||
raise TimeoutError(f"No response received for req_id '{req_id}' from BlueOrbDSLInterop.")
|
||||
|
||||
with _state_lock:
|
||||
response = _first_responses.pop(req_id, None)
|
||||
_first_response_events.pop(req_id, None)
|
||||
if response is None:
|
||||
raise RuntimeError(f"Missing first response for req_id '{req_id}'.")
|
||||
return response
|
||||
|
||||
|
||||
def _start_reader_thread() -> None:
|
||||
global _reader_thread
|
||||
if _reader_thread is not None and _reader_thread.is_alive():
|
||||
return
|
||||
_reader_thread = threading.Thread(target=_reader_loop, daemon=True)
|
||||
_reader_thread.start()
|
||||
|
||||
|
||||
def _reader_loop() -> None:
|
||||
while True:
|
||||
proc = _interop_proc
|
||||
if proc is None or proc.poll() is not None:
|
||||
return
|
||||
assert proc.stdout is not None
|
||||
raw = proc.stdout.readline()
|
||||
if raw == "":
|
||||
return
|
||||
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
response = DslResponse(
|
||||
data_packet=DataPacket(state=[payload.get("data_packet")], deleted_ids=[], added_ids=[]),
|
||||
req_id=payload.get("req_id"),
|
||||
error=payload.get("error"),
|
||||
)
|
||||
req_id = response.req_id
|
||||
if not req_id:
|
||||
continue
|
||||
|
||||
callback: Optional[Callable[[DslResponse], None]]
|
||||
event: Optional[threading.Event]
|
||||
with _state_lock:
|
||||
callback = _callbacks.get(req_id)
|
||||
event = _first_response_events.get(req_id)
|
||||
if event is not None and req_id not in _first_responses:
|
||||
_first_responses[req_id] = response
|
||||
event.set()
|
||||
|
||||
if callback is not None:
|
||||
try:
|
||||
callback(response)
|
||||
except Exception:
|
||||
# Keep the reader alive if callback logic fails.
|
||||
pass
|
||||
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
_here = os.path.dirname(os.path.abspath(__file__))
|
||||
_parent = os.path.dirname(_here)
|
||||
if _parent not in sys.path:
|
||||
sys.path.insert(0, _parent)
|
||||
|
||||
from BlueOrbDSL import get_dsl, start_interop, stop_interop, DslResponse
|
||||
|
||||
|
||||
def on_packet(response: DslResponse) -> None:
|
||||
print("callback from DSL Server, DslResponse:", response)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
start_interop()
|
||||
try:
|
||||
while True:
|
||||
dsl = input("Enter DSL (or 'quit'): ").strip()
|
||||
if dsl.lower() == "quit":
|
||||
break
|
||||
if not dsl:
|
||||
continue
|
||||
response = get_dsl(dsl, str(uuid.uuid4()), on_packet)
|
||||
print("return:", response)
|
||||
finally:
|
||||
stop_interop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,844 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from rich.text import Text
|
||||
from rich.style import Style
|
||||
from textual import events
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.widgets.text_area import TextAreaTheme
|
||||
from textual.widgets import Button, DataTable, Input, Static, TabbedContent, TabPane, TextArea, Tree
|
||||
|
||||
_here = os.path.dirname(os.path.abspath(__file__))
|
||||
_parent = os.path.dirname(_here)
|
||||
if _parent not in sys.path:
|
||||
sys.path.insert(0, _parent)
|
||||
|
||||
from BlueOrbDSL import DslResponse, get_dsl, get_interop_status, start_interop, stop_interop
|
||||
|
||||
|
||||
class DSQALiveConsole(App):
|
||||
DEFAULT_QUERY = (
|
||||
'FROM market.ticks\n'
|
||||
'WHERE symbol IN ("BTCUSD", "ETHUSD")\n'
|
||||
"SELECT symbol, price, change, time"
|
||||
)
|
||||
LAST_QUERY_PATH = os.path.join(_here, "DSQA_LAST_EXECUTED_QUERY.txt")
|
||||
DSL_HIGHLIGHT_QUERY = r"""
|
||||
; comments
|
||||
(comment) @comment
|
||||
|
||||
; assignment vars: lhs in x = SomeStream(...)
|
||||
(assignment_expression
|
||||
left: (identifier) @variable)
|
||||
|
||||
; stream operators (function names ending with Stream)
|
||||
(call_expression
|
||||
function: (identifier) @function
|
||||
(#match? @function "^[A-Za-z_][A-Za-z0-9_]*Stream$"))
|
||||
|
||||
; normal function calls
|
||||
(call_expression
|
||||
function: (identifier) @function.method)
|
||||
|
||||
; arguments and literals
|
||||
(arguments (identifier) @parameter)
|
||||
(string) @string
|
||||
(number) @number
|
||||
(true) @constant.builtin
|
||||
(false) @constant.builtin
|
||||
(null) @constant.builtin
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("ctrl+r", "execute", "Execute"),
|
||||
Binding("f11", "execute", show=False),
|
||||
Binding("alt+enter", "execute", show=False),
|
||||
# Override Textual's default Ctrl+Q quit to avoid accidental VS Code close.
|
||||
Binding("ctrl+q", "noop", show=False),
|
||||
Binding("ctrl+x", "quit", "Quit"),
|
||||
]
|
||||
CSS = """
|
||||
Screen {
|
||||
background: #060a1c;
|
||||
color: #d8dff4;
|
||||
}
|
||||
|
||||
#window_close_button {
|
||||
width: 3;
|
||||
min-width: 3;
|
||||
height: 1;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: #3a1015;
|
||||
color: #ff9ca8;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#window_close_button:hover,
|
||||
#window_close_button:focus {
|
||||
background: #5a1620;
|
||||
color: #ffd6db;
|
||||
}
|
||||
|
||||
#root {
|
||||
padding: 0 1 0 1;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#query_panel {
|
||||
background: #070d27;
|
||||
height: 33%;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#query_header_row {
|
||||
height: 1;
|
||||
border-bottom: solid #596285;
|
||||
}
|
||||
|
||||
#query_header {
|
||||
width: auto;
|
||||
color: #9eb0d4;
|
||||
content-align: left middle;
|
||||
}
|
||||
|
||||
#query_header_spacer {
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
#query_input {
|
||||
border: solid #697597;
|
||||
background: #050b22;
|
||||
color: #cfd7eb;
|
||||
height: 1fr;
|
||||
padding-left: 1;
|
||||
}
|
||||
|
||||
#results_panel {
|
||||
background: #070d27;
|
||||
height: 1fr;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#results_toolbar {
|
||||
height: 3;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
#results_left {
|
||||
width: auto;
|
||||
height: 3;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#stream_toggle_button {
|
||||
width: auto;
|
||||
min-width: 11;
|
||||
margin-top: 0;
|
||||
margin-left: 1;
|
||||
border: none;
|
||||
background: #070d27;
|
||||
color: #79afe7;
|
||||
}
|
||||
|
||||
#stream_toggle_button:hover,
|
||||
#stream_toggle_button:focus {
|
||||
background: #111a3f;
|
||||
color: #9bc9f6;
|
||||
}
|
||||
|
||||
#execute_button {
|
||||
width: 5;
|
||||
min-width: 5;
|
||||
margin-top: 0;
|
||||
margin-left: 1;
|
||||
border: none;
|
||||
background: #10341f;
|
||||
color: #7ef1ac;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#execute_button:hover,
|
||||
#execute_button:focus {
|
||||
background: #1a4f30;
|
||||
color: #b3ffd0;
|
||||
}
|
||||
|
||||
#results_actions {
|
||||
width: auto;
|
||||
height: 3;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#results_spacer {
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
#results_search {
|
||||
width: 30;
|
||||
min-width: 24;
|
||||
margin-right: 1;
|
||||
border: none;
|
||||
background: #060d28;
|
||||
color: #d5def3;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
#search_button,
|
||||
#copy_button,
|
||||
#export_button {
|
||||
width: auto;
|
||||
min-width: 11;
|
||||
margin-left: 1;
|
||||
border: none;
|
||||
background: #070d27;
|
||||
color: #79afe7;
|
||||
}
|
||||
|
||||
#search_button:hover,
|
||||
#copy_button:hover,
|
||||
#export_button:hover,
|
||||
#search_button:focus,
|
||||
#copy_button:focus,
|
||||
#export_button:focus {
|
||||
background: #111a3f;
|
||||
color: #9bc9f6;
|
||||
}
|
||||
|
||||
#results_table {
|
||||
border: solid #697597;
|
||||
background: #050b22;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#results_table .datatable--header,
|
||||
#results_table .datatable--header-hover,
|
||||
#results_table .datatable--header-cursor {
|
||||
background: #0c1535;
|
||||
color: #6ea6df;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#results_table .datatable--even-row {
|
||||
background: #050b22;
|
||||
}
|
||||
|
||||
#results_table .datatable--odd-row {
|
||||
background: #07102a;
|
||||
}
|
||||
|
||||
#results_table .datatable--cursor {
|
||||
background: #19335b;
|
||||
color: #dbe8ff;
|
||||
}
|
||||
|
||||
#results_table .datatable--fixed-cursor {
|
||||
background: #132949;
|
||||
color: #c8dbff;
|
||||
}
|
||||
|
||||
#results_table.error .datatable--odd-row,
|
||||
#results_table.error .datatable--even-row,
|
||||
#results_table.error .datatable--cursor,
|
||||
#results_table.error .datatable--fixed-cursor {
|
||||
color: #f17486;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#results_panel.paused {
|
||||
background: #150f1f;
|
||||
}
|
||||
|
||||
#results_table.paused {
|
||||
background: #130f18;
|
||||
}
|
||||
|
||||
#results_table.paused .datatable--header,
|
||||
#results_table.paused .datatable--header-hover,
|
||||
#results_table.paused .datatable--header-cursor {
|
||||
background: #262338;
|
||||
color: #8b90a1;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#results_tabs {
|
||||
height: 1fr;
|
||||
background: #070d27;
|
||||
}
|
||||
|
||||
#data_tab,
|
||||
#tree_tab {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#ops_tree {
|
||||
border: solid #697597;
|
||||
background: #050b22;
|
||||
color: #cfd7eb;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#bottom_status_bar {
|
||||
border-top: solid #596285;
|
||||
background: #070d27;
|
||||
height: 2;
|
||||
margin: 0;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
#status_right_wrap {
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
#status_row {
|
||||
height: 1;
|
||||
margin-top: 0;
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
#live_status {
|
||||
width: 1fr;
|
||||
color: #b3bed9;
|
||||
content-align: right middle;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="root"):
|
||||
with Vertical(id="query_panel"):
|
||||
with Horizontal(id="query_header_row"):
|
||||
yield Static("DSL Query", id="query_header")
|
||||
yield Static("", id="query_header_spacer")
|
||||
yield Button("✕", id="window_close_button")
|
||||
yield TextArea(
|
||||
text=self._load_last_query(),
|
||||
id="query_input",
|
||||
language="javascript",
|
||||
theme="vscode_dark",
|
||||
show_line_numbers=True,
|
||||
)
|
||||
with Vertical(id="results_panel"):
|
||||
with Horizontal(id="results_toolbar"):
|
||||
with Horizontal(id="results_left"):
|
||||
yield Button("▶", id="execute_button")
|
||||
yield Button("⏸ Pause", id="stream_toggle_button")
|
||||
yield Static("", id="results_spacer")
|
||||
with Horizontal(id="results_actions"):
|
||||
yield Input(placeholder="Search: col:value", id="results_search")
|
||||
yield Button("⌕ Search", id="search_button")
|
||||
yield Button("⧉ Copy", id="copy_button")
|
||||
yield Button("⇩ Export", id="export_button")
|
||||
with TabbedContent(initial="data_tab", id="results_tabs"):
|
||||
with TabPane("Data", id="data_tab"):
|
||||
table = DataTable(id="results_table")
|
||||
table.cursor_type = "cell"
|
||||
table.show_cursor = True
|
||||
table.zebra_stripes = True
|
||||
yield table
|
||||
with TabPane("Operators", id="tree_tab"):
|
||||
tree = Tree("Assemblies", id="ops_tree")
|
||||
tree.show_root = False
|
||||
yield tree
|
||||
with Horizontal(id="bottom_status_bar"):
|
||||
with Horizontal(id="status_right_wrap"):
|
||||
with Horizontal(id="status_row"):
|
||||
yield Static(
|
||||
"Rows: 0 | Rate: 0.0/s | Total Updates: 0 | Uptime: 00:00:00 | Last Update: --:--:--",
|
||||
id="live_status",
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._update_timestamps: deque[float] = deque()
|
||||
self._last_update_at: datetime | None = None
|
||||
self._stream_started_at: float | None = None
|
||||
self._total_updates = 0
|
||||
self._last_row_count = 0
|
||||
self._current_col_names: list[str] = []
|
||||
self._current_rows: list[dict[str, Any]] = []
|
||||
self._column_width_cache: dict[str, int] = {}
|
||||
self._ops_catalog_rows: list[dict[str, Any]] = []
|
||||
self._ops_catalog_error: str | None = None
|
||||
self._ops_catalog_loaded = False
|
||||
self._ops_catalog_loading = False
|
||||
self._interop_is_running = False
|
||||
self._interop_exit_code: int | None = None
|
||||
self._grid_updates_enabled = True
|
||||
self._paused_latest_response: DslResponse | None = None
|
||||
self._ensure_interop_started()
|
||||
self._refresh_ops_tree()
|
||||
self._refresh_ops_tree_async()
|
||||
self._configure_query_highlighting()
|
||||
self._set_pause_visuals()
|
||||
self.set_interval(0.5, self._refresh_status)
|
||||
|
||||
def _configure_query_highlighting(self) -> None:
|
||||
query_input = self.query_one("#query_input", TextArea)
|
||||
query_input.language = "javascript"
|
||||
dsl_theme = TextAreaTheme(
|
||||
"dsqa_ide",
|
||||
base_style=Style.parse("#d7deee on #171a22"),
|
||||
gutter_style=Style.parse("#8a93a8 on #1c212b"),
|
||||
cursor_style=Style.parse("#ffffff on #2b3242"),
|
||||
cursor_line_style=Style.parse("on #202534"),
|
||||
cursor_line_gutter_style=Style.parse("#a6afc4 on #202534"),
|
||||
selection_style=Style.parse("on #2a3f67"),
|
||||
bracket_matching_style=Style.parse("bold #7fd9ff"),
|
||||
syntax_styles={
|
||||
"comment": Style.parse("italic #6f778a"),
|
||||
"variable": Style.parse("bold #4ec9b0"),
|
||||
"function": Style.parse("bold #4fc1ff"),
|
||||
"function.method": Style.parse("bold #70d4ff"),
|
||||
"parameter": Style.parse("#c6d4f8"),
|
||||
"string": Style.parse("#d7ba7d"),
|
||||
"number": Style.parse("#b5cea8"),
|
||||
"constant.builtin": Style.parse("bold #569cd6"),
|
||||
},
|
||||
)
|
||||
query_input.register_theme(dsl_theme)
|
||||
query_input.theme = "dsqa_ide"
|
||||
try:
|
||||
query_input.update_highlight_query(self.DSL_HIGHLIGHT_QUERY)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_shutdown(self) -> None:
|
||||
stop_interop()
|
||||
|
||||
@staticmethod
|
||||
def _ensure_interop_started() -> None:
|
||||
try:
|
||||
start_interop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def action_execute(self) -> None:
|
||||
tabs = self.query_one("#results_tabs", TabbedContent)
|
||||
tabs.active = "data_tab"
|
||||
self._grid_updates_enabled = True
|
||||
self._set_stream_toggle_label()
|
||||
self._set_pause_visuals()
|
||||
self._execute_query_async()
|
||||
|
||||
def action_noop(self) -> None:
|
||||
pass
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
button_id = event.button.id
|
||||
if button_id == "search_button":
|
||||
self._search_results()
|
||||
elif button_id == "copy_button":
|
||||
self._copy_selected_cell()
|
||||
elif button_id == "export_button":
|
||||
self._export_results_csv()
|
||||
elif button_id == "execute_button":
|
||||
self.action_execute()
|
||||
elif button_id == "window_close_button":
|
||||
self.exit()
|
||||
elif button_id == "stream_toggle_button":
|
||||
self._toggle_stream_painting()
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
if event.input.id == "results_search":
|
||||
self._search_results()
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
if getattr(event.widget, "id", None) != "ops_tree":
|
||||
return
|
||||
line = event.style.meta.get("line")
|
||||
if line is None:
|
||||
return
|
||||
tree = self.query_one("#ops_tree", Tree)
|
||||
node = tree.get_node_at_line(line)
|
||||
if node is None:
|
||||
return
|
||||
node_data = node.data
|
||||
if not isinstance(node_data, dict) or node_data.get("kind") != "constructor":
|
||||
return
|
||||
self._append_operator_to_query(self._stringify(node_data.get("op", "")).strip())
|
||||
|
||||
def _append_operator_to_query(self, op_name: str) -> None:
|
||||
if not op_name:
|
||||
return
|
||||
query_input = self.query_one("#query_input", TextArea)
|
||||
existing = query_input.text
|
||||
if existing.strip():
|
||||
separator = "" if existing.endswith("\n") else "\n"
|
||||
query_input.text = f"{existing}{separator}{op_name}"
|
||||
else:
|
||||
query_input.text = op_name
|
||||
query_input.focus()
|
||||
|
||||
def _execute_query_async(self) -> None:
|
||||
self._ensure_interop_started()
|
||||
query = self.query_one(TextArea).text.strip()
|
||||
if not query:
|
||||
return
|
||||
self._column_width_cache.clear()
|
||||
self._save_last_query(query)
|
||||
self._update_timestamps.clear()
|
||||
self._last_update_at = None
|
||||
self._stream_started_at = time.monotonic()
|
||||
self._total_updates = 0
|
||||
self._last_row_count = 0
|
||||
self._paused_latest_response = None
|
||||
self._refresh_status()
|
||||
|
||||
req_id = str(uuid.uuid4())
|
||||
|
||||
def _run() -> None:
|
||||
try:
|
||||
def _on_response(resp: DslResponse) -> None:
|
||||
self.call_from_thread(self._render_response, resp)
|
||||
|
||||
get_dsl(query, req_id, _on_response)
|
||||
except Exception as exc:
|
||||
error_resp = DslResponse(data_packet=None, req_id=req_id, error=str(exc))
|
||||
self.call_from_thread(self._render_response, error_resp)
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _render_response(self, response: DslResponse) -> None:
|
||||
if not self._grid_updates_enabled:
|
||||
self._paused_latest_response = response
|
||||
return
|
||||
self._render_response_internal(response)
|
||||
|
||||
def _render_response_internal(self, response: DslResponse) -> None:
|
||||
self._record_update()
|
||||
table = self.query_one(DataTable)
|
||||
table.remove_class("error")
|
||||
prev_scroll_x = table.scroll_offset.x
|
||||
prev_scroll_y = table.scroll_offset.y
|
||||
table.clear(columns=True)
|
||||
|
||||
if response.error:
|
||||
table.add_class("error")
|
||||
error_text = self._stringify(response.error)
|
||||
error_lines = error_text.splitlines() or [error_text]
|
||||
self._current_col_names = []
|
||||
self._current_rows = []
|
||||
self._last_row_count = len(error_lines)
|
||||
self._refresh_ops_tree()
|
||||
table.add_columns("error")
|
||||
for line in error_lines:
|
||||
table.add_row(Text(line, style="bold #f17486"))
|
||||
return
|
||||
|
||||
if response.data_packet is None or not response.data_packet.state:
|
||||
self._current_col_names = []
|
||||
self._current_rows = []
|
||||
self._last_row_count = 0
|
||||
self._refresh_ops_tree()
|
||||
table.add_columns("empty")
|
||||
table.add_row("no data")
|
||||
return
|
||||
|
||||
data_packet = response.data_packet.state[0]
|
||||
columns = data_packet.get("columns", [])
|
||||
rows = data_packet.get("all", [])
|
||||
if not isinstance(rows, list):
|
||||
rows = []
|
||||
|
||||
col_names = [c.get("name", "") for c in columns]
|
||||
if not col_names:
|
||||
col_names = list(rows[0].keys()) if rows else ["empty"]
|
||||
|
||||
self._current_col_names = col_names
|
||||
self._current_rows = rows
|
||||
self._last_row_count = len(rows)
|
||||
self._refresh_ops_tree()
|
||||
for col_name in col_names:
|
||||
width = self._column_width_cache.get(col_name, len(col_name))
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
cell_text = self._stringify(row.get(col_name, ""))
|
||||
width = max(width, len(cell_text))
|
||||
self._column_width_cache[col_name] = width
|
||||
table.add_column(col_name, width=width)
|
||||
for row in rows:
|
||||
table.add_row(*[self._format_cell(c, row.get(c, "")) for c in col_names])
|
||||
# Preserve user's viewport during streaming refreshes.
|
||||
table.scroll_to(
|
||||
x=min(prev_scroll_x, table.max_scroll_x),
|
||||
y=min(prev_scroll_y, table.max_scroll_y),
|
||||
animate=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _stringify(value: Any) -> str:
|
||||
return "" if value is None else str(value)
|
||||
|
||||
@staticmethod
|
||||
def _format_cell(column_name: str, value: Any) -> str | Text:
|
||||
text = DSQALiveConsole._stringify(value)
|
||||
lowered = column_name.lower()
|
||||
if lowered == "id":
|
||||
return Text(text, style="bold #9eb1d8")
|
||||
if lowered == "symbol":
|
||||
return Text(text, style="bold #dce6ff")
|
||||
if lowered == "price":
|
||||
return Text(text, style="bold #d4ddf2")
|
||||
if lowered == "time":
|
||||
return Text(text, style="#aab5d0")
|
||||
if lowered == "change":
|
||||
cleaned = text.strip()
|
||||
if cleaned.startswith("-"):
|
||||
return Text(text, style="bold #f17486")
|
||||
if cleaned.startswith("+"):
|
||||
return Text(text, style="bold #92d67e")
|
||||
return Text(text, style="bold #c7d3ef")
|
||||
if lowered == "bid":
|
||||
return Text(text, style="bold #8cf3c0 on #123526")
|
||||
if lowered == "ask":
|
||||
return Text(text, style="bold #ffd5d9 on #3b1b25")
|
||||
return text
|
||||
|
||||
def _record_update(self) -> None:
|
||||
now = time.monotonic()
|
||||
self._update_timestamps.append(now)
|
||||
self._total_updates += 1
|
||||
if self._stream_started_at is None:
|
||||
self._stream_started_at = now
|
||||
self._last_update_at = datetime.now()
|
||||
self._refresh_status()
|
||||
|
||||
def _refresh_status(self) -> None:
|
||||
self._refresh_interop_heartbeat()
|
||||
now = time.monotonic()
|
||||
cutoff = now - 1.0
|
||||
while self._update_timestamps and self._update_timestamps[0] < cutoff:
|
||||
self._update_timestamps.popleft()
|
||||
|
||||
rate = float(len(self._update_timestamps))
|
||||
if self._stream_started_at is not None:
|
||||
uptime_seconds = max(0, int(now - self._stream_started_at))
|
||||
uptime = self._format_duration(uptime_seconds)
|
||||
else:
|
||||
uptime = "--:--:--"
|
||||
last_update = self._last_update_at.strftime("%H:%M:%S") if self._last_update_at else "--:--:--"
|
||||
status = Text()
|
||||
if self._interop_is_running:
|
||||
status.append("Interop: ● UP", style="bold #92d67e")
|
||||
elif self._interop_exit_code is not None:
|
||||
status.append(f"Interop: ● DOWN({self._interop_exit_code})", style="bold #f17486")
|
||||
else:
|
||||
status.append("Interop: ● DOWN", style="bold #8b90a1")
|
||||
status.append(" | ")
|
||||
status.append(f"Rows: {self._last_row_count} | Rate: {rate:.1f}/s | ")
|
||||
status.append(f"Total Updates: {self._total_updates}", style="bold #79afe7")
|
||||
status.append(f" | Uptime: {uptime} | ")
|
||||
status.append(f"Last Update: {last_update}", style="bold #79afe7")
|
||||
self.query_one("#live_status", Static).update(status)
|
||||
|
||||
def _refresh_interop_heartbeat(self) -> None:
|
||||
is_running, exit_code = get_interop_status()
|
||||
self._interop_is_running = is_running
|
||||
self._interop_exit_code = exit_code
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(total_seconds: int) -> str:
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
def _search_results(self) -> None:
|
||||
raw = self.query_one("#results_search", Input).value.strip()
|
||||
if ":" not in raw:
|
||||
return
|
||||
col_part, value_part = raw.split(":", 1)
|
||||
target_col = col_part.strip()
|
||||
target_value = value_part.strip()
|
||||
if not target_col:
|
||||
return
|
||||
|
||||
lookup = {name.lower(): idx for idx, name in enumerate(self._current_col_names)}
|
||||
col_idx = lookup.get(target_col.lower())
|
||||
if col_idx is None:
|
||||
return
|
||||
|
||||
col_name = self._current_col_names[col_idx]
|
||||
needle = target_value.lower()
|
||||
for row_idx, row in enumerate(self._current_rows):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
cell_value = self._stringify(row.get(col_name, ""))
|
||||
if needle in cell_value.lower():
|
||||
table = self.query_one("#results_table", DataTable)
|
||||
table.move_cursor(row=row_idx, column=col_idx)
|
||||
table.focus()
|
||||
return
|
||||
|
||||
def _copy_selected_cell(self) -> None:
|
||||
table = self.query_one("#results_table", DataTable)
|
||||
if table.row_count == 0 or not self._current_col_names:
|
||||
return
|
||||
coordinate = table.cursor_coordinate
|
||||
try:
|
||||
cell = table.get_cell_at(coordinate)
|
||||
except Exception:
|
||||
return
|
||||
if isinstance(cell, Text):
|
||||
value = cell.plain
|
||||
else:
|
||||
value = self._stringify(cell)
|
||||
self.copy_to_clipboard(value)
|
||||
|
||||
def _export_results_csv(self) -> None:
|
||||
if not self._current_col_names:
|
||||
return
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
export_path = os.path.join(_here, f"DSQA_RESULTS_EXPORT_{timestamp}.csv")
|
||||
with open(export_path, "w", newline="", encoding="utf-8") as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(self._current_col_names)
|
||||
for row in self._current_rows:
|
||||
if isinstance(row, dict):
|
||||
writer.writerow([self._stringify(row.get(col, "")) for col in self._current_col_names])
|
||||
|
||||
def _refresh_ops_tree(self) -> None:
|
||||
tree = self.query_one("#ops_tree", Tree)
|
||||
root = tree.root
|
||||
root.remove_children()
|
||||
|
||||
if self._ops_catalog_error:
|
||||
root.add(Text(self._ops_catalog_error, style="bold #f17486"))
|
||||
root.expand()
|
||||
return
|
||||
|
||||
grouped: dict[str, dict[str, dict[str, set[str]]]] = {}
|
||||
for row in self._ops_catalog_rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
row_lookup = {self._stringify(k).lower(): v for k, v in row.items()}
|
||||
assembly_name = self._stringify(row_lookup.get("assembly_name", "")).strip() or "(unknown assembly)"
|
||||
namespace = self._stringify(row_lookup.get("namespace", "")).strip() or "(global)"
|
||||
op = self._stringify(row_lookup.get("op", "")).strip() or "(unknown op)"
|
||||
constructor = self._stringify(row_lookup.get("constructor", "")).strip() or "(none)"
|
||||
grouped.setdefault(assembly_name, {}).setdefault(namespace, {}).setdefault(op, set()).add(constructor)
|
||||
|
||||
if not grouped:
|
||||
root.add(Text("No operator metadata available", style="#8ea0c6"))
|
||||
root.expand()
|
||||
return
|
||||
|
||||
for assembly_name in sorted(grouped.keys(), key=str.lower):
|
||||
assembly_node = root.add(Text(assembly_name, style="bold #79afe7"))
|
||||
for namespace in sorted(grouped[assembly_name].keys(), key=str.lower):
|
||||
namespace_node = assembly_node.add(Text(namespace, style="#cfd7eb"))
|
||||
for op in sorted(grouped[assembly_name][namespace].keys(), key=str.lower):
|
||||
op_node = namespace_node.add(
|
||||
Text(op, style="#b8c8e7"),
|
||||
data={"kind": "op", "op": op},
|
||||
)
|
||||
for constructor in sorted(grouped[assembly_name][namespace][op], key=str.lower):
|
||||
op_node.add(
|
||||
Text(constructor, style="#9eb0d4"),
|
||||
data={"kind": "constructor", "op": op, "constructor": constructor},
|
||||
)
|
||||
op_node.expand()
|
||||
namespace_node.expand()
|
||||
assembly_node.expand()
|
||||
root.expand()
|
||||
|
||||
def _refresh_ops_tree_async(self) -> None:
|
||||
if self._ops_catalog_loaded or self._ops_catalog_loading:
|
||||
return
|
||||
self._ensure_interop_started()
|
||||
self._ops_catalog_loading = True
|
||||
req_id = str(uuid.uuid4())
|
||||
|
||||
def _run() -> None:
|
||||
try:
|
||||
def _on_response(resp: DslResponse) -> None:
|
||||
self.call_from_thread(self._update_ops_catalog, resp)
|
||||
|
||||
get_dsl("OpsStream()", req_id, _on_response)
|
||||
except Exception as exc:
|
||||
error_resp = DslResponse(data_packet=None, req_id=req_id, error=str(exc))
|
||||
self.call_from_thread(self._update_ops_catalog, error_resp)
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _update_ops_catalog(self, response: DslResponse) -> None:
|
||||
self._ops_catalog_loading = False
|
||||
if response.error:
|
||||
self._ops_catalog_rows = []
|
||||
self._ops_catalog_error = self._stringify(response.error)
|
||||
self._refresh_ops_tree()
|
||||
return
|
||||
|
||||
if response.data_packet is None or not response.data_packet.state:
|
||||
self._ops_catalog_rows = []
|
||||
self._ops_catalog_error = "OpsStream() returned no data"
|
||||
self._refresh_ops_tree()
|
||||
return
|
||||
|
||||
data_packet = response.data_packet.state[0]
|
||||
rows = data_packet.get("all", [])
|
||||
if not isinstance(rows, list):
|
||||
rows = []
|
||||
normalized_rows: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if isinstance(row, dict):
|
||||
normalized_rows.append(row)
|
||||
self._ops_catalog_rows = normalized_rows
|
||||
self._ops_catalog_error = None
|
||||
self._ops_catalog_loaded = True
|
||||
self._refresh_ops_tree()
|
||||
|
||||
def _toggle_stream_painting(self) -> None:
|
||||
self._grid_updates_enabled = not self._grid_updates_enabled
|
||||
self._set_stream_toggle_label()
|
||||
self._set_pause_visuals()
|
||||
if self._grid_updates_enabled and self._paused_latest_response is not None:
|
||||
latest = self._paused_latest_response
|
||||
self._paused_latest_response = None
|
||||
self._render_response_internal(latest)
|
||||
|
||||
def _set_stream_toggle_label(self) -> None:
|
||||
button = self.query_one("#stream_toggle_button", Button)
|
||||
button.label = "⏸ Pause" if self._grid_updates_enabled else "▶ Play"
|
||||
|
||||
def _set_pause_visuals(self) -> None:
|
||||
table = self.query_one("#results_table", DataTable)
|
||||
panel = self.query_one("#results_panel", Vertical)
|
||||
if self._grid_updates_enabled:
|
||||
table.remove_class("paused")
|
||||
panel.remove_class("paused")
|
||||
else:
|
||||
table.add_class("paused")
|
||||
panel.add_class("paused")
|
||||
|
||||
def _load_last_query(self) -> str:
|
||||
try:
|
||||
with open(self.LAST_QUERY_PATH, "r", encoding="utf-8") as file:
|
||||
text = file.read().strip()
|
||||
return text if text else self.DEFAULT_QUERY
|
||||
except FileNotFoundError:
|
||||
return self.DEFAULT_QUERY
|
||||
except OSError:
|
||||
return self.DEFAULT_QUERY
|
||||
|
||||
def _save_last_query(self, query: str) -> None:
|
||||
try:
|
||||
with open(self.LAST_QUERY_PATH, "w", encoding="utf-8") as file:
|
||||
file.write(query)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
DSQALiveConsole().run()
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>BlueOrbDSLInteropServer</AssemblyName>
|
||||
<RootNamespace>BlueOrbDSLInteropServer</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<Choose>
|
||||
<When Condition="'$(UseProjectRefs)' == 'true'">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\cs\Bts.Spark.Api\Bts.Spark.Api.csproj" />
|
||||
<ProjectReference Include="..\DataStream\DataStream.csproj" />
|
||||
<ProjectReference Include="..\PubSub\PubSub.csproj" />
|
||||
<ProjectReference Include="..\BlueOrbOperatorStreams\BlueOrbOperatorStreams.csproj" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bts.Spark.Api" Version="$(SparkVersion)" />
|
||||
<PackageReference Include="DataStream" Version="$(SparkVersion)" />
|
||||
<PackageReference Include="PubSub" Version="$(SparkVersion)" />
|
||||
<PackageReference Include="BlueOrbOperatorStreams" Version="$(SparkVersion)" />
|
||||
</ItemGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using BCG;
|
||||
|
||||
using bts.BlueOrb;
|
||||
|
||||
using Bts.DataStream;
|
||||
|
||||
using Bts.DataStreamCore;
|
||||
using Bts.DataStreamExpressions;
|
||||
using Bts.PubSub;
|
||||
|
||||
using MathNet.Numerics.Statistics;
|
||||
|
||||
|
||||
namespace BlueOrbDSLInteropServer {
|
||||
// BlueOrbDSLInteropServer reads DSL lines from stdin, validates them,
|
||||
// and writes one DataPacket JSON line to stdout per input.
|
||||
internal static class Program {
|
||||
private const string ExitSignal = ":exit";
|
||||
private static SyncQueue<DslRequest> _work_queue = new SyncQueue<DslRequest>();
|
||||
private static JsonSerializerOptions response_json_options = new JsonSerializerOptions {
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
||||
};
|
||||
private static string _current_req_id;
|
||||
|
||||
// Request from Python
|
||||
private sealed class DslRequest {
|
||||
public string? dsl { get; set; }
|
||||
public string? req_id { get; set; }
|
||||
}
|
||||
|
||||
// Response envelope for Python: includes DataPacket, request id, and optional error.
|
||||
private sealed class DslResponse {
|
||||
public DataPacket? data_packet { get; set; }
|
||||
public string? req_id { get; set; }
|
||||
public string? error { get; set; }
|
||||
}
|
||||
|
||||
private static void Main(string[] args) {
|
||||
//System.Threading.Thread.Sleep(20000);
|
||||
System.Threading.Thread thread = new Thread(_worker_loop);
|
||||
thread.IsBackground = true;
|
||||
thread.Start();
|
||||
response_json_options.Converters.Add(new DataPacketJsonConverter {
|
||||
IncludeCurrent = true,
|
||||
UseObjectRows = true,
|
||||
});
|
||||
string? line;
|
||||
while ((line = Console.ReadLine()) != null) {
|
||||
line = line.Trim();
|
||||
if (line.Length == 0) {
|
||||
continue;
|
||||
}
|
||||
if (string.Equals(line, ExitSignal, StringComparison.OrdinalIgnoreCase)) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
DslRequest req = JsonSerializer.Deserialize<DslRequest>(line);
|
||||
_work_queue.enqueue(req);
|
||||
} catch (Exception ex) {
|
||||
bts.Log.assert(false, ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void _worker_loop() {
|
||||
Dictionary<string, ViewStream> req_map = new Dictionary<string, ViewStream>();
|
||||
while (true) {
|
||||
System.Threading.Thread.Sleep(100);
|
||||
ViewStreamDatabase.instance.doWork();
|
||||
var new_reqs = _work_queue.dequeue_all();
|
||||
if (new_reqs == null || new_reqs.Count == 0) {
|
||||
continue;
|
||||
}
|
||||
// ubsubscribing to previous stream(s)
|
||||
foreach (var kv in req_map) {
|
||||
kv.Value.onChange -= dp_obj => _on_dp(kv.Key, dp_obj);
|
||||
}
|
||||
foreach (var r in new_reqs) {
|
||||
var req_id = string.IsNullOrWhiteSpace(r.req_id) ? Guid.NewGuid().ToString() : r.req_id;
|
||||
_current_req_id = req_id;
|
||||
try {
|
||||
DSLStream dsl_str = new DSLStream(r.dsl);
|
||||
req_map[req_id] = dsl_str;
|
||||
dsl_str.onChange += dp_obj => _on_dp(req_id, dp_obj);
|
||||
} catch (Exception ex) {
|
||||
_write_error(req_id, ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void _on_dp(string req_id, object dp_obj) {
|
||||
// eventhough we unsubscribe to all the other streams except the currrent one,
|
||||
// sometimes the onLastSub is not implemented and we still get updates, this masks them
|
||||
if (req_id != _current_req_id)
|
||||
return;
|
||||
DataPacket dp = dp_obj as DataPacket;
|
||||
DslResponse response = new DslResponse() {
|
||||
data_packet = dp,
|
||||
req_id = req_id,
|
||||
error = ""
|
||||
};
|
||||
var json = JsonSerializer.Serialize(response, response_json_options);
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
|
||||
private static void _write_error(string req_id, string error) {
|
||||
DslResponse response = new DslResponse() {
|
||||
data_packet = null,
|
||||
req_id = req_id,
|
||||
error = error
|
||||
};
|
||||
var json = JsonSerializer.Serialize(response, response_json_options);
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
// namespace BlueOrb.Operators.Signal;
|
||||
|
||||
// using BCG;
|
||||
|
||||
// using bts;
|
||||
|
||||
// using System;
|
||||
// using System.Collections.Generic;
|
||||
// using System.Globalization;
|
||||
// using System.Linq;
|
||||
|
||||
// [DataStreamUserRespresentation("BarBuilderStream")]
|
||||
// public class BarBuilderStream : ViewStream
|
||||
// {
|
||||
// enum BarType
|
||||
// {
|
||||
// TIME,
|
||||
// TICK,
|
||||
// VOLUME,
|
||||
// DOLLAR,
|
||||
// }
|
||||
|
||||
// enum PriceSource
|
||||
// {
|
||||
// trade_price,
|
||||
// mid,
|
||||
// bid,
|
||||
// ask,
|
||||
// }
|
||||
|
||||
// enum VolumeSource
|
||||
// {
|
||||
// trade_size,
|
||||
// none,
|
||||
// }
|
||||
|
||||
// enum SessionReset
|
||||
// {
|
||||
// NONE,
|
||||
// DAY,
|
||||
// RTH,
|
||||
// ETH,
|
||||
// }
|
||||
|
||||
// sealed class BarState
|
||||
// {
|
||||
// public string Contract = string.Empty;
|
||||
// public DateTime StartTs;
|
||||
// public DateTime EndTs;
|
||||
// public double Open;
|
||||
// public double High;
|
||||
// public double Low;
|
||||
// public double Close;
|
||||
// public double Volume;
|
||||
// public int TradeCount;
|
||||
// public double SumPriceSize;
|
||||
// public double SumSize;
|
||||
// public double DollarNotional;
|
||||
// public bool HasData;
|
||||
// public DateTime SessionDay;
|
||||
// public bool InitialSnapshotEmitted;
|
||||
// }
|
||||
|
||||
// readonly ViewStream _priceStream;
|
||||
// readonly BarType _barType;
|
||||
// readonly int _barSize;
|
||||
// readonly PriceSource _priceSource;
|
||||
// readonly VolumeSource _volumeSource;
|
||||
// readonly string _timestampCol;
|
||||
// readonly bool _emitForming;
|
||||
// readonly bool _emitInitialSnapshot;
|
||||
// readonly SessionReset _sessionReset;
|
||||
|
||||
// readonly Dictionary<string, BarState> _stateByContract = new(StringComparer.Ordinal);
|
||||
// readonly Dictionary<string, bool> _validCols = new(StringComparer.OrdinalIgnoreCase);
|
||||
// bool _validated;
|
||||
// int _nextId;
|
||||
// readonly BlueTable _outTable;
|
||||
// readonly IBlueTableSchema _outSchema;
|
||||
|
||||
// public BarBuilderStream(
|
||||
// ViewStream price_stream,
|
||||
// string bar_type,
|
||||
// int bar_size,
|
||||
// string price_source,
|
||||
// string volume_source,
|
||||
// string timestamp_col = "ts_event",
|
||||
// int emit_forming_i = 0,
|
||||
// string session_reset = "NONE",
|
||||
// int emit_initial_snapshot_i = 0
|
||||
// )
|
||||
// {
|
||||
// _priceStream = price_stream;
|
||||
// _barType = ParseBarType(bar_type);
|
||||
// _barSize = bar_size;
|
||||
// _priceSource = ParsePriceSource(price_source);
|
||||
// _volumeSource = ParseVolumeSource(volume_source);
|
||||
// _timestampCol = timestamp_col;
|
||||
// _emitForming = emit_forming_i == 1;
|
||||
// _emitInitialSnapshot = emit_initial_snapshot_i == 1;
|
||||
// _sessionReset = ParseSessionReset(session_reset);
|
||||
|
||||
// if (_barSize <= 0)
|
||||
// {
|
||||
// throw new ArgumentException("bar_size must be > 0");
|
||||
// }
|
||||
|
||||
// _outSchema = BlueDataUtility.form_schema(
|
||||
// "BarBuilder",
|
||||
// Tuple.Create("id", TypeCode.Int32),
|
||||
// Tuple.Create("ts_event", TypeCode.String),
|
||||
// Tuple.Create("bar_start_ts", TypeCode.String),
|
||||
// Tuple.Create("bar_end_ts", TypeCode.String),
|
||||
// Tuple.Create("contract", TypeCode.String),
|
||||
// Tuple.Create("open", TypeCode.Double),
|
||||
// Tuple.Create("high", TypeCode.Double),
|
||||
// Tuple.Create("low", TypeCode.Double),
|
||||
// Tuple.Create("close", TypeCode.Double),
|
||||
// Tuple.Create("volume", TypeCode.Double),
|
||||
// Tuple.Create("trade_count", TypeCode.Int32),
|
||||
// Tuple.Create("vwap", TypeCode.Double),
|
||||
// Tuple.Create("is_forming", TypeCode.Boolean)
|
||||
// );
|
||||
// _outTable = new BlueTable(_outSchema);
|
||||
// }
|
||||
|
||||
// protected override void myOnFirstSubscriber()
|
||||
// {
|
||||
// _priceStream.onChange += _on_price_change;
|
||||
// }
|
||||
|
||||
// protected override void myOnLastSubscriber()
|
||||
// {
|
||||
// _priceStream.onChange -= _on_price_change;
|
||||
// }
|
||||
|
||||
// void _on_price_change(object newValue)
|
||||
// {
|
||||
// var dp = newValue as IDataPacket;
|
||||
// if (dp is null)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// if (!_validated)
|
||||
// {
|
||||
// ValidateInputSchema(dp.newRows.schema);
|
||||
// _validated = true;
|
||||
// }
|
||||
|
||||
// var emitted = new List<IBlueRow>();
|
||||
// foreach (IBlueRow row in dp.addedRows)
|
||||
// {
|
||||
// ProcessEvent(row, emitted);
|
||||
// }
|
||||
|
||||
// if (emitted.Count == 0)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// foreach (var r in emitted)
|
||||
// {
|
||||
// _outTable.addRow(r);
|
||||
// }
|
||||
// var outDp = BlueDataUtility.build_dp_inc(_outTable, myLastValue as IDataPacket);
|
||||
// myFireEvent(outDp);
|
||||
// }
|
||||
|
||||
// void ValidateInputSchema(IBlueTableSchema schema)
|
||||
// {
|
||||
// _validCols.Clear();
|
||||
// foreach (var name in schema.columnNames)
|
||||
// {
|
||||
// _validCols[name] = true;
|
||||
// }
|
||||
|
||||
// if (!_validCols.ContainsKey(_timestampCol))
|
||||
// {
|
||||
// throw new ArgumentException($"Missing required column: {_timestampCol}");
|
||||
// }
|
||||
|
||||
// switch (_priceSource)
|
||||
// {
|
||||
// case PriceSource.trade_price:
|
||||
// RequireCol("trade_price");
|
||||
// break;
|
||||
// case PriceSource.mid:
|
||||
// RequireCol("bid_price");
|
||||
// RequireCol("ask_price");
|
||||
// break;
|
||||
// case PriceSource.bid:
|
||||
// RequireCol("bid_price");
|
||||
// break;
|
||||
// case PriceSource.ask:
|
||||
// RequireCol("ask_price");
|
||||
// break;
|
||||
// }
|
||||
|
||||
// if (_barType is BarType.VOLUME or BarType.DOLLAR)
|
||||
// {
|
||||
// if (_volumeSource == VolumeSource.none || !_validCols.ContainsKey("trade_size"))
|
||||
// {
|
||||
// throw new ArgumentException("VOLUME/DOLLAR require executed trade volume; invalid for BBO-only streams.");
|
||||
// }
|
||||
// }
|
||||
// else if (_volumeSource == VolumeSource.trade_size)
|
||||
// {
|
||||
// RequireCol("trade_size");
|
||||
// }
|
||||
// }
|
||||
|
||||
// void RequireCol(string name)
|
||||
// {
|
||||
// if (!_validCols.ContainsKey(name))
|
||||
// {
|
||||
// throw new ArgumentException($"Missing required column for selected source: {name}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// void ProcessEvent(IBlueRow row, List<IBlueRow> emitted)
|
||||
// {
|
||||
// var ts = ParseTimestamp(row);
|
||||
// var contract = ResolveContract(row);
|
||||
// var price = ResolvePrice(row);
|
||||
// var size = ResolveSize(row);
|
||||
// var state = GetOrCreateState(contract);
|
||||
|
||||
// if (state.HasData && _sessionReset != SessionReset.NONE && ts.Date != state.SessionDay.Date)
|
||||
// {
|
||||
// emitted.Add(ToBarRow(state, ts_event: state.EndTs, isForming: false));
|
||||
// state.HasData = false;
|
||||
// }
|
||||
|
||||
// switch (_barType)
|
||||
// {
|
||||
// case BarType.TIME:
|
||||
// ProcessTimeBar(state, ts, price, size, emitted);
|
||||
// break;
|
||||
// case BarType.TICK:
|
||||
// ProcessThresholdBar(state, ts, price, size, emitted, state.TradeCount + 1 >= _barSize);
|
||||
// break;
|
||||
// case BarType.VOLUME:
|
||||
// ProcessThresholdBar(state, ts, price, size, emitted, state.Volume + size >= _barSize);
|
||||
// break;
|
||||
// case BarType.DOLLAR:
|
||||
// ProcessThresholdBar(state, ts, price, size, emitted, state.DollarNotional + (price * size) >= _barSize);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// void ProcessTimeBar(BarState state, DateTime ts, double price, double size, List<IBlueRow> emitted)
|
||||
// {
|
||||
// var bucketStart = AlignTimeBucket(ts, _barSize);
|
||||
// var bucketEnd = bucketStart.AddSeconds(_barSize);
|
||||
|
||||
// if (!state.HasData)
|
||||
// {
|
||||
// StartBar(state, ts, bucketStart, bucketEnd, price, size);
|
||||
// MaybeEmitInitialSnapshot(state, ts, emitted);
|
||||
// if (_emitForming)
|
||||
// {
|
||||
// emitted.Add(ToBarRow(state, ts, isForming: true));
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (bucketStart < state.StartTs)
|
||||
// {
|
||||
// Log.Gen.error(
|
||||
// $"BarBuilderStream ignored out-of-order event. contract={state.Contract} ts={ToIso(ts)} " +
|
||||
// $"current_bar_start={ToIso(state.StartTs)}");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (bucketStart > state.StartTs)
|
||||
// {
|
||||
// emitted.Add(ToBarRow(state, state.EndTs, isForming: false));
|
||||
// StartBar(state, ts, bucketStart, bucketEnd, price, size);
|
||||
// if (_emitForming)
|
||||
// {
|
||||
// emitted.Add(ToBarRow(state, ts, isForming: true));
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Accumulate(state, price, size);
|
||||
// state.EndTs = bucketEnd;
|
||||
// if (_emitForming)
|
||||
// {
|
||||
// emitted.Add(ToBarRow(state, ts, isForming: true));
|
||||
// }
|
||||
// }
|
||||
|
||||
// void ProcessThresholdBar(
|
||||
// BarState state,
|
||||
// DateTime ts,
|
||||
// double price,
|
||||
// double size,
|
||||
// List<IBlueRow> emitted,
|
||||
// bool shouldClose
|
||||
// )
|
||||
// {
|
||||
// if (!state.HasData)
|
||||
// {
|
||||
// StartBar(state, ts, ts, ts, price, size);
|
||||
// MaybeEmitInitialSnapshot(state, ts, emitted);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Accumulate(state, price, size);
|
||||
// }
|
||||
// state.EndTs = ts;
|
||||
|
||||
// if (_emitForming)
|
||||
// {
|
||||
// emitted.Add(ToBarRow(state, ts, isForming: true));
|
||||
// }
|
||||
// if (shouldClose)
|
||||
// {
|
||||
// emitted.Add(ToBarRow(state, ts, isForming: false));
|
||||
// state.HasData = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// static DateTime AlignTimeBucket(DateTime ts, int seconds)
|
||||
// {
|
||||
// var utc = ts.ToUniversalTime();
|
||||
// long bucketTicks = TimeSpan.FromSeconds(seconds).Ticks;
|
||||
// long aligned = (utc.Ticks / bucketTicks) * bucketTicks;
|
||||
// return new DateTime(aligned, DateTimeKind.Utc);
|
||||
// }
|
||||
|
||||
// void MaybeEmitInitialSnapshot(BarState state, DateTime ts, List<IBlueRow> emitted)
|
||||
// {
|
||||
// if (!_emitInitialSnapshot || state.InitialSnapshotEmitted)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// emitted.Add(ToBarRow(state, ts, isForming: false));
|
||||
// state.InitialSnapshotEmitted = true;
|
||||
// }
|
||||
|
||||
// void StartBar(
|
||||
// BarState state,
|
||||
// DateTime ts,
|
||||
// DateTime barStart,
|
||||
// DateTime barEnd,
|
||||
// double price,
|
||||
// double size
|
||||
// )
|
||||
// {
|
||||
// state.StartTs = barStart;
|
||||
// state.EndTs = barEnd;
|
||||
// state.Open = price;
|
||||
// state.High = price;
|
||||
// state.Low = price;
|
||||
// state.Close = price;
|
||||
// state.Volume = 0;
|
||||
// state.TradeCount = 0;
|
||||
// state.SumPriceSize = 0;
|
||||
// state.SumSize = 0;
|
||||
// state.DollarNotional = 0;
|
||||
// state.SessionDay = ts.Date;
|
||||
// state.HasData = true;
|
||||
// Accumulate(state, price, size);
|
||||
// }
|
||||
|
||||
// static void Accumulate(BarState state, double price, double size)
|
||||
// {
|
||||
// state.High = Math.Max(state.High, price);
|
||||
// state.Low = Math.Min(state.Low, price);
|
||||
// state.Close = price;
|
||||
// state.TradeCount++;
|
||||
// if (size > 0)
|
||||
// {
|
||||
// state.Volume += size;
|
||||
// state.SumSize += size;
|
||||
// state.SumPriceSize += price * size;
|
||||
// state.DollarNotional += price * size;
|
||||
// }
|
||||
// }
|
||||
|
||||
// IBlueRow ToBarRow(BarState state, DateTime ts_event, bool isForming)
|
||||
// {
|
||||
// double vwap = state.SumSize > 0 ? state.SumPriceSize / state.SumSize : state.Close;
|
||||
// var row = new MutableBlueRow(_outSchema);
|
||||
// row.setValue("id", _nextId++);
|
||||
// row.setValue("ts_event", ToIso(ts_event));
|
||||
// row.setValue("bar_start_ts", ToIso(state.StartTs));
|
||||
// row.setValue("bar_end_ts", ToIso(state.EndTs));
|
||||
// row.setValue("contract", state.Contract);
|
||||
// row.setValue("open", state.Open);
|
||||
// row.setValue("high", state.High);
|
||||
// row.setValue("low", state.Low);
|
||||
// row.setValue("close", state.Close);
|
||||
// row.setValue("volume", _volumeSource == VolumeSource.none ? 0.0 : state.Volume);
|
||||
// row.setValue("trade_count", state.TradeCount);
|
||||
// row.setValue("vwap", vwap);
|
||||
// row.setValue("is_forming", isForming);
|
||||
// return row;
|
||||
// }
|
||||
|
||||
// BarState GetOrCreateState(string contract)
|
||||
// {
|
||||
// if (_stateByContract.TryGetValue(contract, out var existing))
|
||||
// {
|
||||
// return existing;
|
||||
// }
|
||||
// var created = new BarState { Contract = contract };
|
||||
// _stateByContract[contract] = created;
|
||||
// return created;
|
||||
// }
|
||||
|
||||
// DateTime ParseTimestamp(IBlueRow row)
|
||||
// {
|
||||
// object? raw = row[_timestampCol];
|
||||
// if (raw is DateTime dt)
|
||||
// {
|
||||
// return dt;
|
||||
// }
|
||||
// if (raw is string s && DateTime.TryParse(s, null, DateTimeStyles.RoundtripKind, out dt))
|
||||
// {
|
||||
// return dt;
|
||||
// }
|
||||
// throw new ArgumentException($"Invalid timestamp value in column '{_timestampCol}'.");
|
||||
// }
|
||||
|
||||
// string ResolveContract(IBlueRow row)
|
||||
// {
|
||||
// if (_validCols.ContainsKey("contract"))
|
||||
// {
|
||||
// return (row["contract"]?.ToString() ?? string.Empty).Trim();
|
||||
// }
|
||||
// if (_validCols.ContainsKey("symbol"))
|
||||
// {
|
||||
// return (row["symbol"]?.ToString() ?? string.Empty).Trim();
|
||||
// }
|
||||
// return string.Empty;
|
||||
// }
|
||||
|
||||
// double ResolvePrice(IBlueRow row)
|
||||
// {
|
||||
// return _priceSource switch
|
||||
// {
|
||||
// PriceSource.trade_price => ToDouble(row["trade_price"], "trade_price"),
|
||||
// PriceSource.mid => (ToDouble(row["bid_price"], "bid_price") + ToDouble(row["ask_price"], "ask_price")) / 2.0,
|
||||
// PriceSource.bid => ToDouble(row["bid_price"], "bid_price"),
|
||||
// PriceSource.ask => ToDouble(row["ask_price"], "ask_price"),
|
||||
// _ => throw new ArgumentOutOfRangeException()
|
||||
// };
|
||||
// }
|
||||
|
||||
// double ResolveSize(IBlueRow row)
|
||||
// {
|
||||
// if (_volumeSource == VolumeSource.none)
|
||||
// {
|
||||
// return 0;
|
||||
// }
|
||||
// return ToDouble(row["trade_size"], "trade_size");
|
||||
// }
|
||||
|
||||
// static double ToDouble(object? value, string colName)
|
||||
// {
|
||||
// var d = BlueDataUtility.GetDouble(value!);
|
||||
// if (!d.HasValue)
|
||||
// {
|
||||
// throw new ArgumentException($"Column '{colName}' must be numeric.");
|
||||
// }
|
||||
// return d.Value;
|
||||
// }
|
||||
|
||||
// static string ToIso(DateTime dt) =>
|
||||
// dt.ToUniversalTime().ToString("O");
|
||||
|
||||
// static BarType ParseBarType(string value)
|
||||
// {
|
||||
// if (Enum.TryParse<BarType>(value, true, out var parsed))
|
||||
// {
|
||||
// return parsed;
|
||||
// }
|
||||
// throw new ArgumentException("bar_type must be one of: TIME | TICK | VOLUME | DOLLAR");
|
||||
// }
|
||||
|
||||
// static PriceSource ParsePriceSource(string value)
|
||||
// {
|
||||
// if (Enum.TryParse<PriceSource>(value, true, out var parsed))
|
||||
// {
|
||||
// return parsed;
|
||||
// }
|
||||
// throw new ArgumentException("price_source must be one of: trade_price | mid | bid | ask");
|
||||
// }
|
||||
|
||||
// static VolumeSource ParseVolumeSource(string value)
|
||||
// {
|
||||
// if (Enum.TryParse<VolumeSource>(value, true, out var parsed))
|
||||
// {
|
||||
// return parsed;
|
||||
// }
|
||||
// throw new ArgumentException("volume_source must be one of: trade_size | none");
|
||||
// }
|
||||
|
||||
// static SessionReset ParseSessionReset(string value)
|
||||
// {
|
||||
// if (!Enum.TryParse<SessionReset>(value, true, out var parsed))
|
||||
// {
|
||||
// throw new ArgumentException("session_reset must be one of: NONE | DAY | RTH | ETH");
|
||||
// }
|
||||
// // TODO: differentiate RTH/ETH session boundaries; currently treated as DAY.
|
||||
// if (parsed is SessionReset.RTH or SessionReset.ETH)
|
||||
// {
|
||||
// return SessionReset.DAY;
|
||||
// }
|
||||
// return parsed;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
|
||||
<PackageReference Include="OpenAI" Version="2.0.0" />
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<Choose>
|
||||
<When Condition="'$(UseProjectRefs)' == 'true'">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\cs\Bts.Spark.Api\Bts.Spark.Api.csproj" />
|
||||
<ProjectReference Include="..\DataStreamCore\DataStreamCore.csproj" />
|
||||
<ProjectReference Include="..\DataStream\DataStream.csproj" />
|
||||
<ProjectReference Include="..\PubSub\PubSub.csproj" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bts.Spark.Api" Version="$(SparkVersion)" />
|
||||
<PackageReference Include="DataStream" Version="$(SparkVersion)" />
|
||||
<PackageReference Include="PubSub" Version="$(SparkVersion)" />
|
||||
</ItemGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<!-- <ItemGroup>
|
||||
<RowType Include="RowTemplates\RefTypeRow.tt" SchemaJson="RowTemplates\PubSubHeartbeatSchema.json" TypeName="PubSubHeartbeatRow" TableName="PubSubHeartbeat" />
|
||||
<RowType Include="RowTemplates\RefTypeRow.tt" SchemaJson="RowTemplates\PubSubRouteSchema.json" TypeName="PubSubRouteRow" TableName="PubSubRoute" />
|
||||
<RowType Include="RowTemplates\RefTypeRow.tt" SchemaJson="RowTemplates\PubSubServerStateSchema.json" TypeName="PubSubServerStateRow" TableName="PubSubServerState" />
|
||||
<RowType Include="RowTemplates\RefTypeRow.tt" SchemaJson="RowTemplates\PubSubServerTagStatsSchema.json" TypeName="PubSubServerTagStatsRow" TableName="PubSubServerTagStats" />
|
||||
<RowType Include="RowTemplates\RefTypeRow.tt" SchemaJson="RowTemplates\TableMetadataSchema.json" TypeName="TableMetadataRow" TableName="TableMetadata" />
|
||||
</ItemGroup> -->
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,294 @@
|
||||
namespace BlueOrb.Internal
|
||||
{
|
||||
using bts;
|
||||
//using bts.api;
|
||||
//using bts.spark;
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
public static class ContractUtils
|
||||
{
|
||||
public record FutureTradableContract(
|
||||
string Symbol,
|
||||
DateTime Expiration,
|
||||
Exchange Exchange,
|
||||
string? ContractText);
|
||||
|
||||
public class FutureSpreadContract
|
||||
{
|
||||
public List<Tuple<double, FutureTradableContract>> legs = new(2);
|
||||
public string Symbol
|
||||
{
|
||||
get
|
||||
{
|
||||
return legs == null || legs.Count == 0 ? string.Empty : legs.ElementAt(0).Item2.Symbol;
|
||||
}
|
||||
}
|
||||
public string? ContractText;
|
||||
}
|
||||
|
||||
public record FutureContinousContract(string Symbol, Exchange Exchange);
|
||||
|
||||
// this is for all future conytracts belonging to a symbol
|
||||
public record FutureWildcardContract(string Symbol, Exchange Exchange);
|
||||
|
||||
public class FutureContract
|
||||
{
|
||||
public FutureWildcardContract? wildcard_contract;
|
||||
public FutureTradableContract? future_tradable_contract;
|
||||
public FutureSpreadContract? spread_tradable_contract;
|
||||
public FutureContinousContract? continous_constract;
|
||||
|
||||
public FutureContract(string txt)
|
||||
{
|
||||
wildcard_contract = parse_wildcard_contract(txt);
|
||||
if (wildcard_contract != null)
|
||||
return;
|
||||
var fc = parse_future_contract(txt);
|
||||
if (fc != null)
|
||||
{
|
||||
future_tradable_contract = fc;
|
||||
return;
|
||||
}
|
||||
var sc = parse_future_spread_contract(txt);
|
||||
if (sc != null)
|
||||
{
|
||||
spread_tradable_contract = sc;
|
||||
return;
|
||||
}
|
||||
// this is not totally true, since ParseFutureSpreadContract does not
|
||||
// parse ICS spreads
|
||||
continous_constract = new(txt, Exchange.CME);
|
||||
}
|
||||
|
||||
[MemberNotNullWhen(true, nameof(continous_constract))]
|
||||
public bool is_continous_contract => continous_constract != null;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(wildcard_contract))]
|
||||
public bool is_wildcard_contract => wildcard_contract != null;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(future_tradable_contract))]
|
||||
public bool is_future_tradable_contract => future_tradable_contract != null;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(spread_tradable_contract))]
|
||||
public bool is_spread_tradable_contract => spread_tradable_contract != null;
|
||||
|
||||
public string symbol()
|
||||
{
|
||||
if (wildcard_contract != null)
|
||||
return wildcard_contract.Symbol;
|
||||
if (continous_constract != null)
|
||||
{
|
||||
return continous_constract.Symbol;
|
||||
}
|
||||
else if (future_tradable_contract != null)
|
||||
{
|
||||
return future_tradable_contract.Symbol;
|
||||
}
|
||||
else if (spread_tradable_contract != null)
|
||||
{
|
||||
return spread_tradable_contract.Symbol;
|
||||
}
|
||||
Log.assert(false);
|
||||
return string.Empty;
|
||||
}
|
||||
public string? contract_text
|
||||
{
|
||||
get
|
||||
{
|
||||
if (future_tradable_contract != null)
|
||||
return future_tradable_contract.ContractText;
|
||||
if (spread_tradable_contract != null)
|
||||
return spread_tradable_contract.ContractText;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#region Parsing
|
||||
public static FutureWildcardContract? parse_wildcard_contract(string contract_txt, Exchange exchange = Exchange.CME)
|
||||
{
|
||||
if (contract_txt.EndsWith("*"))
|
||||
{
|
||||
contract_txt = contract_txt.Replace("*", string.Empty);
|
||||
return new FutureWildcardContract(contract_txt, exchange);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static FutureTradableContract? parse_future_contract(string contract_txt, Exchange exchange = Exchange.CME)
|
||||
{
|
||||
// Validate the future string length
|
||||
if (!contract_txt.All(c => char.IsLetterOrDigit(c)))
|
||||
return null;
|
||||
if (contract_txt.Length < 4)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the symbol and the expiration month/year codes
|
||||
string symbol = contract_txt.Substring(0, contract_txt.Length - 2);
|
||||
char monthCode = contract_txt[contract_txt.Length - 2];
|
||||
char yearCode = contract_txt[contract_txt.Length - 1];
|
||||
|
||||
// Map month code to month, return null if the month code is invalid
|
||||
int month = monthCode switch
|
||||
{
|
||||
'F' => 1, // January
|
||||
'G' => 2, // February
|
||||
'H' => 3, // March
|
||||
'J' => 4, // April
|
||||
'K' => 5, // May
|
||||
'M' => 6, // June
|
||||
'N' => 7, // July
|
||||
'Q' => 8, // August
|
||||
'U' => 9, // September
|
||||
'V' => 10, // October
|
||||
'X' => 11, // November
|
||||
'Z' => 12, // December
|
||||
_ => 0 // Return 0 for invalid month code
|
||||
};
|
||||
|
||||
if (month == 0)
|
||||
{
|
||||
return null; // Invalid month code
|
||||
}
|
||||
|
||||
// Calculate the year based on the current decade
|
||||
int currentYear = DateTime.Now.Year;
|
||||
int decadeStart = currentYear / 10 * 10;
|
||||
int year = decadeStart + (yearCode - '0');
|
||||
|
||||
// If the year is in the past, assume the next decade
|
||||
if (year < currentYear)
|
||||
{
|
||||
year += 10;
|
||||
}
|
||||
|
||||
// Create the FutureContract instance and set its properties
|
||||
DateTime expiration = new DateTime(year, month, 1);
|
||||
return new(symbol, expiration, exchange, contract_txt);
|
||||
}
|
||||
// Method to parse a futures spread, e.g. "CLX4-CLZ4"
|
||||
public static FutureSpreadContract? parse_future_spread_contract(string contract_txt, Exchange exchange = Exchange.CME)
|
||||
{
|
||||
// Split the string by '-' to handle each leg
|
||||
var spreadParts = contract_txt.Split('-');
|
||||
if (spreadParts.Length < 2)
|
||||
{
|
||||
return null; // Not a valid spread format
|
||||
}
|
||||
|
||||
var spreadContract = new FutureSpreadContract { legs = new List<Tuple<double, FutureTradableContract>>() };
|
||||
|
||||
foreach (var part in spreadParts)
|
||||
{
|
||||
// Split each leg by '*', assuming the format is weight*future or just future
|
||||
string[] legParts = part.Split('*');
|
||||
double weight = 1.0;
|
||||
string futurePart;
|
||||
|
||||
if (legParts.Length == 2)
|
||||
{
|
||||
if (!double.TryParse(legParts[0], out weight))
|
||||
{
|
||||
return null; // Invalid weight, parsing failed
|
||||
}
|
||||
futurePart = legParts[1];
|
||||
}
|
||||
else if (legParts.Length == 1)
|
||||
{
|
||||
futurePart = legParts[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null; // Invalid format, too many '*' parts
|
||||
}
|
||||
|
||||
// Parse the future contract
|
||||
var futureContract = parse_future_contract(futurePart, exchange);
|
||||
if (futureContract == null)
|
||||
{
|
||||
return null; // Failed to parse the future part
|
||||
}
|
||||
|
||||
// Add the leg to the spread contract
|
||||
spreadContract.legs.Add(new Tuple<double, FutureTradableContract>(weight, futureContract));
|
||||
}
|
||||
return spreadContract;
|
||||
}
|
||||
|
||||
// public static ContractType DetermineFutureOrSpread(string contract) {
|
||||
// if (contract.Contains("-")) {
|
||||
// return ContractType.FutureSpread;
|
||||
// } else {
|
||||
// return ContractType.Future;
|
||||
// }
|
||||
// }
|
||||
#endregion
|
||||
|
||||
public static ContractType get_type(string contract_txt)
|
||||
{
|
||||
var fc = parse_future_contract(contract_txt);
|
||||
if (fc != null)
|
||||
{
|
||||
return ContractType.Future;
|
||||
}
|
||||
var sc = parse_future_spread_contract(contract_txt);
|
||||
if (sc != null)
|
||||
{
|
||||
return ContractType.FutureSpread;
|
||||
}
|
||||
// tehre are some non calendar spreads we cant parse
|
||||
//Log.assert(false);
|
||||
return ContractType.FutureSpread;
|
||||
}
|
||||
|
||||
public static ContractCategory get_category(string symbol)
|
||||
{
|
||||
var tp = get_symbol_category(symbol);
|
||||
return tp.Item1;
|
||||
}
|
||||
|
||||
public static Tuple<ContractCategory, string> get_symbol_category(string symbol)
|
||||
{
|
||||
var fc = parse_future_contract(symbol);
|
||||
if (fc != null)
|
||||
{
|
||||
return Tuple.Create(ContractCategory.FutureString, fc.Symbol);
|
||||
}
|
||||
var sc = parse_future_spread_contract(symbol);
|
||||
if (sc != null)
|
||||
{
|
||||
return Tuple.Create(ContractCategory.FutureString, sc.Symbol);
|
||||
}
|
||||
return Tuple.Create(ContractCategory.Symbol, symbol);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public enum ContractType
|
||||
{
|
||||
Spot,
|
||||
Option,
|
||||
Future,
|
||||
FutureSpread,
|
||||
OptionSpread
|
||||
}
|
||||
public enum ContractCategory
|
||||
{
|
||||
Symbol = 0,
|
||||
FutureString,
|
||||
SpreadString
|
||||
}
|
||||
public enum CallPut
|
||||
{
|
||||
Call,
|
||||
Put
|
||||
}
|
||||
public enum Exchange
|
||||
{
|
||||
CME
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
namespace BlueOrb.Operators.Data {
|
||||
using BCG;
|
||||
|
||||
using bts;
|
||||
|
||||
using Spark.Core.Diagnostics;
|
||||
|
||||
using BlueOrb.Internal;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
[DataStreamUserRespresentation("ReplayFutureLiveBBOStream")]
|
||||
public class ReplayFutureLiveBBOStream : ViewStream, BCG.Utilities.General.IWorker {
|
||||
private class LastFireTime {
|
||||
public DateTime last_fireitme_file = DateTime.MinValue;
|
||||
public DateTime last_firetime_clock = DateTime.MinValue;
|
||||
}
|
||||
private class RowsWithPointer {
|
||||
public List<IBlueRow>? rows;
|
||||
public int pointer;
|
||||
}
|
||||
|
||||
|
||||
readonly ViewStreamDatabase? _vsd;
|
||||
readonly ContractUtils.FutureContinousContract? _fcc;
|
||||
private readonly LastFireTime _last_fire_time = new LastFireTime();
|
||||
private RowsWithPointer _rows = new RowsWithPointer();
|
||||
|
||||
public ReplayFutureLiveBBOStream(string continous_contract) {
|
||||
Func<string> iso_8601_now = () => {
|
||||
DateTime utc_now = DateTime.UtcNow;
|
||||
long ticks = utc_now.Ticks; // 1 tick = 100 ns
|
||||
long nanoseconds = ticks % TimeSpan.TicksPerSecond * 100;
|
||||
string formatted = utc_now.ToString("yyyy-MM-ddTHH:mm:ss");
|
||||
formatted += $".{nanoseconds:D9}Z";
|
||||
return formatted;
|
||||
};
|
||||
ViewStream rs = new ReplayFutureLiveBBOStream(continous_contract, true);
|
||||
IBlueTableSchema schema_out = BlueDataUtility.form_schema("BBO",
|
||||
Tuple.Create("id", TypeCode.Int32),
|
||||
Tuple.Create("ts_event", TypeCode.String),
|
||||
Tuple.Create("contract", TypeCode.String),
|
||||
Tuple.Create("bid_price", TypeCode.Double),
|
||||
Tuple.Create("bid_size", TypeCode.Int32),
|
||||
Tuple.Create("ask_price", TypeCode.Double),
|
||||
Tuple.Create("ask_size", TypeCode.Int32));
|
||||
rs = new MapStream(rs, s => schema_out,
|
||||
r =>
|
||||
new BlueRow(schema_out, r["id"], /*r.get<string>("ts_event")*/iso_8601_now(), r.get<string>("symbol"),
|
||||
r.get<double>("bid_px_00"), r.get<int>("bid_sz_00"),
|
||||
r.get<double>("ask_px_00"), r.get<int>("ask_sz_00")));
|
||||
mySetRealSource(rs);
|
||||
}
|
||||
|
||||
private ReplayFutureLiveBBOStream(string continous_contract, bool noop)
|
||||
: this(ViewStreamDatabase.instance, new ContractUtils.FutureContract(continous_contract)) { }
|
||||
|
||||
public ReplayFutureLiveBBOStream(ViewStreamDatabase vsd, ContractUtils.FutureContract fc) {
|
||||
var fcc = fc.continous_constract;
|
||||
Log.assert(fcc != null);
|
||||
_vsd = vsd;
|
||||
_fcc = fcc;
|
||||
}
|
||||
|
||||
protected override void myOnFirstSubscriber() {
|
||||
Log.notnull(_fcc);
|
||||
var file_path = Path.Combine(SQLiteHelper.location, $"mbp1_{_fcc.Symbol}_small.csv".ToLower());
|
||||
var rows = BCG.CSVReaderStream.LoadFile(file_path);
|
||||
_rows.rows = new(rows.Count);
|
||||
_rows.rows.AddRange(rows.OfType<IBlueRow>().OrderBy(r => r.get<string>("ts_event")));
|
||||
_rows.pointer = 0;
|
||||
ExceptionAssert.NotNull(_vsd).addWorker(this);
|
||||
}
|
||||
|
||||
protected override void myOnLastSubscriber() {
|
||||
ExceptionAssert.NotNull(_vsd).removeWorker(this);
|
||||
}
|
||||
|
||||
public void doWork() {
|
||||
var rows = ExceptionAssert.NotNull(_rows.rows);
|
||||
IBlueRow cand_row = rows[_rows.pointer];
|
||||
string cand_ts_event = cand_row.get<string>("ts_event");
|
||||
DateTime cand_ts_event_dt = DateTime.Parse(cand_ts_event, null, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
TimeSpan cand_ts_span_file = cand_ts_event_dt - _last_fire_time.last_fireitme_file;
|
||||
TimeSpan ts_clock = DateTime.Now - _last_fire_time.last_firetime_clock;
|
||||
if (ts_clock >= cand_ts_span_file) {
|
||||
_rows.pointer++;
|
||||
_last_fire_time.last_firetime_clock = DateTime.Now;
|
||||
_last_fire_time.last_fireitme_file = cand_ts_event_dt;
|
||||
BlueTable bt_out = new BlueTable(cand_row.schema);
|
||||
bt_out.addRow(cand_row);
|
||||
var dp_out = BlueDataUtility.build_dp(bt_out, myLastValue as IDataPacket);
|
||||
myFireEvent(dp_out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
namespace Blueorb.Operators.Data
|
||||
{
|
||||
using BCG;
|
||||
|
||||
using Spark.Core.Diagnostics;
|
||||
using BlueOrb.Internal;
|
||||
using bts;
|
||||
using BlueOrb.Operators.Core;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
[DataStreamUserRespresentation("ReplayFutureLiveTradeStream")]
|
||||
public class ReplayFutureLiveTradeStream : ViewStream, BCG.Utilities.General.IWorker
|
||||
{
|
||||
private class LastFireTime
|
||||
{
|
||||
public DateTime last_fireitme_file = DateTime.MinValue;
|
||||
public DateTime last_firetime_clock = DateTime.MinValue;
|
||||
}
|
||||
private class RowsWithPointer
|
||||
{
|
||||
public List<IBlueRow>? rows;
|
||||
public int pointer;
|
||||
}
|
||||
|
||||
|
||||
readonly ViewStreamDatabase? _vsd;
|
||||
readonly ContractUtils.FutureContinousContract? _fcc;
|
||||
private readonly LastFireTime _last_fire_time = new LastFireTime();
|
||||
readonly RowsWithPointer _rows = new();
|
||||
|
||||
public ReplayFutureLiveTradeStream(string continous_contract)
|
||||
{
|
||||
Func<string> iso_8601_now = () =>
|
||||
{
|
||||
DateTime utc_now = DateTime.UtcNow;
|
||||
long ticks = utc_now.Ticks; // 1 tick = 100 ns
|
||||
long nanoseconds = ticks % TimeSpan.TicksPerSecond * 100;
|
||||
string formatted = utc_now.ToString("yyyy-MM-ddTHH:mm:ss");
|
||||
formatted += $".{nanoseconds:D9}Z";
|
||||
return formatted;
|
||||
};
|
||||
ViewStream rs = new ReplayFutureLiveTradeStream(continous_contract, true);
|
||||
IBlueTableSchema schema_out = BlueDataUtility.form_schema("BBO",
|
||||
Tuple.Create("id", TypeCode.Int32),
|
||||
Tuple.Create("ts_event", TypeCode.String),
|
||||
Tuple.Create("contract", TypeCode.String),
|
||||
Tuple.Create("trade_size", TypeCode.Double),
|
||||
Tuple.Create("trade_price", TypeCode.Int32),
|
||||
Tuple.Create("aggressor_side", TypeCode.String));
|
||||
rs = new MapStream(rs, s => schema_out,
|
||||
r =>
|
||||
new BlueRow(schema_out, r["id"], /*r.get<string>("ts_event")*/iso_8601_now(), r.get<string>("symbol"),
|
||||
r.get<double>("size"), r.get<double>("price"),
|
||||
r.get<string>("side").StartsWith("B") ? "BUY" : (r.get<string>("side").StartsWith("A") ? "SELL" : "NONE")));
|
||||
rs = new TimeSeriesStream(rs);
|
||||
mySetRealSource(rs);
|
||||
}
|
||||
|
||||
private ReplayFutureLiveTradeStream(string continous_contract, bool noop)
|
||||
: this(ViewStreamDatabase.instance, new ContractUtils.FutureContract(continous_contract)) { }
|
||||
|
||||
public ReplayFutureLiveTradeStream(ViewStreamDatabase vsd, ContractUtils.FutureContract fc)
|
||||
{
|
||||
var fcc = fc.continous_constract;
|
||||
Log.assert(fcc != null);
|
||||
_vsd = vsd;
|
||||
_fcc = fcc;
|
||||
}
|
||||
|
||||
protected override void myOnFirstSubscriber()
|
||||
{
|
||||
Log.notnull(_vsd);
|
||||
Log.notnull(_fcc);
|
||||
var file_path = Path.Combine(SQLiteHelper.location, $"trades_{_fcc.Symbol}.csv");
|
||||
var csv_rd = new BlueOrb.Operators.Core.CSVReaderStream(file_path, false);
|
||||
var rows = BCG.CSVReaderStream.LoadFile(file_path);
|
||||
_rows.rows = new(rows.Count);
|
||||
_rows.rows.AddRange(rows.OfType<IBlueRow>().OrderBy(r => r.get<string>("ts_event")));
|
||||
_rows.pointer = 0;
|
||||
ExceptionAssert.NotNull(_vsd).addWorker(this);
|
||||
}
|
||||
|
||||
protected override void myOnLastSubscriber()
|
||||
{
|
||||
ExceptionAssert.NotNull(_vsd).removeWorker(this);
|
||||
}
|
||||
|
||||
public void doWork()
|
||||
{
|
||||
var rows = ExceptionAssert.NotNull(_rows.rows);
|
||||
IBlueRow cand_row = rows[_rows.pointer];
|
||||
string cand_ts_event = cand_row.get<string>("ts_event");
|
||||
DateTime cand_ts_event_dt = DateTime.Parse(cand_ts_event, null, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
TimeSpan cand_ts_span_file = cand_ts_event_dt - _last_fire_time.last_fireitme_file;
|
||||
TimeSpan ts_clock = DateTime.Now - _last_fire_time.last_firetime_clock;
|
||||
if (ts_clock >= cand_ts_span_file)
|
||||
{
|
||||
_rows.pointer++;
|
||||
_last_fire_time.last_firetime_clock = DateTime.Now;
|
||||
_last_fire_time.last_fireitme_file = cand_ts_event_dt;
|
||||
BlueTable bt_out = new BlueTable(cand_row.schema);
|
||||
bt_out.addRow(cand_row);
|
||||
var dp_out = BlueDataUtility.build_dp(bt_out, myLastValue as IDataPacket);
|
||||
myFireEvent(dp_out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace BlueOrb.Internal
|
||||
{
|
||||
|
||||
public static class SQLiteHelper
|
||||
{
|
||||
public static string location = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? "C:\\BlueOrb_Data\\"
|
||||
: Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? string.Empty, "BlueOrb_Data");
|
||||
public static string db_name = "stream_data.db";
|
||||
|
||||
public static DataTable execute_select_query(string connection_string, string query)
|
||||
{
|
||||
// Create a new DataTable to store the results
|
||||
DataTable dataTable = new DataTable();
|
||||
|
||||
// Create and open a SQLite connection using the provided connection string
|
||||
using (SQLiteConnection connection = new SQLiteConnection(connection_string))
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
// Create a SQLite command with the provided query
|
||||
using (SQLiteCommand command = new SQLiteCommand(query, connection))
|
||||
{
|
||||
// Use SQLiteDataAdapter to fill the DataTable
|
||||
using (SQLiteDataAdapter adapter = new SQLiteDataAdapter(command))
|
||||
{
|
||||
adapter.Fill(dataTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("An error occurred: " + ex.Message);
|
||||
}
|
||||
}
|
||||
// Return the populated DataTable
|
||||
return dataTable;
|
||||
}
|
||||
|
||||
public static List<Dictionary<string, object>> ExecuteBulkRead(string connectionString, string query)
|
||||
{
|
||||
var result = new List<Dictionary<string, object>>();
|
||||
|
||||
using (var connection = new SQLiteConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
// Begin a transaction to optimize read speed
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
using (var command = new SQLiteCommand(query, connection, transaction))
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var row = new Dictionary<string, object>();
|
||||
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
string columnName = reader.GetName(i);
|
||||
object columnValue = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
||||
row[columnName] = columnValue;
|
||||
}
|
||||
|
||||
result.Add(row);
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<Dictionary<string, object>> get_table(string connectionString, string query)
|
||||
{
|
||||
var result = new List<Dictionary<string, object>>();
|
||||
|
||||
using (var connection = new SQLiteConnection(connectionString))
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
using (var command = new SQLiteCommand(query, connection))
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var row = new Dictionary<string, object>();
|
||||
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
string columnName = reader.GetName(i);
|
||||
object columnValue = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
||||
row[columnName] = columnValue;
|
||||
}
|
||||
|
||||
result.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("An error occurred: " + ex.Message);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IEnumerable<Dictionary<string, object>> get_table_2(string connectionString, string query)
|
||||
{
|
||||
using (var connection = new SQLiteConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
using (var command = new SQLiteCommand(query, connection))
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var row = new Dictionary<string, object>();
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
string columnName = reader.GetName(i);
|
||||
object columnValue = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
||||
row[columnName] = columnValue;
|
||||
}
|
||||
yield return row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace BlueOrb.Internal
|
||||
{
|
||||
using bts;
|
||||
using System;
|
||||
using BCG;
|
||||
using System.Collections.Generic;
|
||||
using Bts.DataStreamExpressions;
|
||||
using System.Xml;
|
||||
|
||||
// All this does is take in a dsl expression, parse it and fire the data that come out of running it.
|
||||
// So for example, if an expresison is passed
|
||||
[DataStreamUserRespresentation("dsl")]
|
||||
public class DSLStream : ViewStream
|
||||
{
|
||||
private readonly string _dsl;
|
||||
public DSLStream(string dsl)
|
||||
{
|
||||
Log.Gen.info($"DSLStream.cons dsl: {dsl}");
|
||||
// Remove all colons
|
||||
string sanitizedDsl = dsl.Replace(":", "COLON");
|
||||
_dsl = dsl;
|
||||
ViewStream rs = build_stream(dsl) ?? new LiteralStream("error", $"msg:invalid dsl");
|
||||
mySetRealSource(rs);
|
||||
}
|
||||
|
||||
static ViewStream? build_stream(string stream_expression)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exp = DataStreamExpressionsInterpreter.Evaluate(stream_expression);
|
||||
var rv = exp.getView(new Dictionary<string, StatementExpression>(0));
|
||||
return rv;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//send to blueorb
|
||||
Log.Gen.error($"Could not build stream: {stream_expression} ex: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
|
||||
using BCG.Utilities.General;
|
||||
using BCG;
|
||||
using bts;
|
||||
|
||||
namespace BlueOrb.Internal
|
||||
{
|
||||
[DataStreamUserRespresentation("MillisecondStream")]
|
||||
public class MillisecondStream : ViewStream, IWorker
|
||||
{
|
||||
private DateTime _last_fire_time = DateTime.MinValue;
|
||||
private readonly TimeSpan _period;
|
||||
private readonly int _size;
|
||||
private readonly ViewStreamDatabase _vsd;
|
||||
private IBlueTableSchema _schema;
|
||||
private bts.CircularBufferTable _table;
|
||||
|
||||
public MillisecondStream(int interval_msecs, int size)
|
||||
: this(ViewStreamDatabase.instance, interval_msecs, size)
|
||||
{
|
||||
}
|
||||
|
||||
public MillisecondStream(ViewStreamDatabase vsd, int interval_msecs, int size)
|
||||
{
|
||||
_vsd = vsd;
|
||||
_period = TimeSpan.FromMilliseconds(interval_msecs);
|
||||
_size = size;
|
||||
_schema = BlueDataUtility.form_schema("Milliseconds",
|
||||
Tuple.Create("id", TypeCode.Int32),
|
||||
Tuple.Create("time", TypeCode.DateTime),
|
||||
Tuple.Create("display_time", TypeCode.String));
|
||||
_table = new CircularBufferTable(_schema, _size);
|
||||
}
|
||||
|
||||
protected override void myOnFirstSubscriber()
|
||||
{
|
||||
_vsd.addWorker(this);
|
||||
}
|
||||
|
||||
protected override void myOnLastSubscriber()
|
||||
{
|
||||
_vsd.removeWorker(this);
|
||||
}
|
||||
|
||||
private int _id = 0;
|
||||
public void doWork()
|
||||
{
|
||||
if (DateTime.Now - _last_fire_time >= _period)
|
||||
{
|
||||
_last_fire_time = DateTime.Now;
|
||||
IBlueRow nr = new BlueRow(_schema, _id++, _last_fire_time,
|
||||
_last_fire_time.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||
_table.add_row(nr);
|
||||
var dp = BlueDataUtility.build_dp_inc(_table, myLastValue as DataPacket);
|
||||
myFireEvent(dp);
|
||||
}
|
||||
}
|
||||
}
|
||||
internal class MSecondStream : TimeUnitStream
|
||||
{
|
||||
public MSecondStream(int intervalInms, int size)
|
||||
: base(DateTime.Now, new TimeSpan(0, 0, 0, 0, intervalInms), size, ViewStreamDatabase.instance) { }
|
||||
|
||||
protected override DateTime myRoundTime(DateTime dt) { return dt; }
|
||||
}
|
||||
|
||||
internal class DisplayableMillisecondStream : ViewStream, IStreamMapper
|
||||
{
|
||||
private IBlueTableSchema mySchema = new BlueTableSchema("ms",
|
||||
new ColumnInfo("id", 0, typeof(int)),
|
||||
new ColumnInfo("time", 1, typeof(string)));
|
||||
public DisplayableMillisecondStream(int intervalInMs, int sz)
|
||||
{
|
||||
mySetRealSource(new MapStream(new MSecondStream(intervalInMs, sz), this));
|
||||
}
|
||||
#region IStreamMapper Members
|
||||
|
||||
public IBlueTableSchema getOutputSchema(IBlueTableSchema inputSchema)
|
||||
{
|
||||
return mySchema;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDynamicStreamMapper Members
|
||||
|
||||
public IBlueRow map(IBlueRow row)
|
||||
{
|
||||
DateTime time = row.get_datetime("time");
|
||||
var time_txt = time.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
return new BlueRow(mySchema, row["id"], time_txt);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string myToString(DateTime dt)
|
||||
{
|
||||
string sep = ":";
|
||||
return dt.Hour + sep + dt.Minute + sep + dt.Second + sep + dt.Millisecond;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
namespace BlueOrb.Operators.Signal
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using BCG;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Data;
|
||||
|
||||
|
||||
public enum BarType
|
||||
{
|
||||
TIME,
|
||||
TICK,
|
||||
VOLUME,
|
||||
DOLLAR,
|
||||
}
|
||||
|
||||
public class BarBuilderSchemaInfo
|
||||
{
|
||||
[JsonPropertyName("price_col")]
|
||||
public string price_col { get; set; } = "trade_price";
|
||||
|
||||
[JsonPropertyName("volume_col")]
|
||||
public string volume_col { get; set; } = "trade_size";
|
||||
|
||||
[JsonPropertyName("timestamp_col")]
|
||||
public string timestamp_col { get; set; } = "ts_event";
|
||||
}
|
||||
|
||||
|
||||
public class SingleBar : IBlueRow
|
||||
{
|
||||
private BarBuilderStream _parent_str;
|
||||
private IBlueTableSchema _schema;
|
||||
private MutableBlueRow _row;
|
||||
public SingleBar(BarBuilderStream parent_str)
|
||||
{
|
||||
_parent_str = parent_str;
|
||||
_schema = BlueDataUtility.form_schema(
|
||||
"bar_builder_single_bar",
|
||||
Tuple.Create("id", TypeCode.Int32),
|
||||
Tuple.Create("ts_event", TypeCode.DateTime),
|
||||
Tuple.Create("bar_start_ts", TypeCode.DateTime),
|
||||
Tuple.Create("bar_end_ts", TypeCode.DateTime),
|
||||
Tuple.Create("open", TypeCode.Double),
|
||||
Tuple.Create("high", TypeCode.Double),
|
||||
Tuple.Create("low", TypeCode.Double),
|
||||
Tuple.Create("close", TypeCode.Double),
|
||||
Tuple.Create("volume", TypeCode.Double),
|
||||
Tuple.Create("notional", TypeCode.Double),
|
||||
Tuple.Create("trade_count", TypeCode.Int32),
|
||||
Tuple.Create("n_events", TypeCode.Int32),
|
||||
Tuple.Create("vwap", TypeCode.Double),
|
||||
Tuple.Create("is_active", TypeCode.Boolean)
|
||||
);
|
||||
_row = new MutableBlueRow(_schema);
|
||||
}
|
||||
|
||||
public IBlueTableSchema schema => _row.schema;
|
||||
|
||||
public object this[int index] => _row[index];
|
||||
|
||||
public object this[string col_name] => _row[col_name];
|
||||
|
||||
|
||||
public IBlueRow add_event(IBlueRow r)
|
||||
{
|
||||
// initiaizes _row
|
||||
void reset_new_bar()
|
||||
{
|
||||
_row = new MutableBlueRow(_schema);
|
||||
_row.setValue("id", _parent_str.get_new_id());
|
||||
_row.setValue("volume", 0.0);
|
||||
_row.setValue("notional", 0.0);
|
||||
_row.setValue("trade_count", 0);
|
||||
_row.setValue("n_events", 0);
|
||||
_row.setValue("is_active", true);
|
||||
}
|
||||
|
||||
void absorb_into_row(IBlueRow input_row)
|
||||
{
|
||||
var ts = input_row.get_datetime(_parent_str._schema_info.timestamp_col, DateTime.UtcNow).ToUniversalTime();
|
||||
var price = input_row.get<double>(_parent_str._schema_info.price_col, 0.0);
|
||||
var volume = input_row.get<double>(_parent_str._schema_info.volume_col, 0.0);
|
||||
var notional = price * volume;
|
||||
|
||||
if (_row.is_null("bar_start_ts"))
|
||||
{
|
||||
DateTime bar_start_ts;
|
||||
DateTime bar_end_ts;
|
||||
if (_parent_str._bar_type == BarType.TIME)
|
||||
{
|
||||
var bucket_ticks = (long)(Math.Max(1.0, _parent_str._bar_size) * TimeSpan.TicksPerSecond);
|
||||
var start_ticks = (ts.Ticks / bucket_ticks) * bucket_ticks;
|
||||
bar_start_ts = new DateTime(start_ticks, DateTimeKind.Utc);
|
||||
bar_end_ts = bar_start_ts.AddTicks(bucket_ticks);
|
||||
}
|
||||
else
|
||||
{
|
||||
bar_start_ts = ts;
|
||||
bar_end_ts = ts;
|
||||
}
|
||||
_row.setValue("bar_start_ts", bar_start_ts);
|
||||
_row.setValue("bar_end_ts", bar_end_ts);
|
||||
_row.setValue("open", price);
|
||||
_row.setValue("high", price);
|
||||
_row.setValue("low", price);
|
||||
}
|
||||
else
|
||||
{
|
||||
var high = _row.get<double>("high", price);
|
||||
var low = _row.get<double>("low", price);
|
||||
_row.setValue("high", Math.Max(high, price));
|
||||
_row.setValue("low", Math.Min(low, price));
|
||||
}
|
||||
|
||||
_row.setValue("ts_event", ts);
|
||||
_row.setValue("close", price);
|
||||
_row.setValue("volume", _row.get<double>("volume", 0.0) + volume);
|
||||
_row.setValue("notional", _row.get<double>("notional", 0.0) + notional);
|
||||
_row.setValue("trade_count", _row.get<int>("trade_count", 0) + 1);
|
||||
_row.setValue("n_events", _row.get<int>("n_events", 0) + 1);
|
||||
var running_volume = _row.get<double>("volume", 0.0);
|
||||
var running_notional = _row.get<double>("notional", 0.0);
|
||||
_row.setValue("vwap", running_volume > 0.0 ? running_notional / running_volume : price);
|
||||
}
|
||||
|
||||
bool is_closed()
|
||||
{
|
||||
bool closed;
|
||||
if (_parent_str._bar_type == BarType.TIME)
|
||||
{
|
||||
var ts = _row.get_datetime("ts_event", DateTime.MinValue);
|
||||
var bar_end_ts = _row.get_datetime("bar_end_ts", DateTime.MaxValue);
|
||||
closed = ts >= bar_end_ts;
|
||||
}
|
||||
else if (_parent_str._bar_type == BarType.TICK)
|
||||
{
|
||||
closed = _row.get<int>("n_events", 0) >= _parent_str._bar_size;
|
||||
}
|
||||
else if (_parent_str._bar_type == BarType.VOLUME)
|
||||
{
|
||||
closed = _row.get<double>("volume", 0.0) >= _parent_str._bar_size;
|
||||
}
|
||||
else
|
||||
{
|
||||
closed = _row.get<double>("notional", 0.0) >= _parent_str._bar_size;
|
||||
}
|
||||
_row.setValue("is_active", !closed);
|
||||
return closed;
|
||||
}
|
||||
|
||||
if (_row == null)
|
||||
{
|
||||
reset_new_bar();
|
||||
}
|
||||
absorb_into_row(r);
|
||||
if (is_closed())
|
||||
{
|
||||
IBlueRow closed_row = new BlueRow(_row);
|
||||
reset_new_bar();
|
||||
return closed_row;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class BarState : IBlueRowCollection
|
||||
{
|
||||
private SingleBar _curr_row;
|
||||
private BlueTable _state;
|
||||
|
||||
public BarState(BarBuilderStream parent_str)
|
||||
{
|
||||
_curr_row = new SingleBar(parent_str);
|
||||
_state = new BlueTable(_curr_row.schema);
|
||||
}
|
||||
|
||||
public bool add_event(IBlueRow r)
|
||||
{
|
||||
var closed_bar = _curr_row.add_event(r);
|
||||
if (closed_bar is not null)
|
||||
{
|
||||
// curr bar is closed, only then its added
|
||||
// when bars are evolving we dont add it
|
||||
_state.addRow(closed_bar);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public IBlueRow this[int id] => _state[id];
|
||||
|
||||
public IBlueTableSchema schema => _state.schema;
|
||||
|
||||
public bool containsID(int id) => _state.containsID(id);
|
||||
|
||||
public int Count => _state.Count;
|
||||
|
||||
public System.Collections.IEnumerator GetEnumerator() => _state.GetEnumerator();
|
||||
}
|
||||
|
||||
[DataStreamUserRespresentation("BarBuilderStream")]
|
||||
public class BarBuilderStream : ViewStream
|
||||
{
|
||||
readonly ViewStream _price_stream;
|
||||
internal readonly BarType _bar_type;
|
||||
internal readonly double _bar_size;
|
||||
internal readonly bool _show_open_bars;
|
||||
internal readonly BarBuilderSchemaInfo _schema_info;
|
||||
private BarState _state;
|
||||
|
||||
public BarBuilderStream(ViewStream price_stream,
|
||||
BarType bar_type,
|
||||
double bar_size,
|
||||
bool show_open_bars,
|
||||
BarBuilderSchemaInfo schema_info)
|
||||
{
|
||||
_price_stream = price_stream;
|
||||
_bar_type = bar_type;
|
||||
_bar_size = bar_size;
|
||||
_show_open_bars = show_open_bars;
|
||||
_schema_info = schema_info;
|
||||
_state = new BarState(this);
|
||||
}
|
||||
|
||||
int _id = 0;
|
||||
internal int get_new_id()
|
||||
{
|
||||
return _id++;
|
||||
}
|
||||
|
||||
public BarBuilderStream(ViewStream price_stream,
|
||||
string bar_type,
|
||||
double bar_size,
|
||||
string schema_info_json)
|
||||
:
|
||||
this(price_stream,
|
||||
Enum.Parse<BarType>(bar_type),
|
||||
bar_size,
|
||||
true,
|
||||
_parse_and_validate_schema_info_json(schema_info_json))
|
||||
{
|
||||
}
|
||||
|
||||
public BarBuilderStream(ViewStream price_stream,
|
||||
string bar_type,
|
||||
double bar_size,
|
||||
string price_col,
|
||||
string volume_col,
|
||||
string timestamp_col)
|
||||
:
|
||||
this(price_stream,
|
||||
Enum.Parse<BarType>(bar_type),
|
||||
bar_size,
|
||||
true,
|
||||
_create_and_validate_schema_info(price_col, volume_col, timestamp_col))
|
||||
{
|
||||
}
|
||||
|
||||
protected override void myOnFirstSubscriber()
|
||||
{
|
||||
_price_stream.onChange += _on_price_stream_change;
|
||||
}
|
||||
|
||||
protected override void myOnLastSubscriber()
|
||||
{
|
||||
_price_stream.onChange -= _on_price_stream_change;
|
||||
}
|
||||
|
||||
void _on_price_stream_change(object dp_obj)
|
||||
{
|
||||
DataPacket dp = dp_obj as DataPacket;
|
||||
var sz_0 = _state.Count;
|
||||
foreach (IBlueRow r in dp.addedRows)
|
||||
{
|
||||
_state.add_event(r);
|
||||
}
|
||||
if (_state.Count != sz_0)
|
||||
// we will check for _show_open_bars here
|
||||
{
|
||||
var dp_out = BlueDataUtility.build_dp_inc(_state, myLastValue as DataPacket, false);
|
||||
myFireEvent(dp_out);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static BarBuilderSchemaInfo _parse_and_validate_schema_info_json(string schema_info_json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(schema_info_json))
|
||||
{
|
||||
throw new ArgumentException("schema_info_json cannot be null or empty.", nameof(schema_info_json));
|
||||
}
|
||||
|
||||
var schema_info = JsonSerializer.Deserialize<BarBuilderSchemaInfo>(schema_info_json);
|
||||
_validate_schema_info(schema_info);
|
||||
return schema_info!;
|
||||
}
|
||||
|
||||
static BarBuilderSchemaInfo _create_and_validate_schema_info(
|
||||
string price_col,
|
||||
string volume_col,
|
||||
string timestamp_col)
|
||||
{
|
||||
var schema_info = new BarBuilderSchemaInfo
|
||||
{
|
||||
price_col = price_col,
|
||||
volume_col = volume_col,
|
||||
timestamp_col = timestamp_col,
|
||||
};
|
||||
_validate_schema_info(schema_info);
|
||||
return schema_info;
|
||||
}
|
||||
|
||||
static void _validate_schema_info(BarBuilderSchemaInfo? schema_info)
|
||||
{
|
||||
if (schema_info is null)
|
||||
{
|
||||
throw new ArgumentException("schema_info_json did not deserialize into BarBuilderSchemaInfo.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(schema_info.price_col))
|
||||
{
|
||||
throw new ArgumentException("BarBuilderSchemaInfo.PriceCol cannot be null or empty.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(schema_info.volume_col))
|
||||
{
|
||||
throw new ArgumentException("BarBuilderSchemaInfo.VolumeCol cannot be null or empty.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(schema_info.timestamp_col))
|
||||
{
|
||||
throw new ArgumentException("BarBuilderSchemaInfo.TimestampCol cannot be null or empty.");
|
||||
}
|
||||
|
||||
if (schema_info.price_col.Equals(schema_info.volume_col, StringComparison.OrdinalIgnoreCase) ||
|
||||
schema_info.price_col.Equals(schema_info.timestamp_col, StringComparison.OrdinalIgnoreCase) ||
|
||||
schema_info.volume_col.Equals(schema_info.timestamp_col, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("BarBuilderSchemaInfo columns must be distinct.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace BlueOrb.Internal;
|
||||
|
||||
using BCG;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
[DataStreamUserRespresentation("TestStream")]
|
||||
public class TestStream : ViewStream {
|
||||
public TestStream() {
|
||||
var rs = new BlueOrb.Operators.Core.LiteralStream("table1", "x:123, y:1.001, z:abc;x:123, y:1.001, z:abc");
|
||||
mySetRealSource(rs);
|
||||
}
|
||||
}
|
||||
|
||||
[DataStreamUserRespresentation("OpsStream")]
|
||||
public class OpsStream : ViewStream {
|
||||
public OpsStream() {
|
||||
}
|
||||
|
||||
protected override void myOnFirstSubscriber() {
|
||||
IBlueTableSchema schema = new BlueTableSchema("Ops");
|
||||
schema = schema.add_column(System.TypeCode.Int32, "id");
|
||||
schema = schema.add_column(System.TypeCode.String, "op");
|
||||
schema = schema.add_column(System.TypeCode.String, "namespace");
|
||||
schema = schema.add_column(System.TypeCode.String, "assembly_name");
|
||||
schema = schema.add_column(System.TypeCode.String, "constructor");
|
||||
var loaded_streams = PrimitiveManager.instance.get_loaded_streams();
|
||||
BlueTable rv = new BlueTable(schema);
|
||||
foreach (var kv in loaded_streams.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)) {
|
||||
string sn = kv.Key;
|
||||
Type t = kv.Value;
|
||||
string ns = t.Namespace ?? string.Empty;
|
||||
string assembly_name = t.Assembly.GetName().Name ?? string.Empty;
|
||||
var constructors = t.GetConstructors()
|
||||
.OrderBy(c => c.GetParameters().Length)
|
||||
.ThenBy(c => c.ToString());
|
||||
if (!constructors.Any()) {
|
||||
IBlueRow r = new BlueRow(schema, rv.Count, sn, ns, assembly_name, "(no public constructor)");
|
||||
rv.addRow(r);
|
||||
continue;
|
||||
}
|
||||
foreach (var ctor in constructors) {
|
||||
var parameters = ctor.GetParameters();
|
||||
string constructor = $"{t.Name}({string.Join(", ", parameters.Select(p => p.ParameterType.Name))})";
|
||||
IBlueRow r = new BlueRow(schema, rv.Count, sn, ns, assembly_name, constructor);
|
||||
rv.addRow(r);
|
||||
}
|
||||
}
|
||||
myFireEvent(new DataPacket(rv, rv, new BlueTable(schema)));
|
||||
}
|
||||
|
||||
protected override void myOnLastSubscriber() {
|
||||
base.myOnLastSubscriber();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// namespace Bts.DataStreamTests;
|
||||
|
||||
// using BCG;
|
||||
|
||||
// using BlueOrb.Operators.Core;
|
||||
|
||||
// [TestFixture]
|
||||
// public class BarBuilderStreamTests {
|
||||
// static readonly DateTime T0 = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// [Test]
|
||||
// public void TimeBars_FromTrades_AcrossBoundary() {
|
||||
// var schema = TradeSchema();
|
||||
// var driver = new TestDriverStream(schema);
|
||||
// var bars = new BarBuilderStream(driver, "TIME", 60, "trade_price", "trade_size");
|
||||
// var rows = CaptureAddedRows(bars);
|
||||
|
||||
// var sent = new List<IBlueRow>();
|
||||
// sent.Add(TradeRow(schema, 1, T0.AddSeconds(10), "BTCUSD", 100, 2));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 2, T0.AddSeconds(20), "BTCUSD", 101, 3));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 3, T0.AddMinutes(1).AddSeconds(5), "BTCUSD", 99, 1));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 4, T0.AddMinutes(2), "BTCUSD", 102, 1));
|
||||
// driver.Reset(sent);
|
||||
|
||||
// Assert.That(rows.Count, Is.EqualTo(2));
|
||||
// AssertBar(rows[0], 100, 101, 100, 101, 5, 2, 100.6, false);
|
||||
// AssertBar(rows[1], 99, 99, 99, 99, 1, 1, 99, false);
|
||||
// }
|
||||
|
||||
// [Test]
|
||||
// public void TimeBars_FromBboMid_VolumeIsZero() {
|
||||
// var schema = BboSchema();
|
||||
// var driver = new TestDriverStream(schema);
|
||||
// var bars = new BarBuilderStream(driver, "TIME", 60, "mid", "none");
|
||||
// var rows = CaptureAddedRows(bars);
|
||||
|
||||
// var sent = new List<IBlueRow>();
|
||||
// sent.Add(BboRow(schema, 1, T0.AddSeconds(0), "ETHUSD", 99, 101));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(BboRow(schema, 2, T0.AddSeconds(30), "ETHUSD", 101, 103));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(BboRow(schema, 3, T0.AddMinutes(1).AddSeconds(1), "ETHUSD", 100, 104));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(BboRow(schema, 4, T0.AddMinutes(2), "ETHUSD", 100, 102));
|
||||
// driver.Reset(sent);
|
||||
|
||||
// Assert.That(rows.Count, Is.EqualTo(2));
|
||||
// AssertBar(rows[0], 100, 102, 100, 102, 0, 2, 102, false);
|
||||
// AssertBar(rows[1], 102, 102, 102, 102, 0, 1, 102, false);
|
||||
// }
|
||||
|
||||
// [Test]
|
||||
// public void TickBars_CloseExactlyOnNEvents() {
|
||||
// var schema = TradeSchema();
|
||||
// var driver = new TestDriverStream(schema);
|
||||
// var bars = new BarBuilderStream(driver, "TICK", 3, "trade_price", "trade_size");
|
||||
// var rows = CaptureAddedRows(bars);
|
||||
|
||||
// var sent = new List<IBlueRow>();
|
||||
// sent.Add(TradeRow(schema, 1, T0, "NQ", 10, 1));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 2, T0.AddSeconds(1), "NQ", 11, 1));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 3, T0.AddSeconds(2), "NQ", 9, 1));
|
||||
// driver.Reset(sent);
|
||||
|
||||
// Assert.That(rows.Count, Is.EqualTo(1));
|
||||
// AssertBar(rows[0], 10, 11, 9, 9, 3, 3, 10, false);
|
||||
// }
|
||||
|
||||
// [Test]
|
||||
// public void VolumeBars_CloseOnThreshold() {
|
||||
// var schema = TradeSchema();
|
||||
// var driver = new TestDriverStream(schema);
|
||||
// var bars = new BarBuilderStream(driver, "VOLUME", 10, "trade_price", "trade_size");
|
||||
// var rows = CaptureAddedRows(bars);
|
||||
|
||||
// var sent = new List<IBlueRow>();
|
||||
// sent.Add(TradeRow(schema, 1, T0, "ES", 100, 3));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 2, T0.AddSeconds(1), "ES", 120, 4));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 3, T0.AddSeconds(2), "ES", 110, 5));
|
||||
// driver.Reset(sent);
|
||||
|
||||
// Assert.That(rows.Count, Is.EqualTo(1));
|
||||
// AssertBar(rows[0], 100, 120, 100, 110, 12, 3, (100 * 3 + 120 * 4 + 110 * 5) / 12.0, false);
|
||||
// }
|
||||
|
||||
// [Test]
|
||||
// public void DollarBars_CloseOnThreshold() {
|
||||
// var schema = TradeSchema();
|
||||
// var driver = new TestDriverStream(schema);
|
||||
// var bars = new BarBuilderStream(driver, "DOLLAR", 1000, "trade_price", "trade_size");
|
||||
// var rows = CaptureAddedRows(bars);
|
||||
|
||||
// var sent = new List<IBlueRow>();
|
||||
// sent.Add(TradeRow(schema, 1, T0, "CL", 100, 3));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 2, T0.AddSeconds(1), "CL", 120, 4));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 3, T0.AddSeconds(2), "CL", 110, 3));
|
||||
// driver.Reset(sent);
|
||||
|
||||
// Assert.That(rows.Count, Is.EqualTo(1));
|
||||
// AssertBar(rows[0], 100, 120, 100, 110, 10, 3, 111, false);
|
||||
// }
|
||||
|
||||
// [Test]
|
||||
// public void Validation_VolumeOnBboOnly_Throws() {
|
||||
// var schema = BboSchema();
|
||||
// var driver = new TestDriverStream(schema);
|
||||
// var bars = new BarBuilderStream(driver, "VOLUME", 10, "mid", "trade_size");
|
||||
|
||||
// var row = BboRow(schema, 1, T0, "BTCUSD", 99, 101);
|
||||
// Assert.Throws<ArgumentException>(() => {
|
||||
// using var _ = SubscribeNoOp(bars);
|
||||
// driver.Reset(row);
|
||||
// });
|
||||
// }
|
||||
|
||||
// [Test]
|
||||
// public void EmitForming_EmitsFormingAndFinal() {
|
||||
// var schema = TradeSchema();
|
||||
// var driver = new TestDriverStream(schema);
|
||||
// var bars = new BarBuilderStream(driver, "TICK", 2, "trade_price", "trade_size", emit_forming: true);
|
||||
// var rows = CaptureAddedRows(bars);
|
||||
|
||||
// var sent = new List<IBlueRow>();
|
||||
// sent.Add(TradeRow(schema, 1, T0, "MNQ", 10, 1));
|
||||
// driver.Reset(sent);
|
||||
// sent.Add(TradeRow(schema, 2, T0.AddSeconds(1), "MNQ", 12, 1));
|
||||
// driver.Reset(sent);
|
||||
|
||||
// Assert.That(rows.Count, Is.EqualTo(3));
|
||||
// Assert.That(rows[0].get<bool>("is_forming"), Is.True);
|
||||
// Assert.That(rows[1].get<bool>("is_forming"), Is.True);
|
||||
// Assert.That(rows[2].get<bool>("is_forming"), Is.False);
|
||||
// AssertBar(rows[2], 10, 12, 10, 12, 2, 2, 11, false);
|
||||
// }
|
||||
|
||||
// static IDisposable SubscribeNoOp(ViewStream stream) {
|
||||
// DataStream.ChangeDelegate d = _ => { };
|
||||
// stream.onChange += d;
|
||||
// return new Subscription(() => stream.onChange -= d);
|
||||
// }
|
||||
|
||||
// static List<IBlueRow> CaptureAddedRows(ViewStream stream) {
|
||||
// var rows = new List<IBlueRow>();
|
||||
// stream.onChange += o => {
|
||||
// var dp = (IDataPacket)o;
|
||||
// foreach (IBlueRow r in dp.addedRows) {
|
||||
// rows.Add(new BlueRow(r));
|
||||
// }
|
||||
// };
|
||||
// return rows;
|
||||
// }
|
||||
|
||||
// static BlueTableSchema TradeSchema() =>
|
||||
// new(
|
||||
// new[] {
|
||||
// new ColumnInfo("id", 0, typeof(int)),
|
||||
// new ColumnInfo("ts_event", 1, typeof(string)),
|
||||
// new ColumnInfo("contract", 2, typeof(string)),
|
||||
// new ColumnInfo("trade_price", 3, typeof(double)),
|
||||
// new ColumnInfo("trade_size", 4, typeof(double)),
|
||||
// },
|
||||
// "trade_events"
|
||||
// );
|
||||
|
||||
// static BlueTableSchema BboSchema() =>
|
||||
// new(
|
||||
// new[] {
|
||||
// new ColumnInfo("id", 0, typeof(int)),
|
||||
// new ColumnInfo("ts_event", 1, typeof(string)),
|
||||
// new ColumnInfo("contract", 2, typeof(string)),
|
||||
// new ColumnInfo("bid_price", 3, typeof(double)),
|
||||
// new ColumnInfo("ask_price", 4, typeof(double)),
|
||||
// },
|
||||
// "bbo_events"
|
||||
// );
|
||||
|
||||
// static IBlueRow TradeRow(IBlueTableSchema schema, int id, DateTime ts, string contract, double px, double sz) =>
|
||||
// new BlueRow(schema, id, ts.ToString("O"), contract, px, sz);
|
||||
|
||||
// static IBlueRow BboRow(IBlueTableSchema schema, int id, DateTime ts, string contract, double bid, double ask) =>
|
||||
// new BlueRow(schema, id, ts.ToString("O"), contract, bid, ask);
|
||||
|
||||
// static void AssertBar(
|
||||
// IBlueRow r,
|
||||
// double open,
|
||||
// double high,
|
||||
// double low,
|
||||
// double close,
|
||||
// double volume,
|
||||
// int tradeCount,
|
||||
// double vwap,
|
||||
// bool isForming
|
||||
// ) {
|
||||
// Assert.That(r.get<double>("open"), Is.EqualTo(open).Within(1e-9));
|
||||
// Assert.That(r.get<double>("high"), Is.EqualTo(high).Within(1e-9));
|
||||
// Assert.That(r.get<double>("low"), Is.EqualTo(low).Within(1e-9));
|
||||
// Assert.That(r.get<double>("close"), Is.EqualTo(close).Within(1e-9));
|
||||
// Assert.That(r.get<double>("volume"), Is.EqualTo(volume).Within(1e-9));
|
||||
// Assert.That(r.get<int>("trade_count"), Is.EqualTo(tradeCount));
|
||||
// Assert.That(r.get<double>("vwap"), Is.EqualTo(vwap).Within(1e-9));
|
||||
// Assert.That(r.get<bool>("is_forming"), Is.EqualTo(isForming));
|
||||
// }
|
||||
|
||||
// sealed class Subscription : IDisposable {
|
||||
// readonly Action _dispose;
|
||||
// public Subscription(Action dispose) => _dispose = dispose;
|
||||
// public void Dispose() => _dispose();
|
||||
// }
|
||||
// }
|
||||
+25
-17
@@ -187,24 +187,32 @@
|
||||
Name="Test"
|
||||
DependsOnTargets="Build"></Target>
|
||||
|
||||
<Target Name="CopyNativeAssetsToOutput" AfterTargets="Build">
|
||||
<Copy SourceFiles="@(NativeWindowsAsset)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="True" Condition="Exists('%(NativeWindowsAsset.FullPath)')" />
|
||||
<Copy SourceFiles="@(NativeLinuxAsset)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="True" Condition="Exists('%(NativeLinuxAsset.FullPath)')" />
|
||||
</Target>
|
||||
<ItemGroup Condition="Exists('@(NativeWindowsAsset)')">
|
||||
<Content Include="@(NativeWindowsAsset)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>%(FileName)%(Extension)</TargetPath>
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
<Content Include="@(NativeWindowsAsset)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>runtimes/win-x64/native/%(FileName)%(Extension)</TargetPath>
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>runtimes/win-x64/native</PackagePath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="@(NativeWindowsAsset)"
|
||||
Condition="Exists('@(NativeWindowsAsset)')"
|
||||
Link="%(NativeWindowsAsset.FileName)%(NativeWindowsAsset.Extension)"
|
||||
CopyToOutputDirectory="PreserveNewest"
|
||||
Pack="true"
|
||||
PackagePath="runtimes/win-x64/native/" />
|
||||
<Content Include="@(NativeLinuxAsset)"
|
||||
Condition="Exists('@(NativeLinuxAsset)')"
|
||||
Link="%(NativeLinuxAsset.FileName)%(NativeLinuxAsset.Extension)"
|
||||
CopyToOutputDirectory="PreserveNewest"
|
||||
Pack="true"
|
||||
PackagePath="runtimes/linux-x64/native/" />
|
||||
<ItemGroup Condition="Exists('@(NativeLinuxAsset)')">
|
||||
<Content Include="@(NativeLinuxAsset)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>%(FileName)%(Extension)</TargetPath>
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
<Content Include="@(NativeLinuxAsset)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>runtimes/linux-x64/native/%(FileName)%(Extension)</TargetPath>
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>runtimes/linux-x64/native</PackagePath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,5 +0,0 @@
|
||||
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/all_crypto
|
||||
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/gateio_dev/requirements.txt
|
||||
SPARKD__DATA_DIR=/home/ubuntu/8000/sparkdata/data
|
||||
SPARKD__IMAGE=sparkd_symbols
|
||||
SPARKD__RUNTIME_CONF_DIR=/home/ubuntu/8000/sparkdata/conf
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"currencies": [
|
||||
"USDT"
|
||||
],
|
||||
"futures": [
|
||||
"BTC",
|
||||
"ETH",
|
||||
"SOL",
|
||||
"XRP"
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"currencies": [
|
||||
"USDT"
|
||||
],
|
||||
"futures": [
|
||||
"BTC",
|
||||
"ETH",
|
||||
"SOL",
|
||||
"XRP"
|
||||
],
|
||||
"use_testnet": false,
|
||||
"logging": {
|
||||
"console_level": "WARNING",
|
||||
"file_level": "INFO"
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
PublisherOptions {
|
||||
value {
|
||||
network_interface: "0.0.0.0"
|
||||
heartbeat_ms: 5000
|
||||
heartbeat_boot_seconds: 60
|
||||
skip_options: true
|
||||
hot_initial: 1024
|
||||
hot_growth: 1024
|
||||
cold_initial: 8192
|
||||
cold_growth: 2048
|
||||
channel_prefix: ""
|
||||
disable_heartbeat: true
|
||||
}
|
||||
}
|
||||
|
||||
Logging {
|
||||
value {
|
||||
initial_queue_size: 120001
|
||||
max_queue_growth: 4
|
||||
file {
|
||||
default_level: "Info"
|
||||
subsystem {
|
||||
name: "md"
|
||||
level: "Diag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarketDataConfig {
|
||||
value {
|
||||
# python_md_providers {
|
||||
# venue: VENUE_GATEIO
|
||||
# module_path: "python_md_providers"
|
||||
# provider_type: "gateio_md_provider.GateIoMdProvider"
|
||||
# }
|
||||
python_md_providers {
|
||||
venue: VENUE_BYBIT
|
||||
module_path: "python_interop_plugins"
|
||||
provider_type: "bybit_md_provider.BybitMdProvider"
|
||||
}
|
||||
binance_usds_futures {
|
||||
num_depth_levels: 5
|
||||
url_rest_base: "https://fapi.binance.com"
|
||||
url_ws_base: "wss://fstream.binance.com"
|
||||
security_criteria {
|
||||
clauses {
|
||||
security_types: SECURITY_TYPE_FUTURE
|
||||
symbols: @SymbolLists.list[Gateio].symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
okx {
|
||||
num_depth_levels: 5
|
||||
url_rest_base: "https://www.okx.com"
|
||||
url_ws_base: "wss://ws.okx.com:8443"
|
||||
security_criteria {
|
||||
clauses {
|
||||
security_types: SECURITY_TYPE_FUTURE
|
||||
symbols: @SymbolLists.list[Gateio].symbol
|
||||
}
|
||||
}
|
||||
is_application_ping_pong_enabled: true
|
||||
application_ping_interval_ms: 20000
|
||||
application_pong_timeout_ms: 10000
|
||||
is_protocol_ping_pong_enabled: true
|
||||
protocol_ping_interval_ms: 60000
|
||||
protocol_pong_timeout_ms: 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
SymbolLists {
|
||||
value {
|
||||
list {
|
||||
key: "Gateio"
|
||||
symbol: "SOL"
|
||||
symbol: "BTC"
|
||||
symbol: "ETH"
|
||||
symbol: "XRP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Symbols {
|
||||
value {
|
||||
venues {
|
||||
name: "VENUE_GATEIO"
|
||||
spot: @SymbolLists.list[Gateio].symbol
|
||||
}
|
||||
venues {
|
||||
name: "VENUE_BYBIT"
|
||||
future: @SymbolLists.list[Gateio].symbol
|
||||
}
|
||||
venues {
|
||||
name: "VENUE_BINANCE_USDS_FUTURES"
|
||||
future: @SymbolLists.list[Gateio].symbol
|
||||
}
|
||||
venues {
|
||||
name: "VENUE_OKX"
|
||||
future: @SymbolLists.list[Gateio].symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/ophir_cert_live_wb
|
||||
SPARKD__DATA_DIR=/home/wedbushprod/9000/sparkdata/data
|
||||
SPARKD__IMAGE=sparkd_symbols
|
||||
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/ophir_cert_live_wb/requirements.txt
|
||||
SPARKD__RUNTIME_CONF_DIR=/home/wedbushprod/9000/sparkdata/conf
|
||||
SPARKD__SHM_SIZE=8g
|
||||
SPARKD__MEMORY=16g
|
||||
SPARKD__CONFIG_PORT=9000
|
||||
SPARKD__QUICKBUS_PORT=9001
|
||||
SPARKD__SIM_FIX_PORT=9002
|
||||
@@ -1,140 +0,0 @@
|
||||
DeploymentType {
|
||||
value {
|
||||
type: "dev"
|
||||
}
|
||||
}
|
||||
PublisherOptions {
|
||||
value {
|
||||
network_interface_a: 'ens65f1'
|
||||
network_interface_b: 'ens65f0'
|
||||
#these get overriden if not entered here
|
||||
heartbeat_ms: 5000
|
||||
heartbeat_boot_seconds: 60
|
||||
skip_options: false
|
||||
hot_initial: 1024
|
||||
hot_growth: 1024
|
||||
cold_initial: 8192
|
||||
cold_growth: 2048
|
||||
channel_prefix: ""
|
||||
disable_heartbeat: false
|
||||
}
|
||||
}
|
||||
|
||||
CMEOptions {
|
||||
value {
|
||||
feed_engine {
|
||||
name: "mdp3feed"
|
||||
spin_time: 0
|
||||
channels_per_thread: 3
|
||||
data_wait: 10
|
||||
udp_socket_buffer_size: 268435456
|
||||
}
|
||||
channel {
|
||||
id: "*"
|
||||
feed_engine: "mdp3feed"
|
||||
use_tcp_replay: false
|
||||
recovery_queue_size: 640000
|
||||
lost_packet_wait_timeout_ms: 10
|
||||
onixs_log_mode: CME_ONIXS_LOG_MODE_UNSPECIFIED
|
||||
#max_gap_recovery_cache_size: 4
|
||||
use_natural_refresh: false
|
||||
join_snapshot_on_gap: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logging {
|
||||
value {
|
||||
initial_queue_size: 120001
|
||||
max_queue_growth: 4
|
||||
console {
|
||||
default_level: "Info"
|
||||
subsystem {
|
||||
name: "md"
|
||||
level: "Info"
|
||||
}
|
||||
subsystem {
|
||||
name: "OrderMan"
|
||||
level: "Info"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarketDataConfig {
|
||||
value {
|
||||
glbx {
|
||||
unique_check_value: 15
|
||||
use_databento: false
|
||||
}
|
||||
# recording {
|
||||
# venue: VENUE_GLBX
|
||||
# num_of_seconds_to_run_file_writer: 300
|
||||
# num_of_seconds_to_run_db_writer : 0
|
||||
# n_book_levels: 10
|
||||
# batch_size: 100 #for the db writer
|
||||
# enable_book_recording: true
|
||||
# enable_trade_recording: true
|
||||
# enable_bbo_recording: false
|
||||
# enable_tbbo_recording: false
|
||||
# enable_misc_recording: true
|
||||
#}
|
||||
}
|
||||
}
|
||||
|
||||
JetFIX {
|
||||
value {
|
||||
connection {
|
||||
name: "maw_004_64"
|
||||
venue: VENUE_GLBX
|
||||
socket_connect_host_ip: "69.50.112.177"
|
||||
socket_connect_port: 44040
|
||||
sender_compid: "MAW004"
|
||||
target_compid: "CME"
|
||||
target_subid: "64"
|
||||
cme_market_segment_id: 64
|
||||
start_time: "00:10:00"
|
||||
end_time: "23:55:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
seconds_between_hbs: 15
|
||||
sender_subid: "BTS"
|
||||
seconds_to_pause: 2
|
||||
max_connection_attempts: 100
|
||||
use_ilink3: true
|
||||
}
|
||||
}
|
||||
}
|
||||
OrdermanCfg {
|
||||
value {
|
||||
executor {
|
||||
thread_name: "oman_orders"
|
||||
poll: true
|
||||
hosted_actor: "MDSource"
|
||||
hosted_actor: "OrderManager"
|
||||
hosted_actor: "ExecutionGateway"
|
||||
}
|
||||
executor {
|
||||
thread_name: "oman_slow"
|
||||
poll: false
|
||||
hosted_actor: "*"
|
||||
}
|
||||
fallback_cust_order_handling_inst: "Y"
|
||||
}
|
||||
}
|
||||
NotSimExecution {
|
||||
value {
|
||||
entries {
|
||||
venue: VENUE_GLBX
|
||||
}
|
||||
}
|
||||
}
|
||||
Ports {
|
||||
value {
|
||||
config_port: 9000
|
||||
qbus_port: 9001
|
||||
glbx_sim_fix_port: 9002
|
||||
ice_sim_fix_port: 9003
|
||||
replicator_port: 9080
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/ophir_demo
|
||||
SPARKD__DATA_DIR=/home/ubuntu/8000/sparkdata/data
|
||||
SPARKD__IMAGE=sparkd_symbols
|
||||
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/ophir_demo/requirements.txt
|
||||
SPARKD__RUNTIME_CONF_DIR=/home/ubuntu/8000/sparkdata/conf
|
||||
File renamed without changes.
@@ -0,0 +1,62 @@
|
||||
JetFIX {
|
||||
value {
|
||||
connection {
|
||||
name: "glbx_1"
|
||||
venue: VENUE_GLBX
|
||||
max_order_msgs_per_window: 300
|
||||
window_duration_msecs: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PublisherOptions {
|
||||
value {
|
||||
network_interface: "0.0.0.0"
|
||||
heartbeat_ms: 5000
|
||||
heartbeat_boot_seconds: 60
|
||||
skip_options: true
|
||||
hot_initial: 1024
|
||||
hot_growth: 1024
|
||||
cold_initial: 8192
|
||||
cold_growth: 2048
|
||||
channel_prefix: ""
|
||||
disable_heartbeat: true
|
||||
}
|
||||
}
|
||||
|
||||
Logging {
|
||||
value {
|
||||
initial_queue_size: 120001
|
||||
max_queue_growth: 4
|
||||
console {
|
||||
default_level: "Info"
|
||||
subsystem {
|
||||
name: "md"
|
||||
level: "Warn"
|
||||
}
|
||||
subsystem {
|
||||
name: "OrderMan"
|
||||
level: "Info"
|
||||
}
|
||||
}
|
||||
file {
|
||||
default_level: "Info"
|
||||
subsystem {
|
||||
name: "OrderMan"
|
||||
level: "Diag"
|
||||
}
|
||||
subsystem {
|
||||
name: "Http"
|
||||
level: "Diag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarketDataConfig {
|
||||
value {
|
||||
replay {
|
||||
venue: VENUE_GLBX
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Symbols {
|
||||
value {
|
||||
venues {
|
||||
name: "VENUE_GLBX"
|
||||
future: "ES"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
DeploymentType {
|
||||
value {
|
||||
type: "prod"
|
||||
}
|
||||
}
|
||||
PublisherOptions {
|
||||
value {
|
||||
network_interface_a: 'enp193s0f0np0'
|
||||
network_interface_b: 'enp193s0f1np1'
|
||||
#these get overriden if not entered here
|
||||
heartbeat_ms: 5000
|
||||
heartbeat_boot_seconds: 60
|
||||
skip_options: false
|
||||
hot_initial: 1024
|
||||
hot_growth: 1024
|
||||
cold_initial: 8192
|
||||
cold_growth: 2048
|
||||
channel_prefix: ""
|
||||
disable_heartbeat: false
|
||||
}
|
||||
}
|
||||
|
||||
CMEOptions {
|
||||
value {
|
||||
feed_engine {
|
||||
name: "mdp3feed"
|
||||
spin_time: 0
|
||||
channels_per_thread: 3
|
||||
data_wait: 10
|
||||
udp_socket_buffer_size: 268435456
|
||||
}
|
||||
channel {
|
||||
id: "*"
|
||||
feed_engine: "mdp3feed"
|
||||
use_tcp_replay: false
|
||||
recovery_queue_size: 640000
|
||||
lost_packet_wait_timeout_ms: 10
|
||||
onixs_log_mode: CME_ONIXS_LOG_MODE_UNSPECIFIED
|
||||
#max_gap_recovery_cache_size: 4
|
||||
use_natural_refresh: false
|
||||
join_snapshot_on_gap: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logging {
|
||||
value {
|
||||
initial_queue_size: 120001
|
||||
max_queue_growth: 4
|
||||
console {
|
||||
default_level: "Info"
|
||||
subsystem {
|
||||
name: "md"
|
||||
level: "Info"
|
||||
}
|
||||
subsystem {
|
||||
name: "OrderMan"
|
||||
level: "Info"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarketDataConfig {
|
||||
value {
|
||||
glbx {
|
||||
unique_check_value: 15
|
||||
use_databento: false
|
||||
}
|
||||
# recording {
|
||||
# venue: VENUE_GLBX
|
||||
# num_of_seconds_to_run_file_writer: 300
|
||||
# num_of_seconds_to_run_db_writer : 0
|
||||
# n_book_levels: 10
|
||||
# batch_size: 100 #for the db writer
|
||||
# enable_book_recording: true
|
||||
# enable_trade_recording: true
|
||||
# enable_bbo_recording: false
|
||||
# enable_tbbo_recording: false
|
||||
# enable_misc_recording: true
|
||||
#}
|
||||
}
|
||||
}
|
||||
|
||||
JetFIX {
|
||||
value {
|
||||
connection {
|
||||
name: "u6p_7qb_54"
|
||||
venue: VENUE_GLBX
|
||||
socket_connect_host_ip: "205.209.201.97"
|
||||
socket_connect_port: 10979
|
||||
sender_compid: "U6P7QB"
|
||||
target_compid: "CME"
|
||||
target_subid: "54"
|
||||
cme_market_segment_id: 54
|
||||
start_time: "00:10:00"
|
||||
end_time: "23:55:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
seconds_between_hbs: 15
|
||||
sender_subid: "BTS"
|
||||
seconds_to_pause: 1
|
||||
max_order_msgs_per_window: 300
|
||||
max_connection_attempts: 2
|
||||
use_ilink3: true
|
||||
}
|
||||
connection {
|
||||
name: "u6p_7qb_64"
|
||||
venue: VENUE_GLBX
|
||||
socket_connect_host_ip: "205.209.201.1"
|
||||
socket_connect_port: 10979
|
||||
sender_compid: "U6P7QB"
|
||||
target_compid: "CME"
|
||||
target_subid: "64"
|
||||
cme_market_segment_id: 64
|
||||
start_time: "00:10:00"
|
||||
end_time: "23:55:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
seconds_between_hbs: 15
|
||||
sender_subid: "BTS"
|
||||
seconds_to_pause: 1
|
||||
max_connection_attempts: 2
|
||||
max_order_msgs_per_window: 300
|
||||
use_ilink3: true
|
||||
}
|
||||
}
|
||||
}
|
||||
OrdermanCfg {
|
||||
value {
|
||||
executor {
|
||||
thread_name: "oman_orders"
|
||||
poll: true
|
||||
hosted_actor: "MDSource"
|
||||
hosted_actor: "OrderManager"
|
||||
hosted_actor: "ExecutionGateway"
|
||||
}
|
||||
executor {
|
||||
thread_name: "oman_slow"
|
||||
poll: false
|
||||
hosted_actor: "*"
|
||||
}
|
||||
fallback_cust_order_handling_inst: "Y"
|
||||
}
|
||||
}
|
||||
NotSimExecution {
|
||||
value {
|
||||
entries {
|
||||
venue: VENUE_GLBX
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/ophir_sim
|
||||
SPARKD__DATA_DIR=/home/tns/8000/sparkdata/data
|
||||
SPARKD__IMAGE=sparkd_symbols
|
||||
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/ophir_sim/requirements.txt
|
||||
SPARKD__RUNTIME_CONF_DIR=/home/tns/8000/sparkdata/conf
|
||||
File renamed without changes.
@@ -0,0 +1,104 @@
|
||||
JetFIX {
|
||||
value {
|
||||
connection {
|
||||
name: "glbx_1"
|
||||
venue: VENUE_GLBX
|
||||
max_order_msgs_per_window: 300
|
||||
window_duration_msecs: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PublisherOptions {
|
||||
value {
|
||||
network_interface_a: 'ens3f0np0'
|
||||
network_interface_b: 'ens3f1np1'
|
||||
#these get overriden if not entered here
|
||||
heartbeat_ms: 5000
|
||||
heartbeat_boot_seconds: 60
|
||||
skip_options: false
|
||||
hot_initial: 1024
|
||||
hot_growth: 1024
|
||||
cold_initial: 8192
|
||||
cold_growth: 2048
|
||||
channel_prefix: ""
|
||||
disable_heartbeat: false
|
||||
}
|
||||
}
|
||||
|
||||
CMEOptions {
|
||||
value {
|
||||
feed_engine {
|
||||
name: "mdp3feed"
|
||||
spin_time: 0
|
||||
channels_per_thread: 3
|
||||
data_wait: 10
|
||||
udp_socket_buffer_size: 268435456
|
||||
}
|
||||
channel {
|
||||
id: "*"
|
||||
feed_engine: "mdp3feed"
|
||||
use_tcp_replay: false
|
||||
recovery_queue_size: 640000
|
||||
lost_packet_wait_timeout_ms: 10
|
||||
onixs_log_mode: CME_ONIXS_LOG_MODE_REGULAR
|
||||
#max_gap_recovery_cache_size: 4
|
||||
use_natural_refresh: false
|
||||
join_snapshot_on_gap: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logging {
|
||||
value {
|
||||
initial_queue_size: 120001
|
||||
max_queue_growth: 4
|
||||
console {
|
||||
default_level: "Info"
|
||||
subsystem {
|
||||
name: "md"
|
||||
level: "Info"
|
||||
}
|
||||
subsystem {
|
||||
name: "OrderMan"
|
||||
level: "Info"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarketDataConfig {
|
||||
value {
|
||||
glbx {
|
||||
unique_check_value: 15
|
||||
use_databento: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# JetFIX {
|
||||
# value {
|
||||
# #connection: @JetFIXSimConnection.sim
|
||||
# connection {
|
||||
# name: "mlx_004_54"
|
||||
# venue: VENUE_GLBX
|
||||
# socket_connect_host_ip: "69.50.112.191"
|
||||
# socket_connect_port: 42876
|
||||
# sender_compid: "MLX004"
|
||||
# target_compid: "CME"
|
||||
# target_subid: "54"
|
||||
# cme_market_segment_id: 54
|
||||
# start_time: "00:10:00"
|
||||
# end_time: "23:55:00"
|
||||
# start_day: "sunday"
|
||||
# end_day: "friday"
|
||||
# seconds_between_hbs: 30
|
||||
# sender_subid: "BTS"
|
||||
# use_ilink3: true
|
||||
# # raw_data: "S4PMA"
|
||||
# # auto_fix_seq_num: true
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
|
||||
File renamed without changes.
+2
-4
@@ -1,7 +1,5 @@
|
||||
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/ophir_prod_live_wb
|
||||
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/ophir_sim_live_wb
|
||||
SPARKD__DATA_DIR=/home/wedbushprod/8000/sparkdata/data
|
||||
SPARKD__IMAGE=sparkd_symbols
|
||||
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/ophir_prod_live_wb/requirements.txt
|
||||
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/ophir_sim_live_wb/requirements.txt
|
||||
SPARKD__RUNTIME_CONF_DIR=/home/wedbushprod/8000/sparkdata/conf
|
||||
SPARKD__SHM_SIZE=8g
|
||||
SPARKD__MEMORY=16g
|
||||
File renamed without changes.
@@ -0,0 +1,116 @@
|
||||
JetFIX {
|
||||
value {
|
||||
connection {
|
||||
name: "glbx_1"
|
||||
venue: VENUE_GLBX
|
||||
max_order_msgs_per_window: 300
|
||||
window_duration_msecs: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PublisherOptions {
|
||||
value {
|
||||
network_interface_a: 'enp193s0f0np0'
|
||||
network_interface_b: 'enp193s0f0np0'
|
||||
#these get overriden if not entered here
|
||||
heartbeat_ms: 5000
|
||||
heartbeat_boot_seconds: 60
|
||||
skip_options: false
|
||||
hot_initial: 1024
|
||||
hot_growth: 1024
|
||||
cold_initial: 8192
|
||||
cold_growth: 2048
|
||||
channel_prefix: ""
|
||||
disable_heartbeat: false
|
||||
}
|
||||
}
|
||||
|
||||
CMEOptions {
|
||||
value {
|
||||
feed_engine {
|
||||
name: "mdp3feed"
|
||||
spin_time: 0
|
||||
channels_per_thread: 3
|
||||
data_wait: 10
|
||||
udp_socket_buffer_size: 268435456
|
||||
}
|
||||
channel {
|
||||
id: "*"
|
||||
feed_engine: "mdp3feed"
|
||||
use_tcp_replay: false
|
||||
recovery_queue_size: 640000
|
||||
lost_packet_wait_timeout_ms: 10
|
||||
onixs_log_mode: CME_ONIXS_LOG_MODE_UNSPECIFIED:q
|
||||
#max_gap_recovery_cache_size: 4
|
||||
use_natural_refresh: false
|
||||
join_snapshot_on_gap: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logging {
|
||||
value {
|
||||
initial_queue_size: 120001
|
||||
max_queue_growth: 4
|
||||
console {
|
||||
default_level: "Info"
|
||||
subsystem {
|
||||
name: "md"
|
||||
level: "Info"
|
||||
}
|
||||
subsystem {
|
||||
name: "OrderMan"
|
||||
level: "Info"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MarketDataConfig {
|
||||
value {
|
||||
glbx {
|
||||
unique_check_value: 15
|
||||
use_databento: false
|
||||
}
|
||||
recording{
|
||||
venue: VENUE_GLBX
|
||||
num_of_seconds_to_run_file_writer: 300
|
||||
num_of_seconds_to_run_db_writer : 0
|
||||
n_book_levels: 10
|
||||
batch_size: 100 #for the db writer
|
||||
enable_book_recording: true
|
||||
enable_trade_recording: true
|
||||
enable_bbo_recording: false
|
||||
enable_tbbo_recording: false
|
||||
enable_misc_recording: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# JetFIX {
|
||||
# value {
|
||||
# #connection: @JetFIXSimConnection.sim
|
||||
# connection {
|
||||
# name: "mlx_004_54"
|
||||
# venue: VENUE_GLBX
|
||||
# socket_connect_host_ip: "69.50.112.191"
|
||||
# socket_connect_port: 42876
|
||||
# sender_compid: "MLX004"
|
||||
# target_compid: "CME"
|
||||
# target_subid: "54"
|
||||
# cme_market_segment_id: 54
|
||||
# start_time: "00:10:00"
|
||||
# end_time: "23:55:00"
|
||||
# start_day: "sunday"
|
||||
# end_day: "friday"
|
||||
# seconds_between_hbs: 30
|
||||
# sender_subid: "BTS"
|
||||
# use_ilink3: true
|
||||
# # raw_data: "S4PMA"
|
||||
# # auto_fix_seq_num: true
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
|
||||
File renamed without changes.
@@ -109,8 +109,7 @@ struct ExecutionGatewayProvider::PImpl {
|
||||
return make_gw<ICEExecutionGateway>(conn, conn, *l, o);
|
||||
case api::VENUE_GLBX:
|
||||
if (conn.use_ilink3()) {
|
||||
return make_gw<ilink3::GLBXiLink3ExecutionGateway>(
|
||||
conn, conn, *l, o);
|
||||
make_gw<ilink3::GLBXiLink3ExecutionGateway>(conn, conn, *l, o);
|
||||
}
|
||||
return make_gw<GLBXExecutionGateway>(conn, conn, *l, o);
|
||||
case api::VENUE_DERIBIT:
|
||||
|
||||
@@ -29,6 +29,7 @@ using std::vector;
|
||||
struct OnixsGlbxSubscription::PImpl {
|
||||
GlbxMarketDataConfig config;
|
||||
shared_ptr<IDGenerator> sec_id_store;
|
||||
shared_ptr<IMarketDataListener> listener;
|
||||
|
||||
PImpl(shared_ptr<IDGenerator> sec_id_store, GlbxMarketDataConfig config)
|
||||
: config(std::move(config)), sec_id_store(std::move(sec_id_store)) {}
|
||||
@@ -66,7 +67,8 @@ struct OnixsGlbxSubscription::PImpl {
|
||||
FMT_JOIN(symbols, ", "));
|
||||
vector<shared_ptr<IMDPlatform>> platforms;
|
||||
auto main = std::make_shared<MDP3Main>();
|
||||
main->start({symbols.begin(), symbols.end()}, nullptr, nullptr);
|
||||
main->start(
|
||||
{symbols.begin(), symbols.end()}, nullptr, nullptr, listener);
|
||||
platforms.emplace_back(std::move(main));
|
||||
dispatcher = &utils::Dispatcher::current();
|
||||
heartbeat_timer.emplace(*dispatcher, 250, [&]() {
|
||||
@@ -107,9 +109,10 @@ OnixsGlbxSubscription::OnixsGlbxSubscription(
|
||||
OnixsGlbxSubscription::~OnixsGlbxSubscription() = default;
|
||||
|
||||
void OnixsGlbxSubscription::start(
|
||||
shared_ptr<IMarketDataListener> /*listener*/) {
|
||||
shared_ptr<IMarketDataListener> listener) {
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
bts_assert(!me_->dispatcher_thread.joinable());
|
||||
me_->listener = std::move(listener);
|
||||
me_->dispatcher_thread = std::thread{[&]() { me_->run(); }};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ struct SecurityData {
|
||||
: id(id), handle(h), mbo_state(h), electronic_volume(0) {}
|
||||
uint32_t id; // exchange id
|
||||
MDProducerHandle handle;
|
||||
// Exception for now: MBO still maintains per-security working state locally
|
||||
// in MDP3Listener until GMDL grows equivalent MBO support.
|
||||
MBOWorkingState mbo_state;
|
||||
uint32_t electronic_volume;
|
||||
};
|
||||
|
||||
@@ -1,364 +1,428 @@
|
||||
#include "MDP3Listener.h"
|
||||
#include "spark/md/log_messages/trade_latency_log.h"
|
||||
#include "spark/config/protos/market_data.pb.h"
|
||||
#include "spark/md/protos/co2.pb.h"
|
||||
#include "spark/utils/datetime.h"
|
||||
#include "spark/utils/helper_macros.h"
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
using namespace bts::co2;
|
||||
|
||||
namespace bts {
|
||||
namespace md {
|
||||
|
||||
MDP3Listener::MDP3Listener(
|
||||
CMESecurityIndex &index,
|
||||
MDProducer &producer,
|
||||
MBOWriter *mbo,
|
||||
const std::vector<MDProducerChannel> channels,
|
||||
TradeSummarySignalDispatcher *trade_summary_dispatcher)
|
||||
: _index(index), _producer(producer),
|
||||
_verbose_ticker(config::get<CMEOptions>().verbose_ticker()),
|
||||
_packet_arrived(), _latest_sequence(0),
|
||||
// _settlement_recorder(settles),
|
||||
_symbol_filter_storage(), _symbol_filter(), _channels(channels),
|
||||
_concurrency_monitor(0), _book_updates(), _mbo(mbo),
|
||||
_trade_summary_dispatcher(trade_summary_dispatcher) {
|
||||
_book_updates.reserve(32);
|
||||
}
|
||||
|
||||
MDP3Listener::~MDP3Listener() {}
|
||||
|
||||
//
|
||||
// Warnings/Errors
|
||||
void MDP3Listener::onError(onixs::Handler &, const onixs::ErrorArgs &error) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onError", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Warn, "OnixS Error: {} - {}", error.description(), error.source());
|
||||
}
|
||||
void MDP3Listener::onWarning(
|
||||
onixs::Handler &, const onixs::WarningArgs &warning) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onWarning", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Warn, "OnixS Warn: {} - {}", warning.description(),
|
||||
warning.source());
|
||||
}
|
||||
void MDP3Listener::onInstrumentRecovery(onixs::Handler &h) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onInstrumentRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Info, "Beginning security definition recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onEndOfInstrumentRecovery(
|
||||
onixs::Handler &h, const onixs::RecoveryCompletionArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfInstrumentRecovery",
|
||||
_latest_sequence, (uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Info, "Finished Security definition recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
|
||||
//
|
||||
// Gap Recovery
|
||||
void MDP3Listener::onMarketRecovery(onixs::Handler &h) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onMarketRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_PACKET_LOSS);
|
||||
for (auto &c : _channels) {
|
||||
c.gap();
|
||||
}
|
||||
LOG(md, Info, "Beginning market recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onEndOfMarketRecovery(
|
||||
onixs::Handler &h, const onixs::RecoveryCompletionArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfMarketRecovery",
|
||||
_latest_sequence, (uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_OK);
|
||||
LOG(md, Info, "Finished market recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onTcpRecovery(onixs::Handler &h) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onTcpRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_PACKET_LOSS);
|
||||
LOG(md, Info, "Beginning tcp recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onEndOfTcpRecovery(
|
||||
onixs::Handler &h, const onixs::RecoveryCompletionArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfTcpRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_OK);
|
||||
LOG(md, Info, "Finished tcp recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
|
||||
//
|
||||
// Mark the start/end of a packet
|
||||
void MDP3Listener::onPacket(onixs::Handler &, const onixs::PacketArgs &args) {
|
||||
// Mark the arrival of the packet
|
||||
_packet_arrived = TimeStamp::now();
|
||||
auto seq = args.seqNumber();
|
||||
if (!_book_updates.empty()) {
|
||||
if (_latest_sequence != seq) {
|
||||
LOG(md, Error,
|
||||
"Expected sequence {} got {} did not flush {} book updates.",
|
||||
_latest_sequence, seq, _book_updates.size());
|
||||
}
|
||||
}
|
||||
_latest_sequence = seq;
|
||||
auto onixs_zero = args.receiveTime().sinceEpoch();
|
||||
auto onixs_now = OnixS::CME::MDH::UtcWatch::now().sinceEpoch();
|
||||
|
||||
// needed for clang compilation
|
||||
UNUSED(onixs_zero);
|
||||
UNUSED(onixs_now);
|
||||
// was needed for clang compilation
|
||||
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onPacket", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, onixs_now - onixs_zero);
|
||||
}
|
||||
void MDP3Listener::onEndOfPacket(onixs::Handler &, const onixs::PacketArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfPacket.begin", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
flush_books();
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfPacket.end", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
}
|
||||
|
||||
//
|
||||
// Book updates
|
||||
|
||||
void MDP3Listener::onBookUpdate(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::DirectBook &book) {
|
||||
onBookUpdate<COCONUT_MAX_DEPTH>(sec, book, book_type_direct);
|
||||
}
|
||||
|
||||
void MDP3Listener::onBookUpdate(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::ImpliedBook &book) {
|
||||
onBookUpdate<COCONUT_MAX_IMPLIED_DEPTH>(sec, book, book_type_implied);
|
||||
}
|
||||
|
||||
void MDP3Listener::onBookUpdate(
|
||||
onixs::Handler &, const onixs::Security &, const onixs::MboBook &) {
|
||||
#if 0
|
||||
if (_mbo) {
|
||||
auto data = _index.get_security(sec.id());
|
||||
if (data == nullptr) return;
|
||||
auto key = data->handle.key();
|
||||
if (key == invalid_security_key) return;
|
||||
auto* snapshot = _mbo->allocate_snapshot();
|
||||
// copy from onixs book
|
||||
for (const auto& bid : book.bids()) {
|
||||
snapshot->bids.push_back({ ExchangeOrderKey(bid.id()), convert_price(bid.price(), 0), bid.quantity() });
|
||||
}
|
||||
for (const auto& ask : book.offers()) {
|
||||
snapshot->asks.push_back({ ExchangeOrderKey(ask.id()), convert_price(ask.price(),0), ask.quantity() });
|
||||
}
|
||||
_mbo->validate_snapshot(sec.id(), SecurityKey(key), snapshot);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
//
|
||||
// Trades
|
||||
// Called from onTradeSummary and onTradeRecovery, ends up calling
|
||||
// MDProducerHandle::add_trade
|
||||
void on_trade(
|
||||
SecurityData *data,
|
||||
const onixs::Decimal &price,
|
||||
int size,
|
||||
onixs::AggressorSide::Enum agside,
|
||||
const onixs::Timestamp &onixs_date) {
|
||||
auto ts = utils::TimeStamp::now();
|
||||
PriceSize ps = pricesize_of_price_size(
|
||||
price, data->handle.security().display_factor, size);
|
||||
auto date = ll_of_timestamp(onixs_date);
|
||||
log_trade_latency(
|
||||
TradeLatencyStage::MDP3_RECEIVED, data->handle.security(), date, ps,
|
||||
map_aggressor(agside), data->electronic_volume + size, false);
|
||||
data->handle.add_trade(
|
||||
ts, ps, date, map_aggressor(agside), data->electronic_volume + size);
|
||||
}
|
||||
|
||||
//
|
||||
// RFQ
|
||||
void MDP3Listener::onQuoteRequest(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::QuoteRequest39RelatedSymArgs &arg) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onQuoteRequest", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
auto *data = _index.get_security(sec.id());
|
||||
if (data != nullptr) {
|
||||
auto ts = utils::TimeStamp::now();
|
||||
|
||||
co2::QuoteRequestMessage message;
|
||||
message.set_key(data->handle.key());
|
||||
|
||||
auto time = ll_of_timestamp(arg.message().transactTime());
|
||||
message.set_time(time);
|
||||
|
||||
onixs::StrRef id = arg.message().quoteReqId();
|
||||
message.set_request_id(id.items(), id.size());
|
||||
|
||||
// https://www.cmegroup.com/confluence/display/EPICSANDBOX/MDP+3.0+-+Request+for+Quote
|
||||
int8_t side;
|
||||
if (arg.entry().side(side)) {
|
||||
switch (side) {
|
||||
case 1: // buy
|
||||
case '1':
|
||||
message.set_side(co2::QuoteRequestMessage::BUY);
|
||||
break;
|
||||
case 2: // sell
|
||||
case '2':
|
||||
message.set_side(co2::QuoteRequestMessage::SELL);
|
||||
break;
|
||||
case 8: // cross
|
||||
case '8':
|
||||
message.set_side(co2::QuoteRequestMessage::CROSS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
message.set_tradable(arg.entry().quoteType() == 1);
|
||||
|
||||
int32_t size;
|
||||
if (arg.entry().orderQty(size)) {
|
||||
message.set_size(size);
|
||||
}
|
||||
|
||||
data->handle.add_rfq(ts, message);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Volume
|
||||
void MDP3Listener::onElectronicVolume(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::Volume37Args &args) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onElectronicVolume.volume",
|
||||
_latest_sequence, (uint16_t)_latest_sequence, 0);
|
||||
auto *data = _index.get_security(sec.id());
|
||||
if (data != nullptr) {
|
||||
bts_assert_d(args.entry().entryType().value() == 'e');
|
||||
bts_assert_d(
|
||||
args.entry().updateAction() == onixs::UpdateAction::New ||
|
||||
args.entry().updateAction() == onixs::UpdateAction::Change);
|
||||
data->electronic_volume = args.entry().entrySize();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Non-onixs calls
|
||||
|
||||
void MDP3Listener::add_filter_symbol(const std::string &symbol) {
|
||||
LOG(md, Info, "MDP3Listener::add_filter_symbol symbol: {}", symbol);
|
||||
_symbol_filter_storage.push_back(symbol);
|
||||
_symbol_filter.clear();
|
||||
for (const auto &str : _symbol_filter_storage) {
|
||||
_symbol_filter.insert(onixs::StrRef(str.c_str(), str.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void MDP3Listener::heartbeat() {
|
||||
for (auto &c : _channels) {
|
||||
c.heartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
// Called by MDP3Main::prepare_listener as part of pre-load. Also called by
|
||||
// MDP3Listener::onFuture, onOption and onSpread
|
||||
void MDP3Listener::add_security(
|
||||
const FixedSecurity &security, co2::TradingStatus status) {
|
||||
bts_assert(security.numeric_id != 0);
|
||||
if (security.type == api::SECURITYTYPE_OPTION) {
|
||||
const auto &option = static_cast<const FixedOptionSecurity &>(security);
|
||||
const datetime::Date expiration(
|
||||
option.expiration_year, option.expiration_month,
|
||||
option.expiration_day);
|
||||
if (!option_expiration_is_allowed(expiration)) {
|
||||
LOG(md, Info,
|
||||
"Skipping MDP3 option {} because it expires beyond the configured "
|
||||
"cutoff.",
|
||||
std::string(security.symbol()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Set up the shared memory for this security
|
||||
auto handle = _producer.add_security(security);
|
||||
// Add it to the index so it can receive messages
|
||||
_index.add_security(security.numeric_id, handle);
|
||||
|
||||
handle.update_exchange_status(utils::TimeStamp::now(), status);
|
||||
}
|
||||
|
||||
// Private functions
|
||||
|
||||
void MDP3Listener::flush_books() {
|
||||
// This is called at the end of the packet
|
||||
// Write all book updates to shared memory and reset for the next packet
|
||||
for (auto &entry : _book_updates) {
|
||||
uint32_t sequence = _latest_sequence;
|
||||
if (entry.type_flags == book_type_direct) {
|
||||
entry.handle.update_direct(
|
||||
_packet_arrived, entry.bids.first, entry.asks.first, sequence);
|
||||
} else if (entry.type_flags == book_type_implied) {
|
||||
entry.handle.update_implied(
|
||||
_packet_arrived, entry.bids.second, entry.asks.second, sequence);
|
||||
} else if (entry.type_flags == book_type_both) {
|
||||
entry.handle.update_books(
|
||||
_packet_arrived, entry.bids.first, entry.asks.first,
|
||||
entry.bids.second, entry.asks.second, sequence);
|
||||
}
|
||||
}
|
||||
// Reset pending updates
|
||||
_book_updates.clear();
|
||||
}
|
||||
|
||||
void MDP3Listener::update_all_status(CoconutSecurityStatus status) {
|
||||
_index.walk_security_list(
|
||||
[=](SecurityData &data) { data.handle.set_status(status); });
|
||||
}
|
||||
|
||||
void MDP3Listener::update_group_exchange_status(
|
||||
const onixs::StrRef &group, co2::TradingStatus status) {
|
||||
_index.walk_security_list([=](SecurityData &data) {
|
||||
if (data.handle.security().alt_symbol() == group) {
|
||||
data.handle.update_exchange_status(utils::TimeStamp::now(), status);
|
||||
}
|
||||
});
|
||||
}
|
||||
void MDP3Listener::update_asset_exchange_status(
|
||||
const onixs::StrRef &asset, co2::TradingStatus status) {
|
||||
_index.walk_security_list([=](SecurityData &data) {
|
||||
if (data.handle.security().symbol() == asset) {
|
||||
data.handle.update_exchange_status(utils::TimeStamp::now(), status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
md::MDP3Listener::BookData::BookData(MDProducerHandle &handle)
|
||||
: handle(handle), type_flags(0), bids(), asks() {}
|
||||
|
||||
} // namespace md
|
||||
} // namespace bts
|
||||
#include "MDP3Listener.h"
|
||||
#include "spark/config/protos/market_data.pb.h"
|
||||
#include "spark/domain/Book.h"
|
||||
#include "spark/domain/Security.h"
|
||||
#include "spark/domain/TradeRecord.h"
|
||||
#include "spark/md/protos/co2.pb.h"
|
||||
#include "spark/utils/datetime.h"
|
||||
#include "spark/utils/helper_macros.h"
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
using namespace bts::co2;
|
||||
|
||||
namespace bts {
|
||||
namespace md {
|
||||
|
||||
namespace {
|
||||
|
||||
domain::AggressorSide to_domain_aggressor(onixs::AggressorSide::Enum agside) {
|
||||
switch (agside) {
|
||||
case onixs::AggressorSide::Buy:
|
||||
return domain::AggressorSide::Buy;
|
||||
case onixs::AggressorSide::Sell:
|
||||
return domain::AggressorSide::Sell;
|
||||
default:
|
||||
return domain::AggressorSide::None;
|
||||
}
|
||||
}
|
||||
|
||||
domain::Price to_domain_price(const PriceSize &ps) {
|
||||
if (ps.is_nan()) {
|
||||
return domain::Price::nan();
|
||||
}
|
||||
return domain::Price::from_double(ps.price());
|
||||
}
|
||||
|
||||
domain::BookLevel to_domain_book_level(const PriceSize &ps) {
|
||||
if (ps.is_nan() || ps.size() == 0) {
|
||||
return domain::BookLevel::empty();
|
||||
}
|
||||
domain::BookLevel level;
|
||||
level.price = to_domain_price(ps);
|
||||
level.total_quantity = domain::Quantity(ps.size());
|
||||
level.direct_quantity = domain::Quantity(ps.direct_size());
|
||||
return level;
|
||||
}
|
||||
|
||||
void copy_book_side(
|
||||
const std::array<PriceSize, COCONUT_MAX_DEPTH> &from,
|
||||
domain::BookSide &to) {
|
||||
for (size_t i = 0; i < std::min(from.size(), to.size()); ++i) {
|
||||
to[i] = to_domain_book_level(from[i]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MDP3Listener::MDP3Listener(
|
||||
CMESecurityIndex &index,
|
||||
MDProducer &producer,
|
||||
MBOWriter *mbo,
|
||||
const std::vector<MDProducerChannel> channels,
|
||||
TradeSummarySignalDispatcher *trade_summary_dispatcher,
|
||||
std::shared_ptr<IMarketDataListener> market_data_listener)
|
||||
: _index(index), _producer(producer),
|
||||
_verbose_ticker(config::get<CMEOptions>().verbose_ticker()),
|
||||
_packet_arrived(), _latest_sequence(0),
|
||||
// _settlement_recorder(settles),
|
||||
_symbol_filter_storage(), _symbol_filter(), _channels(channels),
|
||||
_concurrency_monitor(0), _book_updates(), _mbo(mbo),
|
||||
_trade_summary_dispatcher(trade_summary_dispatcher),
|
||||
_market_data_listener(std::move(market_data_listener)) {
|
||||
_book_updates.reserve(32);
|
||||
}
|
||||
|
||||
MDP3Listener::~MDP3Listener() {}
|
||||
|
||||
//
|
||||
// Warnings/Errors
|
||||
void MDP3Listener::onError(onixs::Handler &, const onixs::ErrorArgs &error) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onError", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Warn, "OnixS Error: {} - {}", error.description(), error.source());
|
||||
}
|
||||
void MDP3Listener::onWarning(
|
||||
onixs::Handler &, const onixs::WarningArgs &warning) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onWarning", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Warn, "OnixS Warn: {} - {}", warning.description(),
|
||||
warning.source());
|
||||
}
|
||||
void MDP3Listener::onInstrumentRecovery(onixs::Handler &h) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onInstrumentRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Info, "Beginning security definition recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onEndOfInstrumentRecovery(
|
||||
onixs::Handler &h, const onixs::RecoveryCompletionArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfInstrumentRecovery",
|
||||
_latest_sequence, (uint16_t)_latest_sequence, 0);
|
||||
LOG(md, Info, "Finished Security definition recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
|
||||
//
|
||||
// Gap Recovery
|
||||
void MDP3Listener::onMarketRecovery(onixs::Handler &h) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onMarketRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_PACKET_LOSS);
|
||||
for (auto &c : _channels) {
|
||||
c.gap();
|
||||
}
|
||||
LOG(md, Info, "Beginning market recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onEndOfMarketRecovery(
|
||||
onixs::Handler &h, const onixs::RecoveryCompletionArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfMarketRecovery",
|
||||
_latest_sequence, (uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_OK);
|
||||
LOG(md, Info, "Finished market recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onTcpRecovery(onixs::Handler &h) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onTcpRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_PACKET_LOSS);
|
||||
LOG(md, Info, "Beginning tcp recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
void MDP3Listener::onEndOfTcpRecovery(
|
||||
onixs::Handler &h, const onixs::RecoveryCompletionArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfTcpRecovery", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
update_all_status(COCONUTSECURITYSTATUS_OK);
|
||||
LOG(md, Info, "Finished tcp recovery for channel {}",
|
||||
h.settings().channel());
|
||||
}
|
||||
|
||||
//
|
||||
// Mark the start/end of a packet
|
||||
void MDP3Listener::onPacket(onixs::Handler &, const onixs::PacketArgs &args) {
|
||||
// Mark the arrival of the packet
|
||||
_packet_arrived = TimeStamp::now();
|
||||
auto seq = args.seqNumber();
|
||||
if (!_book_updates.empty()) {
|
||||
if (_latest_sequence != seq) {
|
||||
LOG(md, Error,
|
||||
"Expected sequence {} got {} did not flush {} book updates.",
|
||||
_latest_sequence, seq, _book_updates.size());
|
||||
}
|
||||
}
|
||||
_latest_sequence = seq;
|
||||
auto onixs_zero = args.receiveTime().sinceEpoch();
|
||||
auto onixs_now = OnixS::CME::MDH::UtcWatch::now().sinceEpoch();
|
||||
|
||||
// needed for clang compilation
|
||||
UNUSED(onixs_zero);
|
||||
UNUSED(onixs_now);
|
||||
// was needed for clang compilation
|
||||
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onPacket", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, onixs_now - onixs_zero);
|
||||
}
|
||||
void MDP3Listener::onEndOfPacket(onixs::Handler &, const onixs::PacketArgs &) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfPacket.begin", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
flush_books();
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onEndOfPacket.end", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
}
|
||||
|
||||
//
|
||||
// Book updates
|
||||
|
||||
void MDP3Listener::onBookUpdate(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::DirectBook &book) {
|
||||
onBookUpdate<COCONUT_MAX_DEPTH>(sec, book, book_type_direct);
|
||||
}
|
||||
|
||||
void MDP3Listener::onBookUpdate(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::ImpliedBook &book) {
|
||||
onBookUpdate<COCONUT_MAX_IMPLIED_DEPTH>(sec, book, book_type_implied);
|
||||
}
|
||||
|
||||
void MDP3Listener::onBookUpdate(
|
||||
onixs::Handler &, const onixs::Security &, const onixs::MboBook &) {
|
||||
#if 0
|
||||
if (_mbo) {
|
||||
auto data = _index.get_security(sec.id());
|
||||
if (data == nullptr) return;
|
||||
auto key = data->handle.key();
|
||||
if (key == invalid_security_key) return;
|
||||
auto* snapshot = _mbo->allocate_snapshot();
|
||||
// copy from onixs book
|
||||
for (const auto& bid : book.bids()) {
|
||||
snapshot->bids.push_back({ ExchangeOrderKey(bid.id()), convert_price(bid.price(), 0), bid.quantity() });
|
||||
}
|
||||
for (const auto& ask : book.offers()) {
|
||||
snapshot->asks.push_back({ ExchangeOrderKey(ask.id()), convert_price(ask.price(),0), ask.quantity() });
|
||||
}
|
||||
_mbo->validate_snapshot(sec.id(), SecurityKey(key), snapshot);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
//
|
||||
// Trades
|
||||
// Called from onTradeSummary and onTradeRecovery, ends up calling
|
||||
// MDProducerHandle::add_trade
|
||||
void on_trade(
|
||||
SecurityData *data,
|
||||
const onixs::Decimal &price,
|
||||
int size,
|
||||
onixs::AggressorSide::Enum agside,
|
||||
const onixs::Timestamp &onixs_date,
|
||||
IMarketDataListener *market_data_listener) {
|
||||
auto ts = utils::TimeStamp::now();
|
||||
PriceSize ps = pricesize_of_price_size(
|
||||
price, data->handle.security().display_factor, size);
|
||||
if (market_data_listener != nullptr) {
|
||||
domain::TradeRecord trade;
|
||||
trade.sec_id = domain::SecurityID(data->handle.key());
|
||||
trade.time_stamp = core::TimeStamp::now();
|
||||
trade.agg_side = to_domain_aggressor(agside);
|
||||
trade.trade_quantity = domain::Quantity(size);
|
||||
trade.trade_price = to_domain_price(ps);
|
||||
std::vector<domain::TradeRecord const *> trades{&trade};
|
||||
// GMDL!!
|
||||
market_data_listener->on_trades(trades);
|
||||
return;
|
||||
}
|
||||
auto date = ll_of_timestamp(onixs_date);
|
||||
data->handle.add_trade(
|
||||
ts, ps, date, map_aggressor(agside), data->electronic_volume + size);
|
||||
}
|
||||
|
||||
//
|
||||
// RFQ
|
||||
void MDP3Listener::onQuoteRequest(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::QuoteRequest39RelatedSymArgs &arg) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onQuoteRequest", _latest_sequence,
|
||||
(uint16_t)_latest_sequence, 0);
|
||||
auto *data = _index.get_security(sec.id());
|
||||
if (data != nullptr) {
|
||||
auto ts = utils::TimeStamp::now();
|
||||
|
||||
co2::QuoteRequestMessage message;
|
||||
message.set_key(data->handle.key());
|
||||
|
||||
auto time = ll_of_timestamp(arg.message().transactTime());
|
||||
message.set_time(time);
|
||||
|
||||
onixs::StrRef id = arg.message().quoteReqId();
|
||||
message.set_request_id(id.items(), id.size());
|
||||
|
||||
// https://www.cmegroup.com/confluence/display/EPICSANDBOX/MDP+3.0+-+Request+for+Quote
|
||||
int8_t side;
|
||||
if (arg.entry().side(side)) {
|
||||
switch (side) {
|
||||
case 1: // buy
|
||||
case '1':
|
||||
message.set_side(co2::QuoteRequestMessage::BUY);
|
||||
break;
|
||||
case 2: // sell
|
||||
case '2':
|
||||
message.set_side(co2::QuoteRequestMessage::SELL);
|
||||
break;
|
||||
case 8: // cross
|
||||
case '8':
|
||||
message.set_side(co2::QuoteRequestMessage::CROSS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
message.set_tradable(arg.entry().quoteType() == 1);
|
||||
|
||||
int32_t size;
|
||||
if (arg.entry().orderQty(size)) {
|
||||
message.set_size(size);
|
||||
}
|
||||
|
||||
data->handle.add_rfq(ts, message);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Volume
|
||||
void MDP3Listener::onElectronicVolume(
|
||||
onixs::Handler &,
|
||||
const onixs::Security &sec,
|
||||
const onixs::Volume37Args &args) {
|
||||
tracepoint(
|
||||
bts_trace, trace_order, (char *)"onElectronicVolume.volume",
|
||||
_latest_sequence, (uint16_t)_latest_sequence, 0);
|
||||
auto *data = _index.get_security(sec.id());
|
||||
if (data != nullptr) {
|
||||
bts_assert_d(args.entry().entryType().value() == 'e');
|
||||
bts_assert_d(
|
||||
args.entry().updateAction() == onixs::UpdateAction::New ||
|
||||
args.entry().updateAction() == onixs::UpdateAction::Change);
|
||||
data->electronic_volume = args.entry().entrySize();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Non-onixs calls
|
||||
|
||||
domain::Book MDP3Listener::to_domain_book(const BookData &entry) const {
|
||||
domain::Book book = domain::Book::empty(domain::SecurityID(entry.handle.key()));
|
||||
copy_book_side(entry.bids.first, book.bids());
|
||||
copy_book_side(entry.asks.first, book.asks());
|
||||
return book;
|
||||
}
|
||||
|
||||
void MDP3Listener::add_filter_symbol(const std::string &symbol) {
|
||||
LOG(md, Info, "MDP3Listener::add_filter_symbol symbol: {}", symbol);
|
||||
_symbol_filter_storage.push_back(symbol);
|
||||
_symbol_filter.clear();
|
||||
for (const auto &str : _symbol_filter_storage) {
|
||||
_symbol_filter.insert(onixs::StrRef(str.c_str(), str.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void MDP3Listener::heartbeat() {
|
||||
for (auto &c : _channels) {
|
||||
c.heartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
// Called by MDP3Main::prepare_listener as part of pre-load. Also called by
|
||||
// MDP3Listener::onFuture, onOption and onSpread
|
||||
void MDP3Listener::add_security(
|
||||
const FixedSecurity &security, co2::TradingStatus status) {
|
||||
LOG(md, Info, "MDP3Listener::add_security id: {}", security.numeric_id);
|
||||
bts_assert(security.numeric_id != 0);
|
||||
// Set up the shared memory for this security
|
||||
auto handle = _producer.add_security(security);
|
||||
// Add it to the index so it can receive messages
|
||||
_index.add_security(security.numeric_id, handle);
|
||||
|
||||
if (_market_data_listener != nullptr) {
|
||||
domain::Security domain_security(convert_security(security));
|
||||
std::vector<domain::Security const *> securities{&domain_security};
|
||||
// GMDL!!
|
||||
_market_data_listener->on_securities(securities);
|
||||
}
|
||||
|
||||
handle.update_exchange_status(utils::TimeStamp::now(), status);
|
||||
}
|
||||
|
||||
// Private functions
|
||||
|
||||
void MDP3Listener::flush_books() {
|
||||
// This is called at the end of the packet
|
||||
// Write all book updates to shared memory and reset for the next packet
|
||||
for (auto &entry : _book_updates) {
|
||||
uint32_t sequence = _latest_sequence;
|
||||
if (_market_data_listener != nullptr) {
|
||||
if (entry.type_flags & book_type_direct) {
|
||||
// GMDL!!
|
||||
_market_data_listener->on_book(to_domain_book(entry), _packet_arrived);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry.type_flags == book_type_direct) {
|
||||
entry.handle.update_direct(
|
||||
_packet_arrived, entry.bids.first, entry.asks.first, sequence);
|
||||
} else if (entry.type_flags == book_type_implied) {
|
||||
entry.handle.update_implied(
|
||||
_packet_arrived, entry.bids.second, entry.asks.second, sequence);
|
||||
} else if (entry.type_flags == book_type_both) {
|
||||
entry.handle.update_books(
|
||||
_packet_arrived, entry.bids.first, entry.asks.first,
|
||||
entry.bids.second, entry.asks.second, sequence);
|
||||
}
|
||||
}
|
||||
// Reset pending updates
|
||||
_book_updates.clear();
|
||||
}
|
||||
|
||||
void MDP3Listener::update_all_status(CoconutSecurityStatus status) {
|
||||
_index.walk_security_list(
|
||||
[=](SecurityData &data) { data.handle.set_status(status); });
|
||||
}
|
||||
|
||||
void MDP3Listener::update_group_exchange_status(
|
||||
const onixs::StrRef &group, co2::TradingStatus status) {
|
||||
_index.walk_security_list([=](SecurityData &data) {
|
||||
if (data.handle.security().alt_symbol() == group) {
|
||||
data.handle.update_exchange_status(utils::TimeStamp::now(), status);
|
||||
}
|
||||
});
|
||||
}
|
||||
void MDP3Listener::update_asset_exchange_status(
|
||||
const onixs::StrRef &asset, co2::TradingStatus status) {
|
||||
_index.walk_security_list([=](SecurityData &data) {
|
||||
if (data.handle.security().symbol() == asset) {
|
||||
data.handle.update_exchange_status(utils::TimeStamp::now(), status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
md::MDP3Listener::BookData::BookData(MDProducerHandle &handle)
|
||||
: handle(handle), type_flags(0), bids(), asks() {}
|
||||
|
||||
} // namespace md
|
||||
} // namespace bts
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
||||
#include "MDP3Main.h"
|
||||
#include "ScriptedMdp3Feeder.h"
|
||||
#include "spark/config/config.h"
|
||||
#include "spark/config/protos/market_data.pb.h"
|
||||
#include "spark/config/protos/system_config.pb.h"
|
||||
@@ -16,6 +17,8 @@
|
||||
#include "sparkd/md/onixs/OnixsUtils.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
@@ -40,6 +43,8 @@ struct MDP3Main::PImpl {
|
||||
std::map<std::string, std::unique_ptr<onixs::Handler>> handlers;
|
||||
std::vector<std::unique_ptr<MDP3Listener>> listeners;
|
||||
std::vector<std::unique_ptr<onixs::TcpRecoveryService>> recovery;
|
||||
std::atomic<bool> scripted_running{false};
|
||||
std::thread scripted_thread;
|
||||
PImpl()
|
||||
: index(), recorder(
|
||||
nullptr,
|
||||
@@ -53,7 +58,12 @@ struct MDP3Main::PImpl {
|
||||
MDP3Main::MDP3Main()
|
||||
: me_(std::make_unique<PImpl>()) {}
|
||||
|
||||
MDP3Main::~MDP3Main() = default;
|
||||
MDP3Main::~MDP3Main() {
|
||||
me_->scripted_running = false;
|
||||
if (me_->scripted_thread.joinable()) {
|
||||
me_->scripted_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
const CmeChannelConfig channel_settings_for(const string &id) {
|
||||
const auto &config = config::get<CMEOptions>();
|
||||
@@ -178,7 +188,8 @@ onixs::SessionSettings session_settings_for_channel(
|
||||
void MDP3Main::start(
|
||||
const std::vector<UserSymbol> &symbols,
|
||||
MBOWriter *mbo,
|
||||
TradeSummarySignalDispatcher *tssd) {
|
||||
TradeSummarySignalDispatcher *tssd,
|
||||
std::shared_ptr<IMarketDataListener> market_data_listener) {
|
||||
std::ostringstream oss;
|
||||
for (const auto &us : symbols) {
|
||||
oss << us << " ";
|
||||
@@ -191,7 +202,8 @@ void MDP3Main::start(
|
||||
me_->engines = create_feed_engines(channels);
|
||||
for (const auto &channel : channels) {
|
||||
unique_ptr<onixs::Handler> phandler = prepare_handler(channel.id, mbo);
|
||||
me_->listeners.push_back(prepare_listener(*phandler, channel, mbo, tssd));
|
||||
me_->listeners.push_back(prepare_listener(
|
||||
*phandler, channel, mbo, tssd, market_data_listener));
|
||||
me_->handlers.insert(std::make_pair(channel.id, std::move(phandler)));
|
||||
std::stringstream symstr;
|
||||
for (const auto &sym : channel.symbols) {
|
||||
@@ -210,6 +222,98 @@ void MDP3Main::start(
|
||||
LOG(md, Info, "** CME MDP3 Publisher started **");
|
||||
}
|
||||
|
||||
void MDP3Main::start_scripted(
|
||||
const std::vector<UserSymbol> &symbols,
|
||||
MBOWriter *mbo,
|
||||
TradeSummarySignalDispatcher *tssd,
|
||||
std::shared_ptr<IMarketDataListener> market_data_listener) {
|
||||
std::ostringstream oss;
|
||||
for (const auto &us : symbols) {
|
||||
oss << us << " ";
|
||||
}
|
||||
LOG(md, Info, "MDP3Main::start_scripted symbols: {}", oss.str());
|
||||
|
||||
const auto &channels = parse_channels(symbols);
|
||||
if (channels.empty()) {
|
||||
LOG(md, Warn, "MDP3Main::start_scripted found no CME channels");
|
||||
return;
|
||||
}
|
||||
|
||||
auto channel_it = std::find_if(
|
||||
channels.begin(), channels.end(), [](const CMEChannel &channel) {
|
||||
return std::any_of(
|
||||
channel.symbols.begin(), channel.symbols.end(),
|
||||
[](const UserSymbol &symbol) {
|
||||
return symbol.symbol() == "ES" &&
|
||||
symbol.type() == api::SECURITYTYPE_FUTURE;
|
||||
});
|
||||
});
|
||||
if (channel_it == channels.end()) {
|
||||
channel_it = channels.begin();
|
||||
LOG(md, Warn,
|
||||
"MDP3Main::start_scripted did not find an ES future channel; using channel {}",
|
||||
channel_it->id);
|
||||
}
|
||||
|
||||
const auto &channel = *channel_it;
|
||||
std::vector<MDProducerChannel> producer_channels;
|
||||
for (const auto &symbol : channel.symbols) {
|
||||
producer_channels.push_back(me_->producer.get_channel(
|
||||
symbol.symbol().c_str(), symbol.type(), symbol.exchange()));
|
||||
}
|
||||
|
||||
auto listener = std::make_unique<MDP3Listener>(
|
||||
me_->index, me_->producer, mbo, producer_channels, tssd,
|
||||
market_data_listener);
|
||||
for (const auto &symbol : channel.symbols) {
|
||||
listener->add_filter_symbol(symbol.symbol());
|
||||
}
|
||||
|
||||
ScriptedMdp3Feeder feeder(*listener);
|
||||
feeder.publish_hard_coded_values();
|
||||
|
||||
me_->scripted_running = false;
|
||||
if (me_->scripted_thread.joinable()) {
|
||||
me_->scripted_thread.join();
|
||||
}
|
||||
me_->scripted_running = true;
|
||||
auto *listener_ptr = listener.get();
|
||||
me_->scripted_thread = std::thread([this, listener_ptr]() {
|
||||
ScriptedMdp3Feeder loop_feeder(*listener_ptr);
|
||||
std::uint32_t sequence = 2;
|
||||
int tick_offset = 0;
|
||||
int tick_direction = -1;
|
||||
while (me_->scripted_running.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
if (!me_->scripted_running.load()) {
|
||||
break;
|
||||
}
|
||||
|
||||
tick_offset += tick_direction;
|
||||
if (tick_offset <= -10) {
|
||||
tick_offset = -10;
|
||||
tick_direction = 1;
|
||||
} else if (tick_offset >= 0) {
|
||||
tick_offset = 0;
|
||||
tick_direction = -1;
|
||||
}
|
||||
|
||||
loop_feeder.publish_hard_coded_direct_book(
|
||||
sequence++, 0.25 * tick_offset);
|
||||
}
|
||||
});
|
||||
|
||||
std::stringstream symstr;
|
||||
for (const auto &sym : channel.symbols) {
|
||||
symstr << sym.symbol() << "-" << SecurityType_Name(sym.type()) << " ";
|
||||
}
|
||||
LOG(md, Info, "CME: Initialized scripted channel {} ({})", channel.id,
|
||||
symstr.str());
|
||||
me_->listeners.push_back(std::move(listener));
|
||||
|
||||
LOG(md, Info, "** CME MDP3 Scripted Publisher started **");
|
||||
}
|
||||
|
||||
void MDP3Main::send_heartbeats() {
|
||||
for (const auto &listener : me_->listeners) {
|
||||
listener->heartbeat();
|
||||
@@ -386,7 +490,8 @@ unique_ptr<MDP3Listener> MDP3Main::prepare_listener(
|
||||
onixs::Handler &handler,
|
||||
const CMEChannel &channel,
|
||||
MBOWriter *mbo,
|
||||
TradeSummarySignalDispatcher *trade_summary_dispatcher) {
|
||||
TradeSummarySignalDispatcher *trade_summary_dispatcher,
|
||||
std::shared_ptr<IMarketDataListener> market_data_listener) {
|
||||
LOG(md, Info, "MDP3Main::prepare_listener channel: {}", channel);
|
||||
// We need to get a handler to the producer channels so we can produce
|
||||
// heartbeats
|
||||
@@ -398,7 +503,7 @@ unique_ptr<MDP3Listener> MDP3Main::prepare_listener(
|
||||
|
||||
unique_ptr<MDP3Listener> listener(new MDP3Listener(
|
||||
me_->index, me_->producer, mbo, producer_channels,
|
||||
trade_summary_dispatcher));
|
||||
trade_summary_dispatcher, std::move(market_data_listener)));
|
||||
for (auto &symbol : channel.symbols) {
|
||||
listener->add_filter_symbol(symbol.symbol());
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
#include "CMESecurityIndex.h"
|
||||
#include "MDP3Listener.h"
|
||||
#include "spark/md/IMarketDataListener.h"
|
||||
#include "spark/md/coconut2.h"
|
||||
#include "spark/utils/bts_no_copy.h"
|
||||
#include "onixs.h"
|
||||
#include "sparkd/md/platforms/IMDPlatform.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace bts {
|
||||
@@ -37,7 +40,13 @@ class MDP3Main : public IMDPlatform {
|
||||
void start(
|
||||
const std::vector<UserSymbol> &symbols,
|
||||
MBOWriter *mbo,
|
||||
TradeSummarySignalDispatcher *tssd);
|
||||
TradeSummarySignalDispatcher *tssd,
|
||||
std::shared_ptr<IMarketDataListener> market_data_listener = nullptr);
|
||||
void start_scripted(
|
||||
const std::vector<UserSymbol> &symbols,
|
||||
MBOWriter *mbo,
|
||||
TradeSummarySignalDispatcher *tssd,
|
||||
std::shared_ptr<IMarketDataListener> market_data_listener = nullptr);
|
||||
void send_heartbeats() override;
|
||||
|
||||
void initialize(const std::string &license_store);
|
||||
@@ -47,7 +56,8 @@ class MDP3Main : public IMDPlatform {
|
||||
onixs::Handler &handler,
|
||||
const CMEChannel &channel,
|
||||
MBOWriter *,
|
||||
TradeSummarySignalDispatcher *);
|
||||
TradeSummarySignalDispatcher *,
|
||||
std::shared_ptr<IMarketDataListener> market_data_listener = nullptr);
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "ScriptedMdp3Feeder.h"
|
||||
|
||||
#include "spark/api/domain/Venue.pb.h"
|
||||
#include "spark/api/domain/SecurityType.pb.h"
|
||||
#include "spark/md/IntPrice.h"
|
||||
#include "spark/md/common.h"
|
||||
|
||||
#include <OnixS/CME/MDH/DirectBook.h>
|
||||
#include <OnixS/CME/MDH/Handler.h>
|
||||
#include <OnixS/CME/MDH/HandlerSettings.h>
|
||||
#include <OnixS/CME/MDH/Packet.h>
|
||||
#include <OnixS/CME/MDH/Security.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
namespace bts::md {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t scripted_exchange_security_id = 1000001;
|
||||
|
||||
class ScriptedSecurity : public onixs::Security {
|
||||
public:
|
||||
explicit ScriptedSecurity(uint32_t id) : onixs::Security(id) {}
|
||||
|
||||
using onixs::Security::asset;
|
||||
using onixs::Security::group;
|
||||
using onixs::Security::symbol;
|
||||
|
||||
void sequence(std::uint32_t value) { seqNumber(value); }
|
||||
};
|
||||
|
||||
struct ScriptedPacketHeader {
|
||||
onixs::SequenceNumber seqNumber = 0;
|
||||
onixs::Timestamp sendingTime;
|
||||
};
|
||||
|
||||
PriceSize make_level(double price, float size) {
|
||||
if (size == 0) {
|
||||
return PriceSize();
|
||||
}
|
||||
return PriceSize(IntPrice_from_double<7>(price), size);
|
||||
}
|
||||
|
||||
onixs::Decimal to_onixs_decimal(const PriceSize &level) {
|
||||
// MDP3Listener applies the security display_factor when converting OnixS
|
||||
// prices, so the scripted price must be expressed in pre-display-factor
|
||||
// exchange units here. For ES with display_factor=-2, 7000.00 becomes
|
||||
// mantissa 700000 with exponent 0.
|
||||
return onixs::Decimal(
|
||||
static_cast<onixs::Decimal::Mantissa>(std::llround(level.price() * 100.0)),
|
||||
0);
|
||||
}
|
||||
|
||||
using DirectBookPtr = std::unique_ptr<onixs::DirectBook, void (*)(onixs::DirectBook *)>;
|
||||
|
||||
DirectBookPtr make_direct_book(const ScriptedMdp3Feeder::DirectBookUpdate &update) {
|
||||
DirectBookPtr book(
|
||||
onixs::DirectBookFactory::construct(COCONUT_MAX_DEPTH),
|
||||
&onixs::DirectBookFactory::destruct);
|
||||
std::size_t bid_depth = 0;
|
||||
std::size_t ask_depth = 0;
|
||||
while (bid_depth < update.bids.size() && !update.bids[bid_depth].is_nan() &&
|
||||
update.bids[bid_depth].size() != 0) {
|
||||
++bid_depth;
|
||||
}
|
||||
while (ask_depth < update.asks.size() && !update.asks[ask_depth].is_nan() &&
|
||||
update.asks[ask_depth].size() != 0) {
|
||||
++ask_depth;
|
||||
}
|
||||
book->depth(static_cast<onixs::MbpBookDepth>(std::max(bid_depth, ask_depth)));
|
||||
book->bids().resize(static_cast<onixs::MbpBookDepth>(bid_depth));
|
||||
book->offers().resize(static_cast<onixs::MbpBookDepth>(ask_depth));
|
||||
for (std::size_t i = 0; i < bid_depth; ++i) {
|
||||
book->bids()[i] = onixs::DirectPriceLevel(
|
||||
to_onixs_decimal(update.bids[i]),
|
||||
static_cast<int>(update.bids[i].size()),
|
||||
1);
|
||||
}
|
||||
for (std::size_t i = 0; i < ask_depth; ++i) {
|
||||
book->offers()[i] = onixs::DirectPriceLevel(
|
||||
to_onixs_decimal(update.asks[i]),
|
||||
static_cast<int>(update.asks[i].size()),
|
||||
1);
|
||||
}
|
||||
return book;
|
||||
}
|
||||
|
||||
onixs::Handler &scripted_handler() {
|
||||
static onixs::HandlerSettings settings;
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
settings.channel(1);
|
||||
initialized = true;
|
||||
}
|
||||
static onixs::Handler handler(settings);
|
||||
return handler;
|
||||
}
|
||||
|
||||
ScriptedSecurity &scripted_security() {
|
||||
static ScriptedSecurity security(scripted_exchange_security_id);
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
security.symbol(onixs::StrRef("ESM6", 4));
|
||||
security.group(onixs::StrRef("ES", 2));
|
||||
security.asset(onixs::StrRef("ES", 2));
|
||||
initialized = true;
|
||||
}
|
||||
return security;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ScriptedMdp3Feeder::ScriptedMdp3Feeder(MDP3Listener &listener)
|
||||
: _listener(listener) {}
|
||||
|
||||
void ScriptedMdp3Feeder::publish_hard_coded_future() const {
|
||||
FixedFutureSecurity security;
|
||||
fill_hard_coded_future(security);
|
||||
_listener.add_security(security, co2::TRADINGSTATUS_NORMAL);
|
||||
}
|
||||
|
||||
void ScriptedMdp3Feeder::publish_hard_coded_direct_book() const {
|
||||
publish_hard_coded_direct_book(1, 0.0);
|
||||
}
|
||||
|
||||
void ScriptedMdp3Feeder::publish_hard_coded_direct_book(
|
||||
std::uint32_t sequence, double mid_price_shift) const {
|
||||
const auto update = hard_coded_direct_book(sequence, mid_price_shift);
|
||||
auto book = make_direct_book(update);
|
||||
auto &handler = scripted_handler();
|
||||
auto &security = scripted_security();
|
||||
security.sequence(sequence);
|
||||
|
||||
ScriptedPacketHeader packet_header;
|
||||
packet_header.seqNumber = sequence;
|
||||
onixs::PacketArgs packet(&packet_header, sizeof(packet_header));
|
||||
packet.receiveTime(onixs::Timestamp(update.packet_time.to_uint64()));
|
||||
|
||||
_listener.onPacket(handler, packet);
|
||||
_listener.onBookUpdate(handler, security, *book);
|
||||
_listener.onEndOfPacket(handler, packet);
|
||||
}
|
||||
|
||||
void ScriptedMdp3Feeder::publish_hard_coded_values() const {
|
||||
publish_hard_coded_future();
|
||||
publish_hard_coded_direct_book();
|
||||
}
|
||||
|
||||
void ScriptedMdp3Feeder::fill_hard_coded_future(FixedFutureSecurity &security) {
|
||||
security.key = invalid_security_key;
|
||||
security.numeric_id = scripted_exchange_security_id;
|
||||
security.type = api::SECURITYTYPE_FUTURE;
|
||||
security.exchange = api::Venue::VENUE_GLBX;
|
||||
security.symbol("ES");
|
||||
security.alt_symbol("ES");
|
||||
security.security_desc("ESM6");
|
||||
security.currency("USD");
|
||||
security.display_factor = -2;
|
||||
security.low_tick = 25.0;
|
||||
security.high_tick = 25.0;
|
||||
security.price_break = 0.0;
|
||||
security.cash_per_point = 50.0;
|
||||
security.expiration_year = 2026;
|
||||
security.expiration_month = 6;
|
||||
security.expiration_day = 19;
|
||||
}
|
||||
|
||||
ScriptedMdp3Feeder::DirectBookUpdate
|
||||
ScriptedMdp3Feeder::hard_coded_direct_book(
|
||||
std::uint32_t sequence, double mid_price_shift) {
|
||||
DirectBookUpdate update;
|
||||
const double best_bid = 7000.0 + mid_price_shift;
|
||||
const double best_ask = 7001.0 + mid_price_shift;
|
||||
update.exchange_security_id = scripted_exchange_security_id;
|
||||
update.bids[0] = make_level(best_bid, 10);
|
||||
update.bids[1] = make_level(best_bid - 0.25, 10);
|
||||
update.bids[2] = make_level(best_bid - 0.50, 10);
|
||||
update.asks[0] = make_level(best_ask, 10);
|
||||
update.asks[1] = make_level(best_ask + 0.25, 10);
|
||||
update.asks[2] = make_level(best_ask + 0.50, 10);
|
||||
update.packet_time = utils::TimeStamp::now();
|
||||
update.sequence = sequence;
|
||||
return update;
|
||||
}
|
||||
|
||||
} // namespace bts::md
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "MDP3Listener.h"
|
||||
#include "spark/md/PriceSize.h"
|
||||
#include "spark/utils/TimeStamp.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
namespace bts::md {
|
||||
|
||||
class ScriptedMdp3Feeder {
|
||||
public:
|
||||
struct DirectBookUpdate {
|
||||
uint32_t exchange_security_id = 0;
|
||||
std::array<PriceSize, COCONUT_MAX_DEPTH> bids = {};
|
||||
std::array<PriceSize, COCONUT_MAX_DEPTH> asks = {};
|
||||
utils::TimeStamp packet_time = utils::TimeStamp::now();
|
||||
uint32_t sequence = 1;
|
||||
};
|
||||
|
||||
explicit ScriptedMdp3Feeder(MDP3Listener &listener);
|
||||
|
||||
void publish_hard_coded_future() const;
|
||||
void publish_hard_coded_direct_book() const;
|
||||
void publish_hard_coded_direct_book(std::uint32_t sequence,
|
||||
double mid_price_shift) const;
|
||||
void publish_hard_coded_values() const;
|
||||
|
||||
static void fill_hard_coded_future(FixedFutureSecurity &security);
|
||||
static DirectBookUpdate hard_coded_direct_book(
|
||||
std::uint32_t sequence = 1, double mid_price_shift = 0.0);
|
||||
|
||||
private:
|
||||
MDP3Listener &_listener;
|
||||
};
|
||||
|
||||
} // namespace bts::md
|
||||
@@ -160,7 +160,7 @@ namespace mbts.spark {
|
||||
|
||||
private bool _compute_midmarket {
|
||||
get {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@ using LadderSettings = mbts.spark.LadderSettings;
|
||||
namespace bts.shell {
|
||||
[Export(typeof(IShellDaemon))]
|
||||
class OrderTicketDaemon : IShellDaemon {
|
||||
static readonly bool useTicketLadder = true;
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
@@ -73,23 +71,13 @@ namespace bts.shell {
|
||||
|
||||
void _show_order_ticket(SecurityMD smd) {
|
||||
var settings_src = _settings_hub.subscribe_default_settings()
|
||||
.Select(s => s.GetLadderProfile(smd.security)?.value ?? LadderSettings.Default);
|
||||
.Select(s => s.get_ladder_profile(smd.security)?.value ?? LadderSettings.Default);
|
||||
var accounts_src = _bus.subscribe(SparkTags.AccountsTag);
|
||||
_show_order_window(useTicketLadder ? OrderTicketLadderWindow() : OrderTicketWindow());
|
||||
|
||||
Window OrderTicketWindow() =>
|
||||
new OrderTicket {
|
||||
DataContext = new NewOrderTicketVM(_model, smd, accounts_src, settings_src) {
|
||||
close_on_send = _close_on_send
|
||||
}
|
||||
};
|
||||
|
||||
Window OrderTicketLadderWindow() =>
|
||||
new OrderTicketLadder() {
|
||||
DataContext = new NewOrderTicketLadderVM(
|
||||
_model, smd, accounts_src, settings_src,
|
||||
ladderSecurity => _bus.post(SparkTags.ShowLadderTag, ladderSecurity))
|
||||
};
|
||||
_show_order_window(new OrderTicket {
|
||||
DataContext = new NewOrderTicketVM(_model, smd, accounts_src, settings_src) {
|
||||
close_on_send = _close_on_send
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _show_order_ticket(OrderTicketParams ot_params) {
|
||||
@@ -126,23 +114,13 @@ namespace bts.shell {
|
||||
return;
|
||||
}
|
||||
var md = _md_map.TryGetValue(ori.order.security_key());
|
||||
var accountsSrc = _bus.subscribe(SparkTags.AccountsTag);
|
||||
var orderSrc = Observable.Return((md, ori.order));
|
||||
var calcBnsSrc = Observable.Return(LadderSettings.Default.calculator_buttons);
|
||||
_show_order_window(useTicketLadder ? OrderTicketLadderWindow() : OrderTicketWindow());
|
||||
|
||||
Window OrderTicketWindow() =>
|
||||
new OrderModificationDialog {
|
||||
DataContext = new ExistingOrderTicketVM(_model, orderSrc, accountsSrc, calcBnsSrc, md?.quantity_display)
|
||||
};
|
||||
|
||||
Window OrderTicketLadderWindow() =>
|
||||
new OrderModificationDialog {
|
||||
DataContext = new ExistingOrderTicketLadderVM(
|
||||
_model, orderSrc, accountsSrc, calcBnsSrc,
|
||||
ladderSecurity => _bus.post(SparkTags.ShowLadderTag, ladderSecurity),
|
||||
md?.quantity_display)
|
||||
};
|
||||
_show_order_window(new OrderModificationDialog {
|
||||
DataContext = new ExistingOrderTicketVM(_model,
|
||||
Observable.Return((md, ori.order)),
|
||||
_bus.subscribe(SparkTags.AccountsTag),
|
||||
Observable.Return(LadderSettings.Default.calculator_buttons),
|
||||
md?.quantity_display)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,16 @@ namespace mbts.spark {
|
||||
/// <summary>
|
||||
/// Gets the ladder settings profile for a particular security
|
||||
/// </summary>
|
||||
public static LadderSettingsProfile? GetLadderProfile(
|
||||
public static LadderSettingsProfile? get_ladder_profile(
|
||||
this UserSettings settings, SecurityImmutable security) =>
|
||||
settings.ladder_profiles.FirstOrDefault(lsp =>
|
||||
lsp?.security_criteria?.includes(security) ?? false);
|
||||
|
||||
public static IObservable<LadderSettingsProfile?> subscribe_ladder_profile(
|
||||
this UserSettingsHub settings_hub, SecurityImmutable security) =>
|
||||
settings_hub.subscribe_default_settings().Select(s =>
|
||||
s.get_ladder_profile(security));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default account for a particular security (from the
|
||||
/// <see cref="LadderSettingsProfile"/> associated with that security)
|
||||
@@ -26,7 +31,7 @@ namespace mbts.spark {
|
||||
public static IObservable<AccountTuple?> subscribe_default_account(
|
||||
this UserSettingsHub settings_hub, IBus bus, SecurityImmutable security) {
|
||||
var acc_id_src = settings_hub.subscribe_default_settings().Select(s =>
|
||||
s.GetLadderProfile(security)?.value?.default_account);
|
||||
s.get_ladder_profile(security)?.value?.default_account);
|
||||
return acc_id_src.CombineLatest(
|
||||
bus.subscribe(SparkTags.AccountsTag),
|
||||
(acc_id, accs) => acc_id.HasValue && accs.TryGetValue(acc_id.Value, out var acc_tup) ?
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.shell;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -23,7 +23,6 @@ namespace mbts.ui.spark {
|
||||
/// </summary>
|
||||
partial class MarketViewView : UserControl {
|
||||
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
|
||||
static readonly bool useTicketLadder = true;
|
||||
|
||||
MarketViewVM _vm;
|
||||
DIYGridPopupManager _popup_man_backing;
|
||||
@@ -93,7 +92,7 @@ namespace mbts.ui.spark {
|
||||
|
||||
void _show_mini_orderbook(IReadOnlyList<SecurityImmutable> securities, Columns col) {
|
||||
var (vm, context, title, fg) = _vm.orderbook_params(securities, col);
|
||||
_show_popup(vm, new OrderAuxiliaryPopupHeader {
|
||||
_show_popup(vm, new AuxiliaryDataHeader {
|
||||
context = context,
|
||||
title = title,
|
||||
title_foreground = fg
|
||||
@@ -105,7 +104,7 @@ namespace mbts.ui.spark {
|
||||
|
||||
void _show_mini_fills(IEnumerable<SecurityImmutable> securities, Columns col) {
|
||||
var (vm, context, title, fg) = _vm.fills_params(securities, col);
|
||||
_show_popup(vm, new OrderAuxiliaryPopupHeader {
|
||||
_show_popup(vm, new AuxiliaryDataHeader {
|
||||
context = context,
|
||||
title = title,
|
||||
title_foreground = fg
|
||||
@@ -115,40 +114,29 @@ namespace mbts.ui.spark {
|
||||
async void _show_order_ticket(
|
||||
SecurityMD security_md, double price = double.NaN, double size = 0) {
|
||||
var resources = new OrderTicketResources();
|
||||
resources.MergedDictionaries.Add(new OrderAuxiliaryPopupResources());
|
||||
resources.MergedDictionaries.Add(new AuxiliaryDataResources());
|
||||
|
||||
var account = await _vm.get_default_account(security_md.security);
|
||||
var def = _model.factory.order(security_md.security, account);
|
||||
var calc_buttons = LadderSettings.Default.calculator_buttons;
|
||||
var accounts = _bus.subscribe(SparkTags.AccountsTag);
|
||||
var (vm, header) = OrderTicketFactory();
|
||||
var ticket_vm = new NewOrderTicketVM(
|
||||
_model,
|
||||
security_md,
|
||||
_bus.subscribe(SparkTags.AccountsTag),
|
||||
new(def),
|
||||
price,
|
||||
size,
|
||||
Observable.Return(calc_buttons));
|
||||
|
||||
_show_popup(
|
||||
vm,
|
||||
header,
|
||||
ticket_vm,
|
||||
new OrderTicketHeader { bbo = ticket_vm.bbo },
|
||||
resources,
|
||||
view => Behavior.SetAutoFocusTargetProvider(view, AutoFocusTargetProvider.TypeProvider(typeof(TextBox), view)));
|
||||
|
||||
(ViewModelBase, object) OrderTicketFactory() {
|
||||
if (useTicketLadder) {
|
||||
var vm = new NewOrderTicketLadderVM(
|
||||
_vm.orders_model, security_md, accounts, new(def), price, size,
|
||||
Observable.Return(calc_buttons),
|
||||
ladderSecurity => _bus.post(SparkTags.ShowLadderTag, ladderSecurity));
|
||||
var header = new OrderTicketLadderHeader() {
|
||||
Title = security_md.security.ToString()
|
||||
};
|
||||
return (vm, header);
|
||||
} else {
|
||||
var vm = new NewOrderTicketVM(_model, security_md, accounts, new(def), price, size, Observable.Return(calc_buttons));
|
||||
var header = new OrderTicketHeader { bbo = vm.bbo };
|
||||
return (vm, header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _show_popup(object content, object header) {
|
||||
var resources = new OrderAuxiliaryPopupResources();
|
||||
var resources = new AuxiliaryDataResources();
|
||||
resources.MergedDictionaries.Add(new AutoHideScrollbarResources());
|
||||
_show_popup(content, header, resources);
|
||||
}
|
||||
@@ -160,9 +148,9 @@ namespace mbts.ui.spark {
|
||||
Header = header,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Resources = resources ?? new OrderAuxiliaryPopupResources()
|
||||
Resources = resources ?? new AuxiliaryDataResources()
|
||||
};
|
||||
view.Style = view.Resources[OrderAuxiliaryPopupResources.PopupChildStyleKey] as Style;
|
||||
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
|
||||
init_view?.Invoke(view);
|
||||
_popup_man.show(view);
|
||||
}
|
||||
|
||||
@@ -78,9 +78,6 @@ namespace mbts.spark {
|
||||
public Fill.Types.FillStatus fill_status => _fill.FillStatus;
|
||||
public string fill_key => _fill.FillKey;
|
||||
public string order_key => _order.Key.ToString();
|
||||
// public bool is_aggressor => fill_tuple.is_aggressor;
|
||||
public string display_aggressor_flag_text =>
|
||||
display_aggressor_flag(_fill.FillAggressorFlag);
|
||||
public bool is_selected {
|
||||
get => _is_selected;
|
||||
set {
|
||||
@@ -108,13 +105,6 @@ namespace mbts.spark {
|
||||
AlgoData? _root_algo => fill_tuple.root_algo;
|
||||
AlgoDefinitionMetadata? _root_algo_meta => fill_tuple.root_algo_meta;
|
||||
|
||||
public static string display_aggressor_flag(FillAggressorFlag flag) =>
|
||||
flag switch {
|
||||
FillAggressorFlag.Aggressor => "A",
|
||||
FillAggressorFlag.Passive => "P",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
public static FillsRowVM update(FillsRowVM r, FillTuple ft) {
|
||||
using var _ = _change_detector.detect_changes(r);
|
||||
r._badge_fill = null;
|
||||
|
||||
@@ -254,7 +254,7 @@ namespace mbts.spark {
|
||||
add_col(
|
||||
Columns.Price,
|
||||
DefaultColumnHeaders.Price,
|
||||
r => _get_row(r).security.spec_or_default().format(r.fill.LastPrice),
|
||||
r => _get_row(r).security.spec_or_default().format(r.fill.AvgPrice),
|
||||
new DoubleMinMax<FillTuple>(r => r.fill.LastPrice),
|
||||
DataGridFilterKnownTemplates.DoubleMinMax,
|
||||
fs => fs.price,
|
||||
@@ -295,7 +295,7 @@ namespace mbts.spark {
|
||||
add_col(
|
||||
Columns.IsAggressor,
|
||||
DefaultColumnHeaders.IsAggressor,
|
||||
r => FillsRowVM.display_aggressor_flag(r.fill.FillAggressorFlag),
|
||||
r => r.is_aggressor ? SpecialCharacters.CheckMark : string.Empty,
|
||||
dv_filter(new[] { false, true }, r => r.is_aggressor),
|
||||
DataGridFilterKnownTemplates.YesNo,
|
||||
fs => fs.is_aggressor,
|
||||
|
||||
@@ -27,13 +27,13 @@ namespace Spark.Trading {
|
||||
_sec_greeks_in = sec_greeks_in;
|
||||
}
|
||||
|
||||
public IObservable<double> get_price_source(FairValueKey key, double? errorPrice = double.NaN) {
|
||||
public IObservable<double> get_price_source(FairValueKey key) {
|
||||
return key.type switch {
|
||||
FairValueLine.PriceType.Bid => bid_source(),
|
||||
FairValueLine.PriceType.Ask => ask_source(),
|
||||
FairValueLine.PriceType.Mid => mid_source(),
|
||||
FairValueLine.PriceType.Theo => theo_source(),
|
||||
_ => errorPrice.HasValue ? Observable.Return(errorPrice.Value) : throw new NotImplementedException()
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
IObservable<double> bid_source() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using bts;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.md.auxiliary;
|
||||
using bts.collections;
|
||||
@@ -15,6 +15,7 @@ using mbts.utils;
|
||||
|
||||
using Spark.Core.Collections;
|
||||
using Spark.Core.Disposables;
|
||||
using Spark.Core.Math;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -35,9 +36,8 @@ namespace mbts.spark {
|
||||
using RegionalTickFactors = user.settings.RegionalTickFactors;
|
||||
|
||||
partial class LadderGridHost : Control {
|
||||
const int _min_row_count = 501;
|
||||
const int _max_row_count = 2001;
|
||||
static readonly bool _show_scrollbar = true;
|
||||
static readonly int _initial_row_count;
|
||||
static readonly bool _show_scrollbar;
|
||||
|
||||
readonly VisualHost _visual_host = new();
|
||||
readonly LadderOrdersHost _ordershost;
|
||||
@@ -80,11 +80,13 @@ namespace mbts.spark {
|
||||
LadderVM _vm;
|
||||
LadderTactileAdorner _tactile_adorner;
|
||||
ulong _prices_updated;
|
||||
ulong lastTradeTime;
|
||||
NativePriceSize[] _bids = new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH], _asks = new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH];
|
||||
SecurityMDExtensions.Book _depth = SecurityMDExtensions.Book.Empty;
|
||||
bool _price_dirty = false, _piq_dirty = false;
|
||||
IList<double> _price_list;
|
||||
LadderPriceList _price_list = new() {
|
||||
length = _initial_row_count,
|
||||
};
|
||||
LadderPriceList _pending_price_list = null;
|
||||
IComparer<double> _price_comparer;
|
||||
double _last_origin_price;
|
||||
readonly MarketBreakLines _mkt_break_lines;
|
||||
@@ -105,6 +107,7 @@ namespace mbts.spark {
|
||||
static LadderGridHost() {
|
||||
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
|
||||
_show_scrollbar = knobs.ShowScrollbar;
|
||||
_initial_row_count = (int)knobs.ScrollableTickWindowSize;
|
||||
}
|
||||
|
||||
public LadderGridHost() {
|
||||
@@ -129,7 +132,33 @@ namespace mbts.spark {
|
||||
_render_grid_background();
|
||||
_init_tactile_adorner();
|
||||
}
|
||||
_scroll_to_center();
|
||||
IDisposable scroll_viewer_hook = null;
|
||||
loaded_hook = Disposable.Create(
|
||||
() => {
|
||||
_scroll_viewer_source.value_changed -= on_scroll_viewer;
|
||||
Disposable.dispose(ref scroll_viewer_hook);
|
||||
});
|
||||
on_scroll_viewer(_scroll_viewer);
|
||||
|
||||
void on_scroll_viewer(ScrollViewer new_value) {
|
||||
Disposable.dispose(ref scroll_viewer_hook);
|
||||
if (new_value is null) {
|
||||
return;
|
||||
}
|
||||
scroll_viewer_hook = _connect_scroll_viewer(new_value);
|
||||
delayed_init();
|
||||
// This is a bit of a hack: the scroll viewer drifts away from the intended VerticalOffset
|
||||
// unless it is done after a delay.
|
||||
async void delayed_init() {
|
||||
await Task.Delay(100);
|
||||
await Dispatcher.InvokeAsync(() => {
|
||||
_pending_scroll_to_origin = true;
|
||||
_refresh_prices();
|
||||
_refresh_all_visuals();
|
||||
scroll_to_pending_target();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
recenter_command = new DelegateCommand(_scroll_to_center);
|
||||
DataContextChanged += (s, e) => _connect_vm();
|
||||
@@ -253,6 +282,26 @@ namespace mbts.spark {
|
||||
set => SetValue(PIQColumnWidthProperty, value);
|
||||
}
|
||||
|
||||
#region TopTick
|
||||
public static readonly DependencyProperty TopTickProperty =
|
||||
WPFHelper.create_dp<int, LadderGridHost>(
|
||||
nameof(TopTick),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => {
|
||||
if (o._price_list is null) {
|
||||
return;
|
||||
}
|
||||
o._set_scrollbar_range(high_tick: v);
|
||||
o._refresh_prices();
|
||||
o._refresh_all_visuals();
|
||||
o._ordershost.refresh_orders();
|
||||
});
|
||||
|
||||
public int? TopTick {
|
||||
get => GetValue(TopTickProperty) as int?;
|
||||
set => SetValue(TopTickProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CanUserSendOrders
|
||||
public static readonly DependencyProperty CanUserSendOrdersProperty =
|
||||
@@ -343,7 +392,7 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
int _row_count = _min_row_count;
|
||||
int _row_count => _price_list?.Count ?? 0;
|
||||
|
||||
bts.api.domain.TickDisplaySettingsImmutable _qty_display =>
|
||||
_vm is null ?
|
||||
@@ -376,19 +425,66 @@ namespace mbts.spark {
|
||||
scroll_to_price(_mkt_break_lines.origin);
|
||||
|
||||
public bool scroll_to_price(double price) {
|
||||
if (_scroll_viewer == null || _price_list.is_null_or_empty())
|
||||
if (price.is_nan()) {
|
||||
return false;
|
||||
if (_spec.tickdiff(_price_list[0], price) is long target_row) {
|
||||
int visible_row_count = (int)(_scroll_viewer.ActualHeight / _settings.row_height);
|
||||
int curr_row = visible_row_count + (int)(_scroll_viewer.VerticalOffset / _settings.row_height);
|
||||
if ((target_row > curr_row && target_row < _row_count) || (target_row < (curr_row - visible_row_count) && target_row >= 0)) {
|
||||
_scroll_to_row((int)target_row);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
var tick = _spec.tickdiff(price, 0);
|
||||
if (tick.HasValue) {
|
||||
return scroll_to_tick(tick.Value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double _get_target_top_tick(int mid_viewport_tick) {
|
||||
var curr_rel_off = _scroll_viewer.VerticalOffset + (_scroll_viewer.ViewportHeight / 2.0);
|
||||
var tgt_rel_off = (_price_list.high_tick - mid_viewport_tick) * _settings.row_height;
|
||||
var tick_diff = (curr_rel_off - tgt_rel_off) / _settings.row_height;
|
||||
return _price_list.high_tick + tick_diff;
|
||||
}
|
||||
|
||||
public bool scroll_to_tick(long tick) {
|
||||
if (tick > int.MaxValue) {
|
||||
_scroll_to_tick_log_throttle.Execute(() =>
|
||||
Log.Ui.error($"Unable to scroll to tick {tick} because it exceeds int.MaxValue"));
|
||||
return false;
|
||||
}
|
||||
return scroll_to_tick((int)tick);
|
||||
}
|
||||
|
||||
public bool scroll_to_tick(int tick) {
|
||||
if (_vm is null || _settings is null || _scroll_viewer is null) {
|
||||
return false;
|
||||
}
|
||||
// var center_offset_ticks = (int)(_scrollViewer.ScrollableHeight / (2.0 * _settings.row_height));
|
||||
// The scrollbar's vertical offset won't change. Find the 'top tick' that will put the
|
||||
// target `tick` in the center of the viewport.
|
||||
// var center_offset_diff = _scrollViewer.VerticalOffset - (_scrollViewer.ScrollableHeight / 2.0);
|
||||
// var tick_offset = center_offset_ticks
|
||||
// var tick_offset = (int)(_scrollViewer.ViewportHeight / (2.0 * _settings.row_height));
|
||||
|
||||
// The scrollbar's vertical offset won't change. Find the 'top tick' that will put the
|
||||
// target `tick` in the center of the viewport.
|
||||
checked {
|
||||
var top_tick = (int)_get_target_top_tick(tick);
|
||||
if (_settings.animate_recenter) {
|
||||
var anim = new Int32Animation {
|
||||
From = (int)_price_list.high_tick,
|
||||
To = top_tick,
|
||||
Duration = TimeSpan.FromSeconds(0.15)
|
||||
};
|
||||
var sb = new Storyboard();
|
||||
sb.Children.Add(anim);
|
||||
Storyboard.SetTarget(anim, this);
|
||||
Storyboard.SetTargetProperty(anim, new PropertyPath(TopTickProperty));
|
||||
sb.Begin();
|
||||
} else {
|
||||
_set_scrollbar_range(high_tick: top_tick);
|
||||
_refresh_prices();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public IObservable<ColumnType> subscribe_hover_column() =>
|
||||
Observable.Create<ColumnType>(o => {
|
||||
notify(Mouse.GetPosition(this));
|
||||
@@ -468,6 +564,67 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable _connect_scroll_viewer(ScrollViewer scroll_viewer) {
|
||||
if (scroll_viewer is null) {
|
||||
return null;
|
||||
}
|
||||
const int scroll_tick_padding = 10;
|
||||
// const double scroll_epsilon = 1E-8;
|
||||
var dispatcher = Dispatcher.CurrentDispatcher;
|
||||
bool busy = false;
|
||||
return scroll_viewer.SubscribeScrollChanged(on_scroll_changed);
|
||||
|
||||
async void on_scroll_changed(object sender, ScrollChangedEventArgs e) {
|
||||
if (e.ExtentHeight == e.ViewportHeight || busy) {
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
await dispatcher.InvokeAsync(ensure_tick_padding, DispatcherPriority.Background);
|
||||
busy = false;
|
||||
|
||||
void ensure_tick_padding() {
|
||||
if (_price_list is null || _settings is null) {
|
||||
return;
|
||||
}
|
||||
var pct = _scroll_viewer.VerticalOffset / _scroll_viewer.ScrollableHeight;
|
||||
var new_pl = new LadderPriceList(_price_list);
|
||||
var stp = scroll_tick_padding * _settings.row_height;
|
||||
if (pct == 0) {
|
||||
if (_scroll_viewer.ExtentHeight - _scroll_viewer.ViewportHeight < stp) {
|
||||
new_pl.extend_up(scroll_tick_padding);
|
||||
} else {
|
||||
new_pl.shift_up(scroll_tick_padding);
|
||||
}
|
||||
_price_dirty = true;
|
||||
}
|
||||
if (pct == 1) {
|
||||
if (_scroll_viewer.ExtentHeight - _scroll_viewer.ViewportHeight < stp) {
|
||||
new_pl.extend_down(scroll_tick_padding);
|
||||
} else {
|
||||
new_pl.shift_down(scroll_tick_padding);
|
||||
}
|
||||
_price_dirty = true;
|
||||
}
|
||||
if (new_pl != _price_list) {
|
||||
_pending_price_list = new_pl;
|
||||
}
|
||||
_ordershost.refresh_orders();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _set_scrollbar_range(int? high_tick = null, int? length = null) {
|
||||
if (high_tick.HasValue) {
|
||||
_price_list.high_tick = high_tick.Value;
|
||||
}
|
||||
if (length.HasValue) {
|
||||
_price_list.length = length.Value;
|
||||
}
|
||||
_mkt_break_lines.invalidate_indices();
|
||||
_mkt_break_lines.refresh();
|
||||
_refresh_all_visuals();
|
||||
}
|
||||
|
||||
void _send_parameterized_order(Point point, Side side) {
|
||||
Disposable.dispose(ref _parameterized_order_popup);
|
||||
void on_cannot_send(string reason) {
|
||||
@@ -605,7 +762,10 @@ namespace mbts.spark {
|
||||
Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)) {
|
||||
_vm?.recenter_all_ladders();
|
||||
} else {
|
||||
_scroll_to_center();
|
||||
var tgt = _spec.tickdiff(_mkt_break_lines.origin, 0.0);
|
||||
if (tgt.HasValue) {
|
||||
scroll_to_tick(tgt.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,14 +842,9 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
|
||||
internal int _row_for_price(double price) {
|
||||
if ((_price_list?.Count ?? 0) == 0 || !price.is_real()) {
|
||||
return -1;
|
||||
}
|
||||
return LadderRender.index_of(_price_list, price, _price_comparer);
|
||||
}
|
||||
internal int _row_for_price(double price) => LadderRender.index_of(_price_list, price, _price_comparer);
|
||||
|
||||
// public double y_for_price(double price) => _row_for_price(price) * (_settings?.row_height ?? double.NaN);
|
||||
public double y_for_price(double price) => _row_for_price(price) * (_settings?.row_height ?? double.NaN);
|
||||
#endregion
|
||||
|
||||
#region Column Utility Methods
|
||||
@@ -734,30 +889,11 @@ namespace mbts.spark {
|
||||
render_market_col(_depth.Bids);
|
||||
break;
|
||||
case ColumnType.Price:
|
||||
if (_settings.net_change != LadderSettings.NetChangeTerms.None) {
|
||||
LadderRender.render_prices(
|
||||
column.visual_tup.visual,
|
||||
column.visual_tup.glyph_context,
|
||||
_row_count,
|
||||
_settings.row_height,
|
||||
_price_list,
|
||||
_vm.md.settlement_price,
|
||||
_settings.net_change == LadderSettings.NetChangeTerms.Ticks,
|
||||
column.settings,
|
||||
_spec);
|
||||
} else {
|
||||
LadderRender.render_prices(
|
||||
column.visual_tup.visual,
|
||||
column.visual_tup.glyph_context,
|
||||
_row_count,
|
||||
_settings.row_height,
|
||||
_price_list,
|
||||
column.settings,
|
||||
_spec,
|
||||
_settings.price_display_format);
|
||||
}
|
||||
_refresh_prices(column);
|
||||
break;
|
||||
case ColumnType.Ask:
|
||||
render_market_col(_depth.Asks);
|
||||
break;
|
||||
case ColumnType.Ask: render_market_col(_depth.Asks); break;
|
||||
case ColumnType.Volume:
|
||||
_volume_container.mark_dirty();
|
||||
_refresh_volume_at_price();
|
||||
@@ -950,8 +1086,9 @@ namespace mbts.spark {
|
||||
Storyboard.SetTargetProperty(anim, new PropertyPath(Behavior.AnimatedScrollViewerVerticalOffsetProperty));
|
||||
sb.Begin();
|
||||
} else {
|
||||
_scroll_to_row(_row_count / 2);
|
||||
_scroll_to_row(_price_list.Count / 2);
|
||||
}
|
||||
_refresh_frozen_mkt_lines();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,23 +1102,76 @@ namespace mbts.spark {
|
||||
return Disposable.Create(() => _can_user_send_orders_changed -= o.OnNext);
|
||||
});
|
||||
|
||||
LadderPriceList _create_price_list() {
|
||||
if (_settings is not null) {
|
||||
switch (_settings.net_change) {
|
||||
case LadderSettings.NetChangeTerms.BasisPoints:
|
||||
case LadderSettings.NetChangeTerms.NetBasisPoints:
|
||||
return new() {
|
||||
length = _initial_row_count,
|
||||
regional_tick = new RegionalTickFactors(new user.settings.TickFactors(10000, 1)),
|
||||
};
|
||||
}
|
||||
}
|
||||
return new() {
|
||||
length = _initial_row_count,
|
||||
regional_tick = _spec
|
||||
};
|
||||
}
|
||||
|
||||
#region Timer.Tick, Refresh Methods
|
||||
void _tick() {
|
||||
if (_settings != null) {
|
||||
bool last_trade_changed = _vm.md.get_last_trade(ref lastTradeTime, out _last_trade, out DateTime _, out ulong __);
|
||||
ulong unused = 0;
|
||||
bool last_trade_changed = _vm.md.get_last_trade(ref unused, out _last_trade, out DateTime _, out ulong __);
|
||||
bool depth_changed = _vm.md.get_depth(ref _prices_updated, ref _bids, ref _asks, NativePriceHelpers.COCONUT_MAX_DEPTH);
|
||||
_depth = _vm.security_md.Depth(_bids, _asks, _vm.securities);
|
||||
_mkt_break_lines.refresh();
|
||||
bool prices_changed =
|
||||
_price_dirty ||
|
||||
(double.IsNaN(_last_origin_price) && !double.IsNaN(_mkt_break_lines.origin)) ||
|
||||
_mkt_break_lines.origin >= _price_list[0] || _mkt_break_lines.origin <= _price_list[_price_list.Count - 1];
|
||||
if (!prices_changed && _prices_out_of_range(out double low, out double high)) {
|
||||
prices_changed = _refresh_price_range(low, high);
|
||||
if (_price_list is null) {
|
||||
Log.assert(_price_dirty);
|
||||
_price_list = _create_price_list();
|
||||
var mid_tick = security.tickdiff(_mkt_break_lines.origin, 0) ?? 0;
|
||||
if (mid_tick > int.MaxValue) {
|
||||
Log.Ui.error(
|
||||
$"Unable to set scrollbar range about mid_tick {mid_tick}: " +
|
||||
$"tick count exceeds int.MaxValue");
|
||||
} else {
|
||||
_set_scrollbar_range(high_tick: (int)mid_tick + (_price_list.Count << 1));
|
||||
}
|
||||
_price_dirty = true;
|
||||
}
|
||||
if (prices_changed) {
|
||||
_refresh_price_list();
|
||||
if (_price_dirty) {
|
||||
if (_pending_price_list != null && !_pending_price_list.Equals(_price_list)) {
|
||||
var prev_price_list = _price_list;
|
||||
_price_list = _pending_price_list;
|
||||
_pending_price_list = null;
|
||||
if (prev_price_list.Count != _price_list.Count) {
|
||||
_reset_height();
|
||||
}
|
||||
// maintain previous visible price range if possible
|
||||
double percent_scroll_offset = _scroll_viewer.VerticalOffset / _scroll_viewer.ScrollableHeight;
|
||||
var n_visible_rowsd = _scroll_viewer.ViewportHeight / _settings.row_height;
|
||||
var n_visible_rows = (int)n_visible_rowsd;
|
||||
var offset = n_visible_rowsd - n_visible_rows;
|
||||
var n_rows_to_scroll = _price_list.Count - n_visible_rows - 1;
|
||||
int prev_target_index = (int)(percent_scroll_offset * n_rows_to_scroll);
|
||||
// int prev_target_index = (int)(percent_scroll_offset * prev_price_list.Count - 1);
|
||||
if (prev_price_list.TryGetAt(prev_target_index, out double prev_target_price)) {
|
||||
int new_target_index = LadderRender.index_of(_price_list, prev_target_price, _price_comparer);
|
||||
if (_price_list.within_range(new_target_index)) {
|
||||
// force _scrollViewer to re-calculate ScrollableHeight
|
||||
_scroll_viewer.UpdateLayout();
|
||||
double new_vertical_offset = new_target_index * _settings.row_height;
|
||||
// double new_vertical_offset = ((double)new_target_index / _price_list.Count) * _scrollViewer.ScrollableHeight;
|
||||
_scroll_viewer.ScrollToVerticalOffset(new_vertical_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
_refresh_prices();
|
||||
scroll_to_pending_target();
|
||||
_refresh_all_visuals();
|
||||
} else {
|
||||
scroll_to_pending_target();
|
||||
if (_mkt_break_lines.bid_dirty)
|
||||
_refresh_bid_mkt_break();
|
||||
if (_mkt_break_lines.ask_dirty)
|
||||
@@ -1053,75 +1243,6 @@ namespace mbts.spark {
|
||||
_settings.price_display_format);
|
||||
}
|
||||
|
||||
static readonly TimeSpan _price_out_of_range_render_interval = TimeSpan.FromSeconds(5);
|
||||
DateTime _last_price_range_calc_time = DateTime.MinValue;
|
||||
|
||||
void _refresh_price_list() {
|
||||
if (_settings is null || _spec is null || !_mkt_break_lines.origin.is_real()) {
|
||||
return;
|
||||
}
|
||||
var prev_price_list = _price_list;
|
||||
_price_list = (!(_settings.net_change == LadderSettings.NetChangeTerms.None)) ?
|
||||
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _vm.md.settlement_price, _settings.net_change == LadderSettings.NetChangeTerms.Ticks, _row_count, _settings.row_height, _settings.price_column, _spec) :
|
||||
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _row_count, _settings.row_height, _settings.price_column, _spec, _settings.price_display_format);
|
||||
_ordershost.update_settings(_settings.row_height, _settings.orders_column);
|
||||
if (prev_price_list != null) {
|
||||
// maintain previous visible price range if possible
|
||||
double percent_scroll_offset = _scroll_viewer.VerticalOffset / _scroll_viewer.ScrollableHeight;
|
||||
int prev_target_index = (int)(percent_scroll_offset * prev_price_list.Count);
|
||||
if (prev_price_list.TryGetAt(prev_target_index, out double prev_target_price)) {
|
||||
int new_target_index = LadderRender.index_of(_price_list, prev_target_price, _price_comparer);
|
||||
if (_price_list.within_range(new_target_index)) {
|
||||
// force _scroll_viewer to re-calculate ScrollableHeight
|
||||
_scroll_viewer.UpdateLayout();
|
||||
double new_vertical_offset = ((double)new_target_index / _price_list.Count) * _scroll_viewer.ScrollableHeight;
|
||||
_scroll_viewer.ScrollToVerticalOffset(new_vertical_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _refresh_price_range(double low, double high) {
|
||||
bool prices_changed = false;
|
||||
// The market can be wider than the 1001 rows (ticks) we [initially] allot.
|
||||
// Rendering prices is expensive, and will lock up the ui if done continuously.
|
||||
// Therefore, ensure a minimum time interval has elapsed before rendering prices again.
|
||||
var now = DateTime.Now;
|
||||
if ((now - _last_price_range_calc_time) > _price_out_of_range_render_interval) {
|
||||
SparkUtils.lazy_warn(!double.IsNaN(_mkt_break_lines.origin) && _mkt_break_lines.origin >= low && _mkt_break_lines.origin <= high,
|
||||
() => fmt_log_msg($"market break ({_mkt_break_lines.origin}) is unacceptable for the market range ({low} - {high})"));
|
||||
int prev_row_count = _row_count;
|
||||
var n_ticks_to_low = (int)(_spec.tickdiff(_mkt_break_lines.origin, low) ?? 0).within_range(int.MinValue, int.MaxValue);
|
||||
var n_ticks_to_high = (int)(_spec.tickdiff(high, _mkt_break_lines.origin) ?? 0).within_range(int.MinValue, int.MaxValue);
|
||||
int desired_row_count = Math.Max(n_ticks_to_low, n_ticks_to_high) * 2 + 1;
|
||||
desired_row_count = Math.Max(desired_row_count, _min_row_count);
|
||||
_row_count = Math.Min(_max_row_count, desired_row_count);
|
||||
if (_row_count != prev_row_count) {
|
||||
if (desired_row_count > _max_row_count)
|
||||
Log.Ui.warn(fmt_log_msg($"ticks between market range ({low} - {high}) exceeds the maximum number of rows allowed ({_max_row_count})"));
|
||||
else
|
||||
Log.Ui.info(fmt_log_msg($"increase row count to {_row_count} to accomodate market range ({low} - {high})"));
|
||||
prices_changed = true;
|
||||
_reset_height();
|
||||
} else {
|
||||
prices_changed = _mkt_break_lines.origin != _last_origin_price;
|
||||
}
|
||||
_last_price_range_calc_time = now;
|
||||
//
|
||||
string fmt_log_msg(string msg) =>
|
||||
$"Ladder {security} (id:{security.Id}, low/high tick:{security.ContractSpec.LowTick}/{security.ContractSpec.HighTick}): {msg}.";
|
||||
}
|
||||
return prices_changed;
|
||||
}
|
||||
|
||||
bool _prices_out_of_range(out double low, out double high) {
|
||||
low = _bids == null ? double.NaN : TickUtils.low_tick(_bids, _asks);
|
||||
high = _asks == null ? double.NaN : TickUtils.high_tick(_bids, _asks);
|
||||
return
|
||||
(!double.IsNaN(low) && _price_list[_price_list.Count - 1] > low) ||
|
||||
(!double.IsNaN(high) && _price_list[0] < high);
|
||||
}
|
||||
|
||||
void _refresh_all_visuals() {
|
||||
if (_scroll_viewer is null || _settings is null) {
|
||||
return;
|
||||
@@ -1175,36 +1296,50 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
void _refresh_session_low_line() {
|
||||
if (_settings.session_low_line.show && _mkt_break_lines.low_render_index.HasValue)
|
||||
_render_session_line(_session_low_vis, _mkt_break_lines.low_render_index.Value, _settings.session_low_line.color);
|
||||
if (_settings.session_low_line.show) {
|
||||
_render_session_line(_session_low_vis, _mkt_break_lines.low_render_index ?? -1, _settings.session_low_line.color);
|
||||
}
|
||||
}
|
||||
|
||||
void _refresh_session_high_line() {
|
||||
if (_settings.session_high_line.show && _mkt_break_lines.high_render_index.HasValue)
|
||||
_render_session_line(_session_high_vis, _mkt_break_lines.high_render_index.Value, _settings.session_high_line.color);
|
||||
if (_settings.session_high_line.show) {
|
||||
_render_session_line(_session_high_vis, _mkt_break_lines.high_render_index ?? -1, _settings.session_high_line.color);
|
||||
}
|
||||
}
|
||||
|
||||
void _refresh_last_trade() {
|
||||
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
|
||||
if (last_index > 0)
|
||||
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
|
||||
LadderRender.render_last(
|
||||
_last_vis_tup.visual,
|
||||
_last_vis_tup.glyph_context,
|
||||
_last_trade,
|
||||
last_index,
|
||||
_settings.row_height,
|
||||
_settings.last_column,
|
||||
_fmt_mkt_depth_item);
|
||||
}
|
||||
|
||||
void _refresh_piq() {
|
||||
if (!_settings.piq_column.show)
|
||||
return;
|
||||
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
|
||||
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, _price_list, _vm.price_vs_piq, _settings.row_height, _spec, _settings.piq_column, _fmt_mkt_quantity);
|
||||
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, _price_list, _vm.price_vs_piq, _settings.row_height, _spec, _settings.piq_column, _fmt_mkt_quantity);
|
||||
LadderRender.render_piqs(
|
||||
_piq_vis_tup.visual,
|
||||
_last_vis_tup.glyph_context,
|
||||
_price_list,
|
||||
_vm.price_vs_piq,
|
||||
_settings.row_height,
|
||||
_spec,
|
||||
_settings.piq_column,
|
||||
_fmt_mkt_quantity);
|
||||
}
|
||||
|
||||
void _refresh_sizes() {
|
||||
Log.assert_d(_price_list != null);
|
||||
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
|
||||
var bidItems = _bids.Select(nps => (nps.price, (double)nps.size, nps.price * nps.size));
|
||||
var askItems = _asks.Select(nps => (nps.price, (double)nps.size, nps.price * nps.size));
|
||||
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, bidItems, _settings.row_height, _spec, _settings.bid_column, _fmt_price_level);
|
||||
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, askItems, _settings.row_height, _spec, _settings.ask_column, _fmt_price_level);
|
||||
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _depth.Bids, _settings.row_height, _spec, _settings.bid_column, _fmt_price_level);
|
||||
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _depth.Asks, _settings.row_height, _spec, _settings.ask_column, _fmt_price_level);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1273,9 +1408,12 @@ namespace mbts.spark {
|
||||
void _clear_visuals() {
|
||||
if (_settings == null)
|
||||
return;
|
||||
_price_list = new double[_row_count];
|
||||
for (int i = 0; i < _price_list.Count; i++)
|
||||
_price_list[i] = double.NaN;
|
||||
if (_price_list is null) {
|
||||
_price_list = new LadderPriceList {
|
||||
regional_tick = _spec
|
||||
};
|
||||
}
|
||||
_price_list.reset(null, null, _initial_row_count);
|
||||
Array.Clear(_bids, 0, _bids.Length);
|
||||
Array.Clear(_asks, 0, _asks.Length);
|
||||
_price_dirty = true;
|
||||
@@ -1283,6 +1421,7 @@ namespace mbts.spark {
|
||||
_refresh_bid_mkt_break();
|
||||
_refresh_ask_mkt_break();
|
||||
_refresh_mid_mkt_break();
|
||||
_refresh_fair_value_lines(true);
|
||||
_refresh_session_low_line();
|
||||
_refresh_session_high_line();
|
||||
_refresh_session_range();
|
||||
@@ -1292,38 +1431,58 @@ namespace mbts.spark {
|
||||
#endregion
|
||||
|
||||
#region Market Break, Session Low/High Utils
|
||||
static double _get_top_price(NativePriceSize[] prices) =>
|
||||
prices != null && prices.Length > 0 ? prices[0].price : double.NaN;
|
||||
static double _get_top_price(NativePriceSize[] prices) {
|
||||
if (prices == null) {
|
||||
return double.NaN;
|
||||
}
|
||||
if (prices.Length == 0) {
|
||||
return double.NaN;
|
||||
}
|
||||
if (prices[0].size == 0) {
|
||||
return double.NaN;
|
||||
}
|
||||
return prices[0].price;
|
||||
}
|
||||
|
||||
double _get_bid_market_break() {
|
||||
double best_bid = _get_top_price(_bids);
|
||||
double best_ask = _get_top_price(_asks);
|
||||
return double.IsNaN(best_bid) ?
|
||||
double.IsNaN(best_ask) ? _vm.settlement : _spec.tickdown(best_ask) :
|
||||
best_bid;
|
||||
var v = best_bid
|
||||
.if_nan(_spec.tickdown(best_ask))
|
||||
.if_nan(_vm?.settlement ?? double.NaN);
|
||||
return _spec.round_to_tick(v, RoundingMode.AbsDown);
|
||||
}
|
||||
|
||||
double _normalize_price(double price) =>
|
||||
TickUtils.normalize_price(_spec, price);
|
||||
|
||||
double _get_ask_market_break() {
|
||||
if (_vm is null) {
|
||||
return double.NaN;
|
||||
}
|
||||
double best_bid = _get_top_price(_bids);
|
||||
double best_ask = _get_top_price(_asks);
|
||||
return double.IsNaN(best_ask) ?
|
||||
double.IsNaN(best_bid) ? _vm.settlement : best_bid :
|
||||
_spec.tickdown(best_ask);
|
||||
var v = _spec.tickdown(best_ask)
|
||||
.if_nan(best_bid)
|
||||
.if_nan(_vm?.settlement ?? double.NaN);
|
||||
return _spec.round_to_tick(v, RoundingMode.AbsDown);
|
||||
}
|
||||
|
||||
double _get_mid_market_break() {
|
||||
double best_bid = _get_top_price(_bids);
|
||||
double best_ask = _get_top_price(_asks);
|
||||
return double.IsNaN(best_bid) ?
|
||||
double.IsNaN(best_ask) ? _vm.settlement : _spec.tickdown(best_ask) :
|
||||
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(_spec, best_bid, best_ask);
|
||||
var v = TickUtils.mid_tick(_spec, best_bid, best_ask)
|
||||
.if_nan(best_bid)
|
||||
.if_nan(_spec?.tickdown(best_ask) ?? double.NaN)
|
||||
.if_nan(_vm?.settlement ?? double.NaN);
|
||||
return _spec.round_to_tick(v, RoundingMode.AbsDown);
|
||||
}
|
||||
|
||||
double _get_session_low() => _vm.md.session_low;
|
||||
double _get_session_high() => _vm.md.session_high;
|
||||
double _get_session_low() => _normalize_price(_get_session_low_raw());
|
||||
double _get_session_high() => _normalize_price(_get_session_high_raw());
|
||||
|
||||
double _get_session_low_raw() => _vm?.md?.session_low ?? double.NaN;
|
||||
double _get_session_high_raw() => _vm?.md?.session_high ?? double.NaN;
|
||||
|
||||
void _render_mkt_break(DrawingVisual vis, double price, uint color) =>
|
||||
LadderRender.render_mkt_break(vis, _row_for_price(price), _settings.row_height, color);
|
||||
@@ -1347,6 +1506,15 @@ namespace mbts.spark {
|
||||
_settings = _vm?.get_save_state() ?? s;
|
||||
_pending_scroll_to_origin |= _settings == null;
|
||||
_invalidate_spec();
|
||||
if (_price_list != null) {
|
||||
_pending_price_list = null;
|
||||
if (_price_list.Count > 0) {
|
||||
var high_price = _price_list.First();
|
||||
var new_tickdiff = _spec.count_ticks(high_price);
|
||||
_price_list.high_tick = (long)new_tickdiff;
|
||||
}
|
||||
_price_list.regional_tick = _spec;
|
||||
}
|
||||
_bid_vis_tup.glyph_context = glyph_context(s.bid_column);
|
||||
_ask_vis_tup.glyph_context = glyph_context(s.ask_column);
|
||||
_price_vis_tup.glyph_context = glyph_context(s.price_column);
|
||||
@@ -1430,22 +1598,9 @@ namespace mbts.spark {
|
||||
// if (_scroll_viewer != null) {
|
||||
// _scroll_viewer.Background = s.last_column.background.rgb_brush();
|
||||
// }
|
||||
scroll_to_pending_target();
|
||||
visible_columns_changed?.Invoke();
|
||||
if (_scroll_viewer is not null && _settings is not null) {
|
||||
_vm.md.get_last_trade(ref lastTradeTime, out _last_trade, out DateTime _, out ulong __);
|
||||
_vm.md.get_depth(ref _prices_updated, ref _bids, ref _asks, NativePriceHelpers.COCONUT_MAX_DEPTH);
|
||||
_mkt_break_lines.refresh();
|
||||
double low = _bids == null ? double.NaN : TickUtils.low_tick(_bids, _asks);
|
||||
double high = _asks == null ? double.NaN : TickUtils.high_tick(_bids, _asks);
|
||||
_refresh_price_range(low, high);
|
||||
_refresh_price_list();
|
||||
_refresh_all_visuals();
|
||||
_refresh_piq();
|
||||
_piq_dirty = false;
|
||||
_refresh_volume_at_price();
|
||||
_last_origin_price = _mkt_break_lines.origin;
|
||||
_price_dirty = false;
|
||||
}
|
||||
_refresh_all_visuals();
|
||||
}
|
||||
|
||||
// This is designed to be a redundant null check for _fmt_mkt_depth_item_impl.
|
||||
@@ -1476,9 +1631,6 @@ namespace mbts.spark {
|
||||
return impl is null ? value.ToString() : impl(value, price);
|
||||
}
|
||||
|
||||
string _fmt_mkt_quantity(NativePriceSize nps) =>
|
||||
_fmt_mkt_quantity(nps.size, nps.price);
|
||||
|
||||
void _init_tactile_adorner() {
|
||||
Disposable.dispose(ref _tactile_adorner);
|
||||
if (_scroll_viewer != null)
|
||||
@@ -1593,7 +1745,7 @@ namespace mbts.spark {
|
||||
return null;
|
||||
}
|
||||
if (!double.IsNaN(_session_low)) {
|
||||
_low_index = LadderRender.index_of(_owner._price_list, _session_low, _owner._price_comparer);
|
||||
_low_index = _owner._price_list.IndexOf(_session_low);
|
||||
if (_session_low < _owner._price_list.Last()) {
|
||||
_low_index = _owner._price_list.Count - 1;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using bts.api;
|
||||
using bts.utils;
|
||||
|
||||
using Spark.Core;
|
||||
using Spark.Core.Diagnostics;
|
||||
|
||||
using System;
|
||||
@@ -31,16 +30,6 @@ namespace mbts.spark {
|
||||
regional_tick = other.regional_tick;
|
||||
}
|
||||
|
||||
public LadderPriceList(RegionalTickFactors regionalTick, IList<double> prices) {
|
||||
regional_tick = regionalTick;
|
||||
_length = prices.Count;
|
||||
checked {
|
||||
_high_tick = prices.Count > 0
|
||||
? (int)regionalTick.tickdiff(prices[0], 0).assert_value()
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
|
||||
RegionalTickFactors _regional_tick;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using bts;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.md.auxiliary;
|
||||
using bts.collections;
|
||||
@@ -101,24 +101,6 @@ namespace mbts.spark {
|
||||
settings.font_size, settings.width, row_height, HorizontalAlignment.Right,
|
||||
-4, VerticalAlignment.Center, row * row_height, true);
|
||||
|
||||
public static double[] gen_prices(double mid, int row_count, RegionalTickFactors spec) {
|
||||
// require odd number
|
||||
Log.assert(row_count % 2 == 1);
|
||||
var result = new double[row_count];
|
||||
int range = row_count / 2;
|
||||
int middle = row_count / 2;
|
||||
result[middle] = mid;
|
||||
double up = mid, down = mid;
|
||||
// Starting from the middle iterate out to the edges
|
||||
for (int x = 1; x <= range; ++x) {
|
||||
up = spec.tickup(up);
|
||||
down = spec.tickdown(down);
|
||||
result[middle - x] = up;
|
||||
result[middle + x] = down;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static GlyphRun[] format_prices(
|
||||
IList<double> prices,
|
||||
double reference_price,
|
||||
@@ -214,45 +196,6 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public static double[] render_prices(DrawingVisual price, GlyphContext glyph_context, double mid, double wrt_price, bool format_in_tick_terms, int row_count, double row_height,
|
||||
LadderColumnSettings settings, RegionalTickFactors spec) {
|
||||
var dprices = gen_prices(mid, row_count, spec);
|
||||
render_prices(price, glyph_context, row_count, row_height, dprices, wrt_price, format_in_tick_terms, settings, spec);
|
||||
return dprices;
|
||||
}
|
||||
|
||||
public static double[] render_prices(DrawingVisual price, GlyphContext glyph_context, double mid, int row_count, double row_height,
|
||||
LadderColumnSettings settings, RegionalTickFactors spec, LadderSettings.PriceDisplayFormat price_display_format) {
|
||||
var dprices = gen_prices(mid, row_count, spec);
|
||||
render_prices(price, glyph_context, row_count, row_height, dprices, settings, spec, price_display_format);
|
||||
return dprices;
|
||||
}
|
||||
|
||||
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, IList<double> dprices, double wrt_price, bool format_in_tick_terms, LadderColumnSettings settings, RegionalTickFactors spec) {
|
||||
var foreground = settings.foreground.rgb_brush();
|
||||
//var background = new SolidColorBrush(settings.background.rgb_color());
|
||||
var terms = format_in_tick_terms ? LadderSettings.NetChangeTerms.Ticks : LadderSettings.NetChangeTerms.Dollars;
|
||||
var fprices = format_prices(dprices, wrt_price, terms, row_height, settings, glyph_context, spec);
|
||||
using (var dc = price.RenderOpen())
|
||||
using (_draw_with_clip(dc, settings, row_height, row_count)) {
|
||||
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, row_count * row_height));
|
||||
for (int x = 0; x < row_count; ++x)
|
||||
dc.DrawGlyphRun(foreground, fprices[x]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, IList<double> dprices, LadderColumnSettings settings, RegionalTickFactors spec, LadderSettings.PriceDisplayFormat price_display_format) {
|
||||
var foreground = settings.foreground.rgb_brush();
|
||||
//var background = new SolidColorBrush(settings.background.rgb_color());
|
||||
var fprices = format_prices(dprices, double.NaN, LadderSettings.NetChangeTerms.Dollars, row_height, settings, glyph_context, spec, price_display_format);
|
||||
using (var dc = price.RenderOpen())
|
||||
using (_draw_with_clip(dc, settings, row_height, row_count)) {
|
||||
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, row_count * row_height));
|
||||
for (int x = 0; x < row_count; ++x)
|
||||
dc.DrawGlyphRun(foreground, fprices[x]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void render_grid(DrawingVisual dv, IEnumerable<LadderGridHost.IColumn> columns, int rowcount, double row_height, UInt32 line_color) {
|
||||
var height = rowcount * row_height;
|
||||
var brush = line_color.rgb_brush();
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace bts.shell.orderbook {
|
||||
[ProtoMember(5)]
|
||||
public string[] accounts;
|
||||
|
||||
[ProtoMember(28)]
|
||||
[ProtoMember(5)]
|
||||
public string Tag { get; set; }
|
||||
|
||||
[ProtoMember(6)]
|
||||
|
||||
@@ -407,16 +407,16 @@ namespace bts.shell.pnl {
|
||||
void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
|
||||
var view = new HeaderedContentControl {
|
||||
Content = content,
|
||||
Header = new OrderAuxiliaryPopupHeader {
|
||||
Header = new AuxiliaryDataHeader {
|
||||
context = context_desc,
|
||||
title = title_desc,
|
||||
title_foreground = fg
|
||||
},
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Resources = new OrderAuxiliaryPopupResources()
|
||||
Resources = new AuxiliaryDataResources()
|
||||
};
|
||||
view.Style = view.Resources[OrderAuxiliaryPopupResources.PopupChildStyleKey] as Style;
|
||||
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
|
||||
_popup_man.show(view);
|
||||
}
|
||||
|
||||
|
||||
@@ -250,19 +250,6 @@ namespace BCG
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
private double _get_rep_driver() {
|
||||
var map =
|
||||
_threaded_bb.read_state(SparkTags.OptionDriverBBOTag((_nmsheet_symbol.option_name, _nmsheet_symbol.option_exchange)));
|
||||
if (map == null)
|
||||
return Double.NaN;
|
||||
var price_map = map.get_price_map();
|
||||
if (price_map == null || price_map.Count == 0)
|
||||
return Double.NaN;
|
||||
var md = price_map.ElementAt(0).Value;
|
||||
double pr = md.get_driver();
|
||||
return pr;
|
||||
}
|
||||
|
||||
private IDictionary<DateTime, ATMData> _get_atm_data(ExpStrikeKeyedData<mbts.spark.TheoPoint> theo_data)
|
||||
{
|
||||
NMSheetSettings sheet_settings = _threaded_bb.read_state(_in_bb_sheet_settings_tag) ?? new NMSheetSettings();
|
||||
@@ -274,7 +261,7 @@ namespace BCG
|
||||
exp.security_type == SecurityType.Option)
|
||||
?.ToDictionaryCrushed(kv => kv.Key.expiration, kv => kv.Value)
|
||||
?? new();
|
||||
IDictionary<DateTime, ATMData> atm_data = OptionATMDataSource.compute_atm_data(_get_onefive(), theo_data, option_exp_roll_data, _get_rep_driver());
|
||||
IDictionary<DateTime, ATMData> atm_data = OptionATMDataSource.compute_atm_data(_get_onefive(), theo_data, option_exp_roll_data);
|
||||
if (sheet_settings.setting_atm.select_atm_nearest_strike_only)
|
||||
foreach (ATMData exp_atm_data in atm_data.Values)
|
||||
exp_atm_data.reduce_to_nearest_expiration();
|
||||
|
||||
+1
-4
@@ -5,7 +5,6 @@ using mbts.ui;
|
||||
using mbts;
|
||||
using BCG.Utilities;
|
||||
using mbts.spark;
|
||||
using mbts.ui.converters;
|
||||
|
||||
namespace BCG {
|
||||
/// <summary>
|
||||
@@ -35,10 +34,8 @@ namespace BCG {
|
||||
}
|
||||
|
||||
internal static IDictionary<DateTime, ATMData> compute_atm_data(
|
||||
double one_five, ExpStrikeKeyedData<TheoPoint> theo_data, IDictionary<DateTime, double> opt_exp_rolls,
|
||||
double rep_driver) {
|
||||
double one_five, ExpStrikeKeyedData<TheoPoint> theo_data, IDictionary<DateTime, double> opt_exp_rolls) {
|
||||
IDictionary<DateTime, double> exp_vs_drivers = _get_computed_drivers(theo_data);
|
||||
exp_vs_drivers = exp_vs_drivers.ToDictionary(kv => kv.Key, kv => rep_driver);
|
||||
IDictionary<DateTime, ATMData> rv = new Dictionary<DateTime, ATMData>();
|
||||
if (one_five != 0)
|
||||
foreach (DateTime exp in new List<DateTime>(exp_vs_drivers.Keys))
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
#nullable enable
|
||||
namespace mbts.spark;
|
||||
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.domain;
|
||||
using bts.api.orderman;
|
||||
|
||||
using mbts.co2;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
|
||||
using Spark.Core.Disposables;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
|
||||
class ExistingOrderTicketLadderVM : DisposableViewModelBase, IRequestViewClose {
|
||||
const ushort numPriceLevels = 5;
|
||||
|
||||
readonly Property<SecurityImmutable> security;
|
||||
readonly Property<TickDisplaySettingsImmutable> priceDisplay;
|
||||
readonly Property<TickDisplaySettingsImmutable> quantityDisplay;
|
||||
readonly Property<user.settings.RegionalTickFactors> priceTick;
|
||||
readonly ObservableCollection<OrderTicketLadderPriceLevelVM> bids = [];
|
||||
readonly ObservableCollection<OrderTicketLadderPriceLevelVM> asks = [];
|
||||
|
||||
readonly Property<double> price, currentPrice;
|
||||
readonly Property<uint> currentQuantity;
|
||||
readonly Property<AccountTuple> currentAccount;
|
||||
readonly Property<TimeInForce> currentTimeInForce;
|
||||
readonly Property<CustomOrderDefinition.CustomOneofCase> currentOrderType;
|
||||
readonly Property<bool> priceDirty, quantityDirty, accountDirty, timeInForceDirty, orderTypeDirty;
|
||||
|
||||
Side side = Side.Buy;
|
||||
|
||||
public ExistingOrderTicketLadderVM(
|
||||
OrdersModel model,
|
||||
IObservable<(SecurityMD? SecurityMd, OrderRecordImmutable Order)> orderSource,
|
||||
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
|
||||
IObservable<double[]?> calculatorButtons,
|
||||
Action<SecurityMD> showLadder,
|
||||
TickDisplaySettingsImmutable? quantityDisplay = null
|
||||
) {
|
||||
SecurityMD? currentSecurityMd = null;
|
||||
uint? key = null;
|
||||
DispatcherTimer? mdPollTimer = null;
|
||||
var nativeBids = new NativePriceSize[numPriceLevels];
|
||||
var nativeAsks = new NativePriceSize[numPriceLevels];
|
||||
for (int i = 0; i < numPriceLevels; i++) {
|
||||
nativeBids[i] = NativePriceSize.NO_MARKET;
|
||||
nativeAsks[i] = NativePriceSize.NO_MARKET;
|
||||
Bids.Add(new(Side.Buy));
|
||||
Asks.Add(new(Side.Sell));
|
||||
}
|
||||
|
||||
var secMdSrc = orderSource.Where(ot => ot.SecurityMd.HasValue).Select(ot => ot.SecurityMd!.Value);
|
||||
security = backing(nameof(Security), secMdSrc.Select(smd => smd.security), bts.api.Security.Defaults.SecurityImmutable);
|
||||
priceTick = backing(nameof(PriceTick), user.settings.RegionalTickFactors.Default);
|
||||
PropertiesVm = new OrderPropertiesEditorVM(
|
||||
model,
|
||||
secMdSrc,
|
||||
accountsSource,
|
||||
orderSource.Select(ot => new InlineOrderProperties(model, ot.Order.Def)));
|
||||
this.quantityDisplay = backing(nameof(QuantityDisplay), quantityDisplay ?? bts.api.Security.Defaults.quantity_display_settingsi);
|
||||
priceDisplay = backing(nameof(PriceDisplay), bts.api.Security.Defaults.price_display_settingsi);
|
||||
SizeCalculator = new(0, calculatorButtons);
|
||||
SetPriceCmd = new DelegateCommand<double>(SetPrice);
|
||||
ShowLadderCmd = new DelegateCommand(() => {
|
||||
if (currentSecurityMd.HasValue)
|
||||
showLadder(currentSecurityMd.Value);
|
||||
});
|
||||
|
||||
price = backing(nameof(Price), double.NaN);
|
||||
currentPrice = backing(nameof(CurrentPrice), orderSource.Select(ort => ort.Order.State.Price));
|
||||
currentQuantity = backing(nameof(CurrentQuantity), orderSource.Select(ort => ort.Order.State.Quantity));
|
||||
currentAccount = backing(nameof(CurrentAccount), orderSource.Select(ort => new AccountTuple(ort.Order.Def.AccountId, ort.Order.Def.Account)));
|
||||
currentTimeInForce = backing(nameof(CurrentTimeInForce), orderSource.Select(ort => ort.Order.Def.TimeInForce));
|
||||
currentOrderType = backing(nameof(CurrentOrderType), orderSource.Select(ort => ort.Order.Def.CustomDef.CustomCase));
|
||||
//
|
||||
priceDirty = backing(
|
||||
nameof(PriceDirty),
|
||||
price.observe_current(true).CombineLatest(
|
||||
currentPrice.observe_current(true),
|
||||
(val, curr) => !double.IsNaN(val) && val != curr));
|
||||
quantityDirty = backing(
|
||||
nameof(QuantityDirty),
|
||||
SizeCalculator.observe_actual_value()
|
||||
.CombineLatest(currentQuantity.observe_current(true),
|
||||
(val, curr) => val.HasValue && val.Value != curr));
|
||||
accountDirty = backing(
|
||||
nameof(AccountDirty),
|
||||
PropertiesVm.observe_account(true)
|
||||
.CombineLatest(currentAccount.observe_current(true),
|
||||
(val, curr) => val != curr));
|
||||
timeInForceDirty = backing(
|
||||
nameof(TimeInForceDirty),
|
||||
PropertiesVm.observe_time_in_force(true)
|
||||
.CombineLatest(currentTimeInForce.observe_current(true),
|
||||
(val, curr) => val != curr));
|
||||
orderTypeDirty = backing(
|
||||
nameof(OrderTypeDirty),
|
||||
PropertiesVm.observe_order_type(true)
|
||||
.CombineLatest(currentOrderType.observe_current(true),
|
||||
(val, curr) => val != null && val.order_type != curr));
|
||||
//
|
||||
var priceDirtySrc = priceDirty.Select(vals => vals.newval).StartWith(false);
|
||||
var quantityDirtySrc = quantityDirty.Select(vals => vals.newval).StartWith(false);
|
||||
var sendOrderCmd = new ObservantCommand(SendOrder);
|
||||
SendOrderCmd = sendOrderCmd;
|
||||
var canSendOrderSrc = priceDirty.observe_current(true)
|
||||
.CombineLatest(quantityDirty.observe_current(true), (pd, sd) => pd || sd)
|
||||
.DistinctUntilChanged();
|
||||
var isDirtySrc = Observable.Merge(priceDirty, quantityDirty, accountDirty, timeInForceDirty, orderTypeDirty);
|
||||
_dispose = Disposable.Create(
|
||||
SizeCalculator,
|
||||
PropertiesVm,
|
||||
secMdSrc.OnDispatcher().Subscribe(OnSecurityMd),
|
||||
orderSource.OnDispatcher().Subscribe(ort => {
|
||||
key = ort.Order.Key;
|
||||
if (DoubleHelper.IsNaNOrInfinity(Price))
|
||||
Price = ort.Order.State.Price;
|
||||
if (!SizeCalculator.value.HasValue || !SizeCalculator.value_setter_called)
|
||||
SizeCalculator.bypass_value_setter(ort.Order.State.Quantity, true);
|
||||
}),
|
||||
isDirtySrc.OnDispatcher().Subscribe(_ => RaisePropertyChanged(nameof(DirtyItemsText))),
|
||||
canSendOrderSrc.OnDispatcher().Subscribe(sendOrderCmd));
|
||||
|
||||
void SetPrice(double price) {
|
||||
Price = price;
|
||||
RaisePropertyChanged(nameof(Price));
|
||||
}
|
||||
|
||||
void SendOrder() {
|
||||
if (!key.HasValue) {
|
||||
return;
|
||||
}
|
||||
if (priceDirty.Value) {
|
||||
if (quantityDirty.Value) {
|
||||
model.update_order(key.Value, Price, SizeCalculator.value!.Value).ignore();
|
||||
} else {
|
||||
model.update_order_price(key.Value, Price).ignore();
|
||||
}
|
||||
} else if (quantityDirty.Value) {
|
||||
model.update_order_size(key.Value, SizeCalculator.value!.Value).ignore();
|
||||
} else {
|
||||
SizeCalculator.flash_error();
|
||||
flash_error(nameof(Price)).ignore();
|
||||
return;
|
||||
}
|
||||
RequestClose?.Invoke(this, new EventArgs());
|
||||
}
|
||||
|
||||
void OnSecurityMd(SecurityMD smd) {
|
||||
currentSecurityMd = smd;
|
||||
priceTick.set(smd.security.regional_ticks());
|
||||
priceDisplay.set(smd.price_display);
|
||||
this.quantityDisplay.set(smd.quantity_display);
|
||||
SizeCalculator.display_settings = smd.quantity_display;
|
||||
ulong lastMdPollTime = 0;
|
||||
mdPollTimer?.Stop();
|
||||
mdPollTimer = new DispatcherTimer(TimeSpan.FromSeconds(0.1), DispatcherPriority.DataBind, PollMd, Dispatcher.CurrentDispatcher);
|
||||
|
||||
void PollMd(object? sender, EventArgs e) {
|
||||
if (smd.md.get_depth(ref lastMdPollTime, ref nativeBids, ref nativeAsks, numPriceLevels)) {
|
||||
for (int i = 0; i < numPriceLevels; i++) {
|
||||
asks[numPriceLevels - i - 1].Price = nativeAsks[i].price;
|
||||
asks[numPriceLevels - i - 1].Quantity = nativeAsks[i].size;
|
||||
}
|
||||
for (int i = 0; i < numPriceLevels; i++) {
|
||||
bids[i].Price = nativeBids[i].price;
|
||||
bids[i].Quantity = nativeBids[i].size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? RequestClose;
|
||||
|
||||
public SecurityImmutable Security => security.Value;
|
||||
public string Title => Security.ToString() ?? string.Empty;
|
||||
public double Price { get; set; }
|
||||
public user.settings.RegionalTickFactors PriceTick => priceTick.Value;
|
||||
public TickDisplaySettingsImmutable PriceDisplay => priceDisplay.Value;
|
||||
public TickDisplaySettingsImmutable QuantityDisplay => quantityDisplay.Value;
|
||||
public CalculatorLogic<double> SizeCalculator { get; }
|
||||
public Side Side {
|
||||
get => side;
|
||||
set => Set(ref side, value);
|
||||
}
|
||||
|
||||
public OrderPropertiesEditorVM PropertiesVm { get; }
|
||||
public ObservableCollection<OrderTicketLadderPriceLevelVM> Bids => bids;
|
||||
public ObservableCollection<OrderTicketLadderPriceLevelVM> Asks => asks;
|
||||
|
||||
public double CurrentPrice => currentPrice.Value;
|
||||
public uint CurrentQuantity => currentQuantity.Value;
|
||||
public AccountTuple CurrentAccount => currentAccount.Value;
|
||||
public TimeInForce CurrentTimeInForce => currentTimeInForce.Value;
|
||||
public CustomOrderDefinition.CustomOneofCase CurrentOrderType => currentOrderType.Value;
|
||||
public bool PriceDirty => priceDirty.Value;
|
||||
public bool QuantityDirty => quantityDirty.Value;
|
||||
public bool AccountDirty => accountDirty.Value;
|
||||
public bool TimeInForceDirty => timeInForceDirty.Value;
|
||||
public bool OrderTypeDirty => orderTypeDirty.Value;
|
||||
public bool CanChangeAccount { get; } = false;
|
||||
public bool CanChangeTimeInForce { get; } = false;
|
||||
public bool CanChangeOrderType { get; } = false;
|
||||
public string DirtyItemsText {
|
||||
get {
|
||||
var dirty_items = dirtyItems().ToList();
|
||||
if (dirty_items.Count == 0) {
|
||||
return "(no changes)";
|
||||
}
|
||||
return "change " + string.Join(", ", dirty_items);
|
||||
}
|
||||
}
|
||||
public bool CloseOnSend { get; set; }
|
||||
public ICommand SendOrderCmd { get; }
|
||||
public ICommand SetPriceCmd { get; }
|
||||
public ICommand ShowLadderCmd { get; }
|
||||
|
||||
IEnumerable<string> dirtyItems() {
|
||||
if (priceDirty.Value) yield return "price";
|
||||
if (quantityDirty.Value) yield return "quantity";
|
||||
if (accountDirty.Value) yield return "account";
|
||||
if (timeInForceDirty.Value) yield return "tif";
|
||||
if (orderTypeDirty.Value) yield return "type";
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
#nullable enable
|
||||
namespace mbts.spark;
|
||||
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.domain;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
|
||||
using mbts.co2;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
|
||||
using Spark.Core.Disposables;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
class NewOrderTicketLadderVM : DisposableViewModelBase, IOrderModificationLadderVM {
|
||||
const ushort numPriceLevels = 5;
|
||||
const uint numPriceRowsPerSide = 500;
|
||||
|
||||
readonly Property<SecurityImmutable> security;
|
||||
readonly Property<string> title;
|
||||
readonly Property<TickDisplaySettingsImmutable> priceDisplay;
|
||||
readonly Property<TickDisplaySettingsImmutable> quantityDisplay;
|
||||
readonly Property<user.settings.RegionalTickFactors> priceTick;
|
||||
readonly ObservableCollection<OrderTicketLadderPriceLevelVM> priceLevels = [];
|
||||
Side side = Side.Buy;
|
||||
|
||||
public NewOrderTicketLadderVM(
|
||||
OrdersModel model,
|
||||
SecurityMD securityMd,
|
||||
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
|
||||
OrderDefinitionImmutable def,
|
||||
double price,
|
||||
double size,
|
||||
IObservable<double[]> calculatorButtons,
|
||||
Action<SecurityMD> showLadder)
|
||||
: this(
|
||||
model,
|
||||
Observable.Return(securityMd),
|
||||
accountsSource,
|
||||
Observable.Return(new InlineOrderTicketProperties(model, def, price, size)),
|
||||
calculatorButtons,
|
||||
showLadder) { }
|
||||
|
||||
public NewOrderTicketLadderVM(
|
||||
OrdersModel model,
|
||||
SecurityMD securityMd,
|
||||
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
|
||||
IObservable<LadderSettings> settings,
|
||||
Action<SecurityMD> showLadder)
|
||||
: this(
|
||||
model,
|
||||
Observable.Return(securityMd),
|
||||
accountsSource,
|
||||
settings.Select(s => new InlineOrderTicketProperties(model, s)),
|
||||
settings.Select(s => s.calculator_buttons),
|
||||
showLadder) { }
|
||||
|
||||
public NewOrderTicketLadderVM(
|
||||
OrdersModel model,
|
||||
IObservable<SecurityMD> securityMdSource,
|
||||
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
|
||||
IObservable<InlineOrderTicketProperties> propsSource,
|
||||
IObservable<double[]?> calculatorButtons,
|
||||
Action<SecurityMD> showLadder,
|
||||
TickDisplaySettingsImmutable? quantityDisplay = null
|
||||
) {
|
||||
SecurityMD? currentSecurityMd = null;
|
||||
DispatcherTimer? mdPollTimer = null;
|
||||
var nativeBids = new NativePriceSize[numPriceLevels];
|
||||
var nativeAsks = new NativePriceSize[numPriceLevels];
|
||||
for (int i = 0; i < numPriceLevels; i++) {
|
||||
nativeBids[i] = NativePriceSize.NO_MARKET;
|
||||
nativeAsks[i] = NativePriceSize.NO_MARKET;
|
||||
}
|
||||
for (int i = 0; i < numPriceRowsPerSide; i++) {
|
||||
priceLevels.Add(new(Side.Sell));
|
||||
}
|
||||
for (int i = 0; i < numPriceRowsPerSide; i++) {
|
||||
priceLevels.Add(new(Side.Buy));
|
||||
}
|
||||
|
||||
security = backing(nameof(Security), bts.api.Security.Defaults.SecurityImmutable);
|
||||
title = backing(nameof(Title), security.observe_current(true), s => s.ToString() ?? string.Empty, string.Empty);
|
||||
priceTick = backing(nameof(PriceTick), user.settings.RegionalTickFactors.Default);
|
||||
PropertiesVm = new(model, securityMdSource, accountsSource, propsSource.Select(p => p.order_props));
|
||||
this.quantityDisplay = backing(nameof(QuantityDisplay), quantityDisplay ?? bts.api.Security.Defaults.quantity_display_settingsi);
|
||||
priceDisplay = backing(nameof(PriceDisplay), bts.api.Security.Defaults.price_display_settingsi);
|
||||
SizeCalculator = new(0, calculatorButtons);
|
||||
SendOrderCmd = new DelegateCommand(SendOrder);
|
||||
SetPriceCmd = new DelegateCommand<double>(SetPrice);
|
||||
ShowLadderCmd = new DelegateCommand(() => {
|
||||
if (currentSecurityMd.HasValue)
|
||||
showLadder(currentSecurityMd.Value);
|
||||
});
|
||||
RecenterCmd = new DelegateCommand(Recenter);
|
||||
_dispose = Disposable.Create(
|
||||
securityMdSource.OnDispatcher().Subscribe(OnSecurityMd),
|
||||
SizeCalculator,
|
||||
propsSource.OnDispatcher().Subscribe(p => OnProps(p.price, p.size)));
|
||||
|
||||
void OnProps(
|
||||
InlineUserProperty<double> priceProp, InlineUserProperty<double> sizeProp) {
|
||||
if (priceProp.has_value && (priceProp.force_override || DoubleHelper.IsNaNOrInfinity(Price)))
|
||||
SetPrice(priceProp.value);
|
||||
if (sizeProp.has_value && (sizeProp.force_override || SizeCalculator.value == 0))
|
||||
SizeCalculator.value = sizeProp.value;
|
||||
}
|
||||
|
||||
void SetPrice(double price) {
|
||||
Price = price;
|
||||
RaisePropertyChanged(nameof(Price));
|
||||
}
|
||||
|
||||
void SendOrder() {
|
||||
bool priceValid = validatePrice();
|
||||
bool quantityValid = SizeCalculator.validate();
|
||||
if (priceValid && quantityValid && !HasErrors && !PropertiesVm.HasErrors) {
|
||||
model.send_order(Security, PropertiesVm.create(side), Price, SizeCalculator.value!.Value).ignore();
|
||||
}
|
||||
}
|
||||
|
||||
void Recenter() =>
|
||||
RequestRecenter?.Invoke();
|
||||
|
||||
void OnSecurityMd(SecurityMD smd) {
|
||||
currentSecurityMd = smd;
|
||||
var spec = TickUtils.multiply_tick_size(smd.security.regional_ticks(), 1);
|
||||
security.set(smd.security);
|
||||
priceTick.set(smd.security.regional_ticks());
|
||||
priceDisplay.set(smd.price_display);
|
||||
this.quantityDisplay.set(smd.quantity_display);
|
||||
SizeCalculator.display_settings = smd.quantity_display;
|
||||
ulong lastMdPollTime = 0;
|
||||
mdPollTimer?.Stop();
|
||||
mdPollTimer = new DispatcherTimer(TimeSpan.FromSeconds(0.1), DispatcherPriority.DataBind, PollMd, Dispatcher.CurrentDispatcher);
|
||||
|
||||
void PollMd(object? sender, EventArgs e) {
|
||||
if (smd.md.get_depth(ref lastMdPollTime, ref nativeBids, ref nativeAsks, numPriceLevels)) {
|
||||
if (double.IsNaN(priceLevels[0].Price)) {
|
||||
// Initialize prices and recenter
|
||||
if (!TryGetOrigin(out var origin)) {
|
||||
return;
|
||||
}
|
||||
double price = spec.tickdown(origin, numPriceRowsPerSide - 1);
|
||||
for (int i = priceLevels.Count - 1; i >= 0; i--) {
|
||||
priceLevels[i].Price = price;
|
||||
price = spec.tickup(price);
|
||||
}
|
||||
Recenter();
|
||||
}
|
||||
|
||||
var hi = priceLevels[0].Price;
|
||||
var lo = priceLevels[^1].Price;
|
||||
Log.assert(hi.is_real() && lo.is_real() && hi > lo);
|
||||
var wrappedPrices = priceLevels.WrapSelector(p => p.Price, null);
|
||||
var rPriceComp = spec.price_comparer().reverse();
|
||||
|
||||
// Clear old prices
|
||||
for (int i = 0; i < numPriceLevels; i++) {
|
||||
// asks
|
||||
var px = nativeAsks[i].price;
|
||||
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
|
||||
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
|
||||
if (idx < 0)
|
||||
idx = ~idx;
|
||||
priceLevels[idx].Quantity = 0;
|
||||
}
|
||||
// bids
|
||||
px = nativeBids[i].price;
|
||||
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
|
||||
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
|
||||
if (idx < 0)
|
||||
idx = ~idx;
|
||||
priceLevels[idx].Quantity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Set new prices
|
||||
for (int i = 0; i < numPriceLevels; i++) {
|
||||
// asks
|
||||
var px = nativeAsks[i].price;
|
||||
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
|
||||
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
|
||||
if (idx < 0)
|
||||
idx = ~idx;
|
||||
priceLevels[idx].Quantity = nativeAsks[i].size;
|
||||
priceLevels[idx].Side = Side.Sell;
|
||||
}
|
||||
// bids
|
||||
px = nativeBids[i].price;
|
||||
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
|
||||
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
|
||||
if (idx < 0)
|
||||
idx = ~idx;
|
||||
priceLevels[idx].Quantity = nativeBids[i].size;
|
||||
priceLevels[idx].Side = Side.Buy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TryGetOrigin(out double origin) {
|
||||
if (!nativeBids[0].is_nan) {
|
||||
origin = nativeBids[0].price;
|
||||
} else if (!nativeAsks[0].is_nan) {
|
||||
origin = nativeAsks[0].price;
|
||||
} else {
|
||||
origin = double.NaN;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? RequestRecenter;
|
||||
|
||||
public SecurityImmutable Security => security.Value;
|
||||
public string Title => title.Value;
|
||||
public double Price { get; set; }
|
||||
public user.settings.RegionalTickFactors PriceTick => priceTick.Value;
|
||||
public TickDisplaySettingsImmutable PriceDisplay => priceDisplay.Value;
|
||||
public TickDisplaySettingsImmutable QuantityDisplay => quantityDisplay.Value;
|
||||
public CalculatorLogic<double> SizeCalculator { get; }
|
||||
public Side Side {
|
||||
get => side;
|
||||
set => Set(ref side, value);
|
||||
}
|
||||
|
||||
public OrderPropertiesEditorVM PropertiesVm { get; }
|
||||
public ObservableCollection<OrderTicketLadderPriceLevelVM> PriceLevels => priceLevels;
|
||||
public ICommand SendOrderCmd { get; }
|
||||
public ICommand SetPriceCmd { get; }
|
||||
public ICommand ShowLadderCmd { get; }
|
||||
public ICommand RecenterCmd { get; }
|
||||
|
||||
bool validatePrice() {
|
||||
if (!Price.is_real()) {
|
||||
flash_error(nameof(Price)).ignore();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class OrderTicketLadderPriceLevelVM : ViewModelBase {
|
||||
Color background = Colors.Transparent;
|
||||
Color priceForeground = BTSColors.Control.normal_foreground;
|
||||
Side side;
|
||||
double price = double.NaN;
|
||||
double quantity = double.NaN;
|
||||
|
||||
public OrderTicketLadderPriceLevelVM(Side side) {
|
||||
this.side = side;
|
||||
background = BTSColors.Ladder.price_column_background;
|
||||
priceForeground = BTSColors.Ladder.price_column_foreground;
|
||||
}
|
||||
|
||||
public Side Side {
|
||||
get => side;
|
||||
set => Set(ref side, value, dependent_props: nameof(QuantityForeground));
|
||||
}
|
||||
public double Price {
|
||||
get => price;
|
||||
set => Set(ref price, value);
|
||||
}
|
||||
public double Quantity {
|
||||
get => quantity;
|
||||
set => Set(ref quantity, value);
|
||||
}
|
||||
|
||||
public Color Background {
|
||||
get => background;
|
||||
set => Set(ref background, value);
|
||||
}
|
||||
public Color PriceForeground {
|
||||
get => priceForeground;
|
||||
set => Set(ref priceForeground, value);
|
||||
}
|
||||
public Color QuantityForeground =>
|
||||
side switch {
|
||||
Side.Buy => BTSColors.Ladder.bid_column_foreground,
|
||||
Side.Sell => BTSColors.Ladder.ask_column_foreground,
|
||||
_ => BTSColors.UI.default_foreground,
|
||||
};
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="mbts.ui.spark.OrderModificationLadderControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:api="clr-namespace:bts.api;assembly=Bts.Spark.Api"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:uag="clr-namespace:mbts.ui.spark.algo"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
u:AttachedCommands.MiddleClick="{Binding RecenterCmd}" >
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/view/Colors.xaml"/>
|
||||
<ResourceDictionary Source="/view/Resources/Converters.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/Order/OrderDetailsPopupControl.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Consts -->
|
||||
<sys:Double x:Key="HorizontalSpacing">8.0</sys:Double>
|
||||
<sys:Double x:Key="VerticalSpacing">8.0</sys:Double>
|
||||
<sys:Double x:Key="ButtonHeight">22.0</sys:Double>
|
||||
|
||||
<!-- Converters -->
|
||||
<cv:NullableDoubleConverter x:Key="TickDefaultPriceConverter" DefaultValue="0"/>
|
||||
<cv:ValueMap x:Key="SideToBrushConverter" Default="{x:Static Brushes.Transparent}">
|
||||
<cv:KeyValue Key="{u:Enum Buy, Type=api:Side}" Value="{u:BTSBrush MarketData.bid, Opacity=0.33}" />
|
||||
<cv:KeyValue Key="{u:Enum Sell, Type=api:Side}" Value="{u:BTSBrush MarketData.ask, Opacity=0.33}" />
|
||||
</cv:ValueMap>
|
||||
<cv:ValueMap x:Key="SideToIsBuyConverter" Default="{u:False}">
|
||||
<cv:KeyValue Key="{u:Enum Buy, Type=api:Side}" Value="{u:True}" />
|
||||
</cv:ValueMap>
|
||||
<cv:ValueMap x:Key="SideToIsSellConverter" Default="{u:False}">
|
||||
<cv:KeyValue Key="{u:Enum Sell, Type=api:Side}" Value="{u:True}" />
|
||||
</cv:ValueMap>
|
||||
<cv:ValueMap x:Key="SideToBidVisibilityConverter" Default="{u:Visibility Collapsed}">
|
||||
<cv:KeyValue Key="{u:Enum Buy, Type=api:Side}" Value="{u:Visibility Visible}" />
|
||||
</cv:ValueMap>
|
||||
<cv:ValueMap x:Key="SideToAskVisibilityConverter" Default="{u:Visibility Collapsed}">
|
||||
<cv:KeyValue Key="{u:Enum Sell, Type=api:Side}" Value="{u:Visibility Visible}" />
|
||||
</cv:ValueMap>
|
||||
|
||||
<!-- Styles -->
|
||||
<Style TargetType="Button" BasedOn="{StaticResource DefaultButton}">
|
||||
<Setter Property="Height" Value="{StaticResource ButtonHeight}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
</Style>
|
||||
<Style TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
|
||||
<Setter Property="Height" Value="{StaticResource ButtonHeight}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="usp:OrderModificationLadderControl.IsDirty" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="usp:PriceEntryControl" BasedOn="{StaticResource {x:Type usp:PriceEntryControl}}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="usp:OrderModificationLadderControl.IsDirty" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="c:TickUpDownNullableUInt32" BasedOn="{StaticResource {x:Type c:TickUpDownNullableUInt32}}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="usp:OrderModificationLadderControl.IsDirty" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="PriceLevelItemTemplate">
|
||||
<Button
|
||||
Background="{Binding Background, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
MinWidth="96"
|
||||
Padding="4,2"
|
||||
Command="{Binding DataContext.SetPriceCmd, RelativeSource={RelativeSource AncestorType=usp:OrderModificationLadderControl}}"
|
||||
CommandParameter="{Binding Price}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" MinWidth="40" SharedSizeGroup="Orders" />
|
||||
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="BidSize" />
|
||||
<ColumnDefinition Width="Auto" MinWidth="56" SharedSizeGroup="Price" />
|
||||
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="AskSize" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{Binding QuantityForeground, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
|
||||
FontSize="{x:Static u:FontSizes.Small}"
|
||||
Visibility="{Binding Side, Converter={StaticResource SideToBidVisibilityConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{x:Static cv:TickDisplayConverter.Instance}">
|
||||
<Binding Path="Quantity" Mode="OneWay"/>
|
||||
<Binding Path="DataContext.QuantityDisplay" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationLadderControl}"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{Binding PriceForeground, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
|
||||
FontSize="{x:Static u:FontSizes.Small}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{x:Static cv:TickDisplayConverter.Instance}">
|
||||
<Binding Path="Price" Mode="OneWay"/>
|
||||
<Binding Path="DataContext.PriceDisplay" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationLadderControl}"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="{Binding QuantityForeground, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
|
||||
FontSize="{x:Static u:FontSizes.Small}"
|
||||
Visibility="{Binding Side, Converter={StaticResource SideToAskVisibilityConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{x:Static cv:TickDisplayConverter.Instance}">
|
||||
<Binding Path="Quantity" Mode="OneWay"/>
|
||||
<Binding Path="DataContext.QuantityDisplay" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationLadderControl}"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<ControlTemplate x:Key="BuyRadioButtonTemplate" TargetType="RadioButton">
|
||||
<Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="MouseOver">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="hoverOverlay" Storyboard.TargetProperty="Opacity" To="0.3" BeginTime="0" Duration="0" />
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="#555" Duration="0" />
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="CheckStates">
|
||||
<VisualState x:Name="Checked">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="{u:BTSColor MarketData.bid}" BeginTime="0" Duration="0"/>
|
||||
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="BorderBrush.Color" To="{u:BTSColor MarketData.bid}" BeginTime="0" Duration="0"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Unchecked" />
|
||||
<VisualState x:Name="Indeterminate" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Border
|
||||
x:Name="background"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
CornerRadius="2">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="Transparent" />
|
||||
</Border.Background>
|
||||
<Border.BorderBrush>
|
||||
<SolidColorBrush Color="Black"/>
|
||||
</Border.BorderBrush>
|
||||
</Border>
|
||||
<ContentPresenter
|
||||
Name="presenter"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding HorizontalContentAlignment}" />
|
||||
<Border Name="hoverOverlay" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black" Opacity="0" IsHitTestVisible="False" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
<ControlTemplate x:Key="SellRadioButtonTemplate" TargetType="RadioButton">
|
||||
<Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="MouseOver">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="hoverOverlay" Storyboard.TargetProperty="Opacity" To="0.3" BeginTime="0" Duration="0" />
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="#555" Duration="0" />
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="CheckStates">
|
||||
<VisualState x:Name="Checked">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="{u:BTSColor MarketData.ask}" BeginTime="0" Duration="0"/>
|
||||
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="BorderBrush.Color" To="{u:BTSColor MarketData.ask}" BeginTime="0" Duration="0"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Unchecked" />
|
||||
<VisualState x:Name="Indeterminate" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<Border
|
||||
x:Name="background"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
CornerRadius="2">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="Transparent" />
|
||||
</Border.Background>
|
||||
<Border.BorderBrush>
|
||||
<SolidColorBrush Color="Black"/>
|
||||
</Border.BorderBrush>
|
||||
</Border>
|
||||
<ContentPresenter
|
||||
Name="presenter"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
<Border Name="hoverOverlay" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black" Opacity="0" IsHitTestVisible="False" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
<Style x:Key="BuyRadioButtonStyle" TargetType="RadioButton">
|
||||
<Setter Property="Padding" Value="2" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="{u:BTSBrush UI.default_text}" />
|
||||
<Setter Property="Template" Value="{StaticResource BuyRadioButtonTemplate}" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SellRadioButtonStyle" TargetType="RadioButton">
|
||||
<Setter Property="Padding" Value="2" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="{u:BTSBrush UI.default_text}" />
|
||||
<Setter Property="Template" Value="{StaticResource SellRadioButtonTemplate}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSpacing}}" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Ladder (market depth) -->
|
||||
<Grid Grid.Column="2" Grid.IsSharedSizeScope="True">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Background="{u:BTSBrush header}"
|
||||
TextElement.Foreground="{u:BTSBrush header_text}"
|
||||
TextElement.FontSize="{x:Static u:FontSizes.Small}"
|
||||
TextElement.FontWeight="SemiBold">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" MinWidth="40" SharedSizeGroup="Orders" />
|
||||
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="BidSize" />
|
||||
<ColumnDefinition Width="Auto" MinWidth="56" SharedSizeGroup="Price" />
|
||||
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="AskSize" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" HorizontalAlignment="Center" Text="Orders" />
|
||||
<TextBlock Grid.Column="1" HorizontalAlignment="Center" Text="Bid" />
|
||||
<TextBlock Grid.Column="2" HorizontalAlignment="Center" Text="Price" />
|
||||
<TextBlock Grid.Column="3" HorizontalAlignment="Center" Text="Ask" />
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="1" x:Name="priceLevelsScrollViewer" MaxHeight="240">
|
||||
<ItemsControl ItemsSource="{Binding PriceLevels}" ItemTemplate="{StaticResource PriceLevelItemTemplate}" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- Order Inputs -->
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Buy/Sell toggle -->
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" MinWidth="72" />
|
||||
<ColumnDefinition Width="1*" MinWidth="72" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
GroupName="Side"
|
||||
IsChecked="{Binding Side, Converter={StaticResource SideToIsBuyConverter}}"
|
||||
Style="{StaticResource BuyRadioButtonStyle}">
|
||||
<TextBlock FontWeight="Bold" Foreground="{u:BTSBrush UI.default_text}">
|
||||
Buy
|
||||
</TextBlock>
|
||||
</RadioButton>
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
GroupName="Side"
|
||||
IsChecked="{Binding Side, Converter={StaticResource SideToIsSellConverter}}"
|
||||
Style="{StaticResource SellRadioButtonStyle}">
|
||||
<TextBlock FontWeight="Bold" Foreground="{u:BTSBrush UI.default_text}">
|
||||
Sell
|
||||
</TextBlock>
|
||||
</RadioButton>
|
||||
</Grid>
|
||||
|
||||
<usp:PriceEntryControl
|
||||
Grid.Row="2"
|
||||
usp:OrderModificationControl.IsDirty="{Binding PriceDirty, FallbackValue={x:Null}}"
|
||||
RegionalTick="{Binding PriceTick}"
|
||||
Focusable="False"
|
||||
TickDefaultValue="{Binding CurrentPrice, Converter={StaticResource TickDefaultPriceConverter}, FallbackValue={u:Double 0}}"
|
||||
ToolTip="Price"
|
||||
Value="{Binding Price}" />
|
||||
|
||||
<c:TickUpDownNullableDouble
|
||||
Grid.Row="4"
|
||||
usp:OrderModificationControl.IsDirty="{Binding QuantityDirty, FallbackValue={x:Null}}"
|
||||
CommitOnEnter="True"
|
||||
Focusable="False"
|
||||
Increment="{Binding SizeCalculator.unit_quantity}"
|
||||
Minimum="0"
|
||||
TickDisplaySettings="{Binding SizeCalculator.display_settings}"
|
||||
ToolTip="Quantity"
|
||||
Value="{Binding SizeCalculator.display_value}" />
|
||||
|
||||
<uag:OrderDetailsPopupControl
|
||||
Grid.Row="6"
|
||||
DataContext="{Binding PropertiesVm}"
|
||||
usp:OrderModificationControl.IsDirty="{Binding DataContext.order_type_dirty, RelativeSource={RelativeSource AncestorType=usp:OrderModificationControl}, FallbackValue={u:False}}"
|
||||
HasEditableProperties="{Binding can_expand, FallbackValue={u:False}}"
|
||||
IsAlgoTemplateToolBarReadOnly="True"
|
||||
IsEnabled="{Binding DataContext.can_change_order_type, RelativeSource={RelativeSource AncestorType=usp:OrderModificationControl}, FallbackValue={u:True}}"
|
||||
IsOpen="{Binding is_expanded, FallbackValue={u:False}}"
|
||||
ItemsSource="{Binding algo_definitions_view}"
|
||||
SelectedItem="{Binding selected_algo_definition}"
|
||||
ToolTip="Order/Algo Type">
|
||||
<uag:OrderDetailsPopupControl.Style>
|
||||
<Style TargetType="uag:OrderDetailsPopupControl" BasedOn="{StaticResource {x:Type uag:OrderDetailsPopupControl}}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="usp:OrderModificationControl.IsDirty" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</uag:OrderDetailsPopupControl.Style>
|
||||
</uag:OrderDetailsPopupControl>
|
||||
|
||||
<ComboBox
|
||||
Grid.Row="8"
|
||||
usp:OrderModificationControl.IsDirty="{Binding AccountDirty, FallbackValue={u:False}}"
|
||||
IsEnabled="{Binding CanChangeAccount, FallbackValue={u:True}}"
|
||||
ItemsSource="{Binding PropertiesVm.accounts}"
|
||||
SelectedItem="{Binding PropertiesVm.selected_account}"
|
||||
ToolTip="Account" />
|
||||
|
||||
<ComboBox
|
||||
Grid.Row="10"
|
||||
usp:OrderModificationControl.IsDirty="{Binding TimeInForceDirty, FallbackValue={u:False}}"
|
||||
IsEnabled="{Binding CanChangeTimeInForce, FallbackValue={u:True}}"
|
||||
ItemsSource="{Binding PropertiesVm.time_in_forces}"
|
||||
SelectedItem="{Binding PropertiesVm.selected_time_in_force}"
|
||||
ToolTip="Time In Force" />
|
||||
|
||||
<Button
|
||||
Grid.Row="12"
|
||||
Height="NaN"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="Green"
|
||||
Foreground="{u:BTSBrush UI.default_text}"
|
||||
FontSize="{x:Static u:FontSizes.Largest}"
|
||||
FontWeight="Bold"
|
||||
Command="{Binding SendOrderCmd}"
|
||||
ToolTip="{Binding Side, StringFormat='Send {0} Order'}">
|
||||
Send
|
||||
</Button>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="14"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="{x:Static u:FontSizes.Small}">
|
||||
<Hyperlink
|
||||
Command="{Binding ShowLadderCmd}"
|
||||
ToolTip="Open the original ladder for this security">Open full ladder</Hyperlink>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,51 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
interface IOrderModificationLadderVM {
|
||||
event Action? RequestRecenter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for OrderModificationLadderControl.xaml
|
||||
/// </summary>
|
||||
public partial class OrderModificationLadderControl : UserControl {
|
||||
public static readonly DependencyProperty IsDirtyProperty = DependencyProperty.RegisterAttached(
|
||||
"IsDirty", typeof(bool?), typeof(OrderModificationLadderControl), new PropertyMetadata(null));
|
||||
|
||||
public static bool? GetIsDirty(DependencyObject d) => d.GetValue(IsDirtyProperty) as bool?;
|
||||
public static void SetIsDirty(DependencyObject d, bool? value) => d.SetValue(IsDirtyProperty, value);
|
||||
|
||||
public OrderModificationLadderControl() {
|
||||
InitializeComponent();
|
||||
DataContextChanged += onDataContextChanged;
|
||||
}
|
||||
|
||||
void onDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) {
|
||||
if (e.OldValue is IOrderModificationLadderVM oldVm) {
|
||||
oldVm.RequestRecenter -= recenter;
|
||||
}
|
||||
if (e.NewValue is IOrderModificationLadderVM newVm) {
|
||||
newVm.RequestRecenter += recenter;
|
||||
}
|
||||
}
|
||||
|
||||
void recenter() {
|
||||
var targetOffset = priceLevelsScrollViewer.ScrollableHeight / 2;
|
||||
DoubleAnimation anim = new() {
|
||||
From = priceLevelsScrollViewer.VerticalOffset,
|
||||
To = targetOffset,
|
||||
Duration = TimeSpan.FromSeconds(0.15)
|
||||
};
|
||||
Storyboard sb = new();
|
||||
sb.Children.Add(anim);
|
||||
Storyboard.SetTarget(anim, priceLevelsScrollViewer);
|
||||
Storyboard.SetTargetProperty(anim, new PropertyPath(Behavior.AnimatedScrollViewerVerticalOffsetProperty));
|
||||
sb.Begin();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<Window
|
||||
x:Class="mbts.ui.spark.OrderTicketLadder"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:msys="clr-namespace:bts.api;assembly=Bts.Spark.Api"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
u:WindowStyle.TitleContent="{Binding Title, Mode=OneWay}"
|
||||
BorderBrush="#00C000"
|
||||
BorderThickness="1"
|
||||
Style="{StaticResource BTSWindowTitleTemplate}">
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/view/Resources/Converters.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<sys:Double x:Key="HorizontalSpacing">8.0</sys:Double>
|
||||
<sys:Double x:Key="VerticalSpacing">4.0</sys:Double>
|
||||
<sys:Double x:Key="ButtonHeight">22.0</sys:Double>
|
||||
|
||||
<Style x:Key="SellButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Background" Value="{u:BTSColor MarketData.ask_dark}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Content">
|
||||
<Setter.Value>
|
||||
<TextBlock VerticalAlignment="Center" Text="Sell" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
CornerRadius="0"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{u:BTSColor UI.Button.normal_border}"
|
||||
Background="{u:BTSColor MarketData.ask}"
|
||||
Padding="2"
|
||||
Height="Auto"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True"
|
||||
Name="button_content"
|
||||
/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#99c00000" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{u:BTSColor UI.Button.mouseover_border}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="BuyButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Background" Value="{u:BTSColor MarketData.bid_dark}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Content">
|
||||
<Setter.Value>
|
||||
<TextBlock VerticalAlignment="Center" Text="Buy" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
CornerRadius="0"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{u:BTSColor UI.Button.normal_border}"
|
||||
Background="{u:BTSColor MarketData.bid}"
|
||||
Padding="2"
|
||||
Height="Auto"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True"
|
||||
Name="button_content"
|
||||
/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#99167ed3" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{u:BTSColor UI.Button.mouseover_border}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
<Window.InputBindings>
|
||||
<KeyBinding Key="Esc" Command="{x:Static SystemCommands.CloseWindowCommand}"/>
|
||||
</Window.InputBindings>
|
||||
|
||||
<usp:OrderModificationLadderControl Margin="10,4" />
|
||||
|
||||
<u:WindowStyle.TitleContentTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock HorizontalAlignment="Center" FontWeight="Bold" Text="{Binding}" Style="{StaticResource TextBlock.Dark}" />
|
||||
</DataTemplate>
|
||||
</u:WindowStyle.TitleContentTemplate>
|
||||
</Window>
|
||||
@@ -1,15 +0,0 @@
|
||||
using mbts.ui.utility;
|
||||
|
||||
using System.Windows;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
/// <summary>
|
||||
/// Interaction logic for OrderTicketLadder.xaml
|
||||
/// </summary>
|
||||
public partial class OrderTicketLadder : Window {
|
||||
public OrderTicketLadder() {
|
||||
InitializeComponent();
|
||||
WindowUtilities.listen_for_close_requests(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,117 @@
|
||||
#nullable enable
|
||||
namespace mbts.ui {
|
||||
using bts.utils;
|
||||
|
||||
using Spark.Core.Reflection;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
[MarkupExtensionReturnType]
|
||||
class BTSColorExtension : MarkupExtension {
|
||||
static readonly Dictionary<string, Func<BTSColor>>
|
||||
_color_map = _normalized_color_props(typeof(BTSColors), null).ToDictionary();
|
||||
|
||||
public BTSColorExtension() { }
|
||||
|
||||
public BTSColorExtension(string? name) =>
|
||||
Name = name;
|
||||
|
||||
public BTSColorExtension(string? name, string? mixture) {
|
||||
Name = name;
|
||||
Mixture = mixture;
|
||||
}
|
||||
|
||||
[ConstructorArgument("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[ConstructorArgument("mixture")]
|
||||
public string? Mixture { get; set; }
|
||||
|
||||
public ColorMixtureMode MixtureMode { get; set; } = ColorMixtureMode.Default;
|
||||
|
||||
public Type? Type { get; set; }
|
||||
|
||||
public double Opacity { get; set; } = double.NaN;
|
||||
|
||||
public static BTSColor get_color(string name) =>
|
||||
_color_map.TryGetValue(name, out var color) ?
|
||||
color() : throw new ColorNotFoundException(name);
|
||||
|
||||
public override object? ProvideValue(IServiceProvider service_provider) {
|
||||
if (service_provider is null) {
|
||||
return null;
|
||||
}
|
||||
if (Name is null) {
|
||||
throw new InvalidOperationException("Name is null");
|
||||
}
|
||||
if (!Opacity.is_nan() && !Opacity.is_within_range(0, 1)) {
|
||||
throw new InvalidOperationException("Opacity (pct) must between 0 and 1 (if not NaN)");
|
||||
}
|
||||
|
||||
if (Name.Contains("Launcher.Button")) {
|
||||
}
|
||||
var pvt = service_provider.GetValueTargetProvider() ??
|
||||
throw new InvalidOperationException("IProvideValueTarget is null");
|
||||
var tgt_type = Type ?? _deduce_target_type(pvt);
|
||||
var names = Name.Split(',');
|
||||
var mixtures = Mixture?.Split(',');
|
||||
var color = _get_color_blend(names, mixtures, MixtureMode);
|
||||
if (!Opacity.is_nan()) {
|
||||
color = color.with((byte)(Opacity * byte.MaxValue));
|
||||
}
|
||||
return tgt_type == typeof(Color) || tgt_type == typeof(Color?)
|
||||
? (Color)color
|
||||
: (Brush)color;
|
||||
}
|
||||
|
||||
static Type _deduce_target_type(IProvideValueTarget pvt) {
|
||||
if (pvt.TargetProperty is null) {
|
||||
return typeof(Color);
|
||||
}
|
||||
if (pvt.TargetProperty is DependencyProperty dp) {
|
||||
return dp.PropertyType ?? throw new InvalidOperationException();
|
||||
}
|
||||
if (pvt.TargetObject is Setter setter) {
|
||||
return setter.Property?.PropertyType ?? typeof(Brush);
|
||||
}
|
||||
return typeof(Brush);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a (potentially weighted) blend of colors
|
||||
/// </summary>
|
||||
static BTSColor _get_color_blend(string[] names, string[]? mixture, ColorMixtureMode mode) {
|
||||
if (mixture is null || mixture.Length == 0) {
|
||||
// create a uniform blend
|
||||
return names.Select(get_color).blend(mode);
|
||||
}
|
||||
if (names.Length != mixture.Length) {
|
||||
throw new InvalidOperationException(
|
||||
"Names and Mixture must provide same number of elements");
|
||||
}
|
||||
return names.Select(get_color).Zip(mixture.Select(double.Parse)).blend();
|
||||
}
|
||||
|
||||
static IEnumerable<(string, Func<BTSColor>)> _normalized_color_props(
|
||||
Type type, string? prefix) {
|
||||
var bts_colors = ReflectionHelper.static_property_getters<BTSColor>(type)
|
||||
.Select(tup => (full_name(tup.name), tup.get));
|
||||
var media_colors = ReflectionHelper.static_property_getters<Color>(type)
|
||||
.Select(tup => (full_name(tup.name), new Func<BTSColor>(() => tup.get())));
|
||||
var nested_types = type.GetNestedTypes(BindingFlags.Public | BindingFlags.Static);
|
||||
var nested_colors = nested_types.SelectMany(
|
||||
t => _normalized_color_props(t, full_name(t.Name))).ToList();
|
||||
|
||||
return bts_colors
|
||||
.Concat(media_colors)
|
||||
.Concat(nested_colors);
|
||||
|
||||
string full_name(string prop) =>
|
||||
prefix is null ? prop : $"{prefix}.{prop}";
|
||||
}
|
||||
}
|
||||
}
|
||||
#nullable enable
|
||||
namespace mbts.ui {
|
||||
using bts.utils;
|
||||
|
||||
using Spark.Core.Reflection;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
[MarkupExtensionReturnType]
|
||||
class BTSColorExtension : MarkupExtension {
|
||||
static readonly Dictionary<string, Func<BTSColor>>
|
||||
_color_map = _normalized_color_props(typeof(BTSColors), null).ToDictionary();
|
||||
|
||||
public BTSColorExtension() { }
|
||||
|
||||
public BTSColorExtension(string? name) =>
|
||||
Name = name;
|
||||
|
||||
public BTSColorExtension(string? name, string? mixture) {
|
||||
Name = name;
|
||||
Mixture = mixture;
|
||||
}
|
||||
|
||||
[ConstructorArgument("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[ConstructorArgument("mixture")]
|
||||
public string? Mixture { get; set; }
|
||||
|
||||
public ColorMixtureMode MixtureMode { get; set; } = ColorMixtureMode.Default;
|
||||
|
||||
public Type? Type { get; set; }
|
||||
|
||||
public double Opacity { get; set; } = double.NaN;
|
||||
|
||||
public static BTSColor get_color(string name) =>
|
||||
_color_map.TryGetValue(name, out var color) ?
|
||||
color() : throw new ColorNotFoundException(name);
|
||||
|
||||
public override object? ProvideValue(IServiceProvider service_provider) {
|
||||
if (service_provider is null) {
|
||||
return null;
|
||||
}
|
||||
if (Name is null) {
|
||||
throw new InvalidOperationException("Name is null");
|
||||
}
|
||||
if (!Opacity.is_nan() && !Opacity.is_within_range(0, 1)) {
|
||||
throw new InvalidOperationException("Opacity (pct) must between 0 and 1 (if not NaN)");
|
||||
}
|
||||
|
||||
if (Name.Contains("Launcher.Button")) {
|
||||
}
|
||||
var pvt = service_provider.GetValueTargetProvider() ??
|
||||
throw new InvalidOperationException("IProvideValueTarget is null");
|
||||
var tgt_type = Type ?? _deduce_target_type(pvt);
|
||||
var names = Name.Split(',');
|
||||
var mixtures = Mixture?.Split(',');
|
||||
var color = _get_color_blend(names, mixtures, MixtureMode);
|
||||
if (!Opacity.is_nan()) {
|
||||
color = color.with((byte)(Opacity * byte.MaxValue));
|
||||
}
|
||||
return tgt_type == typeof(Color) ? (Color)color : (Brush)color;
|
||||
}
|
||||
|
||||
static Type _deduce_target_type(IProvideValueTarget pvt) {
|
||||
if (pvt.TargetProperty is null) {
|
||||
return typeof(Color);
|
||||
}
|
||||
if (pvt.TargetProperty is DependencyProperty dp) {
|
||||
return dp.PropertyType ?? throw new InvalidOperationException();
|
||||
}
|
||||
if (pvt.TargetObject is Setter setter) {
|
||||
return setter.Property?.PropertyType ?? typeof(Brush);
|
||||
}
|
||||
return typeof(Brush);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a (potentially weighted) blend of colors
|
||||
/// </summary>
|
||||
static BTSColor _get_color_blend(string[] names, string[]? mixture, ColorMixtureMode mode) {
|
||||
if (mixture is null || mixture.Length == 0) {
|
||||
// create a uniform blend
|
||||
return names.Select(get_color).blend(mode);
|
||||
}
|
||||
if (names.Length != mixture.Length) {
|
||||
throw new InvalidOperationException(
|
||||
"Names and Mixture must provide same number of elements");
|
||||
}
|
||||
return names.Select(get_color).Zip(mixture.Select(double.Parse)).blend();
|
||||
}
|
||||
|
||||
static IEnumerable<(string, Func<BTSColor>)> _normalized_color_props(
|
||||
Type type, string? prefix) {
|
||||
var bts_colors = ReflectionHelper.static_property_getters<BTSColor>(type)
|
||||
.Select(tup => (full_name(tup.name), tup.get));
|
||||
var media_colors = ReflectionHelper.static_property_getters<Color>(type)
|
||||
.Select(tup => (full_name(tup.name), new Func<BTSColor>(() => tup.get())));
|
||||
var nested_types = type.GetNestedTypes(BindingFlags.Public | BindingFlags.Static);
|
||||
var nested_colors = nested_types.SelectMany(
|
||||
t => _normalized_color_props(t, full_name(t.Name))).ToList();
|
||||
|
||||
return bts_colors
|
||||
.Concat(media_colors)
|
||||
.Concat(nested_colors);
|
||||
|
||||
string full_name(string prop) =>
|
||||
prefix is null ? prop : $"{prefix}.{prop}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,7 +380,7 @@
|
||||
<VisualState Name="Normal"/>
|
||||
<VisualState Name="MouseOver">
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="{u:BTSColor UI.ScrollBar.thumb_mouse_over}"/>
|
||||
<ColorAnimation Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="{StaticResource u:BTSColor UI.ScrollBar.thumb_mouse_over}"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState Name="Pressed">
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#nullable enable
|
||||
using mbts.spark;
|
||||
using mbts.ui.shell;
|
||||
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
partial class OrderAuxiliaryPopupResources : ResourceDictionary {
|
||||
public OrderAuxiliaryPopupResources() =>
|
||||
InitializeComponent();
|
||||
|
||||
public static string PopupChildStyleKey => nameof(PopupChildStyleKey);
|
||||
}
|
||||
|
||||
class OrderAuxiliaryPopupHeader : ViewModelBase {
|
||||
string?
|
||||
_title,
|
||||
_context;
|
||||
Color _title_foreground;
|
||||
bool _keep_open;
|
||||
|
||||
public Color title_foreground { get => _title_foreground; set => Set(ref _title_foreground, value); }
|
||||
public string? title { get => _title; set => Set(ref _title, value); }
|
||||
public string? context { get => _context; set => Set(ref _context, value); }
|
||||
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
|
||||
}
|
||||
|
||||
class OrderTicketHeader : ViewModelBase {
|
||||
bool _keep_open;
|
||||
public BBOVM? bbo { get; set; }
|
||||
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
|
||||
}
|
||||
|
||||
class OrderTicketLadderHeader : ViewModelBase {
|
||||
bool keepOpen;
|
||||
string? title;
|
||||
|
||||
public string? Title {
|
||||
get => title;
|
||||
set => Set(ref title, value);
|
||||
}
|
||||
|
||||
public bool KeepOpen {
|
||||
get => keepOpen;
|
||||
set => Set(ref keepOpen, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,10 +102,6 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type sp:NewOrderTicketLadderVM}">
|
||||
<usp:OrderModificationLadderControl />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type sp:ExistingOrderTicketVM}">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
|
||||
+2
-16
@@ -1,5 +1,5 @@
|
||||
<ResourceDictionary
|
||||
x:Class="mbts.ui.spark.OrderAuxiliaryPopupResources"
|
||||
x:Class="mbts.ui.spark.AuxiliaryDataResources"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
@@ -17,7 +17,7 @@
|
||||
<sp:MiniOrderBookView/>
|
||||
</DataTemplate>
|
||||
<!--Header (Title Bar)-->
|
||||
<DataTemplate DataType="{x:Type usp:OrderAuxiliaryPopupHeader}">
|
||||
<DataTemplate DataType="{x:Type usp:AuxiliaryDataHeader}">
|
||||
<Grid MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ContentPresenter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -101,20 +101,6 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type usp:OrderTicketLadderHeader}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock FontWeight="Bold" Text="{Binding Title}" Style="{StaticResource TextBlock.Dark}" />
|
||||
<CheckBox Grid.Column="2" VerticalAlignment="Center" Margin="4,0,0,0" Focusable="False" IsChecked="{Binding StaysOpen, RelativeSource={RelativeSource AncestorType=Popup}}">Keep Open</CheckBox>
|
||||
<Button Grid.Column="3" HorizontalAlignment="Right" Margin="4" Command="{x:Static ApplicationCommands.Close}" CommandTarget="{Binding RelativeSource={RelativeSource AncestorType=HeaderedContentControl}}" Style="{StaticResource RoundCloseButton}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<ControlTemplate x:Key="PopupChildTemplate" TargetType="HeaderedContentControl">
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
@@ -0,0 +1,36 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.shell;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.spark {
|
||||
partial class AuxiliaryDataResources : ResourceDictionary {
|
||||
public AuxiliaryDataResources() =>
|
||||
InitializeComponent();
|
||||
|
||||
public static string PopupChildStyleKey => nameof(PopupChildStyleKey);
|
||||
}
|
||||
|
||||
class AuxiliaryDataHeader : ViewModelBase {
|
||||
string
|
||||
_title,
|
||||
_context;
|
||||
Color _title_foreground;
|
||||
bool _keep_open;
|
||||
|
||||
public Color title_foreground { get => _title_foreground; set => Set(ref _title_foreground, value); }
|
||||
public string title { get => _title; set => Set(ref _title, value); }
|
||||
public string context { get => _context; set => Set(ref _context, value); }
|
||||
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
|
||||
}
|
||||
|
||||
class OrderTicketHeader : ViewModelBase {
|
||||
bool _keep_open;
|
||||
public BBOVM bbo { get; set; }
|
||||
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
|
||||
}
|
||||
|
||||
class ConstrainedTextBlock : TextBlock {
|
||||
}
|
||||
}
|
||||
@@ -26,39 +26,6 @@ namespace mbts.ui {
|
||||
public static void SetDoubleClickParameter(DependencyObject target, object value) { target.SetValue(DoubleClickParameterProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region MiddleClick
|
||||
public static readonly DependencyProperty MiddleClickProperty =
|
||||
DependencyProperty.RegisterAttached("MiddleClick", typeof(ICommand), typeof(AttachedCommands), new PropertyMetadata(onMiddleClickChanged));
|
||||
|
||||
static void onMiddleClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
var el = (UIElement)d;
|
||||
el.ClearMouseBindings(MouseAction.MiddleClick);
|
||||
if (e.NewValue is ICommand cmd) {
|
||||
var mb = new MouseBinding(cmd, new MouseGesture(MouseAction.MiddleClick));
|
||||
var bd = new Binding { Path = new PropertyPath(MiddleClickParameterProperty), Source = d };
|
||||
BindingOperations.SetBinding(mb, InputBinding.CommandParameterProperty, bd);
|
||||
el.InputBindings.Add(mb);
|
||||
}
|
||||
}
|
||||
|
||||
public static ICommand GetMiddleClick(DependencyObject target) =>
|
||||
target.GetValue(MiddleClickProperty) as ICommand;
|
||||
|
||||
public static void SetMiddleClick(DependencyObject target, ICommand value) =>
|
||||
target.SetValue(MiddleClickProperty, value);
|
||||
#endregion
|
||||
|
||||
#region MiddleClickParameter
|
||||
public static readonly DependencyProperty MiddleClickParameterProperty =
|
||||
DependencyProperty.RegisterAttached("MiddleClickParameter", typeof(object), typeof(AttachedCommands));
|
||||
|
||||
public static object GetMiddleClickParameter(DependencyObject target) =>
|
||||
target.GetValue(MiddleClickParameterProperty);
|
||||
|
||||
public static void SetMiddleClickParameter(DependencyObject target, object value) =>
|
||||
target.SetValue(MiddleClickParameterProperty, value);
|
||||
#endregion
|
||||
|
||||
#region Enter
|
||||
public static readonly DependencyProperty EnterProperty = DependencyProperty.RegisterAttached("Enter", typeof(ICommand), typeof(AttachedCommands), new PropertyMetadata(OnEnterChanged));
|
||||
static void OnEnterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Spark.Api.Orders {
|
||||
|
||||
using Spark.Api.Admin;
|
||||
using Spark.Api.MarketData;
|
||||
using Spark.Api.Position;
|
||||
using Spark.Api.Reactive;
|
||||
using Spark.Core.Disposables;
|
||||
using Spark.Core.Reactive;
|
||||
@@ -41,12 +40,10 @@ namespace Spark.Api.Orders {
|
||||
) {
|
||||
_order_service = order_service;
|
||||
var scheduler = ThreadPoolScheduler.Instance;
|
||||
if(interval is not null)
|
||||
Log.Api.info($"originally OrdersClient polling interval {interval}");
|
||||
interval = TimeSpan.FromMilliseconds(1);
|
||||
Log.Api.info($"OrdersClient polling interval {interval}");
|
||||
interval ??= TimeSpan.FromSeconds(0.1);
|
||||
var bootstraps = new DisposableBuilder();
|
||||
|
||||
positions = position_service.subscribei_position_data().Table().Cache();
|
||||
_orders = bootstraps.emplace(
|
||||
admin_client
|
||||
.permission_filter(
|
||||
@@ -76,11 +73,6 @@ namespace Spark.Api.Orders {
|
||||
.Cache()
|
||||
.ToObservable());
|
||||
|
||||
var raw_fills = admin_client.permission_filter(
|
||||
order_service.subscribei_fills().Naggle(interval.Value, scheduler),
|
||||
f => f.Account)
|
||||
.Table();
|
||||
|
||||
var fill_order_tuples_join = fills.LeftJoin(
|
||||
_orders,
|
||||
f => f.val.FillKey,
|
||||
@@ -102,12 +94,6 @@ namespace Spark.Api.Orders {
|
||||
.Cache()
|
||||
.ToObservable());
|
||||
|
||||
var raw_manual_fills = admin_client
|
||||
.permission_filter(
|
||||
position_service.subscribei_manual_fills().Naggle(interval.Value, scheduler),
|
||||
f => f.AccountId)
|
||||
.Table();
|
||||
|
||||
var manual_fill_tuples_account_join = manual_fills_securities.LeftJoin(
|
||||
admin_client.Accounts,
|
||||
mf => mf.val.RecordId,
|
||||
@@ -131,11 +117,6 @@ namespace Spark.Api.Orders {
|
||||
.Cache()
|
||||
.ToObservable());
|
||||
|
||||
positions = bootstraps.emplace(
|
||||
ComputedPositionsDataSource.create(raw_fills, raw_manual_fills)
|
||||
.Cache()
|
||||
.ToObservable());
|
||||
|
||||
_dispose = bootstraps;
|
||||
}
|
||||
public async Task<Response> EnterOrderAsync(
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
#nullable enable
|
||||
namespace Spark.Api.Position {
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
public static class ComputedPositionsDataSource {
|
||||
public static IObservable<TableUpdate<PositionUnitImmutable, int>> create(
|
||||
IObservable<TableUpdate<FillImmutable, string>> fills,
|
||||
IObservable<TableUpdate<ManualFillImmutable, int>> manual_fills) {
|
||||
var record_ids = new Dictionary<AccountSecurityKey, int>();
|
||||
int next_record_id = 1;
|
||||
|
||||
return Observable.CombineLatest(
|
||||
fills,
|
||||
manual_fills,
|
||||
(fill_update, manual_fill_update) => build_snapshot(
|
||||
fill_update.Index.Values,
|
||||
manual_fill_update.Index.Values)
|
||||
.ToDictionary(PositionUnitImmutable.key_fn))
|
||||
.Table(PositionUnitImmutable.key_fn);
|
||||
|
||||
IEnumerable<PositionUnitImmutable> build_snapshot(
|
||||
IEnumerable<FillImmutable> electronic_fills,
|
||||
IEnumerable<ManualFillImmutable> manual_fill_rows) {
|
||||
var positions = new SortedDictionary<AccountSecurityKey, PositionAccumulator>();
|
||||
|
||||
foreach (var fill in electronic_fills) {
|
||||
if (fill.FillStatus == Fill.Types.FillStatus.Busted ||
|
||||
fill.SecurityKey == 0 ||
|
||||
fill.InternalAccountId == 0) {
|
||||
continue;
|
||||
}
|
||||
var key = new AccountSecurityKey(fill.InternalAccountId, fill.SecurityKey);
|
||||
var acc = get_or_create(positions, key, fill.Account);
|
||||
acc.add_electronic_fill(fill);
|
||||
positions[key] = acc;
|
||||
}
|
||||
|
||||
foreach (var fill in manual_fill_rows) {
|
||||
if (fill.SecurityKey == 0 || fill.AccountId == 0) {
|
||||
continue;
|
||||
}
|
||||
var key = new AccountSecurityKey(fill.AccountId, fill.SecurityKey);
|
||||
var acc = get_or_create(positions, key, fill.AccountName);
|
||||
acc.add_manual_fill(fill);
|
||||
positions[key] = acc;
|
||||
}
|
||||
|
||||
foreach (var (key, acc) in positions) {
|
||||
if (!record_ids.TryGetValue(key, out var record_id)) {
|
||||
record_id = next_record_id++;
|
||||
record_ids[key] = record_id;
|
||||
}
|
||||
yield return acc.to_position(record_id);
|
||||
}
|
||||
}
|
||||
|
||||
static PositionAccumulator get_or_create(
|
||||
IDictionary<AccountSecurityKey, PositionAccumulator> positions,
|
||||
AccountSecurityKey key,
|
||||
string account_name) {
|
||||
if (!positions.TryGetValue(key, out var acc)) {
|
||||
return new PositionAccumulator(key.account_id, key.security_id, account_name);
|
||||
} else if (string.IsNullOrEmpty(acc.account) && !string.IsNullOrEmpty(account_name)) {
|
||||
acc.account = account_name;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
struct PositionAccumulator {
|
||||
public PositionAccumulator(uint account_id, uint security_id, string account) {
|
||||
this.account_id = account_id;
|
||||
this.security_id = security_id;
|
||||
this.account = account;
|
||||
day = default;
|
||||
sod = default;
|
||||
local = default;
|
||||
admin_day = default;
|
||||
admin_sod = default;
|
||||
raw_day = default;
|
||||
}
|
||||
|
||||
public readonly uint account_id;
|
||||
public readonly uint security_id;
|
||||
public string account;
|
||||
PositionEntryAccumulator day;
|
||||
PositionEntryAccumulator sod;
|
||||
PositionEntryAccumulator local;
|
||||
PositionEntryAccumulator admin_day;
|
||||
PositionEntryAccumulator admin_sod;
|
||||
PositionEntryAccumulator raw_day;
|
||||
|
||||
public void add_electronic_fill(FillImmutable fill) {
|
||||
var qty = fill.Side == Side.Sell ? -fill.LastShares : fill.LastShares;
|
||||
if (is_sod(fill.FillKind)) {
|
||||
sod.add(qty, fill.LastPrice);
|
||||
return;
|
||||
}
|
||||
day.add(qty, fill.LastPrice);
|
||||
raw_day.add(qty, fill.LastPrice);
|
||||
}
|
||||
|
||||
public void add_manual_fill(ManualFillImmutable fill) {
|
||||
var qty = fill.Side == Side.Sell ? -fill.Quantity : fill.Quantity;
|
||||
switch (fill.FillType) {
|
||||
case ManualFill.Types.FillType.AdminSod:
|
||||
sod.add(qty, fill.Price);
|
||||
admin_sod.add(qty, fill.Price);
|
||||
break;
|
||||
case ManualFill.Types.FillType.AdminDay:
|
||||
day.add(qty, fill.Price);
|
||||
raw_day.add(qty, fill.Price);
|
||||
admin_day.add(qty, fill.Price);
|
||||
break;
|
||||
default:
|
||||
day.add(qty, fill.Price);
|
||||
raw_day.add(qty, fill.Price);
|
||||
local.add(qty, fill.Price);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public PositionUnitImmutable to_position(int record_id) {
|
||||
var sod_entry = sod.to_entry();
|
||||
var day_entry = day.to_entry();
|
||||
var raw_day_entry = raw_day.to_entry();
|
||||
var local_entry = local.to_entry();
|
||||
var admin_sod_entry = admin_sod.to_entry();
|
||||
var admin_day_entry = admin_day.to_entry();
|
||||
|
||||
return new PositionUnitImmutable(new PositionUnit {
|
||||
RecordId = record_id,
|
||||
Account = account,
|
||||
AccountId = account_id,
|
||||
SecurityId = security_id,
|
||||
SodLongPos = sod_entry.LongPos,
|
||||
SodLongAvgPrice = sod_entry.LongAvgPrice,
|
||||
SodShortPos = sod_entry.ShortPos,
|
||||
SodShortAvgPrice = sod_entry.ShortAvgPrice,
|
||||
DayLongPos = day_entry.LongPos,
|
||||
DayLongAvgPrice = day_entry.LongAvgPrice,
|
||||
DayShortPos = day_entry.ShortPos,
|
||||
DayShortAvgPrice = day_entry.ShortAvgPrice,
|
||||
Sod = sod_entry,
|
||||
Day = day_entry,
|
||||
RawDay = raw_day_entry,
|
||||
Local = local_entry,
|
||||
AdminSod = admin_sod_entry,
|
||||
AdminDay = admin_day_entry
|
||||
});
|
||||
}
|
||||
|
||||
static bool is_sod(Fill.Types.FillKind kind) =>
|
||||
kind == Fill.Types.FillKind.Sod ||
|
||||
kind == Fill.Types.FillKind.SodSpreadLeg ||
|
||||
kind == Fill.Types.FillKind.SodStripLeg;
|
||||
}
|
||||
|
||||
struct PositionEntryAccumulator {
|
||||
double long_qty;
|
||||
double long_cost;
|
||||
double short_qty;
|
||||
double short_cost;
|
||||
|
||||
public void add(double signed_qty, double price) {
|
||||
if (signed_qty > 0) {
|
||||
long_qty += signed_qty;
|
||||
long_cost += signed_qty * price;
|
||||
} else if (signed_qty < 0) {
|
||||
var abs_qty = Math.Abs(signed_qty);
|
||||
short_qty += abs_qty;
|
||||
short_cost += abs_qty * price;
|
||||
}
|
||||
}
|
||||
|
||||
public PositionEntry to_entry() =>
|
||||
new() {
|
||||
LongPos = long_qty,
|
||||
LongAvgPrice = long_qty == 0 ? 0 : long_cost / long_qty,
|
||||
ShortPos = short_qty,
|
||||
ShortAvgPrice = short_qty == 0 ? 0 : short_cost / short_qty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ namespace Spark.Trading {
|
||||
using Spark.Api.Data;
|
||||
using Spark.Api.MarketData;
|
||||
using Spark.Api.Orders;
|
||||
using Spark.Api.Position;
|
||||
using Spark.Api.Reactive;
|
||||
using Spark.Core;
|
||||
using Spark.Core.Data;
|
||||
@@ -61,21 +60,13 @@ namespace Spark.Trading {
|
||||
.Cache()
|
||||
.ToObservable());
|
||||
|
||||
var raw_fills = admin_model
|
||||
.permission_filter(
|
||||
order_service.subscribei_fills().Naggle(interval, scheduler),
|
||||
f => f.Account)
|
||||
.Table();
|
||||
|
||||
var raw_manual_fills = admin_model
|
||||
.permission_filter(
|
||||
_position_service.subscribei_manual_fills().Naggle(interval, scheduler),
|
||||
f => f.AccountId)
|
||||
.Table();
|
||||
|
||||
on_positions = bootstraps.emplace(
|
||||
ComputedPositionsDataSource.create(raw_fills, raw_manual_fills)
|
||||
.join_table(sec_resolver, pos => pos.SecurityId)
|
||||
admin_model
|
||||
.permission_filter(
|
||||
_position_service.subscribei_position_data().Naggle(interval, scheduler),
|
||||
p => p.AccountId)
|
||||
.join(sec_resolver, pos => pos.SecurityId)
|
||||
.Table()
|
||||
.Cache()
|
||||
.ToObservable());
|
||||
var users = admin_model.Users
|
||||
|
||||
@@ -8,7 +8,6 @@ using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using bts.co2;
|
||||
using bts.metrics;
|
||||
using bts.md;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -87,7 +86,6 @@ public static class SparkApiServices {
|
||||
services.AddSparkdService<OrderService>();
|
||||
services.AddSingleton<IOrdersClient, OrdersClient>();
|
||||
}
|
||||
services.AddSparkdService<ShellLatencyService>();
|
||||
if (configurationSection.GetValue("EnablePosition", true)) {
|
||||
services.TryAddSingleton<ISecurityResolver>(ActivatorUtilities.GetServiceOrCreateInstance<PassiveSecurityResolver>);
|
||||
services.AddSparkdService<PositionService>()
|
||||
@@ -178,7 +176,6 @@ public static class SparkApiServices {
|
||||
services.AddSparkdService<OrderService>();
|
||||
services.AddSingleton<IOrdersClient, OrdersClient>();
|
||||
}
|
||||
services.AddSparkdService<ShellLatencyService>();
|
||||
if (isPositionsEnabled) {
|
||||
services.AddSparkdService<PositionService>();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using bts.api;
|
||||
using bts.data.ticks;
|
||||
using bts.utils;
|
||||
|
||||
using mbts.co2;
|
||||
using mbts.user.settings;
|
||||
|
||||
using Spark.Core;
|
||||
@@ -114,24 +113,6 @@ namespace mbts.spark {
|
||||
return rv;
|
||||
}
|
||||
|
||||
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
|
||||
double low = bids[bids.Length - 1].price;
|
||||
if (double.IsNaN(low))
|
||||
low = bids.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
|
||||
if (double.IsNaN(low))
|
||||
low = asks[0].price;
|
||||
return low;
|
||||
}
|
||||
|
||||
public static double high_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
|
||||
double high = asks[asks.Length - 1].price;
|
||||
if (double.IsNaN(high))
|
||||
high = asks.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
|
||||
if (double.IsNaN(high))
|
||||
high = bids[0].price;
|
||||
return high;
|
||||
}
|
||||
|
||||
public static long? tickdiff(this ContractSpecificationImmutable spec, double x, double y) =>
|
||||
get_regional_ticks(spec).tickdiff(x, y);
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ class TopOfBookQuoter : IHostedService {
|
||||
throw new ArgumentException("Failed to parse venue", nameof(config));
|
||||
}
|
||||
var quantity = config.Quantity;
|
||||
if (quantity == 0 || !double.IsRealNumber(quantity)) {
|
||||
throw new ArgumentException("Quantity must be real and non-zero", nameof(config));
|
||||
if (!(quantity > 0)) {
|
||||
throw new ArgumentException("Quantity is not positive", nameof(config));
|
||||
}
|
||||
this.config = new(venue, contract, account, config.OffsetTicks, quantity);
|
||||
}
|
||||
@@ -86,12 +86,11 @@ class TopOfBookQuoter : IHostedService {
|
||||
if (!bk.Security.HasValue) {
|
||||
return;
|
||||
}
|
||||
var orderQty = config.Quantity;
|
||||
var bidPrice = bk[orderQty < 0 ? Side.Sell : Side.Buy, 0].Price;
|
||||
orderQty = Math.Abs(orderQty);
|
||||
var bidPrice = bk[Side.Buy, 0].Price;
|
||||
var orderPrice = bidPrice;
|
||||
var orderQty = config.Quantity;
|
||||
if (config.OffsetTicks != 0) {
|
||||
orderPrice = bk.Security.Value.tick_factors().tick(orderPrice, config.OffsetTicks);
|
||||
orderPrice = bk.Security.Value.tick_factors().tickdown(orderPrice, config.OffsetTicks);
|
||||
}
|
||||
if (securityToOrderId.TryGetValue(bk.SecurityId, out var orderId)
|
||||
&& orders.TryGetValue(orderId, out var orderTree)
|
||||
|
||||
@@ -4,7 +4,7 @@ using bts.api;
|
||||
|
||||
public static class VenueParser {
|
||||
public static bool TryParse(string? venueStr, out Venue venue) {
|
||||
if (venueStr is null || (!Enum.TryParse(venueStr, true, out venue) && !Enum.TryParse("VENUE_" + venueStr, true, out venue))) {
|
||||
if (venueStr is null || (!Enum.TryParse(venueStr, false, out venue) && !Enum.TryParse("VENUE_" + venueStr, false, out venue))) {
|
||||
venue = Venue.Unset;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ namespace Bts.Spark.Sdk;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.metrics;
|
||||
|
||||
using global::Spark.Api.Admin;
|
||||
using global::Spark.Api.MarketData;
|
||||
@@ -29,7 +28,6 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
|
||||
readonly IOrdersClient ordersClient;
|
||||
readonly IAdminClient adminClient;
|
||||
readonly ISecurityResolver securityResolver;
|
||||
readonly ShellLatencyService shellLatencyService;
|
||||
readonly ISparkLoginService loginService;
|
||||
readonly SimpleSparkClientConfig config;
|
||||
readonly ILogger<AuthenticatedSimpleSparkClient> logger;
|
||||
@@ -41,7 +39,6 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
|
||||
IOrdersClient ordersClient,
|
||||
IAdminClient adminClient,
|
||||
ISecurityResolver securityResolver,
|
||||
ShellLatencyService shellLatencyService,
|
||||
ISparkLoginService loginService,
|
||||
IOptions<SimpleSparkClientConfig> config,
|
||||
ILogger<AuthenticatedSimpleSparkClient> logger) {
|
||||
@@ -49,7 +46,6 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
|
||||
this.ordersClient = ordersClient;
|
||||
this.adminClient = adminClient;
|
||||
this.securityResolver = securityResolver;
|
||||
this.shellLatencyService = shellLatencyService;
|
||||
this.loginService = loginService;
|
||||
this.config = config.Value;
|
||||
this.logger = logger;
|
||||
@@ -65,7 +61,7 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var user = loginRes.user.Value;
|
||||
simpleClient = new(user.name, user.id, mdClient, ordersClient, shellLatencyService, adminClient, securityResolver, config.OrderPoolCapacity, config.OrderPoolReplenishInterval);
|
||||
simpleClient = new(user.name, user.id, mdClient, ordersClient, adminClient, securityResolver, config.OrderPoolCapacity, config.OrderPoolReplenishInterval);
|
||||
simpleClientSubject.OnNext(simpleClient);
|
||||
logger.LogInformation("SimpleSparkClient started");
|
||||
}
|
||||
@@ -101,9 +97,6 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
|
||||
public IObservable<TableUpdate<PriceTicker, uint>> PriceTicker(SecurityCriteria? securityCriteria) =>
|
||||
simpleClient?.PriceTicker(securityCriteria) ?? simpleClientSubject.Take(1).Select(c => c.PriceTicker(securityCriteria)).FlattenSeries();
|
||||
|
||||
public Task<bool> PingAsync() =>
|
||||
simpleClient?.PingAsync() ?? pingBeforeStartAsync();
|
||||
|
||||
public Task<Response> CreateOrder(OrderDefinition orderDef, double quantity, double price) =>
|
||||
orderOp(cli => cli.CreateOrder(orderDef, quantity, price));
|
||||
|
||||
@@ -141,15 +134,4 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
|
||||
async Task<T> LateImpl() =>
|
||||
await op(await simpleClientSubject.FirstAsync());
|
||||
}
|
||||
|
||||
async Task<bool> pingBeforeStartAsync() {
|
||||
var request = new BufferMessage() {
|
||||
Sequence = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
Data = "spark-sdk-ping"
|
||||
};
|
||||
var result = await shellLatencyService.ping_async(request);
|
||||
return result.success &&
|
||||
result.value.Sequence == request.Sequence &&
|
||||
result.value.Data == request.Data;
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,6 @@ public class ExecutionFacadeSimpleSparkClient : ISimpleSparkClient {
|
||||
public IObservable<TableUpdate<PriceTicker, uint>> PriceTicker(SecurityCriteria? securityCriteria) =>
|
||||
sparkClient.PriceTicker(securityCriteria);
|
||||
|
||||
public Task<bool> PingAsync() =>
|
||||
sparkClient.PingAsync();
|
||||
|
||||
public Task<Response> CreateOrder(OrderDefinition orderDef, double quantity, double price) {
|
||||
logger.LogInformation("CreateOrder {account} {tag} {securityId} {side} {quantity} @ {price}", orderDef.Account, orderDef.Tag, orderDef.SecurityKey, orderDef.Side, quantity, price);
|
||||
return Task.FromResult(facadeResponse());
|
||||
|
||||
@@ -12,8 +12,6 @@ using mbts.spark;
|
||||
using System;
|
||||
|
||||
public interface ISimpleSparkClient {
|
||||
Task<bool> PingAsync();
|
||||
|
||||
// Market Data
|
||||
IObservable<TableUpdate<SecurityImmutable, uint>> Securities(SecurityCriteria? securityCriteria);
|
||||
IObservable<TableUpdate<BookRecord, uint>> Books(SecurityCriteria? securityCriteria);
|
||||
|
||||
@@ -16,14 +16,12 @@ using global::Spark.Core.Disposables;
|
||||
using global::Spark.Core.Reactive;
|
||||
|
||||
using mbts.spark;
|
||||
using bts.metrics;
|
||||
|
||||
using System.Reactive.Linq;
|
||||
|
||||
public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
|
||||
readonly IMarketDataClient mdClient;
|
||||
readonly IOrdersClient ordersClient;
|
||||
readonly ShellLatencyService shellLatencyService;
|
||||
readonly OrderPool limitOrderPool;
|
||||
readonly OrderPool marketOrderPool;
|
||||
readonly TableUpdateDemuxer<OrderRecordTupleWithRoot, uint, uint> orderSecurityDmx;
|
||||
@@ -48,7 +46,6 @@ public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
|
||||
uint userId,
|
||||
IMarketDataClient mdClient,
|
||||
IOrdersClient ordersClient,
|
||||
ShellLatencyService shellLatencyService,
|
||||
IAdminClient adminClient,
|
||||
ISecurityResolver securityResolver,
|
||||
int orderPoolCapacity,
|
||||
@@ -56,7 +53,6 @@ public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
|
||||
) {
|
||||
this.mdClient = mdClient;
|
||||
this.ordersClient = ordersClient;
|
||||
this.shellLatencyService = shellLatencyService;
|
||||
limitOrderPool = new(orderPoolCapacity, () => new() { User = userName, UserId = userId, TimeInForce = TimeInForce.Gtc, CustomDef = new() { Limit = new() } });
|
||||
marketOrderPool = new(orderPoolCapacity, () => new() { User = userName, UserId = userId, TimeInForce = TimeInForce.Gtc, CustomDef = new() { Market = new() } });
|
||||
orderSecurityDmx = new TableUpdateDemuxer<OrderRecordTupleWithRoot, uint, uint>(o => o.order.order.Key, o => o.security_id);
|
||||
@@ -144,17 +140,6 @@ public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PingAsync() {
|
||||
var request = new BufferMessage() {
|
||||
Sequence = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
Data = "spark-sdk-ping"
|
||||
};
|
||||
var result = await shellLatencyService.ping_async(request);
|
||||
return result.success &&
|
||||
result.value.Sequence == request.Sequence &&
|
||||
result.value.Data == request.Data;
|
||||
}
|
||||
|
||||
public IObservable<TableUpdate<BookRecord, uint>> Books(SecurityCriteria? securityCriteria) {
|
||||
var booksSource = this.booksSource;
|
||||
var secFilter = securityCriteria?.ToPredicate();
|
||||
@@ -286,32 +271,28 @@ public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
|
||||
public Task<Response> CreateLimitOrder(string account, Venue venue, string contract, Side side, double quantity, double price, string? tag) {
|
||||
var def = buildOrder(account, venue, contract, limitOrderPool);
|
||||
def.Side = side;
|
||||
if (tag is not null)
|
||||
def.Tag = tag;
|
||||
def.Tag = tag;
|
||||
return ordersClient.TryEnterOrderAsync(def, price, quantity);
|
||||
}
|
||||
|
||||
public Task<Response> CreateLimitOrder(string account, uint securityId, Side side, double quantity, double price, string? tag) {
|
||||
var def = buildOrder(account, securityId, limitOrderPool);
|
||||
def.Side = side;
|
||||
if (tag is not null)
|
||||
def.Tag = tag;
|
||||
def.Tag = tag;
|
||||
return ordersClient.TryEnterOrderAsync(def, price, quantity);
|
||||
}
|
||||
|
||||
public Task<Response> CreateMarketOrder(string account, Venue venue, string contract, Side side, double quantity, string? tag) {
|
||||
var def = buildOrder(account, venue, contract, marketOrderPool);
|
||||
def.Side = side;
|
||||
if (tag is not null)
|
||||
def.Tag = tag;
|
||||
def.Tag = tag;
|
||||
return ordersClient.TryEnterOrderAsync(def, double.NaN, quantity);
|
||||
}
|
||||
|
||||
public Task<Response> CreateMarketOrder(string account, uint securityId, Side side, double quantity, string? tag) {
|
||||
var def = buildOrder(account, securityId, marketOrderPool);
|
||||
def.Side = side;
|
||||
if (tag is not null)
|
||||
def.Tag = tag;
|
||||
def.Tag = tag;
|
||||
return ordersClient.TryEnterOrderAsync(def, double.NaN, quantity);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# source: tools/cpp_helper/generate-cpps.ps1
|
||||
#
|
||||
|
||||
CppConfig
|
||||
{
|
||||
value
|
||||
CppConfig
|
||||
{
|
||||
value
|
||||
{
|
||||
cpp_entry { key: "0A" value: 100 }
|
||||
cpp_entry { key: "0B" value: 100 }
|
||||
@@ -537,31 +537,6 @@ CppConfig
|
||||
cpp_entry { key: "EQ1" value: 50 }
|
||||
cpp_entry { key: "ERL" value: 5 }
|
||||
cpp_entry { key: "ES" value: 50 }
|
||||
cpp_entry { key: "E1A" value: 50}
|
||||
cpp_entry { key: "E1B" value: 50}
|
||||
cpp_entry { key: "E1C" value: 50}
|
||||
cpp_entry { key: "E1D" value: 50}
|
||||
cpp_entry { key: "E2A" value: 50}
|
||||
cpp_entry { key: "E2B" value: 50}
|
||||
cpp_entry { key: "E2C" value: 50}
|
||||
cpp_entry { key: "E2D" value: 50}
|
||||
cpp_entry { key: "E3A" value: 50}
|
||||
cpp_entry { key: "E3B" value: 50}
|
||||
cpp_entry { key: "E3C" value: 50}
|
||||
cpp_entry { key: "E3D" value: 50}
|
||||
cpp_entry { key: "E4A" value: 50}
|
||||
cpp_entry { key: "E4B" value: 50}
|
||||
cpp_entry { key: "E4C" value: 50}
|
||||
cpp_entry { key: "E4D" value: 50}
|
||||
cpp_entry { key: "E5A" value: 50}
|
||||
cpp_entry { key: "E5B" value: 50}
|
||||
cpp_entry { key: "E5C" value: 50}
|
||||
cpp_entry { key: "E5D" value: 50}
|
||||
cpp_entry { key: "EW" value: 50}
|
||||
cpp_entry { key: "EW1" value: 50}
|
||||
cpp_entry { key: "EW2" value: 50}
|
||||
cpp_entry { key: "EW3" value: 50}
|
||||
cpp_entry { key: "EW4" value: 50}
|
||||
cpp_entry { key: "ES1" value: 50 }
|
||||
cpp_entry { key: "ES2" value: 50 }
|
||||
cpp_entry { key: "ESB" value: 1000 }
|
||||
@@ -1026,31 +1001,6 @@ CppConfig
|
||||
cpp_entry { key: "NOO" value: 8900 }
|
||||
cpp_entry { key: "NPG" value: 2500 }
|
||||
cpp_entry { key: "NQ" value: 20 }
|
||||
cpp_entry {key: "Q1A" value: 20}
|
||||
cpp_entry {key: "Q1B" value: 20}
|
||||
cpp_entry {key: "Q1C" value: 20}
|
||||
cpp_entry {key: "Q1D" value: 20}
|
||||
cpp_entry {key: "Q2A" value: 20}
|
||||
cpp_entry {key: "Q2B" value: 20}
|
||||
cpp_entry {key: "Q2C" value: 20}
|
||||
cpp_entry {key: "Q2D" value: 20}
|
||||
cpp_entry {key: "Q3A" value: 20}
|
||||
cpp_entry {key: "Q3B" value: 20}
|
||||
cpp_entry {key: "Q3C" value: 20}
|
||||
cpp_entry {key: "Q3D" value: 20}
|
||||
cpp_entry {key: "Q4A" value: 20}
|
||||
cpp_entry {key: "Q4B" value: 20}
|
||||
cpp_entry {key: "Q4C" value: 20}
|
||||
cpp_entry {key: "Q4D" value: 20}
|
||||
cpp_entry {key: "Q5A" value: 20}
|
||||
cpp_entry {key: "Q5B" value: 20}
|
||||
cpp_entry {key: "Q5C" value: 20}
|
||||
cpp_entry {key: "Q5D" value: 20}
|
||||
cpp_entry {key: "QNE" value: 20}
|
||||
cpp_entry {key: "QN1" value: 20}
|
||||
cpp_entry {key: "QN2" value: 20}
|
||||
cpp_entry {key: "QN3" value: 20}
|
||||
cpp_entry {key: "QN4" value: 20}
|
||||
cpp_entry { key: "NQQ" value: 20 }
|
||||
cpp_entry { key: "NQT" value: 20 }
|
||||
cpp_entry { key: "NR" value: 2500 }
|
||||
@@ -1556,12 +1506,12 @@ CppConfig
|
||||
cpp_entry { key: "ZT" value: 2000 }
|
||||
cpp_entry { key: "ZW" value: 50 }
|
||||
cpp_entry { key: "ZWT" value: 50 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CmeSymbolMics
|
||||
{
|
||||
value
|
||||
CmeSymbolMics
|
||||
{
|
||||
value
|
||||
{
|
||||
cme_mic { key: "0A" value: NYMEX }
|
||||
cme_mic { key: "0B" value: NYMEX }
|
||||
@@ -3062,5 +3012,5 @@ CmeSymbolMics
|
||||
cme_mic { key: "ZT" value: CBT }
|
||||
cme_mic { key: "ZW" value: CBT }
|
||||
cme_mic { key: "ZWT" value: CBT }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,5 @@ OrdermanCfg {
|
||||
poll: false
|
||||
hosted_actor: "*"
|
||||
}
|
||||
fallback_cust_order_handling_inst: "Y"
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,9 @@ FillsSoftCleanup {
|
||||
}
|
||||
|
||||
OrdersSoftCleanup {
|
||||
value {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Mdp3OptionExpirationCutoff {
|
||||
value {
|
||||
max_days_until_expiration: 14
|
||||
}
|
||||
value {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
PlacementInQueue {
|
||||
value{
|
||||
|
||||
@@ -32,17 +32,11 @@ message SoundNotifications {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message FilterExpiredFills {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
// MDP3 will not publish CME options whose maturity is beyond this many days.
|
||||
// A value of zero disables this cutoff.
|
||||
message Mdp3OptionExpirationCutoff {
|
||||
uint32 max_days_until_expiration = 1;
|
||||
}
|
||||
|
||||
message VolumeAtPrice {
|
||||
message FilterExpiredFills {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message VolumeAtPrice {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
@@ -130,8 +124,3 @@ message VolSurfaceUsageUnit{
|
||||
message VolSurfaceUsage{
|
||||
repeated VolSurfaceUsageUnit items = 1;
|
||||
}
|
||||
|
||||
message LogTrades{
|
||||
string sec_name = 1;
|
||||
bool log = 2;
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "spark/orderman/orderman_utils/orderman_common.h"
|
||||
#include "spark/tracing/bts_trace.h"
|
||||
#include "spark/utils/TimeStamp.h"
|
||||
#include "spark/utils/bts_assert.h"
|
||||
#include "spark/utils/bts_chrono.h"
|
||||
#include "spark/utils/datetime.h"
|
||||
#include "spark/utils/thread_manager.h"
|
||||
@@ -98,8 +97,7 @@ struct CoreGateway::PImpl {
|
||||
: cfg(cfg), observer(observer), make_connection(conn_factory),
|
||||
make_store(store_factory), state(State::OFF), start_of_pause(),
|
||||
session_info(), event_queue(), timer_thread(), destroy(false),
|
||||
is_on(false), has_replayed(false), gave_up_permanently(false),
|
||||
send_batch(), recv_batch(),
|
||||
is_on(false), has_replayed(false), send_batch(), recv_batch(),
|
||||
connection_attempts(0), perf_log(l) {
|
||||
send_batch.reserve(1024);
|
||||
recv_batch.reserve(1024);
|
||||
@@ -118,7 +116,6 @@ struct CoreGateway::PImpl {
|
||||
std::atomic<bool> destroy;
|
||||
bool is_on;
|
||||
bool has_replayed;
|
||||
bool gave_up_permanently;
|
||||
std::vector<EventData> send_batch;
|
||||
std::vector<EventData> recv_batch;
|
||||
uint32_t connection_attempts;
|
||||
@@ -332,11 +329,6 @@ void CoreGateway::sync_session_ready(EventData const &) {
|
||||
case State::WSESSION_TERMINATE:
|
||||
break;
|
||||
case State::CONNECTING:
|
||||
LOG(Jetfix, Info,
|
||||
"Jetfix session {} is now READY after socket connect. attempts={} "
|
||||
"max_attempts={}",
|
||||
me->cfg.name(), me->connection_attempts,
|
||||
me->cfg.max_connection_attempts());
|
||||
transition(State::READY);
|
||||
me->observer.ready();
|
||||
break;
|
||||
@@ -354,18 +346,10 @@ void CoreGateway::sync_session_terminated(EventData const &) {
|
||||
break;
|
||||
|
||||
case State::WSESSION_TERMINATE:
|
||||
LOG(Jetfix, Warn,
|
||||
"Session terminated callback received for {} while waiting for "
|
||||
"reader disconnect. attempts_so_far={}",
|
||||
me->cfg.name(), me->connection_attempts);
|
||||
pause_or_shutdown();
|
||||
break;
|
||||
|
||||
case State::READY:
|
||||
LOG(Jetfix, Warn,
|
||||
"Session terminated for {} from READY. Closing socket and waiting "
|
||||
"for reader disconnect.",
|
||||
me->cfg.name());
|
||||
me->session_info->session.reset();
|
||||
me->session_info->connection->shutdown();
|
||||
transition(State::WREADER_DISCONNECT);
|
||||
@@ -373,10 +357,6 @@ void CoreGateway::sync_session_terminated(EventData const &) {
|
||||
break;
|
||||
|
||||
case State::CONNECTING:
|
||||
LOG(Jetfix, Warn,
|
||||
"Session terminated for {} during CONNECTING before READY. "
|
||||
"Closing socket and waiting for reader disconnect.",
|
||||
me->cfg.name());
|
||||
me->session_info->session.reset();
|
||||
me->session_info->connection->shutdown();
|
||||
transition(State::WREADER_DISCONNECT);
|
||||
@@ -396,28 +376,17 @@ void CoreGateway::sync_reader_disconnected(EventData const &) {
|
||||
break;
|
||||
|
||||
case State::READY:
|
||||
LOG(Jetfix, Warn,
|
||||
"Socket reader disconnected for {} from READY. "
|
||||
"The exchange or network closed the TCP session.",
|
||||
me->cfg.name());
|
||||
me->session_info->session.reset();
|
||||
me->observer.disconnect();
|
||||
transition(State::WSESSION_TERMINATE);
|
||||
break;
|
||||
|
||||
case State::CONNECTING:
|
||||
LOG(Jetfix, Warn,
|
||||
"Socket reader disconnected for {} during CONNECTING before READY. "
|
||||
"The TCP socket connected, but the session never completed logon.",
|
||||
me->cfg.name());
|
||||
me->session_info->session.reset();
|
||||
transition(State::WSESSION_TERMINATE);
|
||||
break;
|
||||
|
||||
case State::WREADER_DISCONNECT:
|
||||
LOG(Jetfix, Info,
|
||||
"Reader disconnect completed for {} after session shutdown.",
|
||||
me->cfg.name());
|
||||
pause_or_shutdown();
|
||||
break;
|
||||
|
||||
@@ -461,46 +430,27 @@ void CoreGateway::sync_in_msg_sequenced(EventData const &ev) {
|
||||
bts_assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void CoreGateway::sync_time(EventData const &ev) {
|
||||
switch (me->state) {
|
||||
case State::OFF:
|
||||
break;
|
||||
case State::PAUSED:
|
||||
if (me->gave_up_permanently)
|
||||
break;
|
||||
|
||||
if (datetime::is_at_least_secs_old(
|
||||
me->start_of_pause, me->cfg.seconds_to_pause())) {
|
||||
LOG(Jetfix, Info,
|
||||
"Pause ended for jetfix connection {}. pause_secs={} "
|
||||
"attempts_so_far={} max_attempts={} host={}:{} segment={} "
|
||||
"session_window={}-{} {}-{} replayed={}",
|
||||
me->cfg.name(), me->cfg.seconds_to_pause(),
|
||||
me->connection_attempts, me->cfg.max_connection_attempts(),
|
||||
me->cfg.socket_connect_host_ip(), me->cfg.socket_connect_port(),
|
||||
me->cfg.cme_market_segment_id(), me->cfg.start_time(),
|
||||
me->cfg.end_time(), me->cfg.start_day(), me->cfg.end_day(),
|
||||
me->has_replayed);
|
||||
LOG(Jetfix, Info, "Pause period ended for jetfix connection {}",
|
||||
me->cfg.name());
|
||||
me->session_info = std::make_unique<SessionInfo>();
|
||||
|
||||
me->session_info->store.reset(me->make_store(me->cfg));
|
||||
auto const session_active = me->session_info->store->session_active();
|
||||
LOG(Jetfix, Info,
|
||||
"Loaded FIX store for {}. session_active={} next_attempt={} "
|
||||
"max_attempts={}",
|
||||
me->cfg.name(), session_active, me->connection_attempts + 1,
|
||||
me->cfg.max_connection_attempts());
|
||||
if (session_active) {
|
||||
|
||||
if (me->session_info->store->session_active()) {
|
||||
me->connection_attempts++;
|
||||
LOG(Jetfix, Info,
|
||||
"Attempting socket connect for jetfix connection {}. "
|
||||
"attempt={} max_attempts={}",
|
||||
me->cfg.name(), me->connection_attempts,
|
||||
me->cfg.max_connection_attempts());
|
||||
|
||||
if (!me->has_replayed) {
|
||||
auto msgs_to_replay = me->session_info->store->replay();
|
||||
LOG(Jetfix, Warn,
|
||||
"Replaying {} FIX msgs for {} before reconnect",
|
||||
msgs_to_replay.size(), me->cfg.name());
|
||||
LOG(Jetfix, Warn, "Replaying {} FIX msgs",
|
||||
msgs_to_replay.size());
|
||||
me->observer.replay(msgs_to_replay);
|
||||
me->has_replayed = true;
|
||||
}
|
||||
@@ -508,20 +458,14 @@ void CoreGateway::sync_time(EventData const &ev) {
|
||||
if (me->connection_attempts > me->cfg.max_connection_attempts()) {
|
||||
me->session_info.reset();
|
||||
me->start_of_pause = datetime::now();
|
||||
me->gave_up_permanently = true;
|
||||
LOG(Jetfix, Error,
|
||||
"Failed to connect to exchange gateway {} too many "
|
||||
"times. Giving up permanently. attempts={} "
|
||||
"max_attempts={} pause_secs={} host={}:{} segment={}. "
|
||||
"You must bounce the server to recover.",
|
||||
me->cfg.name(), me->connection_attempts,
|
||||
me->cfg.max_connection_attempts(),
|
||||
me->cfg.seconds_to_pause(),
|
||||
me->cfg.socket_connect_host_ip(),
|
||||
me->cfg.socket_connect_port(),
|
||||
me->cfg.cme_market_segment_id());
|
||||
"times. Giving up permanently. You must bounce "
|
||||
"the server to recover.",
|
||||
me->cfg.name());
|
||||
break;
|
||||
}
|
||||
|
||||
me->session_info->connection.reset(me->make_connection(me->cfg));
|
||||
LOG(Jetfix, Warn, "Trying to connect to exchange for {} ({}/{})",
|
||||
me->cfg.name(), me->connection_attempts,
|
||||
@@ -545,18 +489,11 @@ void CoreGateway::sync_time(EventData const &ev) {
|
||||
me->session_info.reset();
|
||||
me->start_of_pause = datetime::now();
|
||||
LOG(Jetfix, Warn,
|
||||
"Failed to connect to exchange for {}. ({}/{}) "
|
||||
"Will retry after {} seconds.",
|
||||
"Failed to connect to exchange for {}. ({}/{}) ",
|
||||
me->cfg.name(), me->connection_attempts,
|
||||
me->cfg.max_connection_attempts(),
|
||||
me->cfg.seconds_to_pause());
|
||||
me->cfg.max_connection_attempts());
|
||||
}
|
||||
} else {
|
||||
LOG(Jetfix, Warn,
|
||||
"FIX store for {} is not active yet. Deferring reconnect. "
|
||||
"session_window={}-{} {}-{}",
|
||||
me->cfg.name(), me->cfg.start_time(), me->cfg.end_time(),
|
||||
me->cfg.start_day(), me->cfg.end_day());
|
||||
me->session_info.reset();
|
||||
me->start_of_pause = datetime::now();
|
||||
}
|
||||
@@ -579,12 +516,6 @@ void CoreGateway::pause_or_shutdown() {
|
||||
|
||||
if (me->is_on) {
|
||||
me->start_of_pause = datetime::now();
|
||||
LOG(Jetfix, Info,
|
||||
"Pausing jetfix gateway {}. pause_secs={} attempts_so_far={} "
|
||||
"max_attempts={} gave_up_permanently={}",
|
||||
me->cfg.name(), me->cfg.seconds_to_pause(),
|
||||
me->connection_attempts, me->cfg.max_connection_attempts(),
|
||||
me->gave_up_permanently);
|
||||
transition(State::PAUSED);
|
||||
} else
|
||||
transition(State::OFF);
|
||||
|
||||
@@ -114,15 +114,6 @@ void Session::start() {
|
||||
me->start_of_logon = me->now();
|
||||
me->last_seq_num = me->begin_of_this_session;
|
||||
me->last_seq_num_processed = me->store.last_seq_num_processed();
|
||||
LOG(Jetfix, Info,
|
||||
"Starting jetfix session {}. sender_compid={} sender_subid={} "
|
||||
"target_compid={} target_subid={} secure_logon={} raw_data_len={} "
|
||||
"user_name_present={} seq_num={} last_processed={}",
|
||||
me->cfg.name(), me->cfg.sender_compid(), me->cfg.sender_subid(),
|
||||
me->cfg.target_compid(), me->cfg.target_subid(),
|
||||
me->cfg.use_cme_secure_logon(), me->cfg.raw_data().size(),
|
||||
!me->cfg.user_name().empty(), me->last_seq_num + 1,
|
||||
me->last_seq_num_processed);
|
||||
transition(State::WLOGON);
|
||||
internal_send(make_logon(
|
||||
me->last_seq_num + 1, me->last_seq_num_processed, *me->current_time,
|
||||
|
||||
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user