Compare commits

...
Author SHA1 Message Date
John Colvin 9deb8cc237 fix build: Trivial changes to fix emumba_deribit build
* Use "Unix Makefiles" generator when building websocket-library.
  - Ninja tries to build things in parallel, and doesn't build the
    ExternalProject dependencies in the right order. Consequently, the
    websocket-library build fails because it can't find "libspdlog.a".

* Replace a few leftover SPDLOG_ERROR macros w/ LOG_ERROR (the latter
  logs in our system, not spdlog).
2025-01-10 19:36:18 +00:00
Mohammad Arham 11ff46a056 scafolding 2025-01-09 19:56:54 +00:00
John Colvin ba4f8f67e3 fix: Disable binance usds futs symbol filter config
This was a premature optimization, and has become a burden now that we
need to subscribe to large symbol sets for Alphaworx.
2025-01-06 05:00:04 +00:00
John Colvin 9c78380490 fix build (gcc rel): Don't fallthrough switch cases 2025-01-06 03:52:25 +00:00
John Colvin f6166f0674 fix: Show iceberg show qty in algo params grid
Bugs (fixed):
  * In the ladder, you can't view/edit the iceberg show quantity.

  Cause: No view template was defined for nullable double params. Most
  likely, we changed the show qty in the vm from double to nullable
  double at some point, and forgot to reflect this change in the view.
  An easy mistake to make as it would not have broken the build.
2025-01-05 15:58:29 -05:00
John Colvin 6334a902e7 fix: Reclaim keyboard focus when mview popup is clicked 2025-01-05 15:29:11 -05:00
John Colvin 082ca62fff fix: Focus price textbox when mview order ticket popup opens 2025-01-05 15:18:46 -05:00
John Colvin 9dbd8dd7c6 fix: Set focus through FocusScope element if one exists 2025-01-05 15:18:46 -05:00
John Colvin 8f7a4d0df6 Fix typo in AutoFocusTargetProvider dependency prop 2025-01-05 15:18:46 -05:00
John Colvin f372dd56b3 fix: Return synthetic name in get_security_desc
If the security is a synthetic (of course).

Bugs (fixed):
  * Changing limits in the risk ui causes sparkd to assert crash
2025-01-05 20:15:10 +00:00
John Colvin 86eba20d61 build: Make compiler/build type case insensitive 2025-01-05 06:25:11 +00:00
John Colvin 35a985280d fix: Run heartbeat timer in MarketDataGateway
This is why we were getting those heartbeat asserts. Previously, only
ExchangeSimApplication would start this timer. Since MarketDataGateway
doesn't share its MDProducer with ExchangeSimApplication, it must start
one itself.
2025-01-03 16:59:07 +00:00
John Colvin f5e1b3319c cleanup: Don't write to cout in SparkDataDirectory
Don't need that noise.
2025-01-03 16:59:07 +00:00
John Colvin 65c3948e4f cleanup: Remove unused code 2025-01-03 16:59:07 +00:00
John Colvin 002bdff9a3 fix: Don't parse non-existent json files in SparkDataDirectory
Bugs (fixed):
  * Sparkd assert crashes if binance_usds_futures_execution.json file
    doesn't exist
2025-01-03 16:59:07 +00:00
John Colvin bb6e5dd548 diag: Fix sparkd log path in show_last_run.sh 2025-01-03 16:59:07 +00:00
John Colvin 7c1aed3339 fix build (api server): Remove old cpps from source file list 2025-01-03 16:59:07 +00:00
John Colvin 22ae51fb99 build: Clean up build py scripts 2025-01-03 16:59:07 +00:00
John Colvin a53d347f81 fix build (warns): Update cmake min version to 3.10
Versions < 3.10 will soon be deprecated (we just started getting
warnings about it).
2025-01-03 16:59:07 +00:00
John Colvin 838dbcd886 build: In py build scripts, exit with code 1 on failure
Previously, they would exit with 0 no matter what happened.
2025-01-03 16:59:07 +00:00
John Colvin 5ed2309d22 fix: Add switch case for VENUE_BTS in VenueIndex 2025-01-03 16:59:07 +00:00
John Colvin 4d41704dd2 build: Configure admin webapp build entirely in CMakeLists
Get rid of build_admin_web_app.py
2025-01-03 16:59:07 +00:00
John Colvin bd1000e518 build: Get rid of explicit "targets" in CMakePresets for sparkd
So we don't have to worry about keeping it synchronized with the actual
targets in the build (just one more thing that can break). You can build
specific targets via the cmake cli anyways.
2025-01-03 16:59:07 +00:00
John Colvin dd4aa3c2ed build: Update fable in dotnet-tools.json 2025-01-03 16:59:07 +00:00
John Colvin fee9d3f597 build: Update webpack-cli in admin webapp 2025-01-03 16:59:07 +00:00
John Colvin 416f9aef31 vscode: Add Sparkd Test to launch.json 2025-01-03 16:59:07 +00:00
John Colvin 48f5e4c201 vscode: Fix sparkd launch.json 2025-01-03 16:59:07 +00:00
John Colvin 4438b4ca8d feat: Support fractional quantities in synthetic book 2025-01-03 16:59:07 +00:00
John Colvin 73356e0d61 cli: Add cmd to print security info from the db to console
Usage: sparkd md dump --db
2025-01-03 16:59:07 +00:00
John Colvin 4a285fa581 cleanup: Make fn const 2025-01-03 16:59:07 +00:00
John Colvin cfa82c2177 cleanup: Remove erroneous fwd declared class 2025-01-03 16:59:07 +00:00
John Colvin 0410a0431a fix: Encode exp in valid date int when storing in sqlite
The strange bit shifting we were doing before doesn't support large
dates.

Bugs (fixed):
  * Synthetics with Binance perpetual future legs don't appear on the
    front end after sparkd restarts.

  * Cause: Exp dates for Binance perpetual futures aren't properly
    encoded in the database. As a result, when loaded from disk, they
    are mistakenly identified as expired, along with any synthetics that
    have them as legs.
2025-01-03 16:59:07 +00:00
John Colvin 02843f14ff cleanup: Get rid of trivial fn that's only used in one place
Move the logic into the function that used to call it.
2025-01-03 16:59:07 +00:00
John Colvin f1f1d4259c diag: Add logs around security discovery in MDConsumer
This is multi-threaded event driven code. Without descriptive logs in
the right places, diagnosing problems is difficult and tedious.
2025-01-03 16:59:07 +00:00
John Colvin 71fc43af68 cleanup: Flatten nested if blocks with continue statements
Easier to read
2025-01-03 16:59:07 +00:00
John Colvin 350144ffd5 fix: Use security desc to get unique id (along with venue, type)
I'm guessing this typo went unnoticed because it didn't break the build
(another arg of the same was being used instead).
2025-01-03 16:59:07 +00:00
John Colvin da227a5550 Support synths at Binance, Binance USDS Futures, Deribit, and Okx 2025-01-03 16:59:07 +00:00
John Colvin 0c92cf3a87 Add double ops to IntPrice 2025-01-03 16:59:07 +00:00
John Colvin 1188af391e Add trivial fns to TimeStamp for convenience
* += and -= operators
* 'date' fn
* 'to_date_int' fn
2025-01-03 16:59:07 +00:00
John Colvin 0bb85cf915 refactor: Change month and day params from uint32 to uint8
They only need to hold values 1-12 and 1-31, respectively. A single byte
is more than sufficient. Also add ymdhms TimeStamp constructor.
2025-01-03 16:59:07 +00:00
John Colvin d77f5996fe cleanup: Make a couple TimeStamp fns const 2025-01-03 16:59:07 +00:00
John Colvin 940c41a274 Add int32 arithmetic operators to domain::Price 2025-01-03 16:59:07 +00:00
John Colvin e671960dc3 Add double arithmetic operators to domain::Price 2025-01-03 16:59:07 +00:00
John Colvin 8fa34b6b39 fix: Multiply additive factors by exponent in domain::Price
We must not have been using these operators because the implementation
was very broken (yet it doesn't seemed to have caused any bugs).
2025-01-03 16:59:07 +00:00
John Colvin f9d5429b5b cleanup: Remove unused code 2025-01-03 16:53:05 +00:00
Mohammad Arham 2c342482eb Merge branch 'development.crypto' of bitbucket.org:bluetradesys/spark into development.crypto 2025-01-02 15:30:13 -05:00
Mohammad Arham 3aefac26c2 fix: Timer condition was incorrect 2025-01-02 20:15:38 +00:00
John Colvin 1554594cdd diag: Throttle noisy log in Ladder 2025-01-02 12:15:22 -05:00
John Colvin 727a0f38b5 fix: Prevent arithmetic overflow when converting long to int in Ladder 2025-01-02 12:13:58 -05:00
John Colvin 7a0e8cdb26 fix: Properly format spread/synth leg qty in generated desc 2025-01-01 13:12:17 -05:00
John Colvin a4e97b04d7 fix: Don't propagate security criteria changes to other windows
Bugs (fixed):
  * Security criteria changes in mview and time/sales propagate to other
    windows of the same type.
2024-12-31 11:06:24 -05:00
John Colvin 463980a6ef Fix typo in get_binance_md.py 2024-12-29 18:37:54 +00:00
John Colvin 9b25857042 fix: Ensure "earliest_expiration" fn actually returns min exp
Previously, it assumed the first leg would have the earliest exp. While
this may be true for most securities, it isn't guarenteed by our data
model.
2024-12-29 13:26:44 -05:00
John Colvin ef19e7d76b fix: Don't assume all securities have expirations in pnl vm
Bugs (fixed):
  * PNL window crashes if data has Spots (i.e., securities without exps)
2024-12-29 13:17:17 -05:00
Taha Afzal 471faec2cb Added more etsts for fractional qtys 2024-12-28 08:31:16 +00:00
Taha Afzal ce96d42db8 Merging 2024-12-26 07:01:30 +00:00
Mohammad Arham 187894ca84 FIX: makes gateway factory work 2024-12-24 17:56:49 +00:00
John Colvin 63492c4575 fix: Remove erroneous assert in LadderRender 2024-12-21 23:24:52 -05:00
John Colvin e150c611f6 fix: Round fractional order qty to 6 decimal places
It was previously rounding to 2 decimal places, which isn't enough for
many crypto currencies (e.g., BTCUSDT at Binance)
2024-12-21 23:17:52 -05:00
John Colvin bb837d6c26 fix build: Trivial changes in DatabentoDataHandler 2024-12-22 04:00:23 +00:00
John Colvin 965e541ac8 Fix json structures and endpoints for Binance USDS Futures md 2024-12-22 04:00:23 +00:00
John Colvin 1b232aa610 feat: Add Binance USDS Futures execution 2024-12-22 04:00:23 +00:00
John Colvin 25439b094a improvement: Perform sec lookup in const time 2024-12-21 20:08:19 +00:00
John Colvin f927ef2c5e Add conf_json dir to spark data file tree
So we don't have to add proto files to the build for trivial config.
2024-12-21 20:08:19 +00:00
John Colvin d42389b75a Don't needlessly download cme config xml
When we aren't subscribing to cme md (i.e., no glbx symbols in spark
conf).
2024-12-21 20:08:19 +00:00
John Colvin 671cbcf03a Fix bts::Quantity string fmt so it includes fractional qty 2024-12-21 20:01:55 +00:00
John Colvin e589e23f41 Add py script to get md at binance
For quick/easy troubleshooting (much easier than doing stuff in c++)
2024-12-21 20:01:55 +00:00
John Colvin 11fadc0a3e Expose "run" fn in WebsocketClient
So it doesn't have to be hidden in a background thread
2024-12-21 20:01:55 +00:00
John Colvin b8c2d93866 Add ping/pong facilities to WebsocketClient 2024-12-21 20:01:55 +00:00
John Colvin 67a23dd196 Add open/close callbacks to WebsocketClient 2024-12-21 20:01:55 +00:00
John Colvin ff22c4eb5f diag: Change WebsocketClient log category to "Http"
A better fit than "Gen"
2024-12-21 20:01:55 +00:00
John Colvin 5addff4c50 cleanup: Remove redundant fn HttpRequest::perform
This was doing the same thing as HttpClient::send. I likely refactored
this function during development, then forgot to remove it.
2024-12-21 20:01:55 +00:00
John Colvin 142e1bd2da Add HttpMethod "DELETE"
In Binance's REST api, orders are canceled via DELETE requests.
2024-12-21 20:01:55 +00:00
John Colvin d05d91bbe7 diag: Log Http requests 2024-12-21 20:01:55 +00:00
Mohammad Arham 3a90ca9cb5 Add: Johns changes to sec oracle 2024-12-17 14:47:37 +00:00
John Colvin 85a46727be Improve c++ http client facilities 2024-12-11 19:28:36 +00:00
Mohammad Arham 624fd2c976 FIX: BUILD Error 2024-12-09 01:34:29 +00:00
Mohammad Arham 16d599d2d6 Add: OKX execution gateway connected 2024-12-08 20:14:16 -05:00
John Colvin c00e4fcb6b feat: Add Binance Coin Futures market data 2024-12-06 00:39:41 +00:00
John Colvin 03e19afe1d Fix qty display conf for BTC/ETH futs at Binance
Need to show 3 decimal places (qty tick size is 0.001)
2024-12-05 21:05:53 +00:00
John Colvin 058e11eadc fix build: Fix install path for sparkd 2024-12-05 18:31:36 +00:00
John Colvin 6ff52e7107 fix build: Add onixs libs again
Taha accidentally reverted externals
2024-12-04 16:56:46 +00:00
John Colvin ee4c28ed41 Port ws client over from cme-websocket-connector
Need this to implement execution at crypto exchanges (beginning with
Binance and OKX)
2024-12-04 16:56:46 +00:00
Mohammad Arham 50d26f0060 Merge branch 'development' of bitbucket.org:bluetradesys/spark into development 2024-12-03 09:52:09 -05:00
Mohammad Arham 36378e6074 Add:: Recording md to sql 2024-12-03 14:51:32 +00:00
Mohammad Arham 420b46741e ADD: Config for sql writer in mdrecorder 2024-11-24 19:14:45 +00:00
Mohammad Arham e8f65f95bd Wrapped proto write in function in mdrecorder 2024-11-24 19:13:36 +00:00
267 changed files with 19808 additions and 17267 deletions

No files matched your search

+2 -2
View File
@@ -9,7 +9,7 @@
]
},
"fable": {
"version": "4.0.6",
"version": "4.24.0",
"commands": [
"fable"
]
@@ -27,4 +27,4 @@
]
}
}
}
}
+119 -13
View File
@@ -31,7 +31,24 @@
"type": "cppdbg",
"request": "launch",
"program": "/home/bts/spark/build/sparkd-debug-clang/server/sparkd",
"args": ["run"],
"args": [
"run"
],
"stopAtEntry": false,
"cwd": "/home/bts/spark/build/sparkd-debug-clang/server",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
},
{
"name": "spreader tests2",
"type": "cppdbg",
"request": "launch",
"program": "/home/bts/spark/build/sparkd-debug-clang/server/sparkd",
"args": [
"test",
"[spreader2]"
],
"stopAtEntry": false,
"cwd": "/home/bts/spark/build/sparkd-debug-clang/server",
"environment": [],
@@ -110,20 +127,104 @@
},
{
"name": "Sparkd",
"type": "coreclr",
"type": "cppdbg",
"request": "launch",
"preLaunchTask": "Build Sparkd (Debug | Clang)",
"program": "",
"linux": {
"program": "sparkd",
},
"args": [
"run"
],
"cwd": "build/sparkd-debug-clang/server",
"stopAtEntry": false,
"console": "integratedTerminal"
}
"preLaunchTask": "Build Sparkd (Debug)",
"program": "${workspaceFolder}/build/sparkd-debug-clang/server/sparkd",
"args": [
"run"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/sparkd-debug-clang/server",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
},
{
"name": "Sparkd (Skip Build)",
"type": "cppdbg",
"request": "launch",
"program": "",
"linux": {
"program": "${workspaceFolder}/build/sparkd-debug-clang/server/sparkd",
"args": [
"run"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/sparkd-debug-clang/server",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
},
{
"name": "Sparkd Test",
"type": "cppdbg",
"request": "launch",
"program": "",
"linux": {
"preLaunchTask": "Build Sparkd (Debug)",
"program": "${workspaceFolder}/build/sparkd-debug-clang/server/sparkd",
"args": [
"test",
"${input:sparkdTest}",
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/sparkd-debug-clang/server",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
},
{
"name": "Sparkd Test (Skip Build)",
"type": "cppdbg",
"request": "launch",
"program": "",
"linux": {
"program": "${workspaceFolder}/build/sparkd-debug-clang/server/sparkd",
"args": [
"test",
"${input:sparkdTest}",
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/sparkd-debug-clang/server",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
},
],
"inputs": [
{
@@ -154,5 +255,10 @@
"description": "Config Server URL",
"default": "http://localhost:8000/config",
},
{
"id": "sparkdTest",
"type": "promptString",
"description": "Test to run",
},
]
}
}
+17 -5
View File
@@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.12)
project(spark)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20)
if(WIN32)
@@ -87,9 +88,15 @@ else()
add_compile_definitions(BINANCE_US_PRICE_STATS_PATH="/api/v3/ticker/24hr")
# ** Binance USDS Futures **
add_compile_definitions(CCAPI_BINANCE_USDS_FUTURES_URL_REST_BASE="https://dapi.binance.com")
add_compile_definitions(BINANCE_USDS_FUTURES_INSTRUMENTS_PATH="/dapi/v1/exchangeInfo")
add_compile_definitions(BINANCE_USDS_FUTURES_PRICE_STATS_PATH="/dapi/v1/ticker/24hr")
add_compile_definitions(CCAPI_BINANCE_USDS_FUTURES_URL_REST_BASE="https://fapi.binance.com")
add_compile_definitions(BINANCE_USDS_FUTURES_INSTRUMENTS_PATH="/fapi/v1/exchangeInfo")
add_compile_definitions(BINANCE_USDS_FUTURES_PRICE_STATS_PATH="/fapi/v1/ticker/24hr")
add_compile_definitions(BINANCE_USDS_FUTURES_USER_DATA_PATH="/api/v3/userDataStream")
# ** Binance Coin Futures **
add_compile_definitions(CCAPI_BINANCE_COIN_FUTURES_URL_REST_BASE="https://dapi.binance.com")
add_compile_definitions(BINANCE_COIN_FUTURES_INSTRUMENTS_PATH="/dapi/v1/exchangeInfo")
add_compile_definitions(BINANCE_COIN_FUTURES_PRICE_STATS_PATH="/dapi/v1/ticker/24hr")
# ** Deribit **
add_compile_definitions(DERIBIT_INSTRUMENT_PATH="/api/v2/public/get_instrument")
@@ -109,6 +116,8 @@ else()
# subdirs
if (BUILD_SPARKD)
add_subdirectory(src/lib_binance_execution)
add_subdirectory(src/lib_deribit_emumba)
add_subdirectory(src/server/sparkd)
add_subdirectory(src/data_files)
endif()
@@ -145,11 +154,14 @@ else()
install(
DIRECTORY "${CMAKE_BINARY_DIR}/server/plugins"
DESTINATION "${SPARK_INSTALL_PREFIX}/bin")
DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(
FILES
"${SPARK_LIB}/libACE-5.7.5.so"
"${SPARK_LIB}/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.16.0.0"
DESTINATION "${SPARK_INSTALL_PREFIX}/bin")
"${SPARK_LIB}/libonixs-fix-engine.so.4.10.1"
"${SPARK_LIB}/libonixs-fix-engine.so.4"
"${SPARK_LIB}/libonixs-fix-engine.so"
DESTINATION "${CMAKE_INSTALL_BINDIR}")
endif()
endif()
+2 -11
View File
@@ -61,7 +61,7 @@
},
"CMAKE_CXX_STANDARD": "20",
"CMAKE_INSTALL_RPATH": ".",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install/${presetName}/sparkd",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install/${presetName}",
"CMAKE_EXE_LINKER_FLAGS": "-Wno-undef",
"EXTERNALS_ROOT": "${sourceDir}/externals/linux",
"BUILD_SDK": {
@@ -302,16 +302,7 @@
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"hidden": true,
"targets": [
"data_files_dir",
"admin_web_app_bundle",
"install",
"libspark",
"rapidjson_project",
"server_layout",
"sparkd"
]
"hidden": true
},
{
+49
View File
@@ -0,0 +1,49 @@
include_guard(DIRECTORY)
function(protobuf_generate_fs)
set(_options)
set(_one_value_args TARGET_NAME OUT_DIR PROTOC)
set(_multi_value_args PROTO_INCLUDE_DIRS PROTO_SOURCES)
cmake_parse_arguments(_ARGS "${_options}" "${_one_value_args}" "${_multi_value_args}" ${ARGN})
if (NOT DEFINED _ARGS_TARGET_NAME)
set(_ARGS_TARGET_NAME "${PROJECT_NAME}_generate_fs_protos")
endif()
if (NOT DEFINED _ARGS_PROTOC)
set(_ARGS_PROTOC "${SPARK_BIN}/protoc")
endif()
set(_proto_include_cli_args)
foreach(_dir ${_ARGS_PROTO_INCLUDE_DIRS})
list(APPEND _proto_include_cli_args -I ${_dir})
endforeach()
set(_outputs)
foreach(_proto_source ${_ARGS_PROTO_SOURCES})
# get_filename_component(_abs_proto_source ${_proto_source} ABSOLUTE)
get_filename_component(_proto_source_we ${_proto_source} NAME_WE)
# get_filename_component(_proto_source_dir ${_proto_source} DIRECTORY)
list(APPEND _outputs ${_ARGS_OUT_DIR}/${_proto_source_we}.fable.fs)
endforeach()
set(_venv_exec ${CMAKE_SOURCE_DIR}/tools/venv_exec.sh)
set(_plugin_py ${CMAKE_SOURCE_DIR}/src/lib/api/service_compiler/plugin.py)
set(_template_fs ${CMAKE_SOURCE_DIR}/src/lib/api/service_compiler/templates/fs/fable.fs)
add_custom_command(
OUTPUT ${_outputs}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${_venv_exec}
${_ARGS_PROTOC}
--plugin=protoc-gen-service=${_plugin_py}
--service_out=lang=fs,strip_out_path=1:${_ARGS_OUT_DIR}
${_proto_include_cli_args}
${_ARGS_PROTO_SOURCES}
DEPENDS
${_venv_exec}
${_plugin_py}
${_template_fs}
${CMAKE_CURRENT_SOURCE_DIR}/package-lock.json
${_ARGS_PROTO_SOURCES}
)
add_custom_target(${_ARGS_TARGET_NAME} ALL DEPENDS "${_outputs}")
endfunction()
+33
View File
@@ -0,0 +1,33 @@
include_guard(DIRECTORY)
function(protobuf_generate_js)
set(_options)
set(_one_value_args TARGET_NAME OUTPUT)
set(_multi_value_args PROTO_INCLUDE_DIRS PROTO_SOURCES)
cmake_parse_arguments(_ARGS "${_options}" "${_one_value_args}" "${_multi_value_args}" ${ARGN})
if (NOT DEFINED _ARGS_TARGET_NAME)
set(_ARGS_TARGET_NAME "${PROJECT_NAME}_generate_js_protos")
endif()
set(_proto_include_cli_args)
foreach(_dir ${_ARGS_PROTO_INCLUDE_DIRS})
list(APPEND _proto_include_cli_args -p ${_dir})
endforeach()
add_custom_command(
OUTPUT ${_ARGS_OUTPUT}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND npx
--no-install pbjs
-t json-module
-w commonjs
-o ${_ARGS_OUTPUT}
${_proto_include_cli_args}
${_ARGS_PROTO_SOURCES}
DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/package-lock.json
${_ARGS_PROTO_SOURCES}
)
add_custom_target(${_ARGS_TARGET_NAME} ALL DEPENDS ${_ARGS_OUTPUT})
endfunction()
+49
View File
@@ -0,0 +1,49 @@
include_guard(DIRECTORY)
include(${CMAKE_SOURCE_DIR}/cmake/TernaryOperator.cmake)
function(sym_link_dir)
set(_options)
set(_one_value_args TARGET_NAME SOURCE DEST COMMENT)
set(_multi_value_args)
cmake_parse_arguments(_ARGS "${_options}" "${_one_value_args}" "${_multi_value_args}" ${ARGN})
add_custom_command(
OUTPUT ${_ARGS_DEST}
COMMAND ln ARGS -sf ${_ARGS_SOURCE} ${_ARGS_DEST}
)
set_ternary(_comment _ARGS_COMMENT "${_ARGS_COMMENT}" "linking dir ${_ARGS_SOURCE} -> ${_ARGS_DEST}")
add_custom_target(
${_ARGS_TARGET_NAME} ALL
DEPENDS ${_ARGS_DEST}
COMMENT "${_comment}"
)
endfunction()
function(sym_link_file)
set(_options)
set(_one_value_args SOURCE DEST INSTALL_DEST)
set(_multi_value_args)
cmake_parse_arguments(_ARGS "${_options}" "${_one_value_args}" "${_multi_value_args}" ${ARGN})
file(
CREATE_LINK
${_ARGS_SOURCE}
${_ARGS_DEST}
SYMBOLIC
)
if (DEFINED _ARGS_INSTALL_DEST)
install(
FILES
${_ARGS_SOURCE}
DESTINATION ${_ARGS_INSTALL_DEST}
)
endif()
endfunction()
function(sym_link_local_lib LIB_NAME)
set(_lib_file_name "${CMAKE_SHARED_LIBRARY_PREFIX}${LIB_NAME}.so")
file(
CREATE_LINK
"${CMAKE_BINARY_DIR}/src/lib_${LIB_NAME}/${_lib_file_name}"
"${CMAKE_CURRENT_BINARY_DIR}/${_lib_file_name}"
SYMBOLIC
)
endfunction()
+9
View File
@@ -0,0 +1,9 @@
include_guard(GLOBAL)
macro(set_ternary RESULT CONDITION TRUE_VALUE FALSE_VALUE)
if(${CONDITION})
set(${RESULT} ${TRUE_VALUE})
else()
set(${RESULT} ${FALSE_VALUE})
endif()
endmacro()
@@ -0,0 +1,25 @@
include(ExternalProject)
if(NOT TARGET websocket-library_project)
ExternalProject_Add(websocket-library_project
GIT_REPOSITORY https://fintech.websocket:h_ubh-4dsWRB569wwKyg@gitlab.com/cryptoexchanges1/websocket-client-using-epoll.git
GIT_TAG enhancement/config-ssl
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}/websocket-library
-DCMAKE_BUILD_TYPE=Release
-DLOG_LEVEL=INFO
-DENABLE_ENCRYPTION=True
)
endif()
ExternalProject_Get_Property(websocket-library_project BINARY_DIR)
ExternalProject_Get_Property(websocket-library_project SOURCE_DIR)
set(websocket-library_INCLUDE_DIR ${SOURCE_DIR}/include)
set(websocket-library_LIB_DIR ${BINARY_DIR}/src)
add_library(websocket-library STATIC IMPORTED)
set_property(TARGET websocket-library PROPERTY IMPORTED_LOCATION ${websocket-library_LIB_DIR}/libemumba-websocket-library.a)
add_dependencies(websocket-library websocket-library_project)
install(FILES ${websocket-library_LIB_DIR}/libemumba-websocket-library.a DESTINATION ${CMAKE_INSTALL_LIBDIR})
+141 -62
View File
@@ -1,56 +1,42 @@
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.10)
project(Spark.Admin.WebApp)
include(GNUInstallDirs)
include(${CMAKE_SOURCE_DIR}/cmake/safe_dep_glob.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/Protobuf.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/SymLink.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/ProtobufGenerateJs.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/ProtobufGenerateFs.cmake)
# dependencies
set(PROTO_GLOBS
file(
GLOB_RECURSE ${PROJECT_NAME}_PROTO_SOURCES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
../lib/api/*.proto
../lib/api/services/*.proto
../lib/domain_model/*.proto
../lib/sparkproto/*.proto
../lib/md/protos/*.proto
../../externals/linux/include/google/protobuf/descriptor.proto
../../externals/linux/include/google/protobuf/timestamp.proto)
../../externals/linux/include/google/protobuf/timestamp.proto
)
file(
GLOB_RECURSE _proto_rel_SOURCES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
${PROTO_GLOBS})
file(
GLOB_RECURSE _proto_abs_SOURCES
${PROTO_GLOBS})
file(
GLOB_RECURSE _ts_SOURCES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
src/*.ts)
file(
GLOB_RECURSE _fs_SOURCES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
src/*.fs)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/package-lock.json
COMMAND npm install --include=dev
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/package.json
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
set(_tools ${CMAKE_SOURCE_DIR}/tools)
set(_quiet_exec ${_tools}/quiet_exec.sh)
set(_venv_exec ${_tools}/venv_exec.sh)
set(_tools_py ${CMAKE_SOURCE_DIR}/tools/py)
# The tsconfig file is generated as a function of the cmake build type, and
# binary dir. Depending on the build type, it extends either
# 'tsconfig.debug.json' or 'tsconfig.release.json'.
set(_tsconfig_base "${CMAKE_CURRENT_SOURCE_DIR}/tsconfig.$<IF:$<CONFIG:Debug>,debug,release>.json")
set(WEBPACK_MODE $<IF:$<CONFIG:Debug>,"development","production">)
set(_tsconfig "${CMAKE_CURRENT_BINARY_DIR}/tsconfig.json")
set(_venv_python ${CMAKE_SOURCE_DIR}/.venv/bin/python)
set(_gen_tsconfig_py ${_tools_py}/gen_tsconfig.py)
set(_build_admin_web_app_py ${_tools_py}/build_admin_web_app.py)
set(_gen_tsconfig_deps ${_venv_exec} ${_venv_python} ${_gen_tsconfig_py} ${_tsconfig_base})
set(_build_admin_web_app_deps
${_quiet_exec}
${_venv_exec}
@@ -60,49 +46,142 @@ set(_build_admin_web_app_deps
${CMAKE_CURRENT_SOURCE_DIR}/webpack.config.js
${CMAKE_SOURCE_DIR}/src/lib/api/service_compiler/templates/fs/fable.fs
${CMAKE_CURRENT_SOURCE_DIR}/package-lock.json
${_proto_rel_SOURCES}
${${PROJECT_NAME}_PROTO_SOURCES}
${_api_ts_SOURCES}
${_ui_fs_SOURCES}
${_tsconfig})
update_deps_file("${_build_admin_web_app_deps}")
function(npm_install path)
add_custom_command(
OUTPUT ${path}/package-lock.json
COMMAND "npm" ARGS "install" "--include=dev"
DEPENDS ${path}/package.json
WORKING_DIRECTORY ${path}
COMMENT "Run 'npm install' in ${path}")
endfunction()
npm_install(${CMAKE_CURRENT_SOURCE_DIR})
add_custom_command(
OUTPUT ${_tsconfig}
COMMAND ${_venv_exec}
ARGS
${_venv_python}
${_gen_tsconfig_py}
--config-base ${_tsconfig_base}
--config-out ${_tsconfig}
--build-out ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS ${_gen_tsconfig_deps})
${_venv_python}
${_gen_tsconfig_py}
--config-base ${_tsconfig_base}
--config-out ${_tsconfig}
--build-out ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS
${_venv_exec}
${_venv_python}
${_gen_tsconfig_py}
${_tsconfig_base}
${CMAKE_CURRENT_SOURCE_DIR}/package-lock.json
)
protobuf_generate_js(
TARGET_NAME generate_js_protos
PROTO_INCLUDE_DIRS
..
../../externals/linux/include
PROTO_SOURCES ${${PROJECT_NAME}_PROTO_SOURCES}
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/proto.js
)
protobuf_generate_fs(
TARGET_NAME generate_fs_protos
PROTO_INCLUDE_DIRS
..
../../externals/linux/include
PROTO_SOURCES ${${PROJECT_NAME}_PROTO_SOURCES}
OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/fable
)
# Each input file maps to multiple output files with different extensions
# (js, js.map, d.ts, fs). Transpile ts -> d.ts (and js), then d.ts -> fs.
# Who knew front-end development could be so complicated? `\_(=_=)_/`
file(
GLOB_RECURSE ${PROJECT_NAME}_TS_SOURCES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
src/*.ts
)
set(${PROJECT_NAME}_JS_PROTOS)
set(${PROJECT_NAME}_TS_PROTOS)
set(${PROJECT_NAME}_FS_PROTOS)
foreach(_ts_source ${${PROJECT_NAME}_TS_SOURCES})
get_filename_component(_ts_source_we ${_ts_source} NAME_WE)
set(_prefix ${CMAKE_CURRENT_BINARY_DIR}/${_ts_source_we})
list(APPEND ${PROJECT_NAME}_JS_PROTOS ${_prefix}.js ${_prefix}.js.map)
list(APPEND ${PROJECT_NAME}_TS_PROTOS ${_prefix}.d.ts ${_prefix}.d.ts.map)
list(APPEND ${PROJECT_NAME}_FS_PROTOS ${_prefix}.fs)
add_custom_command(
OUTPUT ${_prefix}.fs
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND npx --no-install ts2fable ${_prefix}.d.ts ${_prefix}.fs
DEPENDS ${_prefix}.d.ts
)
endforeach()
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/bundle.js
COMMAND ${_quiet_exec}
ARGS
${_venv_exec}
${_venv_python}
${_build_admin_web_app_py}
--protos ${_proto_abs_SOURCES}
--tsconfig ${_tsconfig}
--mode ${WEBPACK_MODE}
--output-dir ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Building Spark.Admin.WebApp"
DEPENDS ${_build_admin_web_app_deps})
OUTPUT
${${PROJECT_NAME}_JS_PROTOS}
${${PROJECT_NAME}_TS_PROTOS}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND npx
--no-install tsc
-p ${_tsconfig}
DEPENDS
${_tsconfig}
${CMAKE_CURRENT_BINARY_DIR}/proto.js
)
add_custom_target(
transpile_type_script
ALL
DEPENDS
${${PROJECT_NAME}_JS_PROTOS}
${${PROJECT_NAME}_TS_PROTOS}
)
add_custom_target(generate_fsts_stubs ALL DEPENDS ${${PROJECT_NAME}_FS_PROTOS})
add_custom_target(admin_web_app_bundle ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/bundle.js)
sym_link_dir(
TARGET_NAME link_webapp_public
SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/public
DEST ${CMAKE_CURRENT_BINARY_DIR}/public
)
sym_link_dir(
TARGET_NAME link_webapp_root
SOURCE ${CMAKE_CURRENT_BINARY_DIR}
DEST ${CMAKE_SOURCE_DIR}/build/Spark.Admin.WebApp
)
file(
GLOB_RECURSE ${PROJECT_NAME}_FS_SOURCES
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
src/*.fs
)
add_custom_command(
OUTPUT
${CMAKE_CURRENT_BINARY_DIR}/bundle.js
$<$<CONFIG:DEBUG>:${CMAKE_CURRENT_BINARY_DIR}/bundle.js.map>
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
COMMAND dotnet restore
COMMAND dotnet tool restore
COMMAND export SPARK_ADMIN_WEB_APP_OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}
COMMAND dotnet fable -o ${CMAKE_CURRENT_BINARY_DIR} --verbose
COMMAND npm run build -- --mode $<IF:$<CONFIG:Debug>,"development","production">
DEPENDS
${${PROJECT_NAME}_FS_SOURCES}
${${PROJECT_NAME}_FS_PROTOS}
${CMAKE_CURRENT_SOURCE_DIR}/src/Spark.Admin.WebApp.fsproj
)
add_custom_target(
bundle_spark_admin_webapp ALL
DEPENDS
${CMAKE_CURRENT_BINARY_DIR}/bundle.js
$<$<CONFIG:DEBUG>:${CMAKE_CURRENT_BINARY_DIR}/bundle.js.map>
COMMENT "Bundling Spark.Admin.WebApp"
)
# Exclude 'public' dir because it is a symlink
install(
DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
DESTINATION "${CMAKE_INSTALL_BINDIR}"
PATTERN "public" EXCLUDE
)
# Install actual (non-symlinked) 'public' dir
install(
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/public"
DESTINATION "${CMAKE_INSTALL_BINDIR}/${PROJECT_NAME}"
)
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" DESTINATION "${SPARK_INSTALL_PREFIX}/bin")
+2755 -15230
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -42,7 +42,7 @@
"ts-loader": "^9.4.2",
"tsutils": "^3.21.0",
"typescript": "^4.9.5",
"webpack-cli": "^5.0.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.11.1",
"ws": "^8.12.1"
}
+6 -1
View File
@@ -17,7 +17,7 @@ type Tab =
| PositionManagementTab
| MarginTab
let venues = [| Venue.BINANCE; Venue.BinanceUs; Venue.BinanceUsdsFutures; Venue.CBOE; Venue.COINBASE; Venue.DERIBIT; Venue.GLBX; Venue.ICE; Venue.TAIFEX; Venue.SIM; |]
let venues = [| Venue.BINANCE; Venue.BinanceUs; Venue.BinanceUsdsFutures; Venue.CBOE; Venue.COINBASE; Venue.DERIBIT; Venue.OKX; Venue.GLBX; Venue.ICE; Venue.TAIFEX; Venue.SIM; |]
let securityTypes = [| SecurityType.SPOT; SecurityType.FUTURE; SecurityType.OPTION; SecurityType.SPREAD; |]
@@ -74,6 +74,11 @@ type TaifexExchangeField =
| Origin of CustomerOrFirm
| ClearingAccount of string
type OkxExchangeField =
| CTI of CTICode
| Origin of CustomerOrFirm
| ClearingAccount of string
type SIMExchangeField =
| CTI of CTICode
| Origin of CustomerOrFirm
@@ -6,6 +6,7 @@ using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
@@ -127,7 +128,11 @@ namespace mbts.ui.spark {
size,
Observable.Return(calc_buttons));
_show_popup(ticket_vm, new OrderTicketHeader { bbo = ticket_vm.bbo }, resources);
_show_popup(
ticket_vm,
new OrderTicketHeader { bbo = ticket_vm.bbo },
resources,
view => Behavior.SetAutoFocusTargetProvider(view, AutoFocusTargetProvider.TypeProvider(typeof(TextBox), view)));
}
void _show_popup(object content, object header) {
@@ -136,7 +141,7 @@ namespace mbts.ui.spark {
_show_popup(content, header, resources);
}
void _show_popup(object content, object header, ResourceDictionary resources) {
void _show_popup(object content, object header, ResourceDictionary resources, Action<HeaderedContentControl> init_view = null) {
var view = new HeaderedContentControl {
DataContext = content,
Content = content,
@@ -146,6 +151,7 @@ namespace mbts.ui.spark {
Resources = resources ?? new AuxiliaryDataResources()
};
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
init_view?.Invoke(view);
_popup_man.show(view);
}
@@ -10,6 +10,8 @@ using bts.view;
using mbts.persist;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.extensions;
using mbts.ui.settings;
using mbts.ui.spark;
using mbts.ui.utility;
@@ -27,12 +29,18 @@ using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
namespace mbts.spark {
class MarketViewWindow : SparkWindow<MarketViewSettings>, ISharedWindow, ISlowMeasureContentSize {
const string _window_type = "MarketView", _window_type_display_name = "Market View";
static int _window_id_counter = 0;
readonly string _window_id = $"{_window_type}_{Interlocked.Increment(ref _window_id_counter)}";
readonly OrdersModel _orders_model;
readonly ISecurityMetadataStore _sec_meta_store;
readonly IPositionModel _position_model;
@@ -122,7 +130,7 @@ namespace mbts.spark {
Log.notnull(_state);
_menu_display_vm = new SparkMenuDisplayVM();
_menu_display_vm.load_from(_state.menu_display);
var update_securities_cmd = SparkCommands.change_securities_callback(_window_type_display_name);
var update_securities_cmd = SparkCommands.change_securities_callback(_window_id);
var security_md_map_src = Observable.Create<IReadOnlyDictionary<uint, SecurityMD>>(o => {
IDisposable sub = null;
_security_criteria_changed += on_security_criteria_changed;
@@ -286,7 +294,32 @@ namespace mbts.spark {
}
}
protected override void OnParentViewCreated(SparkWindowView parent_view) =>
protected override void OnParentViewCreated(SparkWindowView parent_view) {
_menu_display_vm.safe_bind_to_view(parent_view);
ui.WindowStyle.AddWinFormsHostChangedHandler(parent_view, OnWinFormsHostChanged);
void OnWinFormsHostChanged(object sender, RoutedPropertyChangedEventArgs<WPFPanel> e) {
// If this window is "activated" by the user clicking on a popup window (such as an
// order ticket), it never receives keyboard focus (seems like a bug in Windows). As a
// workaround we explicitly focus the view when the parent WPFPanel gets focus. If the
// user clicks on the popup a second time, it should get keyboard focus.
if (e.OldValue is not null) {
e.OldValue.GotFocus -= OnGotFocus;
}
if (e.NewValue is not null) {
e.NewValue.GotFocus += OnGotFocus;
}
void OnGotFocus(object sender, EventArgs e) =>
_view.Dispatcher.InvokeAsync(StealFocus, DispatcherPriority.Send);
void StealFocus() {
if (_view.IsVisible) {
Log.Ui.info($"MarketViewWindow. Stealing focus from WPFPanel");
_view.Focus();
}
}
}
}
}
}
@@ -24,6 +24,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Windows;
class TimeAndSalesWindow : SparkWindow<TimeAndSalesSettings>, ISharedWindow {
@@ -35,7 +36,9 @@
const double _VERTICAL_SCROLLBAR_OVERLAP = 9;
static readonly TimeSpan _trade_poll_interval = TimeSpan.FromSeconds(0.4);
static int _window_id_counter = 0;
readonly string _window_id = $"{WINDOW_TYPE_NAME}_{Interlocked.Increment(ref _window_id_counter)}";
[DisallowNull]
readonly MDConsumer _consumer;
[DisallowNull]
@@ -133,7 +136,7 @@
}
return result;
});
var update_securities_cmd = SparkCommands.change_securities_callback(WINDOW_DISPLAY_NAME);
var update_securities_cmd = SparkCommands.change_securities_callback(_window_id);
var security_criteria_changed = new Action<SecurityCriteria>(sc => { });
SparkCommand toggle_is_paused_cmd = null;
@@ -314,6 +314,12 @@
<TextBlock Text="{Binding param.display_value, Converter={StaticResource DoubleTextConverter}}"/>
</DataTemplate>
</usp:AlgoParamTemplateSelector.DoubleTemplate>
<!--Nullable Double-->
<usp:AlgoParamTemplateSelector.NullableDoubleTemplate>
<DataTemplate>
<TextBlock Text="{Binding param.display_value, Converter={StaticResource DoubleTextConverter}}"/>
</DataTemplate>
</usp:AlgoParamTemplateSelector.NullableDoubleTemplate>
<!--Enum-->
<usp:AlgoParamTemplateSelector.EnumTemplate>
<DataTemplate>
@@ -435,6 +441,20 @@
</c:TickUpDownDouble>
</DataTemplate>
</usp:AlgoParamTemplateSelector.DoubleTemplate>
<!--Nullable Double-->
<usp:AlgoParamTemplateSelector.NullableDoubleTemplate>
<DataTemplate>
<c:TickUpDownNullableDouble x:Name="tick_up_down" Focusable="False" HorizontalAlignment="Stretch" Value="{Binding param.display_value, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl">
<c:TickUpDownNullableDouble.CommandBindings>
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
</c:TickUpDownNullableDouble.CommandBindings>
<c:TickUpDownNullableDouble.InputBindings>
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
</c:TickUpDownNullableDouble.InputBindings>
</c:TickUpDownNullableDouble>
</DataTemplate>
</usp:AlgoParamTemplateSelector.NullableDoubleTemplate>
<!--Enum-->
<usp:AlgoParamTemplateSelector.EnumTemplate>
<DataTemplate>
@@ -33,6 +33,7 @@ namespace mbts.ui.spark {
public DataTemplate UIntTemplate { get; set; }
public DataTemplate NullableUIntTemplate { get; set; }
public DataTemplate DoubleTemplate { get; set; }
public DataTemplate NullableDoubleTemplate { get; set; }
public DataTemplate EnumTemplate { get; set; }
public DataTemplate NullableEnumTemplate { get; set; }
public DataTemplate StringTemplate { get; set; }
@@ -52,6 +53,7 @@ namespace mbts.ui.spark {
IAlgoParamVM pvm = item as IAlgoParamVM ?? (item as AlgoParamRowVM)?.param;
if (pvm != null) {
if (pvm is IAlgoParamVM<double>) return DoubleTemplate;
if (pvm is IAlgoParamVM<double?>) return NullableDoubleTemplate;
if (pvm is IAlgoParamVM<int>) return IntTemplate;
if (pvm is IAlgoParamVM<int?>) return NullableIntTemplate;
if (pvm is IAlgoParamVM<uint>) return UIntTemplate;
@@ -62,6 +62,7 @@ namespace mbts.spark {
readonly RefValueContainer<IEnumerable<VolumeAtPriceLevelImmutable>> _volume_container = new();
IDisposable _settings_sub, _volume_sub, _tick_sub, _fair_value_lines_sub, _vm_sub;
readonly ThrottledAction _scroll_to_tick_log_throttle = new(TimeSpan.FromSeconds(5));
readonly SingleValuePump<ScrollViewer> _scroll_viewer_source;
ScrollViewer _scroll_viewer {
@@ -452,8 +453,8 @@ namespace mbts.spark {
public bool scroll_to_tick(long tick) {
if (tick > int.MaxValue) {
Log.Ui.error(
$"Unable to scroll to tick {tick} because it exceeds 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);
@@ -79,13 +79,6 @@ namespace mbts.spark {
public static int index_of(IList<double> prices, double price, IComparer<double> comparer, bool relative_idx) {
for (int i = 1; i < prices.Count; i++) {
Log.assert(prices[i - 1] > prices[i]);
var c = comparer.Compare(prices[i - 1], prices[i]);
if (c >= 0) {
c = comparer.Compare(prices[i - 1], prices[i]);
} else {
c = comparer.Compare(prices[i - 1], prices[i]);
}
Log.assert(c < 0);
}
var idx = CollectionHelper.binary_search(prices, price, comparer);
if (relative_idx && idx < 0) {
@@ -254,8 +247,8 @@ namespace mbts.spark {
var price = ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.price;
// prices are in descending order, so get diff from curr_price - price
var tick_diff = spec.tickdiff(curr_price, price).assert_value();
checked {
curr_index += (int)tick_diff;
if (!MathUtils.try_increment(ref curr_index, tick_diff)) {
continue;
}
curr_price = price;
if (curr_index >= 0 && curr_index < prices.Count) {
@@ -289,7 +289,8 @@ namespace bts.shell.pnl {
public AccountTuple account => new(account_id, account_name);
public string symbol => security?.pnl_symbol() ?? string.Empty;
public DateTime expiration => security?.earliest_expiration() ?? DateTime.MinValue;
public DateTime expiration =>
security?.normal_expirations()?.MinOrDefault(DateTime.MinValue) ?? DateTime.MinValue;
public double effective_cpp => override_cpp ?? default_cpp;
/// Market/Calc
+1 -1
View File
@@ -398,7 +398,7 @@
$"Expected rounded qty to be real, non-negative. " +
$"qty: {qty}; rounded_qty: {rounded_qty}; display: {display})");
var iqty = Math.Floor(rounded_qty);
return ((uint)iqty, new() { Fraction = Math.Round(rounded_qty - iqty, 2) });
return ((uint)iqty, new() { Fraction = Math.Round(rounded_qty - iqty, 6) });
}
private CustomOrderDefinitionImmutable? _create_custom_def_for_stop_and_stop_limit(OrderRecordImmutable order, ref double stop_price) {
@@ -20,13 +20,23 @@ namespace mbts.ui {
}
public static class AutoFocusTargetProvider {
public static IAutoFocusTargetProvider<T> TypeProvider<T>(FrameworkElement root, bool focus, bool focus_keyboard, bool release_keyboard, bool capture_mouse, bool release_mouse) where T : FrameworkElement =>
new AnonymousProvider<T>(() => root.descendants_of_type<T>().FirstOrDefault(), focus, focus_keyboard, release_keyboard, capture_mouse, release_mouse);
public static IAutoFocusTargetProvider TypeProvider(Type type, FrameworkElement root, bool focus, bool focus_keyboard, bool release_keybobard, bool capture_mouse, bool release_mouse) =>
public static IAutoFocusTargetProvider TypeProvider(
Type type,
FrameworkElement root,
bool focus = true,
bool focus_keyboard = false,
bool release_keyboard = true,
bool capture_mouse = false,
bool release_mouse = true) =>
new AnonymousProvider<FrameworkElement>(
() => root.visual_children_where(el => type.IsAssignableFrom(el.GetType())).Cast<FrameworkElement>().FirstOrDefault(),
focus, focus_keyboard, release_keybobard, capture_mouse, release_mouse);
() => root.visual_children_where(el => type.IsAssignableFrom(el.GetType()))
.Cast<FrameworkElement>()
.FirstOrDefault(),
focus,
focus_keyboard,
release_keyboard,
capture_mouse,
release_mouse);
private class AnonymousProvider<T> : IAutoFocusTargetProvider<T> where T : FrameworkElement {
readonly Func<T> _get_target;
@@ -1,248 +1,264 @@
using bts.utils;
using mbts.utils;
using Spark.Core.Disposables;
using Spark.ViewModel.Commands;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
namespace mbts.ui {
static class Behavior {
#region CloseWindow
public static readonly DependencyProperty CloseWindowProperty = DependencyProperty.RegisterAttached("CloseWindow", typeof(bool), typeof(Behavior),
new PropertyMetadata(false, (d, dpc_args) => {
var button = d as Button;
if (button != null && (bool)dpc_args.NewValue) {
RoutedEventHandler close_window = (sender, e) => {
var window = button.find_ancestor_of_type<Window>();
if (window != null)
window.Close();
};
button.Click += close_window;
IDisposable dispose = Disposable.Create(() => {
button.Click -= close_window;
close_window = null;
});
IDisposable early_dispose = button.ObserveProperty(CloseWindowProperty, (sender, e) => {
if (!GetCloseWindow(button))
Disposable.dispose(ref dispose);
});
DisposeExtensions.attach_to_disposer(button, dispose);
DisposeExtensions.attach_to_disposer(button, early_dispose);
}
}));
public static bool GetCloseWindow(Button obj) { return (bool)obj.GetValue(CloseWindowProperty); }
public static void SetCloseWindow(Button obj, bool value) { obj.SetValue(CloseWindowProperty, value); }
#endregion
#region AnimatedScrollViewerVerticalOffset
static double GetAnimatedScrollViewerVerticalOffset(DependencyObject obj) =>
(double)obj.GetValue(AnimatedScrollViewerVerticalOffsetProperty);
public static void SetAnimatedScrollViewerVerticalOffset(DependencyObject obj, double value) =>
obj.SetValue(AnimatedScrollViewerVerticalOffsetProperty, value);
public static readonly DependencyProperty AnimatedScrollViewerVerticalOffsetProperty =
DependencyProperty.RegisterAttached("AnimatedScrollViewerVerticalOffset", typeof(double), typeof(Behavior), new PropertyMetadata(0.0, scroll_changed));
static void scroll_changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
ScrollViewer target = d as ScrollViewer;
if (target != null) {
target.ScrollToVerticalOffset((double)e.NewValue);
}
}
#endregion
#region AutoFocusTargetProvider
public static readonly DependencyProperty AutoFocusTargetProviderProperty = DependencyProperty.RegisterAttached("AutoFocusTargetProvider", typeof(bool),
typeof(Behavior), new PropertyMetadata(OnAutoFocusTargetProviderChanged));
static void OnAutoFocusTargetProviderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (e.NewValue is IAutoFocusTargetProvider aftp) {
var el = ExceptionAssert.Argument.Is<FrameworkElement>(d, nameof(d));
d.SetValue(AutoFocusTargetProviderLifeDependencyPropertyKey, new AutoFocusTargetProviderLifeDependency(el, aftp));
} else
d.SetValue(AutoFocusTargetProviderLifeDependencyPropertyKey, null);
}
public static bool GetAutoFocusTargetProvider(DependencyObject d) => (bool)d.GetValue(AutoFocusTargetProviderProperty);
public static void SetAutoFocusTargetProvider(DependencyObject d, bool value) => d.SetValue(AutoFocusTargetProviderProperty, value);
#endregion
#region AutoFocusTargetProviderLifeDependency (ReadOnly)
static readonly DependencyPropertyKey AutoFocusTargetProviderLifeDependencyPropertyKey = DependencyProperty.RegisterAttachedReadOnly("AutoFocusTargetProviderLifeDependency",
typeof(FrameworkElementLifeDependency), typeof(Behavior), new PropertyMetadata(FrameworkElementLifeDependencyHelper.DisposeOldInitializeNew));
public static readonly DependencyProperty AutoFocusTargetProviderLifeDependencyProperty = AutoFocusTargetProviderLifeDependencyPropertyKey.DependencyProperty;
public static FrameworkElementLifeDependency GetAutoFocusTargetProviderLifeDependency(DependencyObject d) => d.GetValue(AutoFocusTargetProviderLifeDependencyProperty) as FrameworkElementLifeDependency;
public static void SetAutoFocusTargetProviderLifeDependency(DependencyObject d, FrameworkElementLifeDependency value) => d.SetValue(AutoFocusTargetProviderLifeDependencyPropertyKey, value);
class AutoFocusTargetProviderLifeDependency : FrameworkElementLifeDependency {
readonly IAutoFocusTargetProvider _focus_target_provider;
public AutoFocusTargetProviderLifeDependency(FrameworkElement root, IAutoFocusTargetProvider focus_target_provider) : base(root) =>
_focus_target_provider = focus_target_provider;
protected override void OnLoaded() {
var el = _focus_target_provider.Target;
if (el != null) {
if (_focus_target_provider.Focus)
el.Focus();
if (_focus_target_provider.FocusKeyboard)
Keyboard.Focus(el);
if (_focus_target_provider.CaptureMouse)
el.CaptureMouse();
}
}
protected override void OnUnloaded() {
var el = _focus_target_provider.Target;
if (el != null) {
if (_focus_target_provider.ReleaseKeyboard) {
ReferenceEquals(Keyboard.FocusedElement, el);
Keyboard.ClearFocus();
}
if (_focus_target_provider.ReleaseMouse)
el.ReleaseMouseCapture();
}
}
}
#endregion
#region EatMouseDown
public static readonly DependencyProperty EatMouseDownProperty =
DependencyProperty.RegisterAttached("EatMouseDown", typeof(bool), typeof(Behavior), new PropertyMetadata(false, OnEatMouseDownChanged));
static void OnEatMouseDownChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var el = (FrameworkElement)d;
if ((bool)e.NewValue) el.MouseDown += _eat_mouse_down;
else el.MouseDown -= _eat_mouse_down;
}
static void _eat_mouse_down(object sender, MouseButtonEventArgs e) =>
e.Handled = true;
public static bool GetEatMouseDown(DependencyObject d) => (bool)d.GetValue(EatMouseDownProperty);
public static void SetEatMouseDown(DependencyObject d, bool value) => d.SetValue(EatMouseDownProperty, value);
#endregion
#region DisablePaste
public static readonly DependencyProperty DisablePasteProperty = DependencyProperty.RegisterAttached(
"DisablePaste", typeof(bool), typeof(Behavior), new PropertyMetadata(false, OnDisablePasteChanged));
static void OnDisablePasteChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if ((bool)e.NewValue)
DataObject.AddPastingHandler(d, _disable_paste_pasting_handler);
else DataObject.RemovePastingHandler(d, _disable_paste_pasting_handler);
}
static void _disable_paste_pasting_handler(object sender, DataObjectPastingEventArgs e) => e.CancelCommand();
public static bool GetDisablePaste(DependencyObject d) => (bool)d.GetValue(DisablePasteProperty);
public static void SetDisablePaste(DependencyObject d, bool value) => d.SetValue(DisablePasteProperty, value);
#endregion
public static readonly DependencyProperty BindingUpdateGroupsProperty =
DependencyProperty.RegisterAttached("BindingUpdateGroups", typeof(BindingUpdateGroupCollection), typeof(Behavior), new PropertyMetadata(null));
public static BindingUpdateGroupCollection GetBindingUpdateGroups(DependencyObject d) => d.GetValue(BindingUpdateGroupsProperty) as BindingUpdateGroupCollection;
public static void SetBindingUpdateGroups(DependencyObject d, BindingUpdateGroupCollection value) => d.SetValue(BindingUpdateGroupsProperty, value);
#region AutoRaiseCanExecuteChanged
public static readonly DependencyProperty AutoRaiseCanExecuteChangedProperty =
DependencyProperty.RegisterAttached("AutoRaiseCanExecuteChanged", typeof(bool), typeof(Behavior),
new PropertyMetadata(false, (d, e) => {
if ((bool)e.NewValue) {
BindingOperations.SetBinding(d, AutoCommandProperty, new Binding(nameof(ButtonBase.Command)) { Source = d, Mode = BindingMode.OneWay });
BindingOperations.SetBinding(d, AutoCommandParameterProperty, new Binding(nameof(ButtonBase.CommandParameter)) { Source = d, Mode = BindingMode.OneWay });
} else {
BindingOperations.ClearBinding(d, AutoCommandProperty);
BindingOperations.ClearBinding(d, AutoCommandParameterProperty);
}
}));
public static bool GetAutoRaiseCanExecuteChanged(DependencyObject d) => (bool)d.GetValue(AutoRaiseCanExecuteChangedProperty);
public static void SetAutoRaiseCanExecuteChanged(DependencyObject d, bool value) => d.SetValue(AutoRaiseCanExecuteChangedProperty, value);
/// Do not bind directly to <see cref="AutoCommandProperty"/> (used internally by <see cref="AutoRaiseCanExecuteChangedProperty"/>
public static readonly DependencyProperty AutoCommandProperty =
DependencyProperty.RegisterAttached("AutoCommand", typeof(ICommand), typeof(Behavior), new PropertyMetadata(null, _try_raise_can_execute));
public static ICommand GetAutoCommand(DependencyObject d) => d.GetValue(AutoCommandProperty) as ICommand;
public static void SetAutoCommand(DependencyObject d, ICommand value) => d.SetValue(AutoCommandProperty, value);
/// Do not bind directly to <see cref="AutoCommandParameterProperty"/> (used internally by <see cref="AutoRaiseCanExecuteChangedProperty"/>
public static readonly DependencyProperty AutoCommandParameterProperty =
DependencyProperty.RegisterAttached("AutoCommandParameter", typeof(object), typeof(Behavior), new PropertyMetadata(null, _try_raise_can_execute));
public static object GetAutoCommandParameter(DependencyObject d) => d.GetValue(AutoCommandParameterProperty);
public static void SetAutoCommandParameter(DependencyObject d, object value) => d.SetValue(AutoCommandParameterProperty, value);
static void _try_raise_can_execute(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
(d.GetValue(ButtonBase.CommandProperty) as IRaiseCanExecuteChangedCommand)?.RaiseCanExecuteChanged();
#endregion
#region FocusOnLoaded
public static readonly DependencyProperty FocusOnLoadedProperty =
DependencyProperty.RegisterAttached("FocusOnLoaded", typeof(bool), typeof(Behavior), new PropertyMetadata(false, OnFocusOnLoadedChanged));
static void OnFocusOnLoadedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var target = (FrameworkElement)d;
if ((bool)e.NewValue) target.Loaded += _target_Loaded;
else target.Loaded -= _target_Loaded;
}
static void _target_Loaded(object sender, RoutedEventArgs e) {
var target = (FrameworkElement)sender;
Keyboard.Focus(target);
}
public static bool GetFocusOnLoaded(DependencyObject d) => (bool)d.GetValue(FocusOnLoadedProperty);
public static void SetFocusOnLoaded(DependencyObject d, bool value) => d.SetValue(FocusOnLoadedProperty, value);
#endregion
#region MirrorLayout
static readonly DependencyProperty[] _layout_props = new[] {
FrameworkElement.HeightProperty,
FrameworkElement.WidthProperty,
FrameworkElement.MarginProperty,
Control.PaddingProperty,
FrameworkElement.HorizontalAlignmentProperty,
Control.HorizontalContentAlignmentProperty,
FrameworkElement.VerticalAlignmentProperty,
Control.VerticalContentAlignmentProperty
};
public static readonly DependencyProperty MirrorLayoutForProperty =
DependencyProperty.RegisterAttached("MirrorLayoutFor", typeof(FrameworkElement), typeof(Behavior),
new PropertyMetadata(OnMirrorLayoutForChanged));
static void OnMirrorLayoutForChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var target = new LazyCast<FrameworkElement, DependencyObject>(d);
if (e.OldValue is FrameworkElement old_src) {
target.value.clear_bindings(_layout_props);
}
if (e.NewValue is FrameworkElement new_src) {
target.value.bind(FrameworkElement.WidthProperty, new_src, FrameworkElement.ActualWidthProperty);
target.value.bind(FrameworkElement.HeightProperty, new_src, FrameworkElement.ActualHeightProperty);
target.value.bind(FrameworkElement.MarginProperty, new_src, FrameworkElement.MarginProperty);
target.value.bind(FrameworkElement.HorizontalAlignmentProperty, new_src, FrameworkElement.HorizontalAlignmentProperty);
target.value.bind(FrameworkElement.VerticalAlignmentProperty, new_src, FrameworkElement.VerticalAlignmentProperty);
if (target.value is Control && new_src is Control) {
target.value.bind(Control.PaddingProperty, new_src, Control.PaddingProperty);
target.value.bind(Control.HorizontalContentAlignmentProperty, new_src, Control.HorizontalContentAlignmentProperty);
target.value.bind(Control.VerticalContentAlignmentProperty, new_src, Control.VerticalContentAlignmentProperty);
}
}
}
public static FrameworkElement GetMirrorLayoutFor(DependencyObject d) => d.GetValue(MirrorLayoutForProperty) as FrameworkElement;
public static void SetMirrorLayoutFor(DependencyObject d, FrameworkElement value) => d.SetValue(MirrorLayoutForProperty, value);
#endregion
}
}
using bts.utils;
using mbts.utils;
using Spark.Core.Disposables;
using Spark.ViewModel.Commands;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
namespace mbts.ui {
static class Behavior {
#region CloseWindow
public static readonly DependencyProperty CloseWindowProperty = DependencyProperty.RegisterAttached("CloseWindow", typeof(bool), typeof(Behavior),
new PropertyMetadata(false, (d, dpc_args) => {
var button = d as Button;
if (button != null && (bool)dpc_args.NewValue) {
RoutedEventHandler close_window = (sender, e) => {
var window = button.find_ancestor_of_type<Window>();
if (window != null)
window.Close();
};
button.Click += close_window;
IDisposable dispose = Disposable.Create(() => {
button.Click -= close_window;
close_window = null;
});
IDisposable early_dispose = button.ObserveProperty(CloseWindowProperty, (sender, e) => {
if (!GetCloseWindow(button))
Disposable.dispose(ref dispose);
});
DisposeExtensions.attach_to_disposer(button, dispose);
DisposeExtensions.attach_to_disposer(button, early_dispose);
}
}));
public static bool GetCloseWindow(Button obj) { return (bool)obj.GetValue(CloseWindowProperty); }
public static void SetCloseWindow(Button obj, bool value) { obj.SetValue(CloseWindowProperty, value); }
#endregion
#region AnimatedScrollViewerVerticalOffset
static double GetAnimatedScrollViewerVerticalOffset(DependencyObject obj) =>
(double)obj.GetValue(AnimatedScrollViewerVerticalOffsetProperty);
public static void SetAnimatedScrollViewerVerticalOffset(DependencyObject obj, double value) =>
obj.SetValue(AnimatedScrollViewerVerticalOffsetProperty, value);
public static readonly DependencyProperty AnimatedScrollViewerVerticalOffsetProperty =
DependencyProperty.RegisterAttached("AnimatedScrollViewerVerticalOffset", typeof(double), typeof(Behavior), new PropertyMetadata(0.0, scroll_changed));
static void scroll_changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
ScrollViewer target = d as ScrollViewer;
if (target != null) {
target.ScrollToVerticalOffset((double)e.NewValue);
}
}
#endregion
#region AutoFocusTargetProvider
public static readonly DependencyProperty AutoFocusTargetProviderProperty = DependencyProperty.RegisterAttached(
"AutoFocusTargetProvider", typeof(IAutoFocusTargetProvider), typeof(Behavior), new PropertyMetadata(OnAutoFocusTargetProviderChanged));
static void OnAutoFocusTargetProviderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (e.NewValue is IAutoFocusTargetProvider aftp) {
var el = ExceptionAssert.Argument.Is<FrameworkElement>(d, nameof(d));
d.SetValue(AutoFocusTargetProviderLifeDependencyPropertyKey, new AutoFocusTargetProviderLifeDependency(el, aftp));
} else
d.SetValue(AutoFocusTargetProviderLifeDependencyPropertyKey, null);
}
public static IAutoFocusTargetProvider GetAutoFocusTargetProvider(DependencyObject d) =>
d.GetValue(AutoFocusTargetProviderProperty) as IAutoFocusTargetProvider;
public static void SetAutoFocusTargetProvider(DependencyObject d, IAutoFocusTargetProvider value) =>
d.SetValue(AutoFocusTargetProviderProperty, value);
#endregion
#region AutoFocusTargetProviderLifeDependency (ReadOnly)
static readonly DependencyPropertyKey AutoFocusTargetProviderLifeDependencyPropertyKey = DependencyProperty.RegisterAttachedReadOnly("AutoFocusTargetProviderLifeDependency",
typeof(FrameworkElementLifeDependency), typeof(Behavior), new PropertyMetadata(FrameworkElementLifeDependencyHelper.DisposeOldInitializeNew));
public static readonly DependencyProperty AutoFocusTargetProviderLifeDependencyProperty = AutoFocusTargetProviderLifeDependencyPropertyKey.DependencyProperty;
public static FrameworkElementLifeDependency GetAutoFocusTargetProviderLifeDependency(DependencyObject d) => d.GetValue(AutoFocusTargetProviderLifeDependencyProperty) as FrameworkElementLifeDependency;
public static void SetAutoFocusTargetProviderLifeDependency(DependencyObject d, FrameworkElementLifeDependency value) => d.SetValue(AutoFocusTargetProviderLifeDependencyPropertyKey, value);
class AutoFocusTargetProviderLifeDependency : FrameworkElementLifeDependency {
readonly IAutoFocusTargetProvider _focus_target_provider;
public AutoFocusTargetProviderLifeDependency(FrameworkElement root, IAutoFocusTargetProvider focus_target_provider) : base(root) =>
_focus_target_provider = focus_target_provider;
protected override void OnLoaded() {
var el = _focus_target_provider.Target;
if (el != null) {
if (_focus_target_provider.Focus) {
var fs = FocusManager.GetFocusScope(el);
if (fs is not null) {
FocusManager.SetFocusedElement(fs, el);
if (fs is UIElement fsUiEl) {
fsUiEl.Focus();
} else if (fs is IInputElement fsInputEl) {
Keyboard.Focus(fsInputEl);
} else {
el.Focus();
}
} else {
el.Focus();
}
}
if (_focus_target_provider.FocusKeyboard)
Keyboard.Focus(el);
if (_focus_target_provider.CaptureMouse)
el.CaptureMouse();
}
}
protected override void OnUnloaded() {
var el = _focus_target_provider.Target;
if (el != null) {
if (_focus_target_provider.ReleaseKeyboard) {
ReferenceEquals(Keyboard.FocusedElement, el);
Keyboard.ClearFocus();
}
if (_focus_target_provider.ReleaseMouse)
el.ReleaseMouseCapture();
}
}
}
#endregion
#region EatMouseDown
public static readonly DependencyProperty EatMouseDownProperty =
DependencyProperty.RegisterAttached("EatMouseDown", typeof(bool), typeof(Behavior), new PropertyMetadata(false, OnEatMouseDownChanged));
static void OnEatMouseDownChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var el = (FrameworkElement)d;
if ((bool)e.NewValue) el.MouseDown += _eat_mouse_down;
else el.MouseDown -= _eat_mouse_down;
}
static void _eat_mouse_down(object sender, MouseButtonEventArgs e) =>
e.Handled = true;
public static bool GetEatMouseDown(DependencyObject d) => (bool)d.GetValue(EatMouseDownProperty);
public static void SetEatMouseDown(DependencyObject d, bool value) => d.SetValue(EatMouseDownProperty, value);
#endregion
#region DisablePaste
public static readonly DependencyProperty DisablePasteProperty = DependencyProperty.RegisterAttached(
"DisablePaste", typeof(bool), typeof(Behavior), new PropertyMetadata(false, OnDisablePasteChanged));
static void OnDisablePasteChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if ((bool)e.NewValue)
DataObject.AddPastingHandler(d, _disable_paste_pasting_handler);
else DataObject.RemovePastingHandler(d, _disable_paste_pasting_handler);
}
static void _disable_paste_pasting_handler(object sender, DataObjectPastingEventArgs e) => e.CancelCommand();
public static bool GetDisablePaste(DependencyObject d) => (bool)d.GetValue(DisablePasteProperty);
public static void SetDisablePaste(DependencyObject d, bool value) => d.SetValue(DisablePasteProperty, value);
#endregion
public static readonly DependencyProperty BindingUpdateGroupsProperty =
DependencyProperty.RegisterAttached("BindingUpdateGroups", typeof(BindingUpdateGroupCollection), typeof(Behavior), new PropertyMetadata(null));
public static BindingUpdateGroupCollection GetBindingUpdateGroups(DependencyObject d) => d.GetValue(BindingUpdateGroupsProperty) as BindingUpdateGroupCollection;
public static void SetBindingUpdateGroups(DependencyObject d, BindingUpdateGroupCollection value) => d.SetValue(BindingUpdateGroupsProperty, value);
#region AutoRaiseCanExecuteChanged
public static readonly DependencyProperty AutoRaiseCanExecuteChangedProperty =
DependencyProperty.RegisterAttached("AutoRaiseCanExecuteChanged", typeof(bool), typeof(Behavior),
new PropertyMetadata(false, (d, e) => {
if ((bool)e.NewValue) {
BindingOperations.SetBinding(d, AutoCommandProperty, new Binding(nameof(ButtonBase.Command)) { Source = d, Mode = BindingMode.OneWay });
BindingOperations.SetBinding(d, AutoCommandParameterProperty, new Binding(nameof(ButtonBase.CommandParameter)) { Source = d, Mode = BindingMode.OneWay });
} else {
BindingOperations.ClearBinding(d, AutoCommandProperty);
BindingOperations.ClearBinding(d, AutoCommandParameterProperty);
}
}));
public static bool GetAutoRaiseCanExecuteChanged(DependencyObject d) => (bool)d.GetValue(AutoRaiseCanExecuteChangedProperty);
public static void SetAutoRaiseCanExecuteChanged(DependencyObject d, bool value) => d.SetValue(AutoRaiseCanExecuteChangedProperty, value);
/// Do not bind directly to <see cref="AutoCommandProperty"/> (used internally by <see cref="AutoRaiseCanExecuteChangedProperty"/>
public static readonly DependencyProperty AutoCommandProperty =
DependencyProperty.RegisterAttached("AutoCommand", typeof(ICommand), typeof(Behavior), new PropertyMetadata(null, _try_raise_can_execute));
public static ICommand GetAutoCommand(DependencyObject d) => d.GetValue(AutoCommandProperty) as ICommand;
public static void SetAutoCommand(DependencyObject d, ICommand value) => d.SetValue(AutoCommandProperty, value);
/// Do not bind directly to <see cref="AutoCommandParameterProperty"/> (used internally by <see cref="AutoRaiseCanExecuteChangedProperty"/>
public static readonly DependencyProperty AutoCommandParameterProperty =
DependencyProperty.RegisterAttached("AutoCommandParameter", typeof(object), typeof(Behavior), new PropertyMetadata(null, _try_raise_can_execute));
public static object GetAutoCommandParameter(DependencyObject d) => d.GetValue(AutoCommandParameterProperty);
public static void SetAutoCommandParameter(DependencyObject d, object value) => d.SetValue(AutoCommandParameterProperty, value);
static void _try_raise_can_execute(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
(d.GetValue(ButtonBase.CommandProperty) as IRaiseCanExecuteChangedCommand)?.RaiseCanExecuteChanged();
#endregion
#region FocusOnLoaded
public static readonly DependencyProperty FocusOnLoadedProperty =
DependencyProperty.RegisterAttached("FocusOnLoaded", typeof(bool), typeof(Behavior), new PropertyMetadata(false, OnFocusOnLoadedChanged));
static void OnFocusOnLoadedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var target = (FrameworkElement)d;
if ((bool)e.NewValue) target.Loaded += _target_Loaded;
else target.Loaded -= _target_Loaded;
}
static void _target_Loaded(object sender, RoutedEventArgs e) {
var target = (FrameworkElement)sender;
Keyboard.Focus(target);
}
public static bool GetFocusOnLoaded(DependencyObject d) => (bool)d.GetValue(FocusOnLoadedProperty);
public static void SetFocusOnLoaded(DependencyObject d, bool value) => d.SetValue(FocusOnLoadedProperty, value);
#endregion
#region MirrorLayout
static readonly DependencyProperty[] _layout_props = new[] {
FrameworkElement.HeightProperty,
FrameworkElement.WidthProperty,
FrameworkElement.MarginProperty,
Control.PaddingProperty,
FrameworkElement.HorizontalAlignmentProperty,
Control.HorizontalContentAlignmentProperty,
FrameworkElement.VerticalAlignmentProperty,
Control.VerticalContentAlignmentProperty
};
public static readonly DependencyProperty MirrorLayoutForProperty =
DependencyProperty.RegisterAttached("MirrorLayoutFor", typeof(FrameworkElement), typeof(Behavior),
new PropertyMetadata(OnMirrorLayoutForChanged));
static void OnMirrorLayoutForChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var target = new LazyCast<FrameworkElement, DependencyObject>(d);
if (e.OldValue is FrameworkElement old_src) {
target.value.clear_bindings(_layout_props);
}
if (e.NewValue is FrameworkElement new_src) {
target.value.bind(FrameworkElement.WidthProperty, new_src, FrameworkElement.ActualWidthProperty);
target.value.bind(FrameworkElement.HeightProperty, new_src, FrameworkElement.ActualHeightProperty);
target.value.bind(FrameworkElement.MarginProperty, new_src, FrameworkElement.MarginProperty);
target.value.bind(FrameworkElement.HorizontalAlignmentProperty, new_src, FrameworkElement.HorizontalAlignmentProperty);
target.value.bind(FrameworkElement.VerticalAlignmentProperty, new_src, FrameworkElement.VerticalAlignmentProperty);
if (target.value is Control && new_src is Control) {
target.value.bind(Control.PaddingProperty, new_src, Control.PaddingProperty);
target.value.bind(Control.HorizontalContentAlignmentProperty, new_src, Control.HorizontalContentAlignmentProperty);
target.value.bind(Control.VerticalContentAlignmentProperty, new_src, Control.VerticalContentAlignmentProperty);
}
}
}
public static FrameworkElement GetMirrorLayoutFor(DependencyObject d) => d.GetValue(MirrorLayoutForProperty) as FrameworkElement;
public static void SetMirrorLayoutFor(DependencyObject d, FrameworkElement value) => d.SetValue(MirrorLayoutForProperty, value);
#endregion
}
}
@@ -1,348 +1,371 @@
using bts;
using mbts.ui.native;
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
namespace mbts.ui {
public partial class WindowStyle : ResourceDictionary {
// 'Top' is 0 because the window's TitleContent overlaps the top-resize-border.
public static readonly Thickness NegativeResizeBorderThickness = new Thickness(-5, 0, -5, -5);
public static readonly ExecutedRoutedEventHandler
CloseWindowExecutedHandler = WindowCallbackHelper.safe_command_executed_handler(w => w.Close(), "closing"),
HideWindowExecutedHandler = WindowCallbackHelper.safe_command_executed_handler(w => w.Visibility = Visibility.Hidden, "hiding"),
MinimizeWindowExecutedHandler = WindowCallbackHelper.command_executed_handler(w => w.WindowState = WindowState.Minimized),
MaximizeWindowExecutedHandler = WindowCallbackHelper.command_executed_handler(w => w.WindowState = w.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized);
#region UnsafeWindow
// UnsafeWindow is described as 'Unsafe' because the window may be on a different dispatcher than the DependencyObject target.
public static readonly DependencyProperty UnsafeWindowProperty =
DependencyProperty.RegisterAttached("UnsafeWindow", typeof(Window), typeof(WindowStyle), new PropertyMetadata(OnUnsafeWindowChanged));
static void OnUnsafeWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (d is UIElement el)
el.RaiseEvent(new RoutedPropertyChangedEventArgs<Window>(e.OldValue as Window, e.NewValue as Window, UnsafeWindowChangedEvent));
}
public static Window GetUnsafeWindow(DependencyObject d) => d.GetValue(UnsafeWindowProperty) as Window;
public static void SetUnsafeWindow(DependencyObject d, Window value) => d.SetValue(UnsafeWindowProperty, value);
#endregion
#region UnsafeWindowChanged (Event)
internal static readonly RoutedEvent UnsafeWindowChangedEvent = EventManager.RegisterRoutedEvent("UnsafeWindowChanged", RoutingStrategy.Direct, typeof(RoutedPropertyChangedEventHandler<Window>), typeof(WindowStyle));
internal static void AddUnsafeWindowChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<Window> handler) =>
el.AddHandler(UnsafeWindowChangedEvent, handler);
internal static void RemoveUnsafeWindowChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<Window> handler) =>
el.RemoveHandler(UnsafeWindowChangedEvent, handler);
#endregion
static class WindowCallbackHelper {
public static ExecutedRoutedEventHandler safe_command_executed_handler(Action<Window> action, string action_name) =>
(s, e) => _safe_execute_on_window(s, action, action_name);
public static ExecutedRoutedEventHandler command_executed_handler(Action<Window> action) =>
(s, e) => _execute_on_window(s, action);
public static RoutedEventHandler routed_event_handler(Action<Window> action) =>
(s, e) => _execute_on_window(s, action);
static void _execute_on_window(object sender, Action<Window> action) {
if (_get_host_window(sender) is Window window)
action(window);
}
static void _safe_execute_on_window(object sender, Action<Window> action, string action_name) {
try {
_execute_on_window(sender, action);
} catch (Exception err) {
Log.Ui.warn($"Exception {action_name} window : {err}");
}
}
static Window _get_host_window(object sender) =>
sender is Window w ? w : sender is DependencyObject d ? Window.GetWindow(d) : null;
}
#region Attached properties to control the style b/c the default Window properties are lame
#region Close button
public static bool GetShowCloseButton(DependencyObject obj) {
return (bool)obj.GetValue(ShowCloseButtonProperty);
}
public static void SetShowCloseButton(DependencyObject obj, bool value) {
obj.SetValue(ShowCloseButtonProperty, value);
}
public static readonly DependencyProperty ShowCloseButtonProperty =
DependencyProperty.RegisterAttached("ShowCloseButton", typeof(bool), typeof(WindowStyle), new PropertyMetadata(true));
#endregion
#region Minimize Button
public static bool GetShowMinimizeButton(DependencyObject obj) {
return (bool)obj.GetValue(ShowMinimizeButtonProperty);
}
public static void SetShowMinimizeButton(DependencyObject obj, bool value) {
obj.SetValue(ShowMinimizeButtonProperty, value);
}
public static readonly DependencyProperty ShowMinimizeButtonProperty =
DependencyProperty.RegisterAttached("ShowMinimizeButton", typeof(bool), typeof(WindowStyle), new PropertyMetadata(true));
#endregion
#region Maximize Button
public static bool GetShowMaximizeButton(DependencyObject obj) {
return (bool)obj.GetValue(ShowMaximizeButtonProperty);
}
public static void SetShowMaximizeButton(DependencyObject obj, bool value) {
obj.SetValue(ShowMaximizeButtonProperty, value);
}
public static readonly DependencyProperty ShowMaximizeButtonProperty =
DependencyProperty.RegisterAttached("ShowMaximizeButton", typeof(bool), typeof(WindowStyle), new PropertyMetadata(false));
#endregion
#region AllowResize
public static bool GetAllowResize(DependencyObject obj) {
return (bool)obj.GetValue(AllowResizeProperty);
}
public static void SetAllowResize(DependencyObject obj, bool value) {
obj.SetValue(AllowResizeProperty, value);
}
public static readonly DependencyProperty AllowResizeProperty =
DependencyProperty.RegisterAttached("AllowResize", typeof(bool), typeof(WindowStyle), new PropertyMetadata(false, (s, e) => {
var wnd = s as Window;
if (wnd != null) {
if ((bool)e.NewValue == true) {
wnd.ResizeMode = ResizeMode.CanResize;
} else {
wnd.ResizeMode = ResizeMode.NoResize;
}
}
}));
#endregion
#region IsNonClientArea Inherited property
public static bool GetIsNonClientArea(DependencyObject obj) {
return (bool)obj.GetValue(IsNonClientAreaProperty);
}
public static void SetIsNonClientArea(DependencyObject obj, bool value) {
obj.SetValue(IsNonClientAreaProperty, value);
}
public static readonly DependencyProperty IsNonClientAreaProperty =
DependencyProperty.RegisterAttached("IsNonClientArea", typeof(bool), typeof(WindowStyle), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits));
#endregion
#region [Custom]SizeToContent property (some additional logic is needed to avoid ugly black border margins on the edge of the window)
public static readonly DependencyProperty SizeToContentProperty = DependencyProperty.RegisterAttached("SizeToContent", typeof(SizeToContent), typeof(WindowStyle), new PropertyMetadata(SizeToContent.Manual, OnSizeToContentChanged));
private static void OnSizeToContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
SizeToContent old_val = (SizeToContent)e.OldValue;
SizeToContent new_val = (SizeToContent)e.NewValue;
Window w = (Window)d;
if (old_val != SizeToContent.Manual) {
if (new_val == SizeToContent.Manual) w.ContentRendered -= InvalidateVisualOnContentRendered;
} else w.ContentRendered += InvalidateVisualOnContentRendered;
w.SizeToContent = new_val;
}
private static void InvalidateVisualOnContentRendered(object sender, EventArgs e) { ((Window)sender).InvalidateVisual(); }
public static SizeToContent GetSizeToContent(Window w) { return (SizeToContent)w.GetValue(SizeToContentProperty); }
public static void SetSizeToContent(Window w, SizeToContent value) { w.SetValue(SizeToContentProperty, value); }
#endregion
#region TitleContent
public static readonly DependencyProperty TitleContentProperty = DependencyProperty.RegisterAttached("TitleContent", typeof(object), typeof(WindowStyle), new PropertyMetadata(null));
public static object GetTitleContent(DependencyObject d) { return d.GetValue(TitleContentProperty); }
public static void SetTitleContent(DependencyObject d, object value) { d.SetValue(TitleContentProperty, value); }
#endregion
#region TitleContentTemplate
public static readonly DependencyProperty TitleContentTemplateProperty = DependencyProperty.RegisterAttached("TitleContentTemplate", typeof(DataTemplate), typeof(WindowStyle), new PropertyMetadata(null));
public static DataTemplate GetTitleContentTemplate(DependencyObject d) { return d.GetValue(TitleContentTemplateProperty) as DataTemplate; }
public static void SetTitleContentTemplate(DependencyObject d, DataTemplate value) { d.SetValue(TitleContentTemplateProperty, value); }
#endregion
#region DisposeVMOnClose
public static readonly DependencyProperty DisposeVMOnCloseProperty = DependencyProperty.RegisterAttached("DisposeVMOnClose", typeof(bool), typeof(WindowStyle), new PropertyMetadata(false, OnDisposeVMOnCloseChanged));
private static void OnDisposeVMOnCloseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var target = d as Window;
target?.SetValue(DisposeVMOnCloseHandlerWrapperPropertyKey, (bool)e.NewValue ? new DisposeVMOnCloseHandlerWrapper(target) : null);
}
public static bool GetDisposeVMOnClose(DependencyObject d) { return (bool)d.GetValue(DisposeVMOnCloseProperty); }
public static void SetDisposeVMOnClose(DependencyObject d, bool value) { d.SetValue(DisposeVMOnCloseProperty, value); }
private static readonly DependencyPropertyKey DisposeVMOnCloseHandlerWrapperPropertyKey = DependencyProperty.RegisterAttachedReadOnly("DisposeVMOnCloseHandlerWrapper", typeof(IDisposable), typeof(WindowStyle), new PropertyMetadata(null, FrameworkElementLifeDependencyHelper.DisposeOld));
internal static readonly DependencyProperty DisposeVMOnCloseHandlerWrapperProperty = DisposeVMOnCloseHandlerWrapperPropertyKey.DependencyProperty;
internal static IDisposable GetDisposeVMOnCloseHandlerWrapper(Window w) { return w.GetValue(DisposeVMOnCloseHandlerWrapperProperty) as IDisposable; }
private static void SetDisposeVMOnCloseHandlerWrapper(Window w, IDisposable value) { w.SetValue(DisposeVMOnCloseHandlerWrapperProperty, value); }
private class DisposeVMOnCloseHandlerWrapper : IDisposable {
public DisposeVMOnCloseHandlerWrapper(Window target) { (_target = target).Closed += _target_Closed; }
private readonly Window _target;
private void _target_Closed(object sender, EventArgs e) { (_target.DataContext as IDisposable)?.Dispose(); }
public void Dispose() { _target.Closed -= _target_Closed; }
}
#endregion
#endregion
private void style_loaded(object sender, RoutedEventArgs _) {
if (sender is DependencyObject) {
var window = Window.GetWindow(((DependencyObject)sender));
if (window != null) {
if (WindowStyle.GetAllowResize(window)) {
set_resize_window_events(sender as FrameworkElement, window);
} else {
// Simpler dragmove interaction
var border = (FrameworkElement)sender;
border.MouseLeftButtonDown += (s, e) => {
if (Mouse.LeftButton == MouseButtonState.Pressed) {
window.DragMove();
}
};
}
// Handle Close command automagically
if (window.CommandBindings.Count == 0) {
window.CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, CloseWindowExecutedHandler));
window.CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, MinimizeWindowExecutedHandler));
window.CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, MaximizeWindowExecutedHandler));
}
}
}
}
private static void set_resize_window_events(FrameworkElement border, Window window) {
HookWndProc(window);
Log.assert(border != null);
var restore_on_move = false;
border.MouseLeftButtonDown += (s, e) => {
FrameworkElement fe = e.OriginalSource as FrameworkElement;
if (fe != null && (bool)fe.GetValue(WindowStyle.IsNonClientAreaProperty)) {
if (e.ClickCount == 2) {
toggle_max(window);
} else {
if (window.WindowState == WindowState.Maximized) { restore_on_move = true; }
if (Mouse.LeftButton == MouseButtonState.Pressed) {
window.DragMove();
}
}
}
};
border.MouseLeftButtonUp += (s, e) => restore_on_move = false;
border.MouseMove += (s, e) => {
if (restore_on_move && window.WindowState == WindowState.Maximized && Mouse.LeftButton == MouseButtonState.Pressed) {
window.WindowState = WindowState.Normal;
window.Top = 0;
if (Mouse.LeftButton == MouseButtonState.Pressed) {
window.DragMove();
}
}
};
}
private static void toggle_max(Window window) {
if (window.WindowState == WindowState.Maximized) {
window.WindowState = WindowState.Normal;
} else {
window.WindowState = WindowState.Maximized;
}
}
private static void HookWndProc(Window window) {
HwndSource source = PresentationSource.FromVisual(window) as HwndSource;
if (source != null) {
source.AddHook(new HwndSourceHook(WndProc));
}
}
private static void UnhookWndProc(Window window) {
HwndSource source = PresentationSource.FromVisual(window) as HwndSource;
if (source != null) {
source.RemoveHook(new HwndSourceHook(WndProc));
}
}
private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) {
WM wmsg = (WM)msg;
if (WM.NCHITTEST == wmsg /*|| WM.MOUSEMOVE == wmsg*/) {
Thickness t = new Thickness(6);
// Get the mouse coordinates in screen space
short x = (short)lparam.ToInt32();
short y = (short)(lparam.ToInt32() >> 16);
// Get the window bounds in screen coords
Win32Rect w;
NativeMethods.GetWindowRect(hwnd, out w);
// Assume that mouse position is inside the window
bool left = x < (w.Left + t.Left);
bool right = x > (w.Right - t.Right);
bool top = y < (w.Top + t.Top);
bool bottom = y > (w.Bottom - t.Bottom);
// A B C
// D E
// F G H
HT ret = HT.CLIENT;
if (top) {
if (left) ret = HT.TOPLEFT; // A
else if (right) ret = HT.TOPRIGHT; // C
else ret = HT.TOP; // B
} else if (bottom) {
if (left) ret = HT.BOTTOMLEFT; // F
else if (right) ret = HT.BOTTOMRIGHT; // H
else ret = HT.BOTTOM; // G
} else {
if (left) ret = HT.LEFT; // D
if (right) ret = HT.RIGHT; // E
}
handled = true;
return new IntPtr((int)ret);
} else if (wmsg == WM.NCCALCSIZE) {
handled = true;
} else if (wmsg == WM.NCPAINT) {
handled = true;
} else if (wmsg == WM.NCACTIVATE) {
handled = true;
return NativeMethods.DefWindowProc(hwnd, msg, wparam, new IntPtr(-1));
} else if (wmsg == WM.ERASEBKGND) {
handled = true;
} else if (wmsg == WM.GETMINMAXINFO) {
SetMinMaxInfo(hwnd, lparam);
handled = true;
}
return IntPtr.Zero;
}
private static void SetMinMaxInfo(IntPtr hwnd, IntPtr lparam) {
var mminfo = (MINMAXINFO)Marshal.PtrToStructure(lparam, typeof(MINMAXINFO));
var hmonitor = NativeMethods.MonitorFromWindow(hwnd, NativeMethods.MONITOR_DEFAULTONNEAREST);
if (hmonitor != IntPtr.Zero) {
MONITORINFO minfo = new MONITORINFO();
minfo.cbSize = Marshal.SizeOf(minfo);
NativeMethods.GetMonitorInfo(hmonitor, ref minfo);
var rc_monitor = minfo.rcMonitor;
var rc_work = minfo.rcWork;
mminfo.ptMaxPosition.x = Math.Abs(rc_work.Left - rc_monitor.Left);
mminfo.ptMaxPosition.y = Math.Abs(rc_work.Top - rc_monitor.Top);
mminfo.ptMaxSize.x = Math.Abs(rc_work.Right - rc_work.Left);
mminfo.ptMaxSize.y = Math.Abs(rc_work.Bottom - rc_work.Top);
Marshal.StructureToPtr(mminfo, lparam, true);
}
}
}
}
using bts;
using mbts.ui.native;
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
namespace mbts.ui {
public partial class WindowStyle : ResourceDictionary {
// 'Top' is 0 because the window's TitleContent overlaps the top-resize-border.
public static readonly Thickness NegativeResizeBorderThickness = new Thickness(-5, 0, -5, -5);
public static readonly ExecutedRoutedEventHandler
CloseWindowExecutedHandler = WindowCallbackHelper.safe_command_executed_handler(w => w.Close(), "closing"),
HideWindowExecutedHandler = WindowCallbackHelper.safe_command_executed_handler(w => w.Visibility = Visibility.Hidden, "hiding"),
MinimizeWindowExecutedHandler = WindowCallbackHelper.command_executed_handler(w => w.WindowState = WindowState.Minimized),
MaximizeWindowExecutedHandler = WindowCallbackHelper.command_executed_handler(w => w.WindowState = w.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized);
#region UnsafeWindow
// UnsafeWindow is described as 'Unsafe' because the window may be on a different dispatcher than the DependencyObject target.
public static readonly DependencyProperty UnsafeWindowProperty =
DependencyProperty.RegisterAttached("UnsafeWindow", typeof(Window), typeof(WindowStyle), new PropertyMetadata(OnUnsafeWindowChanged));
static void OnUnsafeWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (d is UIElement el)
el.RaiseEvent(new RoutedPropertyChangedEventArgs<Window>(e.OldValue as Window, e.NewValue as Window, UnsafeWindowChangedEvent));
}
public static Window GetUnsafeWindow(DependencyObject d) => d.GetValue(UnsafeWindowProperty) as Window;
public static void SetUnsafeWindow(DependencyObject d, Window value) => d.SetValue(UnsafeWindowProperty, value);
#endregion
#region UnsafeWindowChanged (Event)
internal static readonly RoutedEvent UnsafeWindowChangedEvent = EventManager.RegisterRoutedEvent("UnsafeWindowChanged", RoutingStrategy.Direct, typeof(RoutedPropertyChangedEventHandler<Window>), typeof(WindowStyle));
internal static void AddUnsafeWindowChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<Window> handler) =>
el.AddHandler(UnsafeWindowChangedEvent, handler);
internal static void RemoveUnsafeWindowChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<Window> handler) =>
el.RemoveHandler(UnsafeWindowChangedEvent, handler);
#endregion
#region WinFormsHost
public static readonly DependencyProperty WinFormsHostProperty =
DependencyProperty.RegisterAttached("WinFormsHost", typeof(WPFPanel), typeof(WindowStyle), new PropertyMetadata(OnWinFormsHostChanged));
static void OnWinFormsHostChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (d is UIElement el)
el.RaiseEvent(new RoutedPropertyChangedEventArgs<WPFPanel>(e.OldValue as WPFPanel, e.NewValue as WPFPanel, WinFormsHostChangedEvent));
}
public static WPFPanel GetWinFormsHost(DependencyObject d) => d.GetValue(WinFormsHostProperty) as WPFPanel;
public static void SetWinFormsHost(DependencyObject d, WPFPanel value) => d.SetValue(WinFormsHostProperty, value);
#endregion
#region WinFormsHostChanged (Event)
internal static readonly RoutedEvent WinFormsHostChangedEvent = EventManager.RegisterRoutedEvent("WinFormsHostChanged", RoutingStrategy.Direct, typeof(RoutedPropertyChangedEventHandler<WPFPanel>), typeof(WindowStyle));
internal static void AddWinFormsHostChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<WPFPanel> handler) =>
el.AddHandler(WinFormsHostChangedEvent, handler);
internal static void RemoveWinFormsHostChangedHandler(UIElement el, RoutedPropertyChangedEventHandler<WPFPanel> handler) =>
el.RemoveHandler(WinFormsHostChangedEvent, handler);
#endregion
static class WindowCallbackHelper {
public static ExecutedRoutedEventHandler safe_command_executed_handler(Action<Window> action, string action_name) =>
(s, e) => _safe_execute_on_window(s, action, action_name);
public static ExecutedRoutedEventHandler command_executed_handler(Action<Window> action) =>
(s, e) => _execute_on_window(s, action);
public static RoutedEventHandler routed_event_handler(Action<Window> action) =>
(s, e) => _execute_on_window(s, action);
static void _execute_on_window(object sender, Action<Window> action) {
if (_get_host_window(sender) is Window window)
action(window);
}
static void _safe_execute_on_window(object sender, Action<Window> action, string action_name) {
try {
_execute_on_window(sender, action);
} catch (Exception err) {
Log.Ui.warn($"Exception {action_name} window : {err}");
}
}
static Window _get_host_window(object sender) =>
sender is Window w ? w : sender is DependencyObject d ? Window.GetWindow(d) : null;
}
#region Attached properties to control the style b/c the default Window properties are lame
#region Close button
public static bool GetShowCloseButton(DependencyObject obj) {
return (bool)obj.GetValue(ShowCloseButtonProperty);
}
public static void SetShowCloseButton(DependencyObject obj, bool value) {
obj.SetValue(ShowCloseButtonProperty, value);
}
public static readonly DependencyProperty ShowCloseButtonProperty =
DependencyProperty.RegisterAttached("ShowCloseButton", typeof(bool), typeof(WindowStyle), new PropertyMetadata(true));
#endregion
#region Minimize Button
public static bool GetShowMinimizeButton(DependencyObject obj) {
return (bool)obj.GetValue(ShowMinimizeButtonProperty);
}
public static void SetShowMinimizeButton(DependencyObject obj, bool value) {
obj.SetValue(ShowMinimizeButtonProperty, value);
}
public static readonly DependencyProperty ShowMinimizeButtonProperty =
DependencyProperty.RegisterAttached("ShowMinimizeButton", typeof(bool), typeof(WindowStyle), new PropertyMetadata(true));
#endregion
#region Maximize Button
public static bool GetShowMaximizeButton(DependencyObject obj) {
return (bool)obj.GetValue(ShowMaximizeButtonProperty);
}
public static void SetShowMaximizeButton(DependencyObject obj, bool value) {
obj.SetValue(ShowMaximizeButtonProperty, value);
}
public static readonly DependencyProperty ShowMaximizeButtonProperty =
DependencyProperty.RegisterAttached("ShowMaximizeButton", typeof(bool), typeof(WindowStyle), new PropertyMetadata(false));
#endregion
#region AllowResize
public static bool GetAllowResize(DependencyObject obj) {
return (bool)obj.GetValue(AllowResizeProperty);
}
public static void SetAllowResize(DependencyObject obj, bool value) {
obj.SetValue(AllowResizeProperty, value);
}
public static readonly DependencyProperty AllowResizeProperty =
DependencyProperty.RegisterAttached("AllowResize", typeof(bool), typeof(WindowStyle), new PropertyMetadata(false, (s, e) => {
var wnd = s as Window;
if (wnd != null) {
if ((bool)e.NewValue == true) {
wnd.ResizeMode = ResizeMode.CanResize;
} else {
wnd.ResizeMode = ResizeMode.NoResize;
}
}
}));
#endregion
#region IsNonClientArea Inherited property
public static bool GetIsNonClientArea(DependencyObject obj) {
return (bool)obj.GetValue(IsNonClientAreaProperty);
}
public static void SetIsNonClientArea(DependencyObject obj, bool value) {
obj.SetValue(IsNonClientAreaProperty, value);
}
public static readonly DependencyProperty IsNonClientAreaProperty =
DependencyProperty.RegisterAttached("IsNonClientArea", typeof(bool), typeof(WindowStyle), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits));
#endregion
#region [Custom]SizeToContent property (some additional logic is needed to avoid ugly black border margins on the edge of the window)
public static readonly DependencyProperty SizeToContentProperty = DependencyProperty.RegisterAttached("SizeToContent", typeof(SizeToContent), typeof(WindowStyle), new PropertyMetadata(SizeToContent.Manual, OnSizeToContentChanged));
private static void OnSizeToContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
SizeToContent old_val = (SizeToContent)e.OldValue;
SizeToContent new_val = (SizeToContent)e.NewValue;
Window w = (Window)d;
if (old_val != SizeToContent.Manual) {
if (new_val == SizeToContent.Manual) w.ContentRendered -= InvalidateVisualOnContentRendered;
} else w.ContentRendered += InvalidateVisualOnContentRendered;
w.SizeToContent = new_val;
}
private static void InvalidateVisualOnContentRendered(object sender, EventArgs e) { ((Window)sender).InvalidateVisual(); }
public static SizeToContent GetSizeToContent(Window w) { return (SizeToContent)w.GetValue(SizeToContentProperty); }
public static void SetSizeToContent(Window w, SizeToContent value) { w.SetValue(SizeToContentProperty, value); }
#endregion
#region TitleContent
public static readonly DependencyProperty TitleContentProperty = DependencyProperty.RegisterAttached("TitleContent", typeof(object), typeof(WindowStyle), new PropertyMetadata(null));
public static object GetTitleContent(DependencyObject d) { return d.GetValue(TitleContentProperty); }
public static void SetTitleContent(DependencyObject d, object value) { d.SetValue(TitleContentProperty, value); }
#endregion
#region TitleContentTemplate
public static readonly DependencyProperty TitleContentTemplateProperty = DependencyProperty.RegisterAttached("TitleContentTemplate", typeof(DataTemplate), typeof(WindowStyle), new PropertyMetadata(null));
public static DataTemplate GetTitleContentTemplate(DependencyObject d) { return d.GetValue(TitleContentTemplateProperty) as DataTemplate; }
public static void SetTitleContentTemplate(DependencyObject d, DataTemplate value) { d.SetValue(TitleContentTemplateProperty, value); }
#endregion
#region DisposeVMOnClose
public static readonly DependencyProperty DisposeVMOnCloseProperty = DependencyProperty.RegisterAttached("DisposeVMOnClose", typeof(bool), typeof(WindowStyle), new PropertyMetadata(false, OnDisposeVMOnCloseChanged));
private static void OnDisposeVMOnCloseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var target = d as Window;
target?.SetValue(DisposeVMOnCloseHandlerWrapperPropertyKey, (bool)e.NewValue ? new DisposeVMOnCloseHandlerWrapper(target) : null);
}
public static bool GetDisposeVMOnClose(DependencyObject d) { return (bool)d.GetValue(DisposeVMOnCloseProperty); }
public static void SetDisposeVMOnClose(DependencyObject d, bool value) { d.SetValue(DisposeVMOnCloseProperty, value); }
private static readonly DependencyPropertyKey DisposeVMOnCloseHandlerWrapperPropertyKey = DependencyProperty.RegisterAttachedReadOnly("DisposeVMOnCloseHandlerWrapper", typeof(IDisposable), typeof(WindowStyle), new PropertyMetadata(null, FrameworkElementLifeDependencyHelper.DisposeOld));
internal static readonly DependencyProperty DisposeVMOnCloseHandlerWrapperProperty = DisposeVMOnCloseHandlerWrapperPropertyKey.DependencyProperty;
internal static IDisposable GetDisposeVMOnCloseHandlerWrapper(Window w) { return w.GetValue(DisposeVMOnCloseHandlerWrapperProperty) as IDisposable; }
private static void SetDisposeVMOnCloseHandlerWrapper(Window w, IDisposable value) { w.SetValue(DisposeVMOnCloseHandlerWrapperProperty, value); }
private class DisposeVMOnCloseHandlerWrapper : IDisposable {
public DisposeVMOnCloseHandlerWrapper(Window target) { (_target = target).Closed += _target_Closed; }
private readonly Window _target;
private void _target_Closed(object sender, EventArgs e) { (_target.DataContext as IDisposable)?.Dispose(); }
public void Dispose() { _target.Closed -= _target_Closed; }
}
#endregion
#endregion
private void style_loaded(object sender, RoutedEventArgs _) {
if (sender is DependencyObject) {
var window = Window.GetWindow(((DependencyObject)sender));
if (window != null) {
if (WindowStyle.GetAllowResize(window)) {
set_resize_window_events(sender as FrameworkElement, window);
} else {
// Simpler dragmove interaction
var border = (FrameworkElement)sender;
border.MouseLeftButtonDown += (s, e) => {
if (Mouse.LeftButton == MouseButtonState.Pressed) {
window.DragMove();
}
};
}
// Handle Close command automagically
if (window.CommandBindings.Count == 0) {
window.CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, CloseWindowExecutedHandler));
window.CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, MinimizeWindowExecutedHandler));
window.CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, MaximizeWindowExecutedHandler));
}
}
}
}
private static void set_resize_window_events(FrameworkElement border, Window window) {
HookWndProc(window);
Log.assert(border != null);
var restore_on_move = false;
border.MouseLeftButtonDown += (s, e) => {
FrameworkElement fe = e.OriginalSource as FrameworkElement;
if (fe != null && (bool)fe.GetValue(WindowStyle.IsNonClientAreaProperty)) {
if (e.ClickCount == 2) {
toggle_max(window);
} else {
if (window.WindowState == WindowState.Maximized) { restore_on_move = true; }
if (Mouse.LeftButton == MouseButtonState.Pressed) {
window.DragMove();
}
}
}
};
border.MouseLeftButtonUp += (s, e) => restore_on_move = false;
border.MouseMove += (s, e) => {
if (restore_on_move && window.WindowState == WindowState.Maximized && Mouse.LeftButton == MouseButtonState.Pressed) {
window.WindowState = WindowState.Normal;
window.Top = 0;
if (Mouse.LeftButton == MouseButtonState.Pressed) {
window.DragMove();
}
}
};
}
private static void toggle_max(Window window) {
if (window.WindowState == WindowState.Maximized) {
window.WindowState = WindowState.Normal;
} else {
window.WindowState = WindowState.Maximized;
}
}
private static void HookWndProc(Window window) {
HwndSource source = PresentationSource.FromVisual(window) as HwndSource;
if (source != null) {
source.AddHook(new HwndSourceHook(WndProc));
}
}
private static void UnhookWndProc(Window window) {
HwndSource source = PresentationSource.FromVisual(window) as HwndSource;
if (source != null) {
source.RemoveHook(new HwndSourceHook(WndProc));
}
}
private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) {
WM wmsg = (WM)msg;
if (WM.NCHITTEST == wmsg /*|| WM.MOUSEMOVE == wmsg*/) {
Thickness t = new Thickness(6);
// Get the mouse coordinates in screen space
short x = (short)lparam.ToInt32();
short y = (short)(lparam.ToInt32() >> 16);
// Get the window bounds in screen coords
Win32Rect w;
NativeMethods.GetWindowRect(hwnd, out w);
// Assume that mouse position is inside the window
bool left = x < (w.Left + t.Left);
bool right = x > (w.Right - t.Right);
bool top = y < (w.Top + t.Top);
bool bottom = y > (w.Bottom - t.Bottom);
// A B C
// D E
// F G H
HT ret = HT.CLIENT;
if (top) {
if (left) ret = HT.TOPLEFT; // A
else if (right) ret = HT.TOPRIGHT; // C
else ret = HT.TOP; // B
} else if (bottom) {
if (left) ret = HT.BOTTOMLEFT; // F
else if (right) ret = HT.BOTTOMRIGHT; // H
else ret = HT.BOTTOM; // G
} else {
if (left) ret = HT.LEFT; // D
if (right) ret = HT.RIGHT; // E
}
handled = true;
return new IntPtr((int)ret);
} else if (wmsg == WM.NCCALCSIZE) {
handled = true;
} else if (wmsg == WM.NCPAINT) {
handled = true;
} else if (wmsg == WM.NCACTIVATE) {
handled = true;
return NativeMethods.DefWindowProc(hwnd, msg, wparam, new IntPtr(-1));
} else if (wmsg == WM.ERASEBKGND) {
handled = true;
} else if (wmsg == WM.GETMINMAXINFO) {
SetMinMaxInfo(hwnd, lparam);
handled = true;
}
return IntPtr.Zero;
}
private static void SetMinMaxInfo(IntPtr hwnd, IntPtr lparam) {
var mminfo = (MINMAXINFO)Marshal.PtrToStructure(lparam, typeof(MINMAXINFO));
var hmonitor = NativeMethods.MonitorFromWindow(hwnd, NativeMethods.MONITOR_DEFAULTONNEAREST);
if (hmonitor != IntPtr.Zero) {
MONITORINFO minfo = new MONITORINFO();
minfo.cbSize = Marshal.SizeOf(minfo);
NativeMethods.GetMonitorInfo(hmonitor, ref minfo);
var rc_monitor = minfo.rcMonitor;
var rc_work = minfo.rcWork;
mminfo.ptMaxPosition.x = Math.Abs(rc_work.Left - rc_monitor.Left);
mminfo.ptMaxPosition.y = Math.Abs(rc_work.Top - rc_monitor.Top);
mminfo.ptMaxSize.x = Math.Abs(rc_work.Right - rc_work.Left);
mminfo.ptMaxSize.y = Math.Abs(rc_work.Bottom - rc_work.Top);
Marshal.StructureToPtr(mminfo, lparam, true);
}
}
}
}
@@ -1,241 +1,242 @@
namespace mbts.ui.shell {
using bts;
using Spark.Core;
using Spark.Core.Async;
using Spark.Core.Disposables;
using System;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
public class ShellWindowVM : DisposableViewModelBase {
readonly IWindowManager _window_manager;
readonly Property<bool> _edit_mode;
readonly Property<string> _title;
readonly Property<string> _title_override;
readonly Property<bool> _has_custom_title;
IShellWindow _model;
UIElement _ui;
ShellWindowUI _document;
HwndContentHost _hwnd_host;
FloatingWindow _owner;
ImageSource _icon;
ICommand _close_command;
ICommand _rename_command;
ICommand _unrename_command;
public ShellWindowVM(IShellWindow model, IWindowManager wm, IShellThreadFactory threads, string host = null) {
Log.assert(model != null);
_model = model;
_model.IconChanged += on_icon_changed;
_window_manager = wm;
host_id = (model as ITabAffineWindow)?.tab_group ?? host ?? string.Empty;
// init Dispatcher
var stf = (ShellThreadFactory)threads;
if (IsShared) {
Dispatcher = stf.get_dispatcher((ISharedWindow)model);
} else if (IsSlow) {
Dispatcher = stf.create_dispatcher((ISlowWindow)model);
} else {
Dispatcher = Dispatcher.CurrentDispatcher;
}
_edit_mode = backing(nameof(EditMode), false);
_title_override = backing<string>(nameof(TitleOverride));
_title = backing(
nameof(Title),
_model.observe_title()
.CombineLatest(
_title_override.observe_current(true),
(title, title_override) => (title, title_override))
.Select(t => t.title_override ?? t.title)
.OnDispatcher());
_has_custom_title = backing(
nameof(HasCustomTitle),
_title_override.observe_current(true).Select(t => !t.is_null_or_white_space()).OnDispatcher());
model.on_shell_window_vm(this);
_dispose = Disposable.Create(() => {
_model.IconChanged -= on_icon_changed;
if (IsShared) {
// Shared threads continue running
Dispatcher.Invoke(() => Disposable.dispose(ref _model));
} else if (IsSlow) {
// Terminating the background thread will dispose the model on that thread
Log.Ui.info("Beginning asyncronous dispose of slow window {0} .".fmt(_model.GetType().ToString()));
Dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
} else {
Disposable.dispose(ref _model);
}
_ui = null;
Disposable.dispose(ref _hwnd_host);
});
void on_icon_changed() {
_icon = null;
RaisePropertyChanged(nameof(Icon));
}
}
public AsyncEvent<(object sender, ShellWindowVM e)> Closing { get; } = new();
public string host_id { get; set; }
public string OwnerName => _model.Owner?.name ?? string.Empty;
public IShellWindow Model => _model;
public Dispatcher Dispatcher { get; }
// title
public string Title => _title.Value;
public string TitleOverride {
get => _title_override.Value;
set {
_title_override.Value = value;
_edit_mode.set(false);
}
}
public bool HasCustomTitle => _has_custom_title.Value;
public ImageSource Icon =>
_icon ??= _model.Icon ?? _model.Owner?.icon;
public bool EditMode => _edit_mode.Value;
public double InitialWidth { get; private set; }
public double InitialHeight { get; private set; }
public UIElement AppContent =>
_ui ??= _create_contents();
public bool IsSlow => _model is ISlowWindow;
public bool IsShared => _model is ISharedWindow;
public bool HasTabAffinity => _model is ITabAffineWindow;
public ICommand rename_command =>
_rename_command ??= new DelegateCommand(_begin_rename);
public ICommand unrename_command =>
_unrename_command ??= new DelegateCommand(() => TitleOverride = string.Empty);
public ICommand CloseCommand =>
_close_command ??= new DelegateCommand(_begin_close);
internal FloatingWindow owner {
get => _owner;
set {
if (Set(ref _owner, value)) {
_try_set_wpf_bus();
}
}
}
public IObservable<string> observe_title() =>
_title.observe_current(true);
void _begin_rename() {
if (string.IsNullOrEmpty(TitleOverride)) {
_title_override.Value = Title;
}
_edit_mode.set(true);
}
async void _begin_close(object sender) {
if (_window_manager is null) {
return;
}
if (!_window_manager.contains(_model)) {
return;
}
await Closing?.InvokeAsync((sender, this));
// Removing the window will cause this object to be Disposed
// Which will terminate the background thread
// Which will dispose the model (on that thread)
await _window_manager.remove(_model);
}
UIElement _create_contents() {
string error_text = null;
Dispatcher.Invoke(() => {
// Executes possibly on background thread
try {
_document = _model.GetContent();
} catch (Exception ex) {
error_text = ex.Message;
Log.Ui.error($"Error getting window contents: {ex}");
}
}, DispatcherPriority.Send);
if (_document == null && string.IsNullOrEmpty(error_text)) {
error_text = $"No content for window {_model.GetType()}";
}
// If there's an error draw a big red X
if (!error_text.is_null_or_empty()) {
_document = new ShellWindowUI(new TextBlock() {
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Foreground = Brushes.Red,
Text = error_text
});
_model = new ErrorShellWindow {
Content = _document,
Icon = _model.Icon,
Title = $"Error: {_model.GetType()}"
};
RaisePropertyChanged("Title");
}
Log.assert(_document != null);
return _document.content.Match(on_wpf_element, on_handle_ref);
UIElement on_wpf_element(UIElement element) {
if (IsSlow || IsShared) {
// Create a WPFPanel (a Winforms window), then attach it with the HwndContentHost.
// This gives the document content a separate thread affinity from the shell window,
// and lets the win32 layer handle the thread boundary.
var retval = Dispatcher.Invoke(() => {
var interop_window = new WPFPanel(element);
return new HandleRef(interop_window, interop_window.Handle);
}, DispatcherPriority.Send);
_hwnd_host = new HwndContentHost(retval);
InitialHeight = _hwnd_host.InitialHeight;
InitialWidth = _hwnd_host.InitialWidth;
return _hwnd_host;
}
if (element is FrameworkElement fe) {
InitialHeight = fe.Height;
InitialWidth = fe.Width;
fe.Height = fe.Width = double.NaN;
}
return element;
}
UIElement on_handle_ref(HandleRef handle_ref) {
_hwnd_host = new HwndContentHost(handle_ref);
InitialHeight = _hwnd_host.Height;
InitialWidth = _hwnd_host.Width;
return _hwnd_host;
}
}
void _try_set_wpf_bus() {
if (_owner is null) {
return;
}
_document.content.try_match(on_wpf_element);
void on_wpf_element(UIElement element) =>
element.Dispatcher.InvokeAsync(() =>
ui.WindowStyle.SetUnsafeWindow(element, _owner));
}
}
}
namespace mbts.ui.shell {
using bts;
using Spark.Core;
using Spark.Core.Async;
using Spark.Core.Disposables;
using System;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
public class ShellWindowVM : DisposableViewModelBase {
readonly IWindowManager _window_manager;
readonly Property<bool> _edit_mode;
readonly Property<string> _title;
readonly Property<string> _title_override;
readonly Property<bool> _has_custom_title;
IShellWindow _model;
UIElement _ui;
ShellWindowUI _document;
HwndContentHost _hwnd_host;
FloatingWindow _owner;
ImageSource _icon;
ICommand _close_command;
ICommand _rename_command;
ICommand _unrename_command;
public ShellWindowVM(IShellWindow model, IWindowManager wm, IShellThreadFactory threads, string host = null) {
Log.assert(model != null);
_model = model;
_model.IconChanged += on_icon_changed;
_window_manager = wm;
host_id = (model as ITabAffineWindow)?.tab_group ?? host ?? string.Empty;
// init Dispatcher
var stf = (ShellThreadFactory)threads;
if (IsShared) {
Dispatcher = stf.get_dispatcher((ISharedWindow)model);
} else if (IsSlow) {
Dispatcher = stf.create_dispatcher((ISlowWindow)model);
} else {
Dispatcher = Dispatcher.CurrentDispatcher;
}
_edit_mode = backing(nameof(EditMode), false);
_title_override = backing<string>(nameof(TitleOverride));
_title = backing(
nameof(Title),
_model.observe_title()
.CombineLatest(
_title_override.observe_current(true),
(title, title_override) => (title, title_override))
.Select(t => t.title_override ?? t.title)
.OnDispatcher());
_has_custom_title = backing(
nameof(HasCustomTitle),
_title_override.observe_current(true).Select(t => !t.is_null_or_white_space()).OnDispatcher());
model.on_shell_window_vm(this);
_dispose = Disposable.Create(() => {
_model.IconChanged -= on_icon_changed;
if (IsShared) {
// Shared threads continue running
Dispatcher.Invoke(() => Disposable.dispose(ref _model));
} else if (IsSlow) {
// Terminating the background thread will dispose the model on that thread
Log.Ui.info("Beginning asyncronous dispose of slow window {0} .".fmt(_model.GetType().ToString()));
Dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
} else {
Disposable.dispose(ref _model);
}
_ui = null;
Disposable.dispose(ref _hwnd_host);
});
void on_icon_changed() {
_icon = null;
RaisePropertyChanged(nameof(Icon));
}
}
public AsyncEvent<(object sender, ShellWindowVM e)> Closing { get; } = new();
public string host_id { get; set; }
public string OwnerName => _model.Owner?.name ?? string.Empty;
public IShellWindow Model => _model;
public Dispatcher Dispatcher { get; }
// title
public string Title => _title.Value;
public string TitleOverride {
get => _title_override.Value;
set {
_title_override.Value = value;
_edit_mode.set(false);
}
}
public bool HasCustomTitle => _has_custom_title.Value;
public ImageSource Icon =>
_icon ??= _model.Icon ?? _model.Owner?.icon;
public bool EditMode => _edit_mode.Value;
public double InitialWidth { get; private set; }
public double InitialHeight { get; private set; }
public UIElement AppContent =>
_ui ??= _create_contents();
public bool IsSlow => _model is ISlowWindow;
public bool IsShared => _model is ISharedWindow;
public bool HasTabAffinity => _model is ITabAffineWindow;
public ICommand rename_command =>
_rename_command ??= new DelegateCommand(_begin_rename);
public ICommand unrename_command =>
_unrename_command ??= new DelegateCommand(() => TitleOverride = string.Empty);
public ICommand CloseCommand =>
_close_command ??= new DelegateCommand(_begin_close);
internal FloatingWindow owner {
get => _owner;
set {
if (Set(ref _owner, value)) {
_try_set_wpf_bus();
}
}
}
public IObservable<string> observe_title() =>
_title.observe_current(true);
void _begin_rename() {
if (string.IsNullOrEmpty(TitleOverride)) {
_title_override.Value = Title;
}
_edit_mode.set(true);
}
async void _begin_close(object sender) {
if (_window_manager is null) {
return;
}
if (!_window_manager.contains(_model)) {
return;
}
await Closing?.InvokeAsync((sender, this));
// Removing the window will cause this object to be Disposed
// Which will terminate the background thread
// Which will dispose the model (on that thread)
await _window_manager.remove(_model);
}
UIElement _create_contents() {
string error_text = null;
Dispatcher.Invoke(() => {
// Executes possibly on background thread
try {
_document = _model.GetContent();
} catch (Exception ex) {
error_text = ex.Message;
Log.Ui.error($"Error getting window contents: {ex}");
}
}, DispatcherPriority.Send);
if (_document == null && string.IsNullOrEmpty(error_text)) {
error_text = $"No content for window {_model.GetType()}";
}
// If there's an error draw a big red X
if (!error_text.is_null_or_empty()) {
_document = new ShellWindowUI(new TextBlock() {
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Foreground = Brushes.Red,
Text = error_text
});
_model = new ErrorShellWindow {
Content = _document,
Icon = _model.Icon,
Title = $"Error: {_model.GetType()}"
};
RaisePropertyChanged("Title");
}
Log.assert(_document != null);
return _document.content.Match(on_wpf_element, on_handle_ref);
UIElement on_wpf_element(UIElement element) {
if (IsSlow || IsShared) {
// Create a WPFPanel (a Winforms window), then attach it with the HwndContentHost.
// This gives the document content a separate thread affinity from the shell window,
// and lets the win32 layer handle the thread boundary.
var retval = Dispatcher.Invoke(() => {
var interop_window = new WPFPanel(element);
element.SetCurrentValue(ui.WindowStyle.WinFormsHostProperty, interop_window);
return new HandleRef(interop_window, interop_window.Handle);
}, DispatcherPriority.Send);
_hwnd_host = new HwndContentHost(retval);
InitialHeight = _hwnd_host.InitialHeight;
InitialWidth = _hwnd_host.InitialWidth;
return _hwnd_host;
}
if (element is FrameworkElement fe) {
InitialHeight = fe.Height;
InitialWidth = fe.Width;
fe.Height = fe.Width = double.NaN;
}
return element;
}
UIElement on_handle_ref(HandleRef handle_ref) {
_hwnd_host = new HwndContentHost(handle_ref);
InitialHeight = _hwnd_host.Height;
InitialWidth = _hwnd_host.Width;
return _hwnd_host;
}
}
void _try_set_wpf_bus() {
if (_owner is null) {
return;
}
_document.content.try_match(on_wpf_element);
void on_wpf_element(UIElement element) =>
element.Dispatcher.InvokeAsync(() =>
ui.WindowStyle.SetUnsafeWindow(element, _owner));
}
}
}
@@ -1,5 +1,6 @@
namespace bts.api {
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
@@ -36,6 +37,15 @@ namespace bts.api {
};
}
public static IEnumerable<DateTime> normal_expirations(this SecurityImmutable security) =>
security.ContractCase switch {
Security.ContractOneofCase.Future => new[] { security.Future.expiration_date() },
Security.ContractOneofCase.Option => new[] { security.Option.expiration_date() },
Security.ContractOneofCase.Spread => security.Spread.Legs.SelectMany(l => l.Security.normal_expirations()),
Security.ContractOneofCase.Synthetic => security.Synthetic.Legs.SelectMany(l => l.Security.normal_expirations()),
_ => Enumerable.Empty<DateTime>()
};
public static ProductImmutable product(this SecurityImmutable security) =>
new(new() {
Exchange = security.Exchange,
@@ -59,13 +69,7 @@ namespace bts.api {
new((int)future.ExpirationYear, (int)future.ExpirationMonth, (int)future.ExpirationDay);
internal static DateTime earliest_expiration(this SecurityImmutable security) =>
security.ContractCase switch {
Security.ContractOneofCase.Synthetic => security.Synthetic.Legs.First().Security.earliest_expiration(),
Security.ContractOneofCase.Spread => security.Spread.Legs.First().Security.earliest_expiration(),
Security.ContractOneofCase.Future => security.Future.expiration_date(),
Security.ContractOneofCase.Option => security.Option.expiration_date(),
_ => throw new NotImplementedException(),
};
security.normal_expirations().Min();
public static double strike(this SecurityImmutable security) =>
security.HasOption ? security.Option.Strike : double.NaN;
@@ -105,6 +109,7 @@ namespace bts.api {
Security.ExchangeDataOneofCase.Deribit => security.Deribit.SecurityDesc,
Security.ExchangeDataOneofCase.Ftx => security.Ftx.SecurityDesc,
Security.ExchangeDataOneofCase.Binance => security.Binance.SecurityDesc,
Security.ExchangeDataOneofCase.BinanceCoinFutures => security.BinanceCoinFutures.SecurityDesc,
Security.ExchangeDataOneofCase.BinanceUs => security.BinanceUs.SecurityDesc,
Security.ExchangeDataOneofCase.BinanceUsdsFutures => security.BinanceUsdsFutures.SecurityDesc,
Security.ExchangeDataOneofCase.Coinbase => security.Coinbase.SecurityDesc,
@@ -151,12 +156,13 @@ namespace bts.api {
domain.Venue.Deribit => Venue.Deribit,
domain.Venue.Ftx => Venue.Ftx,
domain.Venue.Binance => Venue.Binance,
domain.Venue.BinanceCoinFutures => Venue.BinanceCoinFutures,
domain.Venue.BinanceUs => Venue.BinanceUs,
domain.Venue.BinanceUsdsFutures => Venue.BinanceUsdsFutures,
domain.Venue.Glbx => Venue.Glbx,
domain.Venue.Coinbase => Venue.Coinbase,
domain.Venue.Okx => Venue.Okx,
domain.Venue.Cboe => Venue.Cboe,
domain.Venue.Cboe => Venue.Cboe,
domain.Venue.Taifex => Venue.Taifex,
@@ -171,6 +177,7 @@ namespace bts.api {
Venue.Deribit => domain.Venue.Deribit,
Venue.Ftx => domain.Venue.Ftx,
Venue.Binance => domain.Venue.Binance,
Venue.BinanceCoinFutures => domain.Venue.BinanceCoinFutures,
Venue.BinanceUs => domain.Venue.BinanceUs,
Venue.BinanceUsdsFutures => domain.Venue.BinanceUsdsFutures,
Venue.Coinbase => domain.Venue.Coinbase,
@@ -162,7 +162,7 @@
return leg.signed_quantity() switch {
1.0 => sec_str,
-1.0 => $"-{sec_str}",
_ => ""
_ => $"{leg.signed_quantity()}{sec_str}",
};
}
@@ -172,7 +172,7 @@
return leg.signed_quantity() switch {
1.0 => $"+{sec_str}",
-1.0 => $"-{sec_str}",
_ => $"{leg.signed_quantity():+0;-0}{sec_str}",
_ => $"{leg.signed_quantity():+0.######;-0.######}{sec_str}",
};
}
}
+31
View File
@@ -0,0 +1,31 @@
#nullable enable
namespace bts;
using System;
public class ThrottledAction {
readonly TimeSpan interval;
readonly bool measureEndToNextStart;
DateTime lastInvokedTime = DateTime.MinValue;
public ThrottledAction(TimeSpan interval, bool measureEndToNextStart = true) {
this.interval = interval;
this.measureEndToNextStart = measureEndToNextStart;
}
public bool Execute(Action action) {
var now = DateTime.Now;
var elapsed = now - lastInvokedTime;
if (elapsed < interval) {
return false;
}
if (measureEndToNextStart) {
action();
lastInvokedTime = DateTime.Now;
} else {
lastInvokedTime = now;
action();
}
return true;
}
}
@@ -50,6 +50,30 @@ namespace bts.utils {
public static bool is_nan<T>(T value) where T : INumber<T> =>
NormalNumericOps<T>.is_nan(value);
public static bool can_increment<T>(T value, T addend) where T : INumber<T>, IMinMaxValue<T> {
if (value > T.Zero != addend > T.Zero) {
return true;
}
if (value > T.Zero) {
return addend < T.MaxValue - value;
}
if (value < T.Zero) {
return addend > T.MinValue - value;
}
return true;
}
public static bool try_increment<T>(ref T value, T addend) where T : INumber<T>, IMinMaxValue<T> {
if (can_increment(value, addend)) {
value += addend;
return true;
}
return false;
}
public static bool try_increment(ref int value, long addend) =>
addend <= int.MaxValue && addend >= int.MinValue && try_increment(ref value, (int)addend);
public static float gcd(IEnumerable<float> nums) =>
gcd<float, NumericCastConverter>(nums);
+1 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.10)
project(data_files)
include(${CMAKE_SOURCE_DIR}/cmake/safe_dep_glob.cmake)
+1 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.10)
project(libspark)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
include(GNUInstallDirs)
@@ -0,0 +1,34 @@
syntax = "proto3";
import "lib/api/domain/SecurityCriteria.proto";
import "lib/api/core/RegexFormatting.proto";
package bts.api.md;
option csharp_namespace = "bts.api.md";
option cc_enable_arenas = true;
message BinanceCoinFuturesMarketDataConfig {
uint32 num_depth_levels = 1;
uint32 depth_throttle_ms = 2;
bts.api.domain.SecurityCriteria security_criteria = 3;
// Market data isn't subscribed for securities listed here (even if they are
// included by 'security_criteria'). There are some securities that don't
// fit into our system just yet, such as 'BTCBIDR', which trades in prices
// larger than our cpp 'domain::Price' class supports.
repeated string excluded_securities = 4;
// The list of instruments at Binance is obtained via an 'exchangeInfo' http
// rest request. In the corresponding response, each instrument has a 'pair'
// field, which is analgous to a security desc in our system (a unique
// human-readable string representation of the security). For some
// instruments, the string representation used in the market depth request is
// slightly different.
//
// 'depth_symbol_mappings' is a list of regex find/replace patterns to
// apply (to the 'pair' field) when subscribing to depth.
repeated bts.api.core.RegexFormatting security_desc_formattings = 5;
string url_rest_base = 6;
string url_ws_base = 7;
}
@@ -1,7 +1,6 @@
syntax = "proto3";
import "lib/api/domain/SecurityCriteria.proto";
import "lib/api/core/RegexFormatting.proto";
package bts.api.md;
@@ -9,6 +8,13 @@ option csharp_namespace = "bts.api.md";
option cc_enable_arenas = true;
message BinanceUsdsFuturesMarketDataConfig {
message Subscription {
string symbol = 1;
string pair = 2;
string contract_type = 3;
string base_asset = 4;
string quote_asset = 5;
}
uint32 num_depth_levels = 1;
uint32 depth_throttle_ms = 2;
bts.api.domain.SecurityCriteria security_criteria = 3;
@@ -17,17 +23,7 @@ message BinanceUsdsFuturesMarketDataConfig {
// fit into our system just yet, such as 'BTCBIDR', which trades in prices
// larger than our cpp 'domain::Price' class supports.
repeated string excluded_securities = 4;
// The list of instruments at Binance is obtained via an 'exchangeInfo' http
// rest request. In the corresponding response, each instrument has a 'pair'
// field, which is analgous to a security desc in our system (a unique
// human-readable string representation of the security). For some
// instruments, the string representation used in the market depth request is
// slightly different.
//
// 'depth_symbol_mappings' is a list of regex find/replace patterns to
// apply (to the 'pair' field) when subscribing to depth.
repeated bts.api.core.RegexFormatting security_desc_formattings = 5;
repeated Subscription subscriptions = 8;
string url_rest_base = 6;
string url_ws_base = 7;
+2
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
import "lib/api/md/FTXMarketDataConfig.proto";
import "lib/api/md/BinanceMarketDataConfig.proto";
import "lib/api/md/BinanceCoinFuturesMarketDataConfig.proto";
import "lib/api/md/BinanceUsMarketDataConfig.proto";
import "lib/api/md/CoinbaseMarketDataConfig.proto";
import "lib/api/md/BinanceUsdsFuturesMarketDataConfig.proto";
@@ -21,6 +22,7 @@ package bts.api.md;
message MarketDataConfig {
repeated FTXMarketDataConfig ftx = 1;
repeated BinanceMarketDataConfig binance = 2;
repeated BinanceCoinFuturesMarketDataConfig binance_coin_futures = 11;
repeated BinanceUsdsFuturesMarketDataConfig binance_usds_futures = 3;
repeated BinanceUsMarketDataConfig binance_us = 4;
repeated DeribitMarketDataConfig deribit = 5;
@@ -9,7 +9,9 @@ option cc_enable_arenas = true;
// Config to control the recording of the market data for an exchange.
// This config is part of MarketDataConfig.proto
message MarketDataRecordingConfig {
uint32 num_of_seconds_to_run = 10;
uint32 num_of_seconds_to_run_file_writer=12;
uint32 num_of_seconds_to_run_db_writer=13;
bts.api.Venue venue = 20;
uint32 n_book_levels = 30;
uint32 batch_size = 40;
}
@@ -66,6 +66,7 @@ bool AdminServiceUtils::has_exchange(
case api::Venue::VENUE_DERIBIT: // TODO
case api::Venue::VENUE_FTX:
case api::Venue::VENUE_BINANCE:
case api::Venue::VENUE_BINANCE_COIN_FUTURES:
case api::Venue::VENUE_BINANCE_US:
case api::Venue::VENUE_COINBASE:
case api::Venue::VENUE_BINANCE_USDS_FUTURES:
@@ -185,6 +186,7 @@ AdminServiceUtils::resolve_connection(
case api::Venue::VENUE_FTX:
case api::Venue::VENUE_OKX:
case api::Venue::VENUE_BINANCE:
case api::Venue::VENUE_BINANCE_COIN_FUTURES:
case api::Venue::VENUE_BINANCE_US:
case api::Venue::VENUE_COINBASE:
case api::Venue::VENUE_TAIFEX:
@@ -454,6 +456,7 @@ std::string AdminServiceUtils::find_account_exchange_account(
case api::Venue::VENUE_DERIBIT: // TODO
case api::Venue::VENUE_FTX: // TODO
case api::Venue::VENUE_BINANCE: // TODO
case api::Venue::VENUE_BINANCE_COIN_FUTURES:
case api::Venue::VENUE_BINANCE_US:
case api::Venue::VENUE_COINBASE:
case api::Venue::VENUE_BINANCE_USDS_FUTURES:
@@ -580,6 +583,21 @@ bts::api::orderman::RoutingRecord AdminServiceUtils::get_routing_record(
out_result.set_is_success(true);
return rv;
}
case api::VENUE_OKX: {
rv.set_session("okx_1");
rv.set_account(account_ptr->name());
rv.set_operator_id(user_ptr->name());
// AdminServiceUtils::ExchangeSubAccountResolver resolver;
// auto sub_acc_ptr = resolver.find_entry(&sec, account_ptr->name(), true);
// if (!sub_acc_ptr) {
// out_result.set_is_success(false);
// out_result.set_error_text("Could not resolve sub account");
// return rv;
// }
// rv.set_account_code(sub_acc_ptr->exchange_subaccount());
out_result.set_is_success(true);
return rv;
}
case api::Venue::VENUE_DERIBIT: {
rv.set_session("deribit_1");
rv.set_account(account_ptr->name());
@@ -749,12 +767,18 @@ bts::api::orderman::RoutingRecord AdminServiceUtils::get_routing_record(
out_result.set_is_success(true);
return rv;
}
case api::VENUE_BINANCE_USDS_FUTURES: {
rv.set_session("binance_usds_futures_1");
rv.set_account(account_ptr->name());
rv.set_account(user_ptr->name());
out_result.set_is_success(true);
return rv;
}
case api::VENUE_FTX: // TODO
case api::VENUE_BINANCE: // TODO
case api::VENUE_BINANCE_COIN_FUTURES:
case api::VENUE_BINANCE_US:
case api::Venue::VENUE_COINBASE:
case api::VENUE_BINANCE_USDS_FUTURES:
case api::VENUE_OKX:
case api::Venue::VENUE_UNSET:
case api::Venue::VENUE_BTS:
case api::Venue::VENUE_CBOECFE:
@@ -1145,6 +1169,7 @@ std::string AdminServiceUtils::find_user_exchange_id(
case api::Venue::VENUE_DERIBIT: // TODO
case api::Venue::VENUE_FTX: // TODO
case api::Venue::VENUE_BINANCE: // TODO
case api::Venue::VENUE_BINANCE_COIN_FUTURES:
case api::Venue::VENUE_BINANCE_US:
case api::Venue::VENUE_COINBASE:
case api::Venue::VENUE_BINANCE_USDS_FUTURES:
@@ -1180,6 +1205,7 @@ std::string AdminServiceUtils::find_user_company_id(
case api::Venue::VENUE_DERIBIT:
case api::Venue::VENUE_FTX: // TODO
case api::Venue::VENUE_BINANCE: // TODO
case api::Venue::VENUE_BINANCE_COIN_FUTURES:
case api::Venue::VENUE_BINANCE_US:
case api::Venue::VENUE_COINBASE:
case api::Venue::VENUE_BINANCE_USDS_FUTURES:
@@ -41,6 +41,18 @@ class AdminServiceUtils {
}
return nullptr;
}
bts::config::toggles::ExchangeSubAccountUnit const *find_entry(
const std::string &account, bool is_internal_account) {
for (const auto &esa : exch_sub_accounts.accounts()) {
const auto &a = is_internal_account ? esa.bts_account_name()
: esa.exchange_subaccount();
if (a == account) {
return &esa;
}
}
return nullptr;
}
};
////////////////////////////////////////////
// connection accessors ////////////////////
@@ -74,11 +74,6 @@ function(get_cpp_sources dest)
expression/ParserExceptions.cpp
expression/Value.cpp
expression/tests/expression_parser_tests.cpp
io/http/CurlHandle.cpp
io/http/HTTPClient.cpp
io/http/HTTPHeaders.cpp
io/http/HTTPRequest.cpp
io/http/HTTPResponse.cpp
logging/LoggerHub.cpp
logging/Sink.cpp
logging/logging.cpp
@@ -4,6 +4,7 @@ SecurityMetadataCollection {
scope {
clauses {
symbols: "BTC"
symbols: "ETH"
security_types: SECURITY_TYPE_FUTURE
security_types: SECURITY_TYPE_SPREAD
venues: VENUE_BINANCE
@@ -13,11 +14,11 @@ SecurityMetadataCollection {
value {
quantity_display {
tick_factors {
tick_base: 1
tick_unit: 10
tick_base: 1000
tick_unit: 1
}
display_multiplier: NaN
show_decimal_part: false
show_decimal_part: true
pad_with_leading_zeros: true
show_zero_integer: true
nan_text: ""
@@ -4028,6 +4028,10 @@ JetFIX {
name: "deribit_1"
venue: VENUE_DERIBIT
}
connection {
name: "okx_1"
venue: VENUE_OKX
}
# CME EXECUTION NEEDS TO BE UPDATED, probably below
# connection {
# name: "onixs_sim"
@@ -19,6 +19,11 @@ SymbolLists {
symbol: "BTC"
symbol: "ETH"
}
list {
key: "BINANCE_COIN_FUTURES"
symbol: "BTC"
symbol: "ETH"
}
list {
key: "TAIFEX"
symbol: "QFFH"
@@ -140,6 +145,11 @@ Symbols {
spot: @SymbolLists.list[BINANCE].symbol
spread: @SymbolLists.list[BINANCE].symbol
}
venues {
name: "VENUE_BINANCE_COIN_FUTURES"
future: @SymbolLists.list[BINANCE_COIN_FUTURES].symbol
spread: @SymbolLists.list[BINANCE_COIN_FUTURES].symbol
}
venues {
name: "VENUE_BINANCE_US"
spot: @SymbolLists.list[BINANCE_US].symbol
@@ -13,6 +13,19 @@ MarketDataConfig {
}
}
binance_coin_futures {
num_depth_levels: 5
url_rest_base: "https://testnet.binancefuture.com"
url_ws_base: "wss://dstream.binance.com"
security_criteria {
clauses {
security_types: SECURITY_TYPE_FUTURE
symbols: "BTC"
symbols: "ETH"
}
}
}
binance_us {
num_depth_levels: 5
url_rest_base: "https://api.binance.us"
@@ -13,6 +13,19 @@ MarketDataConfig {
}
}
binance_coin_futures {
num_depth_levels: 5
url_rest_base: "https://dapi.binance.com"
url_ws_base: "wss://dstream.binance.com"
security_criteria {
clauses {
security_types: SECURITY_TYPE_FUTURE
symbols: "BTC"
symbols: "ETH"
}
}
}
binance_usds_futures {
num_depth_levels: 5
url_rest_base: "https://dapi.binance.com"
+28
View File
@@ -28,6 +28,9 @@ class TimeSpan {
static constexpr TimeSpan from_micros(std::int64_t value);
static constexpr TimeSpan from_millis(std::int64_t value);
static constexpr TimeSpan from_seconds(std::int64_t value);
static constexpr TimeSpan from_minutes(std::int64_t value);
static constexpr TimeSpan from_hours(std::int64_t value);
static constexpr TimeSpan from_days(std::int64_t value);
static TimeSpan from_posix_duration(boost::posix_time::time_duration t);
static TimeSpan from_iso_string(const std::string &iso_string);
@@ -53,6 +56,8 @@ constexpr bool operator<(TimeSpan lhs, TimeSpan rhs) noexcept;
constexpr bool operator<=(TimeSpan lhs, TimeSpan rhs) noexcept;
constexpr bool operator>(TimeSpan lhs, TimeSpan rhs) noexcept;
constexpr bool operator>=(TimeSpan lhs, TimeSpan rhs) noexcept;
constexpr TimeSpan operator-(TimeSpan lhs, TimeSpan rhs) noexcept;
constexpr TimeSpan operator+(TimeSpan lhs, TimeSpan rhs) noexcept;
std::ostream &operator<<(std::ostream &out, const TimeSpan &ts) noexcept;
@@ -83,6 +88,21 @@ constexpr TimeSpan TimeSpan::from_seconds(std::int64_t value) {
return TimeSpan{value * nanos_per_second};
}
constexpr TimeSpan TimeSpan::from_minutes(std::int64_t value) {
constexpr const auto nanos_per_minute = 60'000'000'000;
return TimeSpan{value * nanos_per_minute};
}
constexpr TimeSpan TimeSpan::from_hours(std::int64_t value) {
constexpr const auto nanos_per_hour = 3'600'000'000'000;
return TimeSpan{value * nanos_per_hour};
}
constexpr TimeSpan TimeSpan::from_days(std::int64_t value) {
constexpr const auto nanos_per_day = 86'400'000'000'000;
return TimeSpan{value * nanos_per_day};
}
constexpr std::int64_t TimeSpan::nanos() const { return ticks_; }
constexpr std::int64_t TimeSpan::micros() const { return ticks_ / 1'000; }
@@ -130,6 +150,14 @@ constexpr bool operator>=(TimeSpan lhs, TimeSpan rhs) noexcept {
return lhs.nanos() >= rhs.nanos();
}
constexpr TimeSpan operator-(TimeSpan lhs, TimeSpan rhs) noexcept {
return TimeSpan{lhs.nanos() - rhs.nanos()};
}
constexpr TimeSpan operator+(TimeSpan lhs, TimeSpan rhs) noexcept {
return TimeSpan{lhs.nanos() + rhs.nanos()};
}
} // namespace bts::core
FMT_IMPL(bts::core::TimeSpan, t, "{}", t.to_iso_string());
+13 -3
View File
@@ -5,12 +5,19 @@
namespace bts::core {
using std::uint32_t;
using std::uint8_t;
static bool is_valid_ymd(uint32_t year, uint32_t month, uint32_t day) {
static bool is_valid_ymd(uint32_t year, uint8_t month, uint8_t day) {
return year >= 1970 && month >= 1 && month <= 12 && day >= 1 && day <= 31;
}
TimeStamp::TimeStamp(uint32_t year, uint32_t month, uint32_t day) {
TimeStamp::TimeStamp(
uint32_t year,
uint8_t month,
uint8_t day,
uint8_t hour,
uint8_t minute,
uint8_t second) {
bts_assert_msgf(
is_valid_ymd(year, month, day), "Invalid timestamp {}/{}/{} (y/m/d)",
year, month, day);
@@ -18,6 +25,9 @@ TimeStamp::TimeStamp(uint32_t year, uint32_t month, uint32_t day) {
date.tm_year = year - 1900;
date.tm_mon = month - 1;
date.tm_mday = day;
date.tm_hour = hour;
date.tm_min = minute;
date.tm_sec = second;
auto time = std::chrono::system_clock::from_time_t(mktime(&date));
auto duration = time.time_since_epoch();
value_ =
@@ -25,7 +35,7 @@ TimeStamp::TimeStamp(uint32_t year, uint32_t month, uint32_t day) {
}
bool TimeStamp::from_ymd(
std::uint32_t year, std::uint32_t month, std::uint32_t day, TimeStamp *result) {
uint32_t year, uint8_t month, uint8_t day, TimeStamp *result) {
if (!is_valid_ymd(year, month, day)) {
return false;
}
+103 -9
View File
@@ -8,7 +8,6 @@
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/time.hpp>
#include <chrono>
#include <cstdint>
#include <fmt/format.h>
#include <limits>
@@ -16,6 +15,9 @@
namespace bts::core {
#define VALID_YMD(year, month, day) \
(year >= 1970 && month >= 1 && month <= 12 && day >= 1 && day <= 31)
/**
* @brief Represents time as nano-seconds since the unix epoch (Jan 1, 1970)
*/
@@ -23,8 +25,14 @@ struct TimeStamp {
public:
constexpr TimeStamp() = default;
explicit constexpr TimeStamp(std::uint64_t value);
// constructs a TimeStamp from a UTC date
explicit TimeStamp(uint32_t year, uint32_t month, uint32_t day);
TimeStamp(std::uint32_t year, std::uint8_t month, std::uint8_t day);
TimeStamp(
std::uint32_t year,
std::uint8_t month,
std::uint8_t day,
std::uint8_t hour,
std::uint8_t minute,
std::uint8_t second);
static constexpr TimeStamp min();
static constexpr TimeStamp max();
@@ -36,10 +44,33 @@ struct TimeStamp {
static constexpr TimeStamp from_seconds(std::int64_t value);
static TimeStamp from_ptime(boost::posix_time::ptime t);
static TimeStamp from_iso_string(const std::string &iso_string);
// Constructs a TimeStamp from a UTC date
static TimeStamp from_ymd(
std::uint32_t year, std::uint8_t month, std::uint8_t day);
static bool from_ymd(
std::uint32_t year,
std::uint32_t month,
std::uint32_t day,
std::uint8_t month,
std::uint8_t day,
TimeStamp *result);
static TimeStamp from_ymdhms(
std::uint32_t year,
std::uint8_t month,
std::uint8_t day,
std::uint8_t hour,
std::uint8_t minute,
std::uint8_t second);
// Constructs a TimeStamp from a UTC date
static bool from_ymdhms(
std::uint32_t year,
std::uint8_t month,
std::uint8_t day,
std::uint8_t hour,
std::uint8_t minute,
std::uint8_t second,
TimeStamp *result);
constexpr std::uint64_t nanos() const;
@@ -50,8 +81,11 @@ struct TimeStamp {
constexpr double total_millis() const;
constexpr double total_seconds() const;
constexpr std::int64_t to_int64();
inline void to_date(uint32_t &year, uint32_t &month, uint32_t &day);
constexpr std::int64_t to_int64() const;
std::int64_t to_date_int() const;
void to_date(uint32_t &year, uint32_t &month, uint32_t &day) const;
// Gets a TimeStamp representing this date at midnight.
TimeStamp date() const;
constexpr boost::posix_time::ptime to_ptime() const;
std::string to_iso_string() const;
// current epoch ns
@@ -82,7 +116,9 @@ constexpr bool operator>=(TimeStamp lhs, TimeStamp rhs) noexcept;
constexpr TimeSpan operator-(TimeStamp lhs, TimeStamp rhs) noexcept;
constexpr TimeStamp operator-(TimeStamp lhs, TimeSpan rhs) noexcept;
TimeStamp &operator-=(TimeStamp &lhs, TimeSpan rhs) noexcept;
constexpr TimeStamp operator+(TimeStamp lhs, TimeSpan rhs) noexcept;
TimeStamp &operator+=(TimeStamp &lhs, TimeSpan rhs) noexcept;
std::ostream &operator<<(std::ostream &out, const TimeStamp &ts) noexcept;
@@ -92,6 +128,10 @@ std::ostream &operator<<(std::ostream &out, const TimeStamp &ts) noexcept;
constexpr TimeStamp::TimeStamp(std::uint64_t value) : value_(value) {}
inline TimeStamp::TimeStamp(
std::uint32_t year, std::uint8_t month, std::uint8_t day)
: TimeStamp(year, month, day, 0, 0, 0) {}
constexpr TimeStamp TimeStamp::min() { return TimeStamp{0}; }
constexpr TimeStamp TimeStamp::max() {
@@ -144,13 +184,21 @@ constexpr double TimeStamp::total_seconds() const {
return static_cast<double>(value_) / 1000'000'000;
}
constexpr std::int64_t TimeStamp::to_int64() {
constexpr std::int64_t TimeStamp::to_int64() const {
bts_assert(
value_ <=
static_cast<std::int64_t>(std::numeric_limits<std::int64_t>::max()));
return static_cast<std::int64_t>(value_);
}
void TimeStamp::to_date(uint32_t &year, uint32_t &month, uint32_t &day) {
inline std::int64_t TimeStamp::to_date_int() const {
std::uint32_t y, m, d;
to_date(y, m, d);
return y << 6 | m << 4 | d;
}
inline void TimeStamp::to_date(
uint32_t &year, uint32_t &month, uint32_t &day) const {
std::uint64_t epoch_secs = value_ / (1000 * 1000 * 1000);
time_t ts_tt(epoch_secs);
struct tm *ptm = gmtime(&ts_tt);
@@ -159,6 +207,12 @@ void TimeStamp::to_date(uint32_t &year, uint32_t &month, uint32_t &day) {
day = ptm->tm_mday;
}
inline TimeStamp TimeStamp::date() const {
std::uint32_t y, m, d;
to_date(y, m, d);
return from_ymd(y, m, d);
}
constexpr boost::posix_time::ptime TimeStamp::to_ptime() const {
constexpr const auto nanos_per_sec = 1'000'000'000;
constexpr const auto nanos_per_micro = 1'000;
@@ -170,6 +224,36 @@ constexpr boost::posix_time::ptime TimeStamp::to_ptime() const {
boost::posix_time::microseconds(nanos_rem / nanos_per_micro);
}
inline TimeStamp TimeStamp::from_ymd(
std::uint32_t year, std::uint8_t month, std::uint8_t day) {
return {year, month, day, 0, 0, 0};
}
inline TimeStamp TimeStamp::from_ymdhms(
std::uint32_t year,
std::uint8_t month,
std::uint8_t day,
std::uint8_t hour,
std::uint8_t minute,
std::uint8_t second) {
return {year, month, day, hour, minute, second};
}
inline bool TimeStamp::from_ymdhms(
std::uint32_t year,
std::uint8_t month,
std::uint8_t day,
std::uint8_t hour,
std::uint8_t minute,
std::uint8_t second,
TimeStamp *result) {
if (!VALID_YMD(year, month, day)) {
return false;
}
*result = TimeStamp{year, month, day, hour, minute, second};
return true;
}
constexpr bool operator==(TimeStamp lhs, TimeStamp rhs) noexcept {
return lhs.nanos() == rhs.nanos();
}
@@ -202,10 +286,20 @@ constexpr TimeStamp operator-(TimeStamp lhs, TimeSpan rhs) noexcept {
return TimeStamp{lhs.nanos() - rhs.nanos()};
}
inline TimeStamp &operator-=(TimeStamp &lhs, TimeSpan rhs) noexcept {
lhs = lhs - rhs;
return lhs;
}
constexpr TimeStamp operator+(TimeStamp lhs, TimeSpan rhs) noexcept {
return TimeStamp{lhs.nanos() + rhs.nanos()};
}
inline TimeStamp &operator+=(TimeStamp &lhs, TimeSpan rhs) noexcept {
lhs = lhs + rhs;
return lhs;
}
} // namespace bts::core
FMT_IMPL(bts::core::TimeStamp, t, "{}", t.to_iso_string());
-8
View File
@@ -43,8 +43,6 @@ class BookT {
void reset_merged_quantity();
void reset_n_orders();
template <class Container>
void apply_changes(Side side, const Container &changes);
/**
* Removes price levels of the non-dominant side that cross the dominant
@@ -203,12 +201,6 @@ bool BookT<BS>::is_sorted() const {
return Side::forboth([&](Side s) { return operator[](s).is_sorted(s); });
}
template <class BS>
template <class Container>
void BookT<BS>::apply_changes(Side side, const Container &changes) {
_books[static_cast<std::size_t>(side.value)].apply_changes(side, changes);
}
template <class BS>
void from_json(const nlohmann::json &j, BookT<BS> &book) {
io::JsonReader jr{&j};
+11 -2
View File
@@ -33,6 +33,8 @@ api::domain::Venue DomainSecurityUtils::convert(bts::api::Venue venue) {
return DOMAIN_VENUE(SIM);
case bts::api::Venue::VENUE_BINANCE:
return DOMAIN_VENUE(BINANCE);
case bts::api::Venue::VENUE_BINANCE_COIN_FUTURES:
return DOMAIN_VENUE(BINANCE_COIN_FUTURES);
case bts::api::Venue::VENUE_BINANCE_US:
return DOMAIN_VENUE(BINANCE_US);
case bts::api::VENUE_BINANCE_USDS_FUTURES:
@@ -65,6 +67,8 @@ bts::api::Venue DomainSecurityUtils::convert(api::domain::Venue venue) {
return api::Venue::VENUE_FTX;
case DOMAIN_VENUE(BINANCE):
return api::Venue::VENUE_BINANCE;
case DOMAIN_VENUE(BINANCE_COIN_FUTURES):
return api::Venue::VENUE_BINANCE_COIN_FUTURES;
case DOMAIN_VENUE(BINANCE_US):
return api::Venue::VENUE_BINANCE_US;
case DOMAIN_VENUE(BINANCE_USDS_FUTURES):
@@ -166,8 +170,6 @@ std::string DomainSecurityUtils::get_exchange_id(const bts::api::Security &s) {
bool DomainSecurityUtils::get_security_desc(
const bts::api::Security &security, std::string &desc) {
switch (security.ExchangeData_case()) {
case bts::api::Security::ExchangeDataCase::EXCHANGEDATA_NOT_SET:
bts_assert(false);
case bts::api::Security::ExchangeDataCase::kCboe:
desc = security.cboe().security_desc();
break;
@@ -189,6 +191,9 @@ bool DomainSecurityUtils::get_security_desc(
case bts::api::Security::ExchangeDataCase::kBinance:
desc = security.binance().security_desc();
break;
case bts::api::Security::ExchangeDataCase::kBinanceCoinFutures:
desc = security.binance_coin_futures().security_desc();
break;
case bts::api::Security::ExchangeDataCase::kBinanceUs:
desc = security.binance_us().security_desc();
break;
@@ -205,6 +210,10 @@ bool DomainSecurityUtils::get_security_desc(
desc = security.ftx().security_desc();
break;
default:
if (security.has_synthetic()) {
desc = security.synthetic().name();
break;
}
return false;
}
return true;
+206 -6
View File
@@ -3,6 +3,7 @@
#include "Side.h"
#include "lib/core/FMT_IMPL.h"
#include "lib/io/JsonReader.h"
#include "lib/utils/bts_array_size.h"
#include "lib/utils/bts_assert.h"
#include <cmath>
@@ -33,6 +34,22 @@
namespace bts::domain {
static const double powers_10[] = {
1E-20, 1E-19, 1E-18, 1E-17, 1E-16, 1E-15, 1E-14, 1E-13, 1E-12, 1E-11, 1E-10,
1E-9, 1E-8, 1E-7, 1E-6, 1E-5, 1E-4, 1E-3, 1E-2, 1E-1, 1E0, 1E1,
1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, 1E9, 1E10, 1E11, 1E12,
1E13, 1E14, 1E15, 1E16, 1E17, 1E18, 1E19, 1E20};
static const int max_abs_power_10 = BTS_ARRAYSIZE(powers_10) / 2;
inline double pow10(int power) {
const int offset = power + max_abs_power_10;
const int powers_10_sz = BTS_ARRAYSIZE(powers_10);
bts_assert_d(offset >= 0 && offset < powers_10_sz);
if (offset >= 0 && offset < powers_10_sz) {
return powers_10[offset];
}
return std::numeric_limits<double>::quiet_NaN();
}
class Price {
public:
Price() = default;
@@ -120,6 +137,40 @@ class Price {
constexpr Price &operator*=(Price factor) noexcept;
constexpr Price &operator/=(Price factor) noexcept;
constexpr bool operator==(double factor) const noexcept;
constexpr bool operator!=(double factor) const noexcept;
constexpr bool operator<(double factor) const noexcept;
constexpr bool operator<=(double factor) const noexcept;
constexpr bool operator>(double factor) const noexcept;
constexpr bool operator>=(double factor) const noexcept;
constexpr Price operator+(double factor) const noexcept;
constexpr Price operator-(double factor) const noexcept;
constexpr Price operator*(double factor) const noexcept;
constexpr Price operator/(double factor) const noexcept;
constexpr Price &operator+=(double factor) noexcept;
constexpr Price &operator-=(double factor) noexcept;
constexpr Price &operator*=(double factor) noexcept;
constexpr Price &operator/=(double factor) noexcept;
constexpr bool operator==(std::int32_t factor) const noexcept;
constexpr bool operator!=(std::int32_t factor) const noexcept;
constexpr bool operator<(std::int32_t factor) const noexcept;
constexpr bool operator<=(std::int32_t factor) const noexcept;
constexpr bool operator>(std::int32_t factor) const noexcept;
constexpr bool operator>=(std::int32_t factor) const noexcept;
constexpr Price operator+(std::int32_t factor) const noexcept;
constexpr Price operator-(std::int32_t factor) const noexcept;
constexpr Price operator*(std::int32_t factor) const noexcept;
constexpr Price operator/(std::int32_t factor) const noexcept;
constexpr Price &operator+=(std::int32_t factor) noexcept;
constexpr Price &operator-=(std::int32_t factor) noexcept;
constexpr Price &operator*=(std::int32_t factor) noexcept;
constexpr Price &operator/=(std::int32_t factor) noexcept;
constexpr bool operator==(std::int64_t factor) const noexcept;
constexpr bool operator!=(std::int64_t factor) const noexcept;
constexpr bool operator<(std::int64_t factor) const noexcept;
@@ -431,7 +482,8 @@ inline void to_json(nlohmann::json &j, const Price &price) {
if (is_nan() || factor.is_nan()) { \
return nan(); \
} \
return Price{_mantissa op factor._mantissa}; \
return Price{_mantissa op static_cast<std::int64_t>( \
factor._mantissa * pow10(factor.exponent() - exponent()))}; \
}
// NOLINTNEXTLINE
@@ -440,7 +492,103 @@ inline void to_json(nlohmann::json &j, const Price &price) {
if (factor.is_nan()) { \
_mantissa = nan()._mantissa; \
} else if (!is_nan()) { \
_mantissa op factor._mantissa; \
_mantissa op static_cast<std::int64_t>( \
factor._mantissa * pow10(factor.exponent() - exponent())); \
} \
return *this; \
}
// NOLINTNEXTLINE
#define PRICE_DOUBLE_COMPARISON(op, nan_part) \
constexpr bool Price::operator op(double factor) const noexcept { \
return nan_part _mantissa op factor; \
}
// NOLINTNEXTLINE
#define PRICE_DOUBLE_BIN_OP(op) \
constexpr Price Price::operator op(double factor) const noexcept { \
if (is_nan() || std::isnan(factor)) { \
return nan(); \
} \
return Price{static_cast<std::int64_t>(_mantissa op factor)}; \
}
// NOLINTNEXTLINE
#define PRICE_DOUBLE_EXP_BIN_OP(op) \
constexpr Price Price::operator op(double factor) const noexcept { \
if (is_nan() || std::isnan(factor)) { \
return nan(); \
} \
return Price{_mantissa op static_cast<std::int64_t>( \
factor * pow10(-exponent()))}; \
}
// NOLINTNEXTLINE
#define PRICE_DOUBLE_BIN_REF_OP(op) \
constexpr Price &Price::operator op(double factor) noexcept { \
if (!is_nan()) { \
if (std::isnan(factor)) { \
*this = nan(); \
} else { \
_mantissa op factor; \
} \
} \
return *this; \
}
// NOLINTNEXTLINE
#define PRICE_DOUBLE_EXP_BIN_REF_OP(op) \
constexpr Price &Price::operator op(double factor) noexcept { \
if (!is_nan()) { \
if (std::isnan(factor)) { \
*this = nan(); \
} else { \
_mantissa op factor *pow10(-exponent()); \
} \
} \
return *this; \
}
// NOLINTNEXTLINE
#define PRICE_INT32_COMPARISON(op, nan_part) \
constexpr bool Price::operator op(std::int32_t factor) const noexcept { \
return nan_part _mantissa op static_cast<std::int64_t>( \
factor * pow10(-exponent())); \
}
// NOLINTNEXTLINE
#define PRICE_INT32_BIN_OP(op) \
constexpr Price Price::operator op(std::int32_t factor) const noexcept { \
if (is_nan()) { \
return *this; \
} \
return Price{_mantissa op static_cast<std::int64_t>( \
factor * pow10(-exponent()))}; \
}
#define PRICE_INT32_EXP_BIN_OP(op) \
constexpr Price Price::operator op(std::int32_t factor) const noexcept { \
if (is_nan()) { \
return *this; \
} \
return Price{_mantissa op static_cast<std::int64_t>( \
factor * pow10(-exponent()))}; \
}
// NOLINTNEXTLINE
#define PRICE_INT32_BIN_REF_OP(op) \
constexpr Price &Price::operator op(std::int32_t factor) noexcept { \
if (!is_nan()) { \
_mantissa op factor; \
} \
return *this; \
}
// NOLINTNEXTLINE
#define PRICE_INT32_EXP_BIN_REF_OP(op) \
constexpr Price &Price::operator op(std::int32_t factor) noexcept { \
if (!is_nan()) { \
_mantissa op static_cast<std::int64_t>(factor * pow10(-exponent())); \
} \
return *this; \
}
@@ -460,6 +608,15 @@ inline void to_json(nlohmann::json &j, const Price &price) {
return Price{_mantissa op factor}; \
}
#define PRICE_INT64_EXP_BIN_OP(op) \
constexpr Price Price::operator op(std::int64_t factor) const noexcept { \
if (is_nan()) { \
return *this; \
} \
return Price{_mantissa op( \
factor * static_cast<std::int64_t>(pow10(-exponent())))}; \
}
// NOLINTNEXTLINE
#define PRICE_INT64_BIN_REF_OP(op) \
constexpr Price &Price::operator op(std::int64_t factor) noexcept { \
@@ -469,6 +626,15 @@ inline void to_json(nlohmann::json &j, const Price &price) {
return *this; \
}
// NOLINTNEXTLINE
#define PRICE_INT64_EXP_BIN_REF_OP(op) \
constexpr Price &Price::operator op(std::int64_t factor) noexcept { \
if (!is_nan()) { \
_mantissa op static_cast<std::int64_t>(factor * pow10(-exponent())); \
} \
return *this; \
}
PRICE_PRICE_COMPARISON(!=, is_nan() ||)
PRICE_PRICE_COMPARISON(==, !is_nan() &&)
PRICE_PRICE_COMPARISON(<, !is_nan() &&)
@@ -486,6 +652,40 @@ PRICE_PRICE_BIN_REF_OP(-=)
PRICE_PRICE_BIN_REF_OP(*=)
PRICE_PRICE_BIN_REF_OP(/=)
PRICE_DOUBLE_COMPARISON(!=, is_nan() ||)
PRICE_DOUBLE_COMPARISON(==, !is_nan() &&)
PRICE_DOUBLE_COMPARISON(<, !is_nan() &&)
PRICE_DOUBLE_COMPARISON(<=, !is_nan() &&)
PRICE_DOUBLE_COMPARISON(>, !is_nan() &&)
PRICE_DOUBLE_COMPARISON(>=, !is_nan() &&)
PRICE_DOUBLE_EXP_BIN_OP(+)
PRICE_DOUBLE_EXP_BIN_OP(-)
PRICE_DOUBLE_BIN_OP(*)
PRICE_DOUBLE_BIN_OP(/)
PRICE_DOUBLE_EXP_BIN_REF_OP(+=)
PRICE_DOUBLE_EXP_BIN_REF_OP(-=)
PRICE_DOUBLE_BIN_REF_OP(*=)
PRICE_DOUBLE_BIN_REF_OP(/=)
PRICE_INT32_COMPARISON(!=, is_nan() ||)
PRICE_INT32_COMPARISON(==, !is_nan() &&)
PRICE_INT32_COMPARISON(<, !is_nan() &&)
PRICE_INT32_COMPARISON(<=, !is_nan() &&)
PRICE_INT32_COMPARISON(>, !is_nan() &&)
PRICE_INT32_COMPARISON(>=, !is_nan() &&)
PRICE_INT32_EXP_BIN_OP(+)
PRICE_INT32_EXP_BIN_OP(-)
PRICE_INT32_BIN_OP(*)
PRICE_INT32_BIN_OP(/)
PRICE_INT32_EXP_BIN_REF_OP(+=)
PRICE_INT32_EXP_BIN_REF_OP(-=)
PRICE_INT32_BIN_REF_OP(*=)
PRICE_INT32_BIN_REF_OP(/=)
PRICE_INT64_COMPARISON(!=, is_nan() ||)
PRICE_INT64_COMPARISON(==, !is_nan() &&)
PRICE_INT64_COMPARISON(<, !is_nan() &&)
@@ -493,13 +693,13 @@ PRICE_INT64_COMPARISON(<=, !is_nan() &&)
PRICE_INT64_COMPARISON(>, !is_nan() &&)
PRICE_INT64_COMPARISON(>=, !is_nan() &&)
PRICE_INT64_BIN_OP(+)
PRICE_INT64_BIN_OP(-)
PRICE_INT64_EXP_BIN_OP(+)
PRICE_INT64_EXP_BIN_OP(-)
PRICE_INT64_BIN_OP(*)
PRICE_INT64_BIN_OP(/)
PRICE_INT64_BIN_REF_OP(+=)
PRICE_INT64_BIN_REF_OP(-=)
PRICE_INT64_EXP_BIN_REF_OP(+=)
PRICE_INT64_EXP_BIN_REF_OP(-=)
PRICE_INT64_BIN_REF_OP(*=)
PRICE_INT64_BIN_REF_OP(/=)
+33 -22
View File
@@ -14,6 +14,7 @@ namespace securities {
using bts::api::Security;
using bts::api::Venue;
using std::optional;
api::SecurityType security_type(api::Security const &security) {
return security_type(security.Contract_case());
@@ -37,7 +38,7 @@ api::SecurityType security_type(api::Security::ContractCase contract_case) {
return api::SecurityType::SECURITYTYPE_UNSET;
}
boost::gregorian::date smallest_leg(
std::optional<core::TimeStamp> smallest_leg(
::google::protobuf::RepeatedPtrField<::bts::api::ContractLeg> const &l) {
bts_assert(l.size());
auto smallest = expiration(l[0].security());
@@ -50,49 +51,59 @@ boost::gregorian::date smallest_leg(
return smallest;
}
boost::gregorian::date expiration(api::Security const &security) {
std::optional<core::TimeStamp> expiration(api::Security const &security) {
switch (security.Contract_case()) {
case api::Security::kFuture:
return {
boost::gregorian::greg_year(security.future().expiration_year()),
boost::gregorian::greg_month(security.future().expiration_month()),
boost::gregorian::greg_day(security.future().expiration_day())};
case api::Security::kOption:
return {
boost::gregorian::greg_year(security.option().expiration_year()),
boost::gregorian::greg_month(security.option().expiration_month()),
boost::gregorian::greg_day(security.option().expiration_day())};
case api::Security::kFuture: {
const auto &fut = security.future();
core::TimeStamp exp;
if (core::TimeStamp::from_ymd(
fut.expiration_year(), fut.expiration_month(),
fut.expiration_day(), &exp)) {
return exp;
}
break;
}
case api::Security::kOption: {
const auto &opt = security.option();
core::TimeStamp exp;
if (core::TimeStamp::from_ymd(
opt.expiration_year(), opt.expiration_month(),
opt.expiration_day(), &exp)) {
return exp;
}
break;
}
case api::Security::kSpread:
return smallest_leg(security.spread().legs());
case api::Security::kSynthetic:
return smallest_leg(security.synthetic().legs());
default:
return {};
break;
}
return std::nullopt;
}
bool is_expired(
api::Security const &security,
datetime::PTime const &now,
core::TimeStamp now,
uint32_t sod_hh,
uint32_t sod_mm) {
auto e = expiration(security);
if (e == boost::gregorian::date())
auto maybe_exp = expiration(security);
if (!maybe_exp) {
return false;
else {
auto etime = datetime::PTime{
e, boost::posix_time::time_duration(sod_hh, sod_mm, 0)};
return etime < now;
}
const auto exp = *maybe_exp;
return exp < now && (exp.date() + core::TimeSpan::from_hours(sod_hh) +
core::TimeSpan::from_minutes(sod_mm)) < now;
}
bool is_expired(api::Security const &security, datetime::PTime const &now) {
bool is_expired(api::Security const &security, core::TimeStamp now) {
const auto &sod = config::get<config::SODTime>();
return is_expired(security, now, sod.sod_hh(), sod.sod_mm());
}
bool is_expired(api::Security const &security) {
return is_expired(security, datetime::local_now());
return is_expired(security, core::TimeStamp::now());
}
void write_date_string(
+3 -3
View File
@@ -237,13 +237,13 @@ round_to_quote_price(api::Security const &sec, Price in, api::Side side) {
raw_round_to_quote_price(p, sec.contract_spec().low_tick(), side));
}
boost::gregorian::date expiration(api::Security const &sec);
std::optional<core::TimeStamp> expiration(api::Security const &sec);
bool is_expired(api::Security const &security);
// for testing
bool is_expired(api::Security const &security, datetime::PTime const &now);
bool is_expired(api::Security const &security, core::TimeStamp now);
bool is_expired(
api::Security const &security,
datetime::PTime const &now,
core::TimeStamp now,
uint32_t sod_hh,
uint32_t sod_mm);
+15
View File
@@ -15,6 +15,7 @@ enum Venue {
VENUE_DERIBIT = 400;
VENUE_FTX = 410;
VENUE_BINANCE = 420;
VENUE_BINANCE_COIN_FUTURES = 425;
VENUE_BINANCE_USDS_FUTURES = 430;
VENUE_BINANCE_US = 440;
VENUE_COINBASE = 450;
@@ -106,6 +107,11 @@ message BinanceExchangeData {
string security_desc = 2;
}
message BinanceCoinFuturesExchangeData {
int64 exchange_id = 1;
string security_desc = 2;
}
message BinanceUsdsFuturesExchangeData {
int64 exchange_id = 1;
string security_desc = 2;
@@ -164,12 +170,20 @@ message OptionContract {
message SpotContract{
}
// his tells the relationshio of a leg with thr previous leg
enum PriceCalc {
ADD = 0;
MULT = 1;
Div = 2;
}
message ContractLeg {
Security security = 10;
double quantity = 20;
Side side = 30;
double price_multiplier = 40;
double leg_delta = 50;
PriceCalc price_calc = 60;
}
message SpreadContract {
@@ -228,6 +242,7 @@ message Security {
DeribitExchangeData deribit = 240;
FTXExchangeData ftx = 250;
BinanceExchangeData binance = 260;
BinanceCoinFuturesExchangeData binance_coin_futures = 265;
BinanceUsdsFuturesExchangeData binance_usds_futures = 270;
BinanceUsExchangeData binance_us = 280;
CoinbaseExchangeData coinbase = 290;
+25 -2
View File
@@ -1,15 +1,22 @@
#pragma once
#include "lib/domain_model/Security.pb.h"
#include "lib/core/FMT_IMPL.h"
#include "lib/core/EnumMacros.h"
#include "lib/core/FMT_IMPL.h"
#include "lib/domain_model/Security.pb.h"
#include "lib/io/JsonReader.h"
#include "lib/logging/logging.h"
#include "lib/utils/string_utils.h"
#include <nlohmann/json.hpp>
namespace bts::api {
/**
* Parses Venue from a string. Unlike `bts::api::SecurityType_Parse`, this implementation is case
* insensitive, and makes the 'SECURITYTYPE_' prefix optional.
*/
bool security_type_parse_smart(const std::string &str, SecurityType *result);
PB_ENUM_FROM_JSON(SecurityType)
PB_ENUM_TO_JSON(SecurityType)
@@ -19,6 +26,22 @@ std::ostream &operator<<(std::ostream &out, const SecurityType &type) noexcept;
// Definition
//////////////////////////////////////////////////
inline bool security_type_parse_smart(
const std::string &str, SecurityType *result) {
if (SecurityType_Parse(str, result)) {
return true;
}
std::string s = bts::system::string_utils::copy_to_upper(str);
if (SecurityType_Parse(s, result)) {
return true;
}
const char *prefix = "SECURITYTYPE_";
if (!s.starts_with(prefix)) {
s.insert(0, prefix);
}
return SecurityType_Parse(s, result);
}
inline std::ostream &operator<<(
std::ostream &out, const SecurityType &type) noexcept {
out << SecurityType_Name(type);
+22
View File
@@ -5,11 +5,18 @@
#include "lib/domain_model/Security.pb.h"
#include "lib/io/JsonReader.h"
#include "lib/logging/logging.h"
#include "lib/utils/string_utils.h"
#include <nlohmann/json.hpp>
namespace bts::api {
/**
* Parses Venue from a string. Unlike `bts::api::Venue_Parse`, this implementation is case
* insensitive, and makes the 'VENUE_' prefix optional.
*/
bool venue_parse_smart(const std::string &str, Venue *result);
PB_ENUM_FROM_JSON(Venue)
PB_ENUM_TO_JSON(Venue)
@@ -19,6 +26,21 @@ std::ostream &operator<<(std::ostream &out, Venue venue) noexcept;
// Definition
//////////////////////////////////////////////////
inline bool venue_parse_smart(const std::string &str, Venue *result) {
if (Venue_Parse(str, result)) {
return true;
}
std::string s = bts::system::string_utils::copy_to_upper(str);
if (Venue_Parse(s, result)) {
return true;
}
const char *prefix = "VENUE_";
if (!s.starts_with(prefix)) {
s.insert(0, prefix);
}
return Venue_Parse(s, result);
}
inline std::ostream &operator<<(std::ostream &out, Venue venue) noexcept {
out << Venue_Name(venue);
return out;
+1 -1
View File
@@ -116,4 +116,4 @@ using UserLogin = utils::ValueWrapper<struct UserLoginTag, std::string>;
} // namespace bts
FMT_IMPL(bts::Price, p, "{}", p.value());
FMT_IMPL(bts::Quantity, q, "{}", q.value());
FMT_IMPL(bts::Quantity, q, "{}", q.full_value());
+8 -23
View File
@@ -210,37 +210,22 @@ auto fut_text = " id: 6\n"
TEST_CASE("expiration_tests", "[security]") {
SECTION("test_expiration_retrieval") {
using namespace boost::gregorian;
auto spread = utils::pb::from_string<api::Security>(spread_txt);
REQUIRE(
securities::expiration(spread) ==
date(greg_year(2019), greg_month(1), greg_day(20)));
core::TimeStamp actual_exp = *securities::expiration(spread);
REQUIRE((actual_exp == core::TimeStamp{2019, 1, 20}));
auto &leg = spread.spread().legs(1).security();
REQUIRE(
securities::expiration(leg) ==
date(greg_year(3020), greg_month(6), greg_day(19)));
actual_exp = *securities::expiration(leg);
REQUIRE((actual_exp == core::TimeStamp{3020, 6, 19}));
REQUIRE(securities::is_expired(spread));
REQUIRE(!securities::is_expired(leg));
}
SECTION("test_expiration_sod_boundary") {
using namespace boost::gregorian;
auto fut = utils::pb::from_string<api::Security>(fut_text);
REQUIRE(securities::is_expired(
fut, datetime::make_ptime(2019, 1, 21, 0, 0, 0, 0), 16, 30));
REQUIRE(!securities::is_expired(
fut, datetime::make_ptime(2019, 1, 19, 0, 0, 0, 0), 16, 30));
REQUIRE(!securities::is_expired(
fut, datetime::make_ptime(2019, 1, 20, 16, 29, 0, 0), 16, 30));
REQUIRE(securities::is_expired(
fut, datetime::make_ptime(2019, 1, 20, 16, 31, 0, 0), 16, 30));
REQUIRE(securities::is_expired(fut, {2019, 1, 21}, 16, 30));
REQUIRE(!securities::is_expired(fut, {2019, 1, 19}, 16, 30));
REQUIRE(!securities::is_expired(fut, {2019, 1, 20, 16, 29, 0}, 16, 30));
REQUIRE(securities::is_expired(fut, {2019, 1, 20, 16, 31, 0}, 16, 30));
}
}
-21
View File
@@ -1,21 +0,0 @@
#include "CurlHandle.h"
#include <curl/curl.h>
#include <stdexcept>
namespace bts::http {
CurlHandle::CurlHandle() {
const auto eCode = curl_global_init(CURL_GLOBAL_ALL);
if (eCode != CURLE_OK) {
throw std::runtime_error{"Error initializing libCURL"};
}
}
CurlHandle::~CurlHandle() { curl_global_cleanup(); }
CurlHandle &CurlHandle::instance() {
static CurlHandle inst{};
return inst;
}
} // namespace bts::http
-21
View File
@@ -1,21 +0,0 @@
#pragma once
namespace bts::http {
class CurlHandle {
public:
static CurlHandle &instance();
CurlHandle(CurlHandle const &) = delete;
CurlHandle(CurlHandle &&) = delete;
CurlHandle &operator=(CurlHandle const &) = delete;
CurlHandle &operator=(CurlHandle &&) = delete;
~CurlHandle();
private:
CurlHandle();
};
} // namespace bts::http
-55
View File
@@ -1,55 +0,0 @@
#include "HTTPClient.h"
#include "HTTPRequest.h"
#include "HTTPResponse.h"
#include "lib/io/URL.h"
#include "lib/logging/logging.h"
#include <curl/curl.h>
#include <fmt/format.h>
namespace bts::io::http {
static size_t write_data_(
void *ptr, size_t size, size_t nmemb, std::string *data) {
data->append((char *)ptr, size * nmemb);
return size * nmemb;
}
HTTPResponse HTTPClient::get(const HTTPRequest &request) {
auto *curl = curl_easy_init();
auto url_str = request.url().to_string();
// LOG(md, bts::Info, "do GET: {}", url_str);
curl_easy_setopt(curl, CURLOPT_URL, url_str.c_str());
struct curl_slist *request_headers = NULL;
for (const auto &[k, v] : request.headers()) {
request_headers = curl_slist_append(
request_headers, fmt::format("{}: {}", k, v).c_str());
}
std::string response_string;
std::string header_string;
std::string response_header_string;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data_);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_string);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &header_string);
char *url;
long response_code;
double elapsed;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &elapsed);
curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &url);
// curl_easy_getinfo(curl, CURLINFO_HEADER_OUT, &response_header_string);
curl_easy_perform(curl);
curl_easy_cleanup(curl);
curl = NULL;
HTTPResponse response;
response.headers() = HTTPHeaders(response_header_string);
response.body() = std::move(response_string);
response.code() = response_code;
// LOG(md, bts::Info, "do GET: {}. Received response:\n{}", url_str, response);
return response;
}
} // namespace bts::io::http
-35
View File
@@ -1,35 +0,0 @@
#include <algorithm>
#include <atomic>
#include <cstddef> // std::size_t
#include <cstdio> // snprintf
#include <cstdlib>
#include <cstring> // strerror, strlen, memcpy, strcpy
#include <ctime>
#include <curl/curl.h>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory> // std::unique_ptr
#include <mutex>
#include <sstream>
#include <stdarg.h> // va_start, etc.
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <unordered_map>
#include <vector>
namespace bts::io::http {
class HTTPRequest;
class HTTPResponse;
class HTTPClient {
public:
// HTTPClient(const std::string &url);
HTTPResponse get(const HTTPRequest &request);
};
} // namespace bts::io::http
-17
View File
@@ -1,17 +0,0 @@
#include "HTTPRequest.h"
namespace bts::io::http {
io::URL &HTTPRequest::url() { return url_; }
const io::URL &HTTPRequest::url() const { return url_; }
HTTPHeaders &HTTPRequest::headers() { return headers_; }
const HTTPHeaders &HTTPRequest::headers() const { return headers_; }
std::string &HTTPRequest::body() { return body_; }
const std::string &HTTPRequest::body() const { return body_; }
} // namespace bts::io::http
-26
View File
@@ -1,26 +0,0 @@
#pragma once
#include "HTTPHeaders.h"
#include "lib/io/URL.h"
#include <string>
#include <vector>
namespace bts::io::http {
class HTTPRequest {
public:
io::URL &url();
const io::URL &url() const;
HTTPHeaders &headers();
const HTTPHeaders &headers() const;
std::string &body();
const std::string &body() const;
private:
io::URL url_;
HTTPHeaders headers_;
std::string body_;
};
} // namespace bts::io::http
+3 -1
View File
@@ -14,7 +14,9 @@ namespace bts::io::http {
#define FOR_ALL_VALUES(per_value) \
per_value(HttpMethod, NONE) \
per_value(HttpMethod, GET) \
per_value(HttpMethod, POST)
per_value(HttpMethod, POST) \
per_value(HttpMethod, PUT) \
per_value(HttpMethod, DELETE)
// clang-format on
/**
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <cstddef>
#include <curl/curl.h>
#include <string>
#include <unordered_map>
namespace bts::http {
class CurlHeaders {
public:
CurlHeaders() = default;
explicit CurlHeaders(
const std::unordered_map<std::string, std::string> &items) {
for (const auto &[k, v] : items) {
append(k, v);
}
}
~CurlHeaders() {
if (data_) {
curl_slist_free_all(data_);
}
}
void append(const std::string &key, const std::string &value) {
const std::string str = key + ": " + value;
data_ = curl_slist_append(data_, str.c_str());
}
curl_slist *data() { return data_; }
private:
curl_slist *data_ = NULL;
};
} // namespace bts::http
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <atomic>
#include <curl/curl.h>
#include <curl/easy.h>
namespace bts::http {
struct CurlSession {
static inline std::atomic<int> ref_count = 0;
CURL *curl = nullptr;
CurlSession() {
if (++ref_count == 1) {
curl_global_init(CURL_GLOBAL_ALL);
}
curl = curl_easy_init();
}
~CurlSession() {
curl_easy_cleanup(curl);
if (--ref_count == 0) {
curl_global_cleanup();
}
}
};
} // namespace bts::http
+167
View File
@@ -0,0 +1,167 @@
#include "http_client.h"
#include "lib/io/JsonDumper.h"
#include "lib/io/http/HttpMethod.h"
#include "lib/io/http/curl_headers.h"
#include "lib/io/http/curl_session.h"
#include "lib/io/http/http_request.h"
#include "lib/io/http/http_response.h"
#include "lib/io/http/i_http_response.h"
#include "lib/io/http/query_params.h"
#include "lib/logging/logging.h"
#include <curl/curl.h>
#include <curl/easy.h>
#include <string>
namespace bts::http {
using bts::io::JsonDumper;
using nlohmann::json;
using std::exception;
using std::int64_t;
using std::pair;
using std::string;
using std::tuple;
using std::unordered_map;
using std::vector;
static size_t write_data(
void *ptr, size_t size, size_t nmemb, IHttpResponse *data) {
return data->write((char *)ptr, size, nmemb);
}
static size_t write_headers(
void *ptr, size_t size, size_t nmemb, IHttpResponse *data) {
return data->write_headers((char *)ptr, size, nmemb);
}
void HttpClient::get(
string url,
unordered_map<string, string> headers,
IHttpResponse *response) {
HttpRequest req;
req.url = std::move(url);
req.headers = std::move(headers);
req.method = io::http::HttpMethod::GET;
send(req, response);
}
void HttpClient::post(
string url,
string body,
unordered_map<string, string> headers,
IHttpResponse *response) {
HttpRequest req;
req.url = std::move(url);
req.body = std::move(body);
req.headers = std::move(headers);
req.method = io::http::HttpMethod::POST;
send(req, response);
}
void HttpClient::put(
string url,
string body,
unordered_map<string, string> headers,
IHttpResponse *response) {
HttpRequest req;
req.url = std::move(url);
req.body = std::move(body);
req.headers = std::move(headers);
req.method = io::http::HttpMethod::PUT;
send(req, response);
}
void HttpClient::del(
string url,
string body,
unordered_map<string, string> headers,
IHttpResponse *response) {
HttpRequest req;
req.url = std::move(url);
req.body = std::move(body);
req.headers = std::move(headers);
req.method = io::http::HttpMethod::DELETE;
send(req, response);
}
void HttpClient::send(const HttpRequest &req, IHttpResponse *response) {
LOG(Http, Info, "HttpClient::send. {}", JsonDumper::dump(req));
CurlSession session;
CURL *curl = session.curl;
CurlHeaders curl_headers{req.headers};
// Prepare req
curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
curl_easy_setopt(curl, CURLOPT_URL, req.url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curl_headers.data());
if (response != nullptr) {
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_headers);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, response);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
}
switch (req.method.value) {
case io::http::HttpMethod::GET:
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
break;
case io::http::HttpMethod::POST:
curl_easy_setopt(curl, CURLOPT_HTTPPOST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, req.body.size());
break;
case io::http::HttpMethod::PUT:
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.body.c_str());
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
// curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, req.body.size());
break;
case io::http::HttpMethod::DELETE:
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.body.c_str());
break;
case io::http::HttpMethod::NONE:
default:
break;
}
// Send it
CURLcode curl_code = curl_easy_perform(curl);
long response_code;
double elapsed;
char *effective_url = NULL;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &elapsed);
curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url);
const char *err = curl_easy_strerror(curl_code);
try {
response->write_result(response_code, string{err}, string{effective_url});
} catch (const exception &ex) {
LOG(Http, Error, "HttpClient::perform. IHttpResponse threw: {}",
string{ex.what()});
} catch (...) {
LOG(Http, Error, "HttpClient::perform. IHttpResponse threw unknown ex.");
}
}
tuple<string, string, int64_t> HttpClient::get_oauth_token(
const string &url, const string &client_id, const string &client_secret) {
LOG(Gen, Info, "Requesting oauth token from {}", url);
vector<pair<string, string>> query_params{
{"grant_type", "client_credentials"},
{"client_id", client_id},
{"client_secret", client_secret}};
HttpResponse res;
HttpClient::post(url, query_params_string(query_params), {}, &res);
LOG(Gen, Info, "Received oauth token:\n{}", JsonDumper::dump(res));
try {
const auto j = json::parse(res.body);
return {
j.at("access_token").get<string>(), j.at("token_type").get<string>(),
j.at("expires_in").get<int64_t>()};
} catch (const exception &e) {
LOG(Gen, Error, "Failed to parse oauth response json.{}\n{}",
string{e.what()}, res.body);
}
return {};
}
} // namespace bts::http
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include "lib/io/http/http_request.h"
#include "lib/io/http/i_http_response.h"
#include <cstdint>
#include <string>
#include <tuple>
#include <unordered_map>
namespace bts::http {
class HttpClient {
public:
/**
* Performs an http GET request
*/
static void get(
std::string url,
std::unordered_map<std::string, std::string> headers,
IHttpResponse *response);
/**
* Performs an http POST request
*/
static void post(
std::string url,
std::string body,
std::unordered_map<std::string, std::string> headers,
IHttpResponse *response);
/**
* Performs an http PUT request
*/
static void put(
std::string url,
std::string body,
std::unordered_map<std::string, std::string> headers,
IHttpResponse *response);
/**
* Performs an http DELETE request
*/
static void del(
std::string url,
std::string body,
std::unordered_map<std::string, std::string> headers,
IHttpResponse *response);
/**
* Requests an oauth access token
*
* @return tuple of: access token, token type, and seconds until expiration
*/
static std::tuple<std::string, std::string, std::int64_t> get_oauth_token(
const std::string &url,
const std::string &client_id,
const std::string &client_secret);
static void send(const HttpRequest &req, IHttpResponse *res);
};
} // namespace bts::http
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include "lib/io/http/HttpMethod.h"
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
namespace bts::http {
class HttpRequest {
public:
std::string url;
std::string body;
io::http::HttpMethod method;
std::unordered_map<std::string, std::string> headers;
void reset();
};
//////////////////////////////////////////////////
// Definition
//////////////////////////////////////////////////
inline void HttpRequest::reset() {
url.clear();
body.clear();
method = io::http::HttpMethod::NONE;
headers.clear();
}
inline void to_json(nlohmann::json &j, const HttpRequest &req) {
j["url"] = req.url;
j["body"] = req.body;
j["method"] = req.method.name();
nlohmann::json hdrs;
for (const auto &[k, v] : req.headers) {
hdrs[k] = v;
}
j["headers"] = hdrs;
}
inline void from_json(const nlohmann::json &j, HttpRequest &req) {
j.at("url").get_to(req.url);
j.at("body").get_to(req.body);
j.at("method").get_to(req.method);
const auto &hdrs = j.at("headers");
for (auto it = hdrs.begin(); it != hdrs.end(); it++) {
it->get_to(req.headers[it.key()]);
}
}
} // namespace bts::http
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include "lib/core/FMT_IMPL.h"
#include "lib/io/http/i_http_response.h"
#include <cstddef>
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
namespace bts::http {
/**
* Basic implementation of `IHttpResponse`, with some useful state.
*/
class HttpResponse : public IHttpResponse {
public:
std::string url;
std::unordered_map<std::string, std::string> headers;
std::string body;
std::int64_t code;
std::string errors;
std::size_t write(char *cstr, std::size_t sz, std::size_t n) override;
std::size_t write_headers(
char *cstr, std::size_t sz, std::size_t n) override;
void write_result(
std::int64_t code,
std::string &&errors,
std::string &&effective_url) override;
constexpr bool ok() const;
void reset();
};
void to_json(nlohmann::json &j, const HttpResponse &res);
void from_json(const nlohmann::json &j, HttpResponse &res);
std::ostream &operator<<(
std::ostream &out, const HttpResponse &response) noexcept;
//////////////////////////////////////////////////
// Definition
//////////////////////////////////////////////////
inline std::size_t HttpResponse::write(
char *cstr, std::size_t sz, std::size_t n) {
body.append(cstr, sz * n);
return sz * n;
}
inline std::size_t HttpResponse::write_headers(
char *cstr, std::size_t sz, std::size_t n) {
std::string str{cstr, sz * n};
const auto colon_idx = str.find(':');
if (colon_idx >= str.size()) {
headers[str] = {};
} else {
headers[str.substr(0, colon_idx)] =
str.substr(colon_idx + 1, str.size() - colon_idx - 1);
}
return sz * n;
}
inline void HttpResponse::write_result(
std::int64_t code, std::string &&errors, std::string &&effective_url) {
this->errors = errors;
this->code = code;
this->url = effective_url;
}
constexpr bool HttpResponse::ok() const { return code >= 200 && code < 300; }
inline void HttpResponse::reset() {
url.clear();
headers.clear();
body.clear();
errors.clear();
code = 0;
}
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
HttpResponse, url, headers, body, code, errors);
inline std::ostream &operator<<(
std::ostream &out, const HttpResponse &res) noexcept {
return out << nlohmann::json{res}.dump();
}
} // namespace bts::http
FMT_IMPL(bts::http::HttpResponse, r, "{}", nlohmann::json{r}.dump());
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace bts::http {
/**
* Simple http response interface, modeled after cURL callbacks
*/
class IHttpResponse {
public:
virtual ~IHttpResponse() = default;
virtual std::size_t write(char *cstr, std::size_t sz, std::size_t n) = 0;
virtual std::size_t write_headers(char *cstr, std::size_t sz, std::size_t n);
virtual void write_result(
std::int64_t code, std::string &&errors, std::string &&effective_url);
};
//////////////////////////////////////////////////
// Definition
//////////////////////////////////////////////////
inline std::size_t IHttpResponse::write_headers(
char * /*cstr*/, std::size_t sz, std::size_t n) {
return sz * n;
}
inline void IHttpResponse::write_result(
std::int64_t /*code*/,
std::string && /*errors*/,
std::string && /*effective_url*/) {}
} // namespace bts::http
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include "lib/core/FMT_IMPL.h"
#include <algorithm>
#include <string>
#include <unordered_map>
namespace bts::http {
template <class Iter>
inline std::string query_params_string(Iter first, Iter last) {
std::string rv;
for (auto it = first; it != last; it++) {
if (it->first.empty()) {
continue;
}
if (!rv.empty()) {
rv.append("&");
}
rv.append(it->first);
if (!it->second.empty()) {
rv.append("=");
rv.append(it->second);
}
}
return rv;
}
template <class Range>
inline std::string query_params_string(Range &&range) {
return query_params_string(range.begin(), range.end());
}
class QueryParams : public std::unordered_map<std::string, std::string> {
public:
std::string to_string() const;
};
inline std::string QueryParams::to_string() const {
std::string rv;
auto it = begin();
if (it == end()) {
return "";
}
rv.append(it->first + "=" + it->second);
while (++it != end()) {
rv.append("&" + it->first + "=" + it->second);
}
return rv;
}
inline bool operator==(const QueryParams &lhs, const QueryParams &rhs) {
return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
}
inline bool operator!=(const QueryParams &lhs, const QueryParams &rhs) {
return !(lhs == rhs);
}
} // namespace bts::http
FMT_IMPL(bts::http::QueryParams, p, "{}", p.to_string());
+410
View File
@@ -0,0 +1,410 @@
#include "websocket_client.h"
#include "lib/core/TimeSpan.h"
#include "lib/core/TimeStamp.h"
#include "lib/io/websocket/websocket_client_config.h"
#include "lib/io/websocket/websocket_client_state.h"
#include "lib/io/websocket/websocket_close_status.h"
#include "lib/io/websocket/websocket_error_code.h"
#include "lib/io/websocket/websocketpp_exception_formatter.h"
#include "lib/logging/logging.h"
#include "lib/utils/formatters/exception_formatter.h"
#include <atomic>
#include <exception>
#include <fmt/format.h>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <stdexcept>
#include <string>
#include <thread>
#include <websocketpp/close.hpp>
#include <websocketpp/common/connection_hdl.hpp>
#include <websocketpp/common/memory.hpp>
#include <websocketpp/common/system_error.hpp>
#include <websocketpp/common/thread.hpp>
#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/config/core_client.hpp>
#include <websocketpp/roles/client_endpoint.hpp>
namespace bts::websocket {
struct WebsocketClientSnapshot {
int client_id;
WebsocketClientState state;
int version;
};
} // namespace bts::websocket
FMT_IMPL(
bts::websocket::WebsocketClientSnapshot,
t,
"id: {}; state: {}; version: {}",
t.client_id,
t.state,
t.version);
namespace bts::websocket {
using namespace std::chrono_literals;
using bts::core::TimeSpan;
using bts::core::TimeStamp;
using nlohmann::json;
using std::lock_guard;
using std::map;
using std::mutex;
using std::optional;
using std::size_t;
using std::string;
using std::thread;
using std::uint32_t;
using websocketpp::config::asio_tls_client;
using WebsocketppClient =
websocketpp::client<websocketpp::config::asio_tls_client>;
using ConnectionHdl = websocketpp::connection_hdl;
using ConnectionPtr = WebsocketppClient::connection_ptr;
using MessageTypePtr = websocketpp::config::core_client::message_type::ptr;
using SslContext = boost::asio::ssl::context;
using State = WebsocketClientState;
// ws endpoint http://markets.api.uat.cmegroup.com/marketdatastream/v1
#define _ASSERT_MSGF(condition, ...) \
bts_assert_msgf(condition, "WebsocketClient::" __VA_ARGS__)
#define DIAG(...) LOG(Http, Diag, __VA_ARGS__)
#define DIAG_FN(...) LOG(Http, Diag, "WebsocketClient::" __VA_ARGS__)
#define INFO(...) LOG(Http, Info, __VA_ARGS__)
#define INFO_FN(...) LOG(Http, Info, "WebsocketClient::" __VA_ARGS__)
#define WARN(...) LOG(Http, Warn, __VA_ARGS__)
#define WARN_FN(...) LOG(Http, Warn, "WebsocketClient::" __VA_ARGS__)
#define ERROR(...) LOG(Http, Error, __VA_ARGS__)
#define ERROR_FN(...) LOG(Http, Error, "WebsocketClient::" __VA_ARGS__)
struct WebsocketClient::PImpl {
WebsocketClientConfig config;
WebsocketppClient client{};
WebsocketppClient::connection_ptr con;
optional<OpenHandler> open_handler;
optional<MessageHandler> message_handler;
optional<CloseHandler> close_handler;
size_t sub_id_counter{0};
string server_name;
optional<thread> io_thread;
int client_id{client_id_counter++};
mutex instance_mx;
State state;
int version = 0;
~PImpl() {
if (io_thread) {
io_thread->join();
}
}
int set_state(State state) {
if (this->state == state) {
return version;
}
version++;
INFO(
"State changing from {} -> {}. id: {}; version: {}", this->state,
state, client_id, version);
this->state = state;
return version;
}
constexpr WebsocketClientSnapshot snapshot() const {
return {client_id, state, version};
}
void close(websocketpp::close::status::value code, std::string reason) {
lock_guard<mutex> lock{instance_mx};
INFO_FN(
"close. {}; code: {}; reason: {}", snapshot(),
WebsocketCloseStatus{code}, reason);
switch (state.value) {
case State::OPENING:
WARN_FN(
"close. Cannot close while websocket is still opening. {}",
snapshot());
// Wait until we get the 'open' callback, then initiate the close
set_state(State::WAITING_TO_CLOSE);
return;
case State::OPEN:
set_state(State::CLOSING);
break;
default:
return;
}
websocketpp::lib::error_code ec;
client.close(con->get_handle(), code, reason, ec);
if (ec) {
ERROR_FN(
"close. {}; code: {}; reason: {}; error: {}", snapshot(),
WebsocketCloseStatus{code}, reason, ec);
}
}
websocketpp::lib::error_code send_text(const string &text) {
INFO_FN("send_text. {}; text: {}", snapshot(), text);
websocketpp::lib::error_code ec;
client.send(con, text, websocketpp::frame::opcode::value::TEXT, ec);
if (ec) {
ERROR_FN("send_text. {}; text: {}; err: {}", snapshot(), text, ec);
}
return ec;
}
/**
* Connects to the websocket server
*/
void connect(
const map<string, string> &headers, bool run_in_background = true) {
lock_guard<mutex> lock{instance_mx};
INFO_FN("connect. {}", snapshot());
switch (state.value) {
case State::IDLE:
break;
default:
WARN_FN("connect. Unexpected state. {}", snapshot());
return;
}
set_state(State::OPENING);
try {
init_logging();
init_handlers();
// Get connection
websocketpp::lib::error_code ec;
con = client.get_connection(config.url, ec);
if (ec) {
ERROR_FN("connect_no_timeout. {}; err: {}", snapshot(), ec);
return;
}
// Add headers
for (const auto &[k, v] : headers) {
con->append_header(k, v);
}
// Connect
client.connect(con);
if (run_in_background) {
bts_assert_msgf(
!io_thread, "Websocket io_thread already instantiated. {}",
snapshot());
io_thread = std::thread{[this]() { run(); }};
if (config.connect_timeout > core::TimeSpan::zero()) {
wait_for_connect(config.connect_timeout);
}
}
} catch (const std::exception &ex) {
ERROR_FN(
"connect_no_timeout. Failed to connect. {}; err: {}", snapshot(),
ex);
set_state(State::IDLE);
// Don't wait for connect
return;
}
}
void wait_for_connect(const TimeSpan timeout) {
INFO_FN("wait_for_connect. {}; timeout: {}", snapshot(), timeout);
const auto start = TimeStamp::now();
while (state == State::OPENING) {
const auto elapsed = TimeStamp::now() - start;
const auto rem = timeout - elapsed;
if (rem < TimeSpan::zero()) {
throw std::runtime_error{fmt::format(
"Timed out waiting for connection after {} secs. {}",
elapsed.total_seconds(), snapshot())};
}
WARN_FN(
"wait_for_connect. Still waiting. {}; timeout: {}; rem: {}",
snapshot(), timeout, rem);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void run() {
INFO("Websocket io thread begin. {}", snapshot());
try {
client.run();
} catch (const websocketpp::exception &ex) {
ERROR_FN(
"io_thread. Websocket io threw websocketpp::exception. {}; "
"err: {}",
snapshot(), ex);
} catch (const std::exception &ex) {
ERROR_FN(
"io_thread. Websocket io threw std::exception. {}; err: {}",
snapshot(), ex);
} catch (...) {
bts_assert_msgf(
false,
"io_thread. Websocket io threw unknown exception. Could "
"this have been thrown by a callback delegate? {}",
snapshot());
}
INFO("Websocket 'run' thread end. {}", snapshot());
}
private:
static std::atomic<int> client_id_counter;
auto on_tls_init(ConnectionHdl /*hdl*/) {
INFO_FN("on_tls_init. state: {}; version: {}", state, version);
auto result =
websocketpp::lib::make_shared<SslContext>(SslContext::sslv23_client);
result->set_verify_mode(boost::asio::ssl::verify_none);
return result;
}
void init_handlers() {
INFO_FN("init_handlers. state: {}; version: {}", state, version);
bts_assert(!io_thread);
client.init_asio();
client.set_message_handler(
[this](auto hdl, auto msg) { on_message(hdl, msg); });
client.set_open_handler([this](auto hdl) { on_open(hdl); });
client.set_ping_handler([](auto /*hdl*/, auto /*msg*/) {
LOG(Http, Info, "Received ping");
return true;
});
client.set_pong_timeout(0);
client.set_pong_handler([](auto /*hdl*/, auto /*msg*/) {
LOG(Http, Info, "Received pong");
return true;
});
client.set_tls_init_handler(
[this](auto hdl) { return on_tls_init(hdl); });
client.set_close_handler([this](auto hdl) { on_close(hdl); });
client.set_fail_handler([this](auto hdl) {
const auto ec = client.get_con_from_hdl(hdl)->get_ec();
ERROR_FN("fail_handler. {}; err: {}", snapshot(), ec.message());
});
}
void init_logging() {
INFO_FN("init_logging. {}", snapshot());
client.set_access_channels(websocketpp::log::alevel::all);
// client.clear_access_channels(websocketpp::log::alevel::frame_payload);
client.set_error_channels(websocketpp::log::elevel::all);
}
void on_open(ConnectionHdl hdl) {
lock_guard<mutex> lock{instance_mx};
INFO_FN("on_open. {}", snapshot());
const auto ec = client.get_con_from_hdl(hdl)->get_ec();
if (ec) {
ERROR_FN("on_open. {}; err: {}", snapshot(), ec.message());
}
switch (state.value) {
case State::OPENING:
break;
default:
WARN_FN("on_open. Unexpected state. {}", snapshot());
return;
}
set_state(State::OPEN);
if (open_handler) {
(*open_handler)(ec);
}
}
void on_message(ConnectionHdl /*hdl*/, MessageTypePtr msg) {
DIAG("on_message. {}", snapshot());
const auto &payload = msg->get_payload();
if (message_handler) {
(*message_handler)(payload);
return;
}
ERROR_FN(
"on_message. Message handler is null. {}; payload: \n{}", snapshot(),
payload);
}
void on_close(ConnectionHdl hdl) {
lock_guard<mutex> lock{instance_mx};
INFO_FN("on_close. {}", snapshot());
switch (state.value) {
case State::CLOSING:
break;
default:
WARN_FN("on_close. Unexpected state. {}", snapshot());
break;
}
set_state(State::IDLE);
const auto con = client.get_con_from_hdl(hdl);
const auto ec = con->get_ec();
if (ec) {
ERROR_FN(
"on_close. {}; local_code: {}; "
"local_reason: {}; remote_code: {}; remote_reason: {}; err: {}",
snapshot(), con->get_local_close_code(),
con->get_local_close_reason(), con->get_remote_close_code(),
con->get_remote_close_reason(), ec.message());
}
if (close_handler) {
(*close_handler)(ec);
}
}
};
WebsocketClient::WebsocketClient(WebsocketClientConfig config)
: me_(std::make_unique<PImpl>(std::move(config))) {}
WebsocketClient::~WebsocketClient() {
me_->close(websocketpp::close::status::going_away, "");
}
std::atomic<int> WebsocketClient::PImpl::client_id_counter = 0;
WebsocketClient::CloseHandler &WebsocketClient::open_handler() {
if (!me_->open_handler) {
me_->open_handler.emplace();
}
return *me_->open_handler;
}
WebsocketClient::MessageHandler &WebsocketClient::message_handler() {
if (!me_->message_handler) {
me_->message_handler.emplace();
}
return *me_->message_handler;
}
WebsocketClient::CloseHandler &WebsocketClient::close_handler() {
if (!me_->close_handler) {
me_->close_handler.emplace();
}
return *me_->close_handler;
}
void WebsocketClient::connect(
const std::map<std::string, std::string> &headers, bool run_in_background) {
me_->connect(headers, run_in_background);
}
void WebsocketClient::run() { me_->run(); }
websocketpp::lib::error_code WebsocketClient::send_text(const string &text) {
return me_->send_text(text);
}
void WebsocketClient::disconnect() {
me_->close(
websocketpp::close::status::going_away,
"WebsocketClient::disconnect was called");
}
bool WebsocketClient::is_connected() const {
lock_guard<mutex> lock{me_->instance_mx};
return me_->state == State::OPEN;
}
void WebsocketClient::ping(const string &payload) { me_->con->ping(payload); }
} // namespace bts::websocket
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "lib/io/websocket/websocket_client_config.h"
#include "lib/utils/Delegate.h"
#include <memory>
#include <string>
#include <websocketpp/config/core_client.hpp>
#include <websocketpp/message_buffer/message.hpp>
namespace bts::websocket {
class WebsocketClient {
public:
explicit WebsocketClient(WebsocketClientConfig config);
~WebsocketClient();
using OpenHandler = bts::core::Delegate<void(const std::error_code &)>;
using MessageHandler = bts::core::Delegate<void(const std::string &)>;
using CloseHandler = bts::core::Delegate<void(const std::error_code &)>;
void connect(
const std::map<std::string, std::string> &headers = {},
bool run_in_background = true);
void disconnect();
bool is_connected() const;
void run();
CloseHandler &open_handler();
MessageHandler &message_handler();
CloseHandler &close_handler();
websocketpp::lib::error_code send_text(const std::string &text);
void ping(const std::string &payload = "");
private:
struct PImpl;
std::unique_ptr<PImpl> me_;
};
} // namespace bts::websocket
@@ -0,0 +1,13 @@
#pragma once
#include "lib/core/TimeSpan.h"
namespace bts::websocket {
struct WebsocketClientConfig {
std::string url;
core::TimeSpan connect_timeout;
core::TimeSpan pong_timeout;
};
} // namespace bts::websocket
@@ -0,0 +1,25 @@
#pragma once
#include "lib/core/EnumMacros.h"
#include "lib/core/FMT_IMPL.h"
#include <nlohmann/json.hpp>
namespace bts::websocket {
// clang-format off
#define FOR_ALL_VALUES(op) \
op(WebsocketClientState, IDLE) \
op(WebsocketClientState, OPENING) \
op(WebsocketClientState, OPEN) \
op(WebsocketClientState, WAITING_TO_CLOSE) \
op(WebsocketClientState, CLOSING)
// clang-format on
ENUM_CLASS_DEF(WebsocketClientState, FOR_ALL_VALUES);
#undef FOR_ALL_VALUES
} // namespace bts::websocket
FMT_IMPL(bts::websocket::WebsocketClientState, s, "{}", s.name());
@@ -0,0 +1,24 @@
#pragma once
#include "lib/core/FMT_IMPL.h"
#include "lib/utils/ValueWrapper.h"
#include <websocketpp/close.hpp>
namespace bts::websocket {
/**
* Simple wrapper to provide a means implement a custom formatter for
* `websocketpp::close::status::value` (an alias for a primitive numeric type)
*/
using WebsocketCloseStatus = utils::ValueWrapper<
struct WebsocketCloseStatusTag,
websocketpp::close::status::value>;
} // namespace bts::websocket
FMT_IMPL(
bts::websocket::WebsocketCloseStatus,
s,
"{}",
websocketpp::close::status::get_string(*s));
@@ -0,0 +1,7 @@
#pragma once
#include "lib/core/FMT_IMPL.h"
#include <websocketpp/common/system_error.hpp>
FMT_IMPL(websocketpp::lib::error_code, ec, "{}", ec.message());
@@ -0,0 +1,7 @@
#pragma once
#include "lib/core/FMT_IMPL.h"
#include <websocketpp/error.hpp>
FMT_IMPL(websocketpp::exception, e, "{}", std::string{e.what()});
+11
View File
@@ -78,12 +78,20 @@ class IntPrice {
_exponent = temp._exponent;
return *this;
}
IntPrice &operator+=(double o) {
_mantissa += o * pow10(-_exponent);
return *this;
}
IntPrice &operator-=(const IntPrice &o) {
auto temp = *this - o;
_mantissa = temp._mantissa;
_exponent = temp._exponent;
return *this;
}
IntPrice &operator-=(double o) {
_mantissa -= o * pow10(-_exponent);
return *this;
}
IntPrice operator-() { return IntPrice(-_mantissa, _exponent); }
IntPrice operator*(const IntPrice &other) const {
return op(other, [](int64_t m1, int64_t m2) { return m1 * m2; });
@@ -91,6 +99,9 @@ class IntPrice {
IntPrice operator*(const int &other) const {
return IntPrice(_mantissa * other, _exponent);
}
IntPrice operator*(double v) const {
return IntPrice((int64_t)(_mantissa * v), _exponent);
}
IntPrice &operator*=(const int &other) {
_mantissa *= other;
return *this;
+17 -12
View File
@@ -439,6 +439,7 @@ class SyntheticConsumerInst {
void add_security(const api::Security &sec) {
Mutex::scoped_lock lock(_workmutex);
if (_data.find(sec.id()) == _data.end()) {
LOG(md, Info, "MDConsumer::add_security. {}", sec);
auto ptr = to_strategy(make_shared<api::Security>(sec));
vector<IncompleteStrategy> vec;
vec.push_back(ptr);
@@ -503,19 +504,23 @@ class SyntheticConsumerInst {
if (iter != real_handles.end()) {
// real handle
legs.push_back(iter->second);
} else {
auto iter2 = existing.find(leg_key);
if (iter2 != existing.end()) {
// existing synthetic handle
legs.push_back(MDConsumerHandle(iter2->second));
} else {
auto iter3 = result.find(leg_key);
if (iter3 != result.end()) {
// new synthetic handle?
legs.push_back(MDConsumerHandle(iter3->second));
}
}
continue;
}
auto iter2 = existing.find(leg_key);
if (iter2 != existing.end()) {
// existing synthetic handle
legs.push_back(MDConsumerHandle(iter2->second));
continue;
}
auto iter3 = result.find(leg_key);
if (iter3 != result.end()) {
// new synthetic handle?
legs.push_back(MDConsumerHandle(iter3->second));
continue;
}
LOG(md, Warn,
"MDConsumer::get_synthetic_handles. Unresolved leg. id: {}",
leg_key);
}
// If we found all the legs
if (legs.size() ==
-33
View File
@@ -124,7 +124,6 @@ struct DepthTraversal {
levels[x].size = depth[x].size();
}
}
#ifdef PRECISE_SYNTHETICS
double available_size() const {
double available = 0.0;
auto cur = head;
@@ -160,38 +159,6 @@ struct DepthTraversal {
}
return price / size;
}
#else
double available_size() const {
double available = 0.0;
auto cur = head;
while (cur < levels.size() && available < 1) {
available += levels[cur].size / qty;
++cur;
}
return available;
}
double consume(double size) {
bts_assert_d(size <= available_size());
double take = size * qty;
double price = 0.0;
while (take > 0) {
if (levels[head].size >= take) {
price = levels[head].price * price_mult;
levels[head].size -= take;
if (levels[head].size <= 0)
++head;
break;
} else {
double available = levels[head].size;
price = levels[head].price * price_mult;
levels[head].size = 0;
++head;
take -= available;
}
}
return price;
}
#endif
};
void copy_synthetic_side(
+196 -52
View File
@@ -1,7 +1,10 @@
#include "SecurityCache.h"
#include "SecurityDB.h"
#include "lib/domain_model/Security.h"
#include "lib/domain_model/Security.pb.h"
#include "lib/logging/logging.h"
#include "lib/md/security_type_index.h"
#include "lib/md/venue_index.h"
#include "lib/utils/Dispatcher.h"
#include "lib/utils/LockBox.h"
@@ -13,9 +16,152 @@
namespace bts {
namespace md {
using std::string;
using std::unordered_map;
struct SecurityCacheIndex {
btree::map<SecurityKey, SecurityPtr> cache;
btree::map<ExchangeSecurityKey, SecurityPtr> ex_cache;
VenueIndex<SecurityTypeIndex<unordered_map<string, uint32_t>>>
venue_type_ids;
const bts::api::Security *get_security(
api::Venue venue, const std::string &desc) {
auto id = get_id_(venue, desc);
auto sec_it = cache.find(SecurityKey{id});
if (sec_it == cache.end()) {
return nullptr;
}
return sec_it->second.get();
}
const bts::api::Security *get_security(
api::Venue venue, api::SecurityType type, const std::string &desc) {
auto id = get_id_(venue, type, desc);
auto sec_it = cache.find(SecurityKey{id});
if (sec_it == cache.end()) {
return nullptr;
}
return sec_it->second.get();
}
const Security *try_get_security(SecurityKey key) const {
auto iter = cache.find(key);
if (iter != cache.end()) {
return iter->second.get();
}
return (const api::Security *)nullptr;
}
const Security *try_get_security(ExchangeSecurityKey key) const {
auto iter = ex_cache.find(key);
if (iter != ex_cache.end()) {
return iter->second.get();
}
return (const api::Security *)nullptr;
}
std::vector<const Security *> get_securities(
api::Venue venue, api::SecurityType type, const std::string &name) {
std::vector<api::Security const *> result;
const bool all_types = type == api::SECURITYTYPE_UNSET;
const bool all_venues = venue == api::VENUE_UNSET;
const bool all_names = name.empty();
for (auto const &p : cache) {
const auto &s = *p.second;
if ((all_types ||
type == securities::security_type(s.Contract_case())) &&
(all_venues || venue == s.exchange()) &&
(all_names || name == securities::descriptor(s))) {
result.push_back(p.second.get());
}
}
return result;
}
std::vector<const Security *> get_securities(
std::function<bool(const Security &)> include) {
std::vector<api::Security const *> result;
for (auto const &p : cache) {
auto &s = *p.second;
if (include(s)) {
result.push_back(&s);
}
}
return result;
}
bool add_live_security(SecurityPtr security) {
bts_assert(security);
if (security->has_glbx()) {
ExchangeSecurityKey key(security->id());
ex_cache.insert(std::make_pair(key, security));
}
SecurityKey key(security->id());
// Insert but don't overwrite existing
auto result =
cache.insert(std::make_pair(SecurityKey(security->id()), security));
return result.second;
}
const Security *add_db_security(SecurityPtr security) {
bts_assert(security);
if (security->has_glbx()) {
ExchangeSecurityKey key(security->id());
ex_cache.insert(std::make_pair(key, security));
}
SecurityKey key(security->id());
auto it = cache.insert(std::make_pair(key, security));
const Security *active_sec = it.first->second.get();
// If the security was just added, active_sec is equal to security.
// If it was already present, it must be a recently inserted 'live'
// security and we should use it (instead of the db security).
venue_type_ids.get_or_add(security->exchange())
.get_or_add(bts::securities::security_type(*active_sec))
.try_emplace(
bts::securities::descriptor(*active_sec), active_sec->id());
return active_sec;
}
private:
std::uint32_t get_id_(api::Venue venue, const std::string &desc) {
auto id = get_id_(venue, api::SECURITYTYPE_SPOT, desc);
if (id != 0) {
return id;
}
id = get_id_(venue, api::SECURITYTYPE_FUTURE, desc);
if (id != 0) {
return id;
}
id = get_id_(venue, api::SECURITYTYPE_OPTION, desc);
if (id != 0) {
return id;
}
id = get_id_(venue, api::SECURITYTYPE_SPREAD, desc);
if (id != 0) {
return id;
}
id = get_id_(venue, api::SECURITYTYPE_SYNTHETIC, desc);
if (id != 0) {
return id;
}
return 0;
}
std::uint32_t get_id_(
api::Venue venue, api::SecurityType type, const std::string &desc) {
auto &type_store = venue_type_ids.get_or_add(venue);
auto &ids = type_store.get_or_add(type);
auto it = ids.find(desc);
if (it != ids.end()) {
return it->second;
}
return 0;
}
};
struct SecurityCacheImpl {
utils::LockBox<btree::map<SecurityKey, SecurityPtr>> cache;
utils::LockBox<btree::map<ExchangeSecurityKey, SecurityPtr>> ex_cache;
utils::LockBox<SecurityCacheIndex> cache;
tbb::concurrent_queue<SecurityPtr> update_queue;
void enqueue_update_db(const SecurityPtr &);
void flush();
@@ -52,23 +198,13 @@ SecurityCache::SecurityCache() : _me(std::make_shared<SecurityCacheImpl>()) {}
SecurityCache::~SecurityCache() {}
const Security *SecurityCache::try_get_security(SecurityKey key) const {
return _me->cache.lock([key](btree::map<SecurityKey, SecurityPtr> &t) {
auto iter = t.find(key);
if (iter != t.end()) {
return iter->second.get();
}
return (const api::Security *)nullptr;
});
return _me->cache.lock(
[key](SecurityCacheIndex &t) { return t.try_get_security(key); });
}
const Security *SecurityCache::try_get_security(ExchangeSecurityKey key) const {
return _me->ex_cache.lock(
[key](btree::map<ExchangeSecurityKey, SecurityPtr> &t) {
auto iter = t.find(key);
if (iter != t.end()) {
return iter->second.get();
}
return (const api::Security *)nullptr;
});
return _me->cache.lock(
[key](SecurityCacheIndex &t) { return t.try_get_security(key); });
}
const Security *SecurityCache::get_security(SecurityKey key) {
@@ -79,62 +215,70 @@ const Security *SecurityCache::get_security(SecurityKey key) {
if (security) {
LOG(md, Info, "Loaded security from database: {} {}",
security->symbol(), security->id());
return add_db_security(security);
return _me->cache.lock([&security](SecurityCacheIndex &cache) {
return cache.add_db_security(security);
});
}
return nullptr;
}
return result;
}
const Security *SecurityCache::get_security(
api::Venue venue, const std::string &name) {
return _me->cache.lock([venue, name](SecurityCacheIndex &cache) {
return cache.get_security(venue, name);
});
}
const Security *SecurityCache::get_security(
api::Venue venue, api::SecurityType type, const std::string &name) {
return _me->cache.lock([venue, type, name](SecurityCacheIndex &cache) {
return cache.get_security(venue, type, name);
});
}
std::vector<const Security *> SecurityCache::get_securities(
api::Venue venue, api::SecurityType type, const std::string &name) {
return _me->cache.lock([venue, type, &name](SecurityCacheIndex &cache) {
return cache.get_securities(venue, type, name);
});
}
std::vector<const Security *> SecurityCache::get_securities(
std::function<bool(const Security &)> include) {
std::vector<api::Security const *> result;
_me->cache.lock([&result, include](SecurityCacheIndex &cache) {
for (auto const &p : cache.cache) {
auto &s = *p.second;
if (include(s)) {
result.push_back(&s);
}
}
});
return result;
}
std::vector<api::Security const *> SecurityCache::get_securities() {
std::vector<api::Security const *> result;
result.reserve(100000);
_me->cache.lock([&result](auto &cache) {
for (auto const &p : cache) {
_me->cache.lock([&result](SecurityCacheIndex &cache) {
for (const auto &p : cache.cache) {
result.push_back(p.second.get());
}
});
return result;
}
const Security *SecurityCache::add_db_security(const SecurityPtr &security) {
bts_assert(security);
LOG(md, Info, "SecurityCache::add_db_security security: {}",
bts::securities::descriptor(*security));
if (security->has_glbx()) {
_me->ex_cache.lock([&security](auto &cache) {
ExchangeSecurityKey key(security->id());
cache.insert(std::make_pair(key, security));
});
}
return _me->cache.lock([&security](auto &cache) {
SecurityKey key(security->id());
auto result = cache.insert(std::make_pair(key, security));
// If result was inserted result.first->second is equal to security
// If the value was already present it must be a recently inserted 'live'
// security and we should use it
return result.first->second.get();
});
}
bool SecurityCache::add_live_security(const SecurityPtr &security) {
bts_assert(security);
LOG(md, Diag, "SecurityCache::add_live_security security: {}",
bts::securities::descriptor(*security));
if (security->has_glbx()) {
_me->ex_cache.lock([&security](auto &cache) {
ExchangeSecurityKey key(security->id());
cache.insert(std::make_pair(key, security));
});
}
return _me->cache.lock([&](auto &cache) {
SecurityKey key(security->id());
// Insert but don't overwrite existing
auto result =
cache.insert(std::make_pair(SecurityKey(security->id()), security));
return _me->cache.lock([&](SecurityCacheIndex &cache) {
const bool is_new_sec = cache.add_live_security(security);
_me->enqueue_update_db(security);
return result.second;
return is_new_sec;
});
}
+26 -1
View File
@@ -23,6 +23,32 @@ class SecurityCache {
// Load security from cache with fallback to database
const Security *get_security(SecurityKey key);
/**
* Finds the first security with matching @p venue and @p name. Params that are
* empty/unset are ignored.
*/
const Security *get_security(api::Venue venue, const std::string &name);
/**
* Finds the first security with matching @p venue @p type and @p name. Params that
* are empty/unset are ignored.
*/
const Security *get_security(
api::Venue venue, api::SecurityType type, const std::string &name);
/**
* Finds all securities with matching @p venue @p type and @p name. Params that
* are empty/unset are ignored.
*/
std::vector<const Security *> get_securities(
api::Venue venue, api::SecurityType type, const std::string &name);
/**
* Finds all securities where @p include returns true.
*/
std::vector<const Security *> get_securities(
std::function<bool(const Security &)> include);
// Load all securities from cache, no fallback to database
std::vector<api::Security const *> get_securities();
@@ -33,7 +59,6 @@ class SecurityCache {
// returns true if the cache was updated
bool add_live_security(const SecurityPtr &security);
const Security *add_db_security(const SecurityPtr &security);
void flush_to_disk();
};
+13 -14
View File
@@ -53,12 +53,10 @@ std::unique_ptr<SQLite::Database> open_db() {
return db;
}
datetime::Date sec_expiration_cut_off() {
boost::gregorian::date_duration max_exp_age(
config::get<SecurityExpirationCutoff>().max_days_after_expiration());
auto pt = (datetime::now() - max_exp_age);
return datetime::Date(pt.date().year(), pt.date().month(), pt.date().day());
core::TimeStamp sec_expiration_cut_off() {
return core::TimeStamp::now().date() -
core::TimeSpan::from_days(config::get<SecurityExpirationCutoff>()
.max_days_after_expiration());
}
size_t load_securities(
@@ -72,7 +70,8 @@ size_t load_securities(
SQLite::Statement select_stmt(
*db, "select key,symbol,venue,type,security_blob from securities where "
"expiration > :exp");
select_stmt.bind(":exp", sec_expiration_cut_off().to_short());
select_stmt.bind(
":exp", (long long)(sec_expiration_cut_off().to_date_int()));
Security security;
while (select_stmt.tryExecuteStep() == SQLITE_ROW) {
@@ -156,14 +155,14 @@ Result insert_or_replace(const std::vector<SecurityPtr> &securities) {
insert.bind(3, psecurity->exchange());
insert.bind(4, securitytype_of(*psecurity));
insert.bind(5, buffer);
auto greg_date = securities::expiration(*psecurity);
if (greg_date == boost::gregorian::date()) {
insert.bind(6, datetime::Date(9999, 1, 1).to_short());
auto exp = securities::expiration(*psecurity);
if (!exp) {
const long long exp_int =
std::numeric_limits<std::int64_t>::max();
insert.bind(6, exp_int);
} else {
ushort year = greg_date.year();
ushort month = greg_date.month();
ushort day = greg_date.day();
insert.bind(6, datetime::Date(year, month, day).to_short());
const long long exp_int = exp->to_date_int();
insert.bind(6, exp_int);
}
insert.exec();
insert.reset();
-2
View File
@@ -22,8 +22,6 @@ class Security;
namespace md {
namespace db {
struct SecurityDB;
// Loads securities matching the given filter for producers to pre-populate
// security lists note: possible to use this to optimize initial memory layout?
// sort by expiration?
+3 -1
View File
@@ -55,7 +55,9 @@ static persistence::StoreRow get_row(
static persistence::StoreRow get_row(
const api::Security &s, const std::string &prefix = "") {
return get_row(s.exchange(), bts::securities::security_type(s), prefix);
return get_row(
s.exchange(), bts::securities::security_type(s),
securities::descriptor(s), prefix);
}
struct SecurityIdRequest {
+20
View File
@@ -17,10 +17,30 @@ const api::Security *SecurityOracle::get_security(
return _cache.try_get_security(key);
}
const api::Security *SecurityOracle::get_security(
api::Venue venue, const std::string &name) const {
return _cache.get_security(venue, name);
}
const api::Security *SecurityOracle::get_security(
api::Venue venue, api::SecurityType type, const std::string &name) const {
return _cache.get_security(venue, type, name);
}
std::vector<api::Security const *> SecurityOracle::get_securities() const {
return _cache.get_securities();
}
std::vector<api::Security const *> SecurityOracle::get_securities(
api::Venue venue, api::SecurityType type, const std::string &name) const {
return _cache.get_securities(venue, type, name);
}
std::vector<api::Security const *> SecurityOracle::get_securities(
std::function<bool(const api::Security &)> include) const {
return _cache.get_securities(include);
}
void SecurityOracle::troubleshoot_log_all_securities() const {
size_t sec_no = 0;
for (const auto s : get_securities()) {
+10
View File
@@ -20,6 +20,16 @@ class SecurityOracle {
~SecurityOracle();
const api::Security *get_security(SecurityKey key) const;
const api::Security *get_security(ExchangeSecurityKey key) const;
const api::Security *get_security(api::Venue venue, const std::string &name) const;
const api::Security *get_security(api::Venue venue, api::SecurityType type, const std::string &name) const;
/**
* Finds all securities where @p include returns true.
*/
std::vector<const Security *> get_securities(
std::function<bool(const Security &)> include) const;
std::vector<api::Security const *> get_securities(
api::Venue venue, api::SecurityType type, const std::string &name) const;
std::vector<api::Security const *> get_securities() const;
// this iterates through all securities and simply logs them. Use it
// ONLY for troubleshooting
+11
View File
@@ -129,6 +129,11 @@ void convert_security(const FixedSecurity &source, Security *presult) {
dest.mutable_binance()->set_security_desc(source.security_desc());
dest.mutable_binance()->set_exchange_id(source.numeric_id);
break;
case Venue::VENUE_BINANCE_COIN_FUTURES:
dest.mutable_binance_coin_futures()->set_security_desc(
source.security_desc());
dest.mutable_binance_coin_futures()->set_exchange_id(source.numeric_id);
break;
case Venue::VENUE_BINANCE_US:
dest.mutable_binance_us()->set_security_desc(source.security_desc());
dest.mutable_binance_us()->set_exchange_id(source.numeric_id);
@@ -248,6 +253,8 @@ uint64_t exchange_id(const Security &security) {
return security.ftx().exchange_id();
case Security::ExchangeDataCase::kBinance:
return security.binance().exchange_id();
case Security::ExchangeDataCase::kBinanceCoinFutures:
return security.binance_coin_futures().exchange_id();
case Security::ExchangeDataCase::kBinanceUs:
return security.binance_us().exchange_id();
case Security::ExchangeDataCase::kCoinbase:
@@ -324,6 +331,10 @@ void convert_security(
dest.numeric_id = src.binance().exchange_id();
dest.security_desc(src.binance().security_desc());
break;
case Security::ExchangeDataCase::kBinanceCoinFutures:
dest.numeric_id = src.binance_coin_futures().exchange_id();
dest.security_desc(src.binance_coin_futures().security_desc());
break;
case Security::ExchangeDataCase::kBinanceUs:
dest.numeric_id = src.binance_us().exchange_id();
dest.security_desc(src.binance_us().security_desc());
+4
View File
@@ -59,6 +59,10 @@ constexpr bool SymbolLoader::venue_has_synths(Venue venue) {
case VENUE(SIMICE):
case VENUE(ICE):
case VENUE(GLBX):
case VENUE(BINANCE):
case VENUE(BINANCE_USDS_FUTURES):
case VENUE(DERIBIT):
case VENUE(OKX):
return true;
default:
return false;
+3
View File
@@ -133,6 +133,9 @@ std::string to_str(const std::vector<T> &vec) {
}
const vector<CMEChannel> parse_channels(const vector<UserSymbol> &symbols) {
LOG(md, Info, "parse_channels symbols: {}", FMT_JOIN(symbols, ", "));
if (symbols.empty()) {
return {};
}
await_cme_config();
vector<CMEChannel> result;
bool skip_options = config::get<PublisherOptions>().skip_options();
+3
View File
@@ -179,6 +179,9 @@ struct MDConsumerGroup {
case api::VENUE_BINANCE:
this->mic = "BINANCE";
break;
case api::VENUE_BINANCE_COIN_FUTURES:
this->mic = "BINANCE_COIN_FUTURES";
break;
case api::VENUE_BINANCE_US:
this->mic = "BINANCE_US";
break;
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include "lib/domain_model/Security.pb.h"
#include "lib/utils/bts_assert.h"
#include <optional>
namespace bts::md {
template <class T>
class SecurityTypeIndex {
std::optional<T> spot;
std::optional<T> future;
std::optional<T> option;
std::optional<T> spread;
std::optional<T> synthetic;
public:
std::optional<T> &get(api::SecurityType type) { return get_(*this, type); }
const std::optional<T> &get(api::SecurityType type) const {
return get_(*this, type);
}
template <class... Args>
T &get_or_add(api::SecurityType type, Args... args) {
std::optional<T> &val = get_(*this, type);
if (!val) {
val.emplace(args...);
}
return *val;
}
private:
template <typename U, class... Args>
static auto &get_(U &self, api::SecurityType type) {
switch (type) {
case api::SECURITYTYPE_SPOT:
return self.spot;
case api::SECURITYTYPE_OPTION:
return self.option;
case api::SECURITYTYPE_FUTURE:
return self.future;
case api::SECURITYTYPE_SPREAD:
return self.spread;
case api::SECURITYTYPE_SYNTHETIC:
return self.synthetic;
case api::SECURITYTYPE_UNSET:
case api::SecurityType_INT_MIN_SENTINEL_DO_NOT_USE_:
case api::SecurityType_INT_MAX_SENTINEL_DO_NOT_USE_:
default:
bts_assert_msgf(
false, "SecurityType '{}' not present in VenueIndex",
api::SecurityType_Name(type));
static std::optional<T> default_rv;
return default_rv;
}
}
};
} // namespace bts::md
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include "lib/domain_model/Security.pb.h"
#include "lib/utils/bts_assert.h"
#include <optional>
namespace bts::md {
template <class T>
class VenueIndex {
std::optional<T> binance;
std::optional<T> binance_coin_futures;
std::optional<T> binance_us;
std::optional<T> binance_usds_futures;
std::optional<T> bts;
std::optional<T> okx;
std::optional<T> deribit;
std::optional<T> cboe;
std::optional<T> cboecfe;
std::optional<T> glbx;
std::optional<T> ice;
std::optional<T> taifex;
std::optional<T> coinbase;
public:
std::optional<T> &get(api::Venue venue) { return get_(*this, venue); }
const std::optional<T> &get(api::Venue venue) const {
return get_(*this, venue);
}
template <class... Args>
T &get_or_add(api::Venue venue, Args... args) {
std::optional<T> &val = get_(*this, venue);
if (!val) {
val.emplace(args...);
}
return *val;
}
private:
template <typename U, class... Args>
static auto &get_(U &self, api::Venue venue) {
switch (venue) {
case api::VENUE_CBOE:
return self.cboe;
case api::VENUE_CBOECFE:
return self.cboecfe;
case api::VENUE_GLBX:
return self.glbx;
case api::VENUE_ICE:
return self.ice;
case api::VENUE_DERIBIT:
return self.deribit;
case api::VENUE_BINANCE:
return self.binance;
case api::VENUE_BINANCE_COIN_FUTURES:
return self.binance_coin_futures;
case api::VENUE_BINANCE_USDS_FUTURES:
return self.binance_usds_futures;
case api::VENUE_BINANCE_US:
return self.binance_us;
case api::VENUE_COINBASE:
return self.coinbase;
case api::VENUE_OKX:
return self.okx;
case api::VENUE_TAIFEX:
return self.taifex;
case api::VENUE_BTS:
return self.bts;
case api::VENUE_FTX:
case api::VENUE_SIM:
case api::VENUE_SIMICE:
case api::VENUE_UNSET:
case api::Venue_INT_MIN_SENTINEL_DO_NOT_USE_:
case api::Venue_INT_MAX_SENTINEL_DO_NOT_USE_:
default:
bts_assert_msgf(
false, "Venue '{}' not present in VenueIndex",
api::Venue_Name(venue));
static std::optional<T> default_rv;
return default_rv;
}
}
};
} // namespace bts::md
+26 -6
View File
@@ -13,6 +13,7 @@
#include "lib/logging/logging.h"
#include "lib/md/MDClient.h"
#include "lib/md/MDConsumer.h"
#include "lib/md/UserSymbol.h"
#include "lib/mercury/actors/Timer.h"
#include "lib/mercury/executor/Executor.h"
#include "lib/metrics/metrics.h"
@@ -24,16 +25,23 @@
#include "lib/orderman/actors/SecuritySrc.h"
#include "lib/orderman/actors/ServiceBridge.h"
#include "lib/orderman/actors/order_manager/OrderManager.h"
#include "lib/orderman/actors/order_manager/OrderServicesData.h"
#include "lib/orderman/actors/order_manager/order_gateway_definition.h"
#include "lib/orderman/events/ExchangePositionEvent.h"
#include "lib/orderman/events/OrderOpResultEvent.h"
#include "lib/orderman/gateways/execution_gateway_provider.h"
#include "lib/orderman/gateways/i_execution_gateway_provider.h"
#include "lib/orderman/order_factory/DynamicOrderRegistry.h"
#include "lib/orderman/orderman_utils/ExchangeOpenOrders.h"
#include "lib/orderman/orders/Order.h"
#include "lib/os/spark_data_directory.h"
#include "lib/plugin/SparkServices.h"
#include "lib/plugin/plugin.h"
#include "lib/utils/PBUtils.h"
#include "lib/utils/bts_chrono.h"
#include "lib/utils/thread_manager.h"
#include "lib/utils/type_name.h"
#include "lib_binance_execution/binance_execution/execution_gateway_provider.h"
#include <bits/stdint-uintn.h>
#include <boost/algorithm/string/predicate.hpp>
@@ -43,6 +51,8 @@
namespace bts {
namespace orderman {
using std::unique_ptr;
using std::vector;
struct ActorThread {
ActorThread(
@@ -126,10 +136,13 @@ struct OrderServiceServer::PImpl {
const Exchange_open_ordersStatefulPublisher &open_orders_publisher,
md::MDClient &mdc,
DynamicOrderRegistry const &d,
std::vector<std::unique_ptr<IExecutionGatewayProvider>>
gateway_providers,
metrics::MetricsContainer &stats,
trace::PerfLog &log)
: context(ctx), service_bridge(), service_ready(false), known_orders(),
known_fills(), publish_orders(), publish_fills(),
: context(ctx), service_bridge(),
gateway_providers(std::move(gateway_providers)), service_ready(false),
known_orders(), known_fills(), publish_orders(), publish_fills(),
exchange_pos_publisher(exchange_pos_publisher),
open_orders_publisher(open_orders_publisher),
admin_service(ctx.locate<api::admin::AdminService>()),
@@ -137,11 +150,15 @@ struct OrderServiceServer::PImpl {
margin_service(ctx.locate<api::position::MarginService>()),
md_consumer(mdc.current_consumer()), stats(stats),
dyn_order_registry(d), log(log), in_flight_order_ops(),
current_req_op_id(0) {}
current_req_op_id(0) {
LOG(OrderMan, Info, "OrderServiceServer ctr. n_gateway_providers: {}",
this->gateway_providers.size());
}
bts::api::ServiceContext &context;
ServiceBridge *service_bridge;
std::vector<std::unique_ptr<IExecutionGatewayProvider>> gateway_providers;
std::vector<std::unique_ptr<ActorThread>> actor_threads;
std::atomic<bool> service_ready;
@@ -169,6 +186,7 @@ OrderServiceServer::OrderServiceServer(
bts::api::ServiceContext &ctx,
md::MDClient &mdc,
DynamicOrderRegistry const &d,
std::vector<std::unique_ptr<IExecutionGatewayProvider>> gateway_providers,
metrics::MetricsContainer &stats,
trace::PerfLog &log,
md::TradeSummarySignalDispatcher &tssd)
@@ -179,6 +197,7 @@ OrderServiceServer::OrderServiceServer(
make_exchange_open_orders_stateful_publisher(),
mdc,
d,
std::move(gateway_providers),
stats,
log)) {
LOG(md, Info, "OrderServiceServer cons t_id: {} t_name: {}",
@@ -209,7 +228,10 @@ OrderServiceServer::OrderServiceServer(
bts_assert_msg(
thread, "Could not find a configured thread for OrderManager. See "
"the OrdermanCfg key");
thread->executor.spawn<OrderManager>(d, me->log, mdc.oracle());
{
thread->executor.spawn<OrderManager>(
d, me->log, mdc.oracle(), std::move(me->gateway_providers));
}
// Timer
thread = find_thread_for_actor("Timer", me->actor_threads);
@@ -404,7 +426,6 @@ void OrderServiceServer::create_order(
void OrderServiceServer::cancel_order(
const api::orderman::Cancel &cancel,
const api::Response<api::orderman::Response> &r) {
if (!me->service_ready) {
api::orderman::Response response;
response.set_order_key(cancel.order_key());
@@ -435,7 +456,6 @@ void OrderServiceServer::update_order(
void OrderServiceServer::detach_order(
const api::orderman::Detach &detach,
const api::Response<api::orderman::Response> &r) {
if (!me->service_ready) {
api::orderman::Response response;
response.set_order_key(detach.order_key());
+3
View File
@@ -5,6 +5,7 @@
#include "lib/md/MDClient.h"
#include "lib/mercury/actors/IPollableActor.h"
#include "lib/orderman/events/ExchangeOpenOrdersEvent.h"
#include "lib/orderman/gateways/i_execution_gateway_provider.h"
#include <memory>
namespace bts {
@@ -28,6 +29,8 @@ class OrderServiceServer : public api::orderman::OrderServiceBase {
bts::api::ServiceContext &context,
md::MDClient &,
DynamicOrderRegistry const &,
std::vector<std::unique_ptr<IExecutionGatewayProvider>>
gateway_providers,
metrics::MetricsContainer &,
trace::PerfLog &,
md::TradeSummarySignalDispatcher &);
+1 -1
View File
@@ -190,7 +190,7 @@ SessionServices &OrderServices::get_session_services(
for (auto &s : _sessions)
if (s.session_name() == for_session)
return s;
bts_assert(false);
bts_assert_msgf(false, "No session named '{}'", for_session);
}
void OrderServices::unsubscribe_from_order_creation_reply(
@@ -1,10 +1,15 @@
#include "OrderManager.h"
#include "lib/config/protos/jetfix.pb.h"
#include "lib/core/FMT.h"
#include "lib/jetfix/common/OutFIX.h"
#include "lib/logging/logging.h"
#include "lib/md/SecurityOracle.h"
#include "lib/mercury/events/TimerEvent.h"
#include "lib/orderman/OrderServices.h"
#include "lib/orderman/actors/order_manager/OrderServicesData.h"
#include "lib/orderman/actors/order_manager/order_gateway_definition.h"
#include "lib/orderman/actors/order_manager/session_services_data.h"
#include "lib/orderman/events/AccountPnLUpdateEvent.h"
#include "lib/orderman/events/ExcessLiquidityUpdateEvent.h"
#include "lib/orderman/events/ExchangeOpenOrdersEvent.h"
@@ -17,10 +22,13 @@
#include "lib/orderman/events/StoredOrderEvent.h"
#include "lib/orderman/events/TheoEvent.h"
#include "lib/orderman/gateways/IExecutionGateway.h"
#include "lib/orderman/gateways/i_execution_gateway_provider.h"
#include "lib/orderman/order_factory/OrderFactory.h"
#include "lib/orderman/orderman_utils/ExchangePosition.h"
#include "lib/orderman/orders/Order.h"
#include "lib/orderman/securities/ISecurityDB.h"
#include "lib/os/spark_data_directory.h"
#include "lib/tracing/PerfLog.h"
#include "lib/utils/bts_assert.h"
#include "lib/utils/inplace_function.h"
#include "lib/utils/thread_manager.h"
@@ -42,13 +50,27 @@ enum class OrderManager::State {
READY
};
static std::vector<OrderGatewayDefinition> create_gateways(
std::vector<std::unique_ptr<IExecutionGatewayProvider>> providers) {
std::vector<OrderGatewayDefinition> gws;
for (auto &p : providers) {
auto defs = p->create();
for (auto &def : defs) {
gws.push_back(std::move(def));
}
}
return gws;
}
struct OrderManager::PImpl {
PImpl(
OrderManager &owner,
DynamicOrderRegistry const &b,
trace::PerfLog &l,
md::SecurityOracle o)
: state(State::WAIT_ADMIN), services_data(owner, b, l, o) {}
md::SecurityOracle o,
std::vector<OrderGatewayDefinition> gateways)
: state(State::WAIT_ADMIN),
services_data(owner, b, l, o, std::move(gateways)) {}
State state;
std::unordered_map<OrderKey, Order *> orders;
@@ -61,8 +83,12 @@ struct OrderManager::PImpl {
};
OrderManager::OrderManager(
DynamicOrderRegistry const &b, trace::PerfLog &l, md::SecurityOracle o)
: me(new PImpl(*this, b, l, o)) {
DynamicOrderRegistry const &b,
trace::PerfLog &l,
md::SecurityOracle o,
std::vector<std::unique_ptr<IExecutionGatewayProvider>> gw_providers)
: me(std::make_unique<PImpl>(
*this, b, l, o, create_gateways(std::move(gw_providers)))) {
LOG(OrderMan, Info, "OrderManager cons t_id: {} t_name: {}",
os::get_thread_id(), os::get_thread_name());
auto id_gen = &me->services_data.id_gen;
@@ -437,6 +463,14 @@ std::vector<jetfix::OutFIXPtr> &OrderManager::free_buffers(
void OrderManager::init_order_services() {
auto &log = me->services_data.log;
const std::string session_names = fmt::to_string(fmt::join(
me->services_data.sessions |
ranges::views::transform(
[](const std::unique_ptr<SessionServicesData> &s) {
return s->session;
}),
", "));
LOG(OrderMan, Info, "init_order_services. sessions: {}", session_names);
for (auto &s : me->services_data.sessions) {
auto gw_name = GatewayName(s->session);
Loaded 100 of 267 files, more files were not shown because too many files have changed in this diff. Show more