Compare commits

...
Author SHA1 Message Date
Taha Afzal 66ad51280f Reversehedging fixed 2025-04-08 22:13:33 +00:00
arham 029edd8a86 Format: ran clang format on file 2025-04-07 15:39:57 +00:00
arham dfdcbab2a0 Add: config changes to make server controller work and set up hk zen server 2025-04-07 15:33:26 +00:00
Taha Afzal 6d7c0702f0 Fixing build: 2025-04-05 23:38:05 +00:00
Taha Afzal 86eaddea93 Fixing a bug, where the spreader was isidentifying the quoting leg 2025-04-05 15:38:30 +00:00
John Colvin 57f8f3ff58 ui: Add Sniper and Reverse Hedge controls 2025-04-04 00:19:38 -04:00
John Colvin 775df2850d Add Sniper and Reverse Hedge params to spreader proto 2025-04-04 00:19:38 -04:00
Taha Afzal cb2b15f10b Adding IOC to SimExecution 2025-04-03 23:24:30 +00:00
Taha Afzal 60362a4c95 Adding reverse-hedge to spreader 2025-04-03 23:24:30 +00:00
John Colvin c6d7a8f38c ui (pnl): Use dark theme fg in "Selected" and "Total" labels
Previously, it was black and hard to read since the background is dark.
2025-03-26 14:46:44 -04:00
John Colvin 7931c62b46 ui (pnl): Hide "Override Cpp" menu item
The overrides aren't respected in (new) pnl calcs.
2025-03-26 14:35:23 -04:00
John Colvin 58518451c8 ui (pnl): Show realized/unrealized pnl in row group totals 2025-03-26 14:31:04 -04:00
John Colvin e3bad78aef fix (pnl ui): Get cash per point from security
Previously, it was hardcoded to 1.
2025-03-24 16:54:37 -04:00
John Colvin 8e9ab8f2eb fix: Don't apply cme cpp overrides to non-cme securities
Bugs (fixed):
  * The cash per point for many securities is wrong. For example, it is
    5 for all BTC securities (which is wrong).

    Cause: cpp override config that should be specific to cme is applied
    to ALL securities.
2025-03-24 20:20:53 +00:00
John Colvin 8512f414d6 fix: Don't duplicate animations when migrating pnl window settings 2025-03-24 14:53:09 -04:00
John Colvin 513fc353cf ui (pnl): Increase default width for a few columns
I noticed some data was truncated when connected to Alphaworx prod
server.
2025-03-24 14:39:40 -04:00
John Colvin 4a04beafd2 cleanup: Consoildate order extension methods 2025-03-24 11:19:51 -04:00
John Colvin 79a3251f26 fix: Make security comparison deterministic again 2025-03-24 11:19:51 -04:00
John Colvin 8785e0fecd fix: Bootstrap all observers w/ snapshot on first update
Previously, the Subscribe impl only bootstrapped the first observer
(i.e., only in first call to Subscribe).

Bugs (fixed):
  * The ladder crashes if there are working orders when the
    ticks-per-row setting is changed.

    Cause: The ladder viewmodel resubscribes to order updates each time
    the ticks per row setting changes (as this determines the size of
    the price buckets into which orders are grouped). The IObservable
    "Bootstrap" extension fn doesn't properly clean up state leftover
    from the previous subcsription when this happens.
2025-03-24 11:17:04 -04:00
John Colvin 04251c96b6 ui (pnl): Compute pnl as sum of realized + unrealized
This addresses a subtle inconsistency in the pnl window.

The old "start_pnl" and "day_pnl" values (from IPNLUnit) are computed
with respect to the mid market for both long and short positions, while
the new "unrealized" pnl values are computed with respect to the side of
the market needed to close the position (i.e., bid market for long pos,
ask for short). Consequently, the combined pnl columns weren't equal to
the sum of realized and unrealized pnl.
2025-03-24 10:13:24 -04:00
John Colvin c2d400af1a fix (pnl ui): Don't overwrite menu display settings w/ defaults 2025-03-24 10:13:24 -04:00
John Colvin 133cc16628 ui: Add menu item to open mview from pnl
Populated with the same securities as the pnl window
2025-03-24 10:13:24 -04:00
John Colvin 16f7aede93 fix: Add case for Synthetic sec type in proto conversions 2025-03-24 10:13:24 -04:00
John Colvin 2b9488faff ui: Make pnl default settings pretty
* Make profit (green) and loss (red) easier to read against dark bg
* Hide 'SOD' and 'Day' columns (just show totals)
2025-03-24 10:13:24 -04:00
John Colvin 51b6d592d3 fix (pnl ui): Negate short avg cost when translating to avg price 2025-03-23 15:17:41 -04:00
John Colvin 1eb4ceba0a ui: Add columns for long/short avg pnl
When looking at avg cost, you often really want to know the difference
between it and the current market. These columns provide that information
so you don't need to do the arithmetic in your head.
2025-03-23 15:17:41 -04:00
John Colvin dbf5cadb44 ui: Add columns for realized/unrealized pnl 2025-03-23 15:17:41 -04:00
John Colvin 90f1bb0851 refactor: Move PNL data types to spark_api csproj 2025-03-23 13:08:46 -04:00
John Colvin 547d83e1b8 cleanup: Remove unused pnl enum WaterfallPriceFlags 2025-03-23 13:08:41 -04:00
John Colvin e888149c41 Add order msg throttle to sparkd config profiles 2025-03-20 17:29:43 +00:00
John Colvin b944c3980f Show 6 decimals in spreader order log
So we can better track those rascally iceberg orders w/ small fractional
quantities
2025-03-20 14:30:35 +00:00
Taha Afzal c2a8266106 merging 2025-03-20 01:23:05 +00:00
Mohammad Arham 4277ba61b9 Fix: Okx execution only sends 2025-03-19 13:26:57 -04:00
Mohammad Arham 35cfe9533b Add filter for okx er we that were caused by orders from outside the system 2025-03-19 13:26:39 -04:00
John Colvin d49efef93d Increase ram to 8gb in alphaworx_prod config
The previous value of 4gb wasn't enough, as the container would
occasionally crash with a SIGKILL interrupt.
2025-03-19 15:26:14 +00:00
John Colvin 502ed13456 fix: Round IcebergAlgo remaining quantity
Bugs (fixed, 2nd attempt):
  * Iceberg orders w/ fractional quantities can get into a state where
    they send an infinite stream of orders w/ 0 qty
2025-03-19 14:43:24 +00:00
John Colvin 716c4dd674 tool: Add script to dump sparkd logs to sqlite db 2025-03-19 14:41:23 +00:00
John Colvin 4a2b189a07 diag: Format Price and Quantity w/ up to 6 decimal places 2025-03-17 13:00:15 +00:00
John Colvin a42edaf9fa diag: Trivial fix for log msgs in SpreaderOrder
* Format qty with fractional part
* Remove redundant log
2025-03-17 13:00:15 +00:00
John Colvin cbc142ab15 fix: Account for rounding error in IcebergAlgo fills
Bugs (fixed):
  * Iceberg orders w/ fractional quantities can get into a state where
    they send an infinite stream of orders w/ 0 qty

    Note: I also changed the number of decimal places used when rounding
    order quantities from 6 to 7 (the level of precision guarenteed by
    IEEE 754).
2025-03-17 13:00:15 +00:00
John Colvin 3980f2d030 Add order msg throttle to alphaworx_prod config 2025-03-17 13:00:15 +00:00
John Colvin 00f6d269c9 fix: Set binance routing record "operator_id" to username (not account)
Must have been a typo
2025-03-17 00:51:54 +00:00
John Colvin 10b2d38de8 tool: Add cmd to tail sparkd log file using docker compose 2025-03-17 00:51:54 +00:00
John Colvin 6d4d8f6ac1 fix: Disable ccapi order book cross check for Binance md
When enabled, the checks fail regularly (usually more than once per
hour). Sometimes many of them happen in rapid succession before sparkd
crashes. I'm disabling them in the hopes that the crosses are benign,
and this will stop the crashes.
2025-03-17 00:51:54 +00:00
John Colvin 5db89e1e8d diag: Tone down noisy order management logs
* EMNativeOrder
* OrderRouter - only log routing record result if there are errors
* OrderServiceServer
* OrderStore - also avoid calling pb::to_string twice per change (it's
  expensive)
* ServiceBridge
2025-03-17 00:51:47 +00:00
John Colvin 379a46afa2 Add order msg throttle to sim_dev config 2025-03-15 20:07:14 +00:00
John Colvin d9eff37184 fix: Don't let rounding error accumulate in mview order summarizer
Bugs (fixed):
  * Sometimes mview shows that a security has working orders when it
    doesn't. It appears to be rounding-related as the price and qty of
    the phony working orders is NaN and 0, respectively.

  Cause: Rounding error accumulates in the order summarizer. If it gets
  bad enough, the working quantity (which is computed incrementally as
  orders transition to/from a working state) may still be non-zero after
  all orders become non-working.
2025-03-15 15:21:56 -04:00
John Colvin 3d055de139 cleanup: Rename _tick_factors -> _qty_tick in mview order summarizer
It is specifically for quantity (not price).
2025-03-15 15:21:56 -04:00
John Colvin 648be7dd97 improvement: Don't resort orderbook on every order update
This causes performace issues when the orderbook is sufficiently large
(noticeable when around 20k orders).
2025-03-15 15:21:56 -04:00
John Colvin 2cac9affe4 fix: Use consistent price comp in ladder order stream
Bugs (fixed):
  * The ladder occasionally crashes when orders are changing.

    Cause: Subtle differences between the ordinal and equality comparers
    made it possible for 2 prices to be considered equal by the former,
    and not equal by the latter. Since the ladder indexes orders by
    price, a "Key not found" exception could be thrown if the equality
    comparer detected a price change that the ordinal comparer did not.
2025-03-15 15:21:55 -04:00
John Colvin 30ff3ba707 Add some null checks in LadderGridHost 2025-03-15 15:21:55 -04:00
John Colvin 9d3afd68aa ui: Format Perp and Spot exps as "Perp" and "None" 2025-03-15 15:21:55 -04:00
John Colvin 8c3de1789b fix: Apply leg price mults when computing synth book
This is necessary to support synths with BTC-USDC-SWAP legs at Okx
(because 1 "contract" == 0.01 BTC).

Bugs (fixed):
  * Price multiplier has no effect in synthetic book
2025-03-14 19:32:04 +00:00
John Colvin b251e1d47e build: Generate semantic version header from git
Dump it to console and log file when sparkd runs
2025-03-14 16:00:06 +00:00
arham 6ccd48a8ba Fix, 1e-5 orders working okx 2025-03-13 17:49:14 +00:00
John Colvin 41d84f7f73 Change externals submodule url from ssh to https 2025-03-13 17:08:04 +00:00
John Colvin 795062864d fix: Fix typo in permission filter comparison (change == to !=)
Bugs (fixed):
  * The orderbook and ladder blink/flicker when orders change if logged
    in as non-bts user.

    Cause: The permission filter acted as if the predicate itself
    changed everytime an order changed. As a result, it would fire one
    update to cleared all values, then fire another to add items that
    pass the "new" predicate.
2025-03-13 10:39:33 -04:00
John Colvin 1ff72e9444 fix (ui): Show fractional order qty in activity log
Also fix formatting - change "price @ qty" to "qty @ price"
2025-03-13 10:39:33 -04:00
John Colvin e13b5066a3 fix: Preserve fractional qty when modifying existing order in ticket 2025-03-13 10:39:33 -04:00
Mohammad Arham 5ba7901258 Fix: undo special case for btc swap and add ccp for swaps 2025-03-12 20:47:44 +00:00
Mohammad Arham bc6eb59110 Fix: Change cttype from optional to string 2025-03-12 20:45:27 +00:00
John Colvin 5ae5a5dcbf fix: Add switch case for PositionSide::BOTH in binance futs gw
In this case, the position is signed (unlike LONG and SHORT).
2025-03-12 14:55:56 +00:00
John Colvin 7966bcce77 fix: Change trade id from uint32 to int64 2025-03-12 04:27:54 +00:00
Mohammad Arham ff86b10895 Fix: temp fix to move okx btc swap to config for min tick 2025-03-11 20:35:24 +00:00
John Colvin ea3c0717d8 fix: Remove erroneous assert from binance usds futs gateway 2025-03-11 19:48:11 +00:00
John Colvin d0abca3fd9 Fix symbols and account alphaworx_prod config 2025-03-11 19:47:34 +00:00
John Colvin 7895fe7357 Remove taifex and glbx from NotSimExecution in conf profiles 2025-03-11 15:24:08 +00:00
Mohammad Arham 6f5f4f5cc2 Fix: okx excution working now 2025-03-11 14:25:42 +00:00
John Colvin 066a0ba693 ui: Use dark theme foreground in mini orderbook, fills popups
It was black in a couple places before, making it hard to read.
2025-03-10 14:47:58 -04:00
John Colvin c2b67b4813 fix: Check for null in orderbook root algo type filter 2025-03-10 14:47:58 -04:00
John Colvin ecad456a2d fix: Apply exp filter to popup grids in pnl window 2025-03-10 14:47:58 -04:00
Taha Afzal 2cb3cfdcdd Spreader's orders were hardcoded to be DAY, now it gets it from the parent 2025-03-10 17:22:08 +00:00
Taha Afzal 0ebc8e5b4b Orders in DB are deleted on server bounce 2025-03-10 15:42:12 +00:00
John Colvin 2072105c0f cleanup: Add using aliases to make code more concise 2025-03-10 14:53:07 +00:00
John Colvin d663c86aa0 improvement: Only load synth metadata from disk once in md dumper
Instead of every time a synth is dumped (i.e., do it lazily, outside the
loop body)
2025-03-10 14:53:07 +00:00
John Colvin 4a2dfa46a1 improvement: Print delimiter line when dumping secs to console 2025-03-10 14:53:07 +00:00
John Colvin 6e35db6a24 improvement: Apply filter to shared mem secs in md dumper
Previously, filter only applied to db securities (i.e., when --db flag
was used)
2025-03-10 14:52:39 +00:00
John Colvin b45dc3bc2d fix: Preserve create time when order is updated
Bugs (fixed):
  * Order create time mirrors last update time (it should never change).
2025-03-10 05:24:49 +00:00
John Colvin a1912907b8 Add brief readmes for sparkd config profiles 2025-03-10 05:24:49 +00:00
John Colvin b1b3dcdeb8 Add sim_dev config profile for sparkd 2025-03-10 05:24:49 +00:00
John Colvin 03dc8ef767 Add boto3 to py venv, so we can retrieve secrets from aws 2025-03-10 05:24:49 +00:00
John Colvin 975bf56c2a fix: Don't spam binance w/ listen key requests
Sleep for a few seconds before trying again on failure.
2025-03-10 05:24:49 +00:00
John Colvin 3f58c2ce84 Add script to install aws cli
The aws cli can be used to configure credentials in the shell in order
to retrieve secrets (e.g., production api keys for alphaworx, etc.).
2025-03-10 05:24:49 +00:00
John Colvin 96f28acf91 Add dev config profile for sparkd 2025-03-10 05:24:49 +00:00
John Colvin c2c2658e21 Add alphaworx_prod config profile for sparkd 2025-03-10 05:24:49 +00:00
John Colvin d6b8e359cd Expand environment vars in binance gw config 2025-03-10 03:05:59 +00:00
John Colvin 84a0850d95 Tweak binance_dev config profile
* Factor out repeated symbol list into SymbolLists entry
* Change "binance_dev" account to "BINANCE_DEV" in exe gw json
* Adjust log level
* Remove unused config:
  - ExchangeSubAccounts
  - JetFIX_Connection.max_order_msgs_per_window
2025-03-10 03:05:59 +00:00
John Colvin 4effdc4160 diag: Log when http buffers are resized in binance exe gws 2025-03-10 03:05:59 +00:00
John Colvin b9fe39f6e3 refactor: Make MDRecorder join the run thread in destructor
This wasn't broken, but detached threads bother me.
2025-03-10 03:05:59 +00:00
John Colvin b555850015 build: Package & deploy sparkd with docker compose 2025-03-10 03:05:59 +00:00
John Colvin 23794fa30d devcontainer: Add docker in docker
Also remove default port bindings in devcontainer (so the "docker in
docker" sparkd can use them). It's easy enough to manually bind ports
in vs code.
2025-03-08 05:20:08 +00:00
John Colvin 90b5a309a3 cleanup: Make a couple fns const
Probably a typo, since these fns don't mutate anything.
2025-03-08 05:20:08 +00:00
John Colvin 07beda3fc6 fix: Support fractional show qty in iceberg orders 2025-03-07 20:42:24 +00:00
183 changed files with 6937 additions and 2906 deletions

No files matched your search

+6
View File
@@ -90,6 +90,12 @@ RUN apt-get update && apt-get install -y libpcap0.8-dev
# Databento (deps)
RUN apt-get update && apt-get -y install libzstd-dev
# Docker in Docker
RUN apt-get update && apt-get install -y \
docker.io \
docker-compose \
docker-compose-v2
# Set up default executables (update-alternatives)
RUN \
update-alternatives --install /usr/bin/clang clang /usr/bin/clang-$clang_major 100; \
+4 -11
View File
@@ -12,17 +12,10 @@
// example, if 8000-8003 is taken, go with 9000-9003, then 10000-10003, etc.
"runArgs": [
"--shm-size",
"8gb",
"-p",
"8000:8000",
"-p",
"8001:8001",
"-p",
"8002:8002",
"-p",
"8003:8003",
"-p",
"12345:12345"
"8gb"
],
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
],
"customizations": {
"vscode": {
+48
View File
@@ -0,0 +1,48 @@
# This file defines the global default configuration for docker deployments.
# It is tracked in source control. For temporary modifications, please create
# a separate file called ".env.local" (it's in .gitignore, so won't be tracked).
# If a variable is defined in both files, the value in .env.local will be used.
#
# About the deployment procedure:
# We use Docker Compose to build images and launch containers for various services
# that make up our software suite:
# - sparkd
# - TODO: pubsub
# - TODO: md-publisher
# - TODO: webapp (DSQA on the web)
# - TODO: api-server (websocket based)
# From inside a devcontainer, you can launch all services in separate containers
# on your machine by running:
#
# tools/deploy.sh
#
# This script builds release versions for each service, packaged (generally) in
# self-contained directories. Next, it runs a few "docker compose" commands to
# build docker images loaded with the self-contained binaries from the previous
# step, and start containers with those images to run each service. To view the
# commands that deploy.sh runs internally (without actually running them), run:
#
# tools/deploy.sh --dry-run
#
# For additional information/help, run:
#
# tools/deploy.sh --help
#
### Sparkd ###
# The internal image for sparkd to use. For more info, view:
# src/server/sparkd/Dockerfile
SPARKD__IMAGE=sparkd
# The config dir for this service to use
SPARKD__CONFIG_DIR=src/server/sparkd/configurations/dev
# Mount a specific volume or dir to "~/sparkdata/data" inside the docker.
# Defaults to a volume named "sparkd_<config_port>" (e.g., sparkd_8000).
# SPARKD__DATA_DIR=/home/bts/sparkdata/data
SPARKD__SOURCE_DIR=install/sparkd-release-gcc
SPARKD__RUNTIME_CONF_DIR=/home/ubuntu/8000/sparkdata/conf/
SPARKD__CONFIG_PORT=8000
SPARKD__QUICKBUS_PORT=8001
SPARKD__SIM_FIX_PORT=8002
SPARKD__SHM_SIZE=4gb
# TODO: add config for other services (pubsub, md-publisher, webapp, api-server, etc.)
+1
View File
@@ -59,3 +59,4 @@ trace_data/
.devcontainer/
*.Generated.cs
*.lst
.env.local
+1 -1
View File
@@ -1,6 +1,6 @@
[submodule "externals"]
path = externals
url = git@bitbucket.org:bluetradesys/externals.git
url = https://bitbucket.org/bluetradesys/externals.git
[submodule "vcpkg"]
path = vcpkg
url = https://github.com/microsoft/vcpkg.git
+1 -1
View File
@@ -326,4 +326,4 @@
"description": "Test to run",
},
]
}
}
+2
View File
@@ -1,8 +1,10 @@
cmake_minimum_required(VERSION 3.12)
project(spark)
include(GNUInstallDirs)
include(${CMAKE_SOURCE_DIR}/cmake/GitVersion.cmake)
set(CMAKE_CXX_STANDARD 20)
get_version_from_git()
if(WIN32)
set(platform "windows")
else()
+48
View File
@@ -0,0 +1,48 @@
include_guard(GLOBAL)
function(get_version_from_git)
find_package(Git QUIET)
if(NOT Git_FOUND)
message(WARNING "Git not found")
return()
endif()
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --tags --always
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_TAG
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE GIT_RESULT
)
if(NOT GIT_RESULT EQUAL 0)
message(WARNING "Failed to get git tag")
return()
endif()
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_SHORT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(PROJECT_VERSION_BUILD_METADATA ${GIT_COMMIT_SHORT_HASH})
set(PROJECT_VERSION_BUILD_METADATA ${GIT_COMMIT_SHORT_HASH} PARENT_SCOPE)
string(REGEX REPLACE "^v" "" CLEAN_TAG "${GIT_TAG}")
if(CLEAN_TAG MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-.*)?$")
set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1})
set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1} PARENT_SCOPE)
set(PROJECT_VERSION_MINOR ${CMAKE_MATCH_2})
set(PROJECT_VERSION_MINOR ${CMAKE_MATCH_2} PARENT_SCOPE)
set(PROJECT_VERSION_PATCH ${CMAKE_MATCH_3})
set(PROJECT_VERSION_PATCH ${CMAKE_MATCH_3} PARENT_SCOPE)
set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
set(PROJECT_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}" PARENT_SCOPE)
set(PROJECT_VERSION_FULL "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}+${GIT_COMMIT_SHORT_HASH}")
set(PROJECT_VERSION_FULL "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}+${GIT_COMMIT_SHORT_HASH}" PARENT_SCOPE)
else()
message(WARNING "Tag '${CLEAN_TAG}' does not match semver format")
endif()
endfunction()
+11
View File
@@ -0,0 +1,11 @@
#ifndef BTS_SPARK_VERSION_H
#define BTS_SPARK_VERSION_H
#define BTS_SPARK_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define BTS_SPARK_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define BTS_SPARK_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define BTS_SPARK_VERSION_BUILD_METADATA @PROJECT_VERSION_BUILD_METADATA@
#define BTS_SPARK_VERSION "@PROJECT_VERSION@"
#define BTS_SPARK_VERSION_FULL "@PROJECT_VERSION_FULL@"
#endif
+100
View File
@@ -0,0 +1,100 @@
services:
sparkd:
volumes:
- ${SPARKD__DATA_DIR:-sparkd_data}:/home/bts/sparkdata/data
build:
dockerfile: src/server/sparkd/Dockerfile
args:
- IMAGE=${SPARKD__IMAGE}
- SOURCE_DIR=${SPARKD__SOURCE_DIR}
- CONFIG_DIR=${SPARKD__CONFIG_DIR}
shm_size: "${SPARKD__SHM_SIZE:-512mb}"
command: >
sh -c "
echo bts | sudo -S chown -R bts:bts /home/bts/sparkdata/data
if [ -e "/home/bts/sparkdata/data/logging.sparkd/log" ]; then
mv "/home/bts/sparkdata/data/logging.sparkd/log" "/home/bts/sparkdata/data/logging.sparkd/log_$(date '+%Y%m%d-%H%M')"
fi
if [ -e "/home/bts/sparkdata/conf/.env.local" ]; then
echo 'Sourcing /home/bts/sparkdata/conf/.env.local'
set -a
. /home/bts/sparkdata/conf/.env.local
set +a
fi
./sparkd run"
env_file:
- .env
- ${SPARKD__CONFIG_DIR}/.env
ports:
- "${SPARKD__CONFIG_PORT}:${SPARKD__CONFIG_PORT}"
- "${SPARKD__QUICKBUS_PORT}:${SPARKD__QUICKBUS_PORT}"
- "${SPARKD__SIM_FIX_PORT}:${SPARKD__SIM_FIX_PORT}"
deploy:
resources:
limits:
memory: "${SPARKD__MEMORY:-4gb}"
sparkd-symbols:
volumes:
- ${SPARKD__DATA_DIR:-sparkd_data}:/home/bts/sparkdata/data
- ${SPARKD__RUNTIME_CONF_DIR:-sparkd_conf}:/home/bts/sparkdata/conf
build:
dockerfile: src/server/sparkd/Dockerfile
args:
- IMAGE=${SPARKD__IMAGE}
- SOURCE_DIR=${SPARKD__SOURCE_DIR}
- CONFIG_DIR=${SPARKD__CONFIG_DIR}
shm_size: "${SPARKD__SHM_SIZE:-512mb}"
command: >
sh -c "
echo bts | sudo -S chown -R bts:bts /home/bts/sparkdata/data
if [ -e "/home/bts/sparkdata/data/logging.sparkd/log" ]; then
mv "/home/bts/sparkdata/data/logging.sparkd/log" "/home/bts/sparkdata/data/logging.sparkd/log_$(date '+%Y%m%d-%H%M')"
fi
ls -alh /home/bts/sparkdata/conf/
cat /home/bts/sparkdata/conf/.env.local
if [ -e "/home/bts/sparkdata/conf/.env.local" ]; then
echo 'Sourcing /home/bts/sparkdata/conf/.env.local'
set -a
. /home/bts/sparkdata/conf/.env.local
set +a
fi
./sparkd run"
env_file:
- .env
- ${SPARKD__CONFIG_DIR}/.env
ports:
- "${SPARKD__CONFIG_PORT}:${SPARKD__CONFIG_PORT}"
- "${SPARKD__QUICKBUS_PORT}:${SPARKD__QUICKBUS_PORT}"
- "${SPARKD__SIM_FIX_PORT}:${SPARKD__SIM_FIX_PORT}"
deploy:
resources:
limits:
memory: "${SPARKD__MEMORY:-4gb}"
sparkd-tail:
image: bluetradingsystems/linux_dotnet7
volumes:
- type: volume
read_only: true
source: sparkd_data
target: /home/bts/sparkdata/data/logging.sparkd
volume:
subpath: logging.sparkd
command: ["tail", "-f", "/home/bts/sparkdata/data/logging.sparkd/log"]
sparkd-ping-test:
depends_on:
- sparkd
profiles:
- ping-test
build:
context: .
dockerfile_inline: |
FROM bluetradingsystems/linux_dotnet7
USER "root"
RUN apt-get update && apt-get -y install iputils-ping
command: [ "ping", "sparkd" ]
volumes:
sparkd_data:
name: "sparkd_${SPARKD__CONFIG_PORT:-8000}"
@@ -0,0 +1,38 @@
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y pkg-config
RUN apt-get update && apt-get install -y locales
RUN apt-get update && apt-get install -y ca-certificates
RUN apt-get update && apt-get install -y libssh2-1-dev
RUN apt-get update && apt-get install -y software-properties-common
RUN apt-get update && apt-get install -y sudo
# GCC
ARG gcc_major=13
RUN add-apt-repository ppa:ubuntu-toolchain-r/test \
&& apt-get update \
&& apt-get install -y g++-$gcc_major
RUN apt-get update \
&& apt-get install -y dotnet7
RUN locale-gen en_US.UTF-8
ARG docker_user="bts"
ARG docker_uid="1000"
ARG docker_pass="bts"
RUN \
groupadd -g "$docker_uid" "$docker_user" \
&& useradd \
-m \
-u "$docker_uid" \
-g "$docker_uid" \
-G sudo \
--password "$(openssl passwd -1 "$docker_pass")" \
"$docker_user"
USER bts
WORKDIR /home/bts
+14
View File
@@ -1,11 +1,20 @@
astroid==2.7.3
autopep8==1.5.7
blinker==1.9.0
boto3==1.37.9
botocore==1.37.9
certifi==2023.5.7
charset-normalizer==3.1.0
click==8.1.8
colorama==0.4.4
Flask==3.1.0
idna==3.4
isort==5.9.3
itsdangerous==2.2.0
Jinja2==3.1.6
jmespath==1.0.1
lazy-object-proxy==1.6.0
MarkupSafe==3.0.2
mccabe==0.6.1
platformdirs==2.3.0
protobuf==4.21.12
@@ -14,9 +23,14 @@ pycodestyle==2.7.0
pycparser==2.20
pylint==2.10.2
PyMeta3==0.5.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
requests==2.31.0
s3transfer==0.11.4
six==1.17.0
termcolor==1.1.0
toml==0.10.2
urllib3==2.0.3
Werkzeug==3.1.3
wrapt==1.12.1
yapf==0.31.0
@@ -1,6 +1,8 @@
namespace mbts.spark {
using bts.api;
using Spark.Api.Data;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -23,14 +25,14 @@ namespace mbts.spark {
foreach (var kv in driver_map.get_price_map()) {
var exp = kv.Key;
var dmd = kv.Value;
IEnumerable<SecurityMD> exp_secs = sc_data.get_securities(
(sec_md) => {
var s = sec_md.security;
return s.security_type() == SecurityType.Option &&
s.Symbol == driver_map.option_symbol &&
s.earliest_expiration() == exp.expiration;
}
);
IEnumerable<SecurityMD> exp_secs = sc_data.get_securities((sec_md) => {
var s = sec_md.security;
return s.security_type() == SecurityType.Option &&
s.Symbol == driver_map.option_symbol &&
s.earliest_expiration() == (Security.is_perpetual_exp(exp.expiration)
? ExpirationDate.Perpetual
: ExpirationDate.FixedDate(exp.expiration));
});
foreach (var sec_md in exp_secs) {
rv[sec_md.security.Id] = kv.Value;
}
@@ -69,7 +69,7 @@ namespace mbts.spark {
case SecurityType.Future:
case SecurityType.Option:
rv.GetOrAdd((sec.Exchange, sec.security_type(), sec.Symbol), _ => new())
.Add(sec.earliest_expiration());
.Add(sec.earliest_expiration().Value);
break;
}
}
@@ -11,8 +11,9 @@ namespace Spark.Trading {
using mbts.analytics;
using mbts.spark;
using mbts.ui;
using Spark.Api.Data;
using Spark.Api.MarketData;
using Spark.Api.Orders;
using Spark.Api.Reactive;
using Spark.Core;
@@ -81,6 +82,11 @@ namespace Spark.Trading {
pnl = bus.subscribe(SparkTags.ReportBlackboardTags.PNL.PNLData)
.Select(d => d.position_data.ToDictionary(u => u.id(), PNLRowData.from_pnl_unit))
.Table(PNLRowData.key_fn)
.JoinBbo(
bus.subscribe(SparkTags.SecuritiesTag),
pnlRow => pnlRow.key.security_id,
(pnlRow, bbo) => pnlRow.with(bid_price: bbo.BidPrice, ask_price: bbo.AskPrice),
PNLRowData.key_fn)
.FullJoin(
order_data.working_account_security_orders,
PNLRowData.key_fn,
File diff suppressed because it is too large. Load diff
@@ -629,16 +629,16 @@ namespace mbts.spark {
static (string desc, Color fg) _fills_title(ColKey key) =>
key switch {
ColKey.Position => ("Day Fills", Colors.Black),
ColKey.RawDayPosition => ("Raw Day Fills", Colors.Black),
_ => ("Fills", Colors.Black),
ColKey.Position => ("Day Fills", BTSColors.DayFills.foreground),
ColKey.RawDayPosition => ("Raw Day Fills", BTSColors.DayFills.foreground),
_ => ("Fills", BTSColors.UI.default_text),
};
static (string desc, Color fg) _orderbook_title(ColKey key) =>
key switch {
ColKey.WorkingBids => ((string desc, Color fg))("Working Buy Orders", BTSColors.MarketData.bid),
ColKey.WorkingAsks => ((string desc, Color fg))("Working Sell Orders", BTSColors.MarketData.ask),
_ => ("Working Orders", Colors.Black),
_ => ("Working Orders", BTSColors.UI.default_text),
};
static GridSettings _hide_cols(GridSettings grid, IEnumerable<OrderBookGridView.ColumnKeys> columns) =>
@@ -626,6 +626,22 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
Grid.Column="6"
x:Name="PART_SpreaderType_Label"
VerticalAlignment="Center"
Margin="{StaticResource Margin.LHeaderPanelItem}"
Text="Type"
/>
<ComboBox
x:Name="PART_SpreaderType_DropDown"
Grid.Column="7"
HorizontalAlignment="Stretch"
Margin="{StaticResource Margin.LHeaderPanelItem}"
ItemsSource="{TemplateBinding SpreaderTypeItemsSource}"
ItemTemplate="{StaticResource EnumDescriptionTemplate}"
SelectedItem="{Binding SpreaderType, RelativeSource={RelativeSource TemplatedParent}}"
/>
<!-- <TextBlock
Grid.Column="6"
x:Name="PART_HedgePricing_Label"
VerticalAlignment="Center"
@@ -642,7 +658,7 @@
ItemTemplate="{StaticResource EnumDescriptionTemplate}"
SelectedItem="{Binding HedgePricing, RelativeSource={RelativeSource TemplatedParent}}"
Visibility="Collapsed"
/>
/> -->
</Grid>
<c:DataGridEx
x:Name="PART_DataGrid"
@@ -237,6 +237,29 @@ namespace mbts.ui.spark {
}
#endregion
#region SpreaderTypeItemsSource
public static readonly DependencyProperty SpreaderTypeItemsSourceProperty =
WPFHelper.create_dp<IEnumerable, ExecutionStrategyEditor>(
nameof(SpreaderTypeItemsSource));
public IEnumerable SpreaderTypeItemsSource {
get => GetValue(SpreaderTypeItemsSourceProperty) as IEnumerable;
set => SetValue(SpreaderTypeItemsSourceProperty, value);
}
#endregion
#region SpreaderType
public static readonly DependencyProperty SpreaderTypeProperty =
WPFHelper.create_dp<object, ExecutionStrategyEditor>(
nameof(SpreaderType),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault);
public object SpreaderType {
get => GetValue(SpreaderTypeProperty);
set => SetValue(SpreaderTypeProperty, value);
}
#endregion
public override void OnApplyTemplate() =>
_connect_leg_tuples();
@@ -117,6 +117,14 @@ namespace bts.shell {
public static QuoteInsideMarketDistanceThrottle QuoteInsideMarketDistanceThrottle => new QuoteInsideMarketDistanceThrottle();
#endregion
public static SniperQuoteParams SniperQuoteParams => new() {
NTicks = 0
};
public static ReverseHedgeQuoteParams ReverseHedgeQuoteParams => new() {
NTicks = 0
};
#region PreHedge
public static PreHedgeParams PreHedgeParams => new PreHedgeParams {
MaxPriceFromInsideMarket = PreHedgeMaxPriceFromInsideMarket,
@@ -36,6 +36,8 @@
ContractPickerVM="{Binding contract_picker_vm}"
HedgePricing="{Binding hedge_pricing}"
HedgePricingItemsSource="{Binding hedge_pricing_options}"
SpreaderType="{Binding spreader_type}"
SpreaderTypeItemsSource="{Binding spreader_type_options}"
UseFractionalTicks="{Binding use_fractional_ticks}"
IntegerTickSize="{Binding integer_tick_size}"
/>
@@ -1,23 +0,0 @@
using mbts.ui.spark;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
partial class SyntheticEditorVM {
class IsQuoteModel : RowModelBase<bool> {
public override string display_name { get; } = "Enable Quoting";
public override string help_text { get; } = "This parameter is in the form of a check box, where if the box is checked the leg will be quoted versus not checked, it will not be shown in the market. If it is not actively quoted in the market, the leg will still be used for calculation of the entire synthetic.";
public override string man_page { get; } = string.Empty;
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.IsActiveQuoting;
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) =>
leg_tup.leg_params.IsActiveQuoting = value;
public override bool has_errors(IReadOnlyCollection<LegSpreaderParamsTuple> leg_tups) =>
!leg_tups.Any(lt => lt.leg_params.IsActiveQuoting);
}
}
}
@@ -0,0 +1,25 @@
using mbts.ui.spark;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark;
partial class SyntheticEditorVM {
class QuoteEnabledModel : RowModelBase<bool> {
public override string display_name => string.Empty;
public override string help_text => string.Empty;
public override string man_page => string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote };
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.IsActiveQuoting;
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) =>
leg_tup.leg_params.IsActiveQuoting = value;
public override bool has_errors(IReadOnlyCollection<LegSpreaderParamsTuple> leg_tups) =>
!leg_tups.Any(lt => lt.leg_params.IsActiveQuoting);
}
}
@@ -0,0 +1,35 @@
using bts.shell;
using mbts.ui.spark;
namespace mbts.spark;
partial class SyntheticEditorVM {
class ReverseHedgeEnabledModel : RowModelBase<bool> {
readonly ReverseHedgeTicksModel _ticks_model;
public ReverseHedgeEnabledModel(ReverseHedgeTicksModel ticks_model) =>
_ticks_model = ticks_model;
public override string display_name => string.Empty;
public override string help_text => string.Empty;
public override string man_page => string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.ReverseHedge };
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.ReverseHedgeQuoteParams is not null;
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) {
if (value) {
if (leg_tup.leg_params.ReverseHedgeQuoteParams is null) {
leg_tup.leg_params.ReverseHedgeQuoteParams = SyntheticDataHelper.Defaults.ReverseHedgeQuoteParams;
_ticks_model.apply_value(leg_tup);
}
} else {
leg_tup.leg_params.ReverseHedgeQuoteParams = null;
}
_ticks_model.set_enabled(leg_tup, value);
}
}
}
@@ -0,0 +1,42 @@
using bts.spark;
using mbts.ui.spark;
using System.Collections.Generic;
namespace mbts.spark;
partial class SyntheticEditorVM {
internal class ReverseHedgeTicksModel : RowModelBase<int> {
readonly Dictionary<int, int> _value_store = new();
public override string display_name => "Ticks";
public override string help_text => string.Empty;
public override string man_page => string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.ReverseHedge };
public void apply_value(LegSpreaderParamsTuple leg_tup) =>
_get_params(leg_tup).NTicks = _value_from_store(leg_tup);
public override int get_value(LegSpreaderParamsTuple leg_tup) =>
_get_params(leg_tup)?.NTicks ?? _value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, int value) {
_value_store[leg_tup.id] = value;
var s = _get_params(leg_tup);
if (s is not null) {
s.NTicks = value;
}
}
protected override bool _get_enabled(LegSpreaderParamsTuple leg_tup) =>
_get_params(leg_tup) != null;
ReverseHedgeQuoteParams _get_params(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.ReverseHedgeQuoteParams;
int _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.get_value(leg_tup.id, _ => 0);
}
}
@@ -94,7 +94,9 @@ namespace mbts.spark {
}
private static class Sections {
public static ParameterSection Quote => new ParameterSection("Quote");
public static ParameterSection Sniper => new("Sniper", true);
public static ParameterSection ReverseHedge => new("Reverse Hedge", true);
public static ParameterSection Quote => new("Quote", true);
public static ParameterSection PreHedge => new ParameterSection("Pre-Hedge");
public static ParameterSection PostHedge => new ParameterSection("Post-Hedge");
}
@@ -0,0 +1,35 @@
using bts.shell;
using mbts.ui.spark;
namespace mbts.spark;
partial class SyntheticEditorVM {
class SniperEnabledModel : RowModelBase<bool> {
readonly SniperTicksModel _ticks_model;
public SniperEnabledModel(SniperTicksModel ticks_model) =>
_ticks_model = ticks_model;
public override string display_name => string.Empty;
public override string help_text => string.Empty;
public override string man_page => string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.Sniper };
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.SniperQuoteParams is not null;
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) {
if (value) {
if (leg_tup.leg_params.SniperQuoteParams is null) {
leg_tup.leg_params.SniperQuoteParams = SyntheticDataHelper.Defaults.SniperQuoteParams;
_ticks_model.apply_value(leg_tup);
}
} else {
leg_tup.leg_params.SniperQuoteParams = null;
}
_ticks_model.set_enabled(leg_tup, value);
}
}
}
@@ -0,0 +1,42 @@
using bts.spark;
using mbts.ui.spark;
using System.Collections.Generic;
namespace mbts.spark;
partial class SyntheticEditorVM {
internal class SniperTicksModel : RowModelBase<int> {
readonly Dictionary<int, int> _value_store = new();
public override string display_name => "Ticks";
public override string help_text => string.Empty;
public override string man_page => string.Empty;
public override ParameterSection[] section_path { get; } = new[] { Sections.Sniper };
public void apply_value(LegSpreaderParamsTuple leg_tup) =>
_get_params(leg_tup).NTicks = _value_from_store(leg_tup);
public override int get_value(LegSpreaderParamsTuple leg_tup) =>
_get_params(leg_tup)?.NTicks ?? _value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, int value) {
_value_store[leg_tup.id] = value;
var s = _get_params(leg_tup);
if (s is not null) {
s.NTicks = value;
}
}
protected override bool _get_enabled(LegSpreaderParamsTuple leg_tup) =>
_get_params(leg_tup) != null;
SniperQuoteParams _get_params(LegSpreaderParamsTuple leg_tup) =>
leg_tup.leg_params.SniperQuoteParams;
int _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.get_value(leg_tup.id, _ => 0);
}
}
@@ -23,6 +23,7 @@ using System.Windows.Input;
using System.Windows.Media;
using HedgePricing = bts.spark.ExecutionStrategy.Types.HedgePricing;
using SpreaderType = bts.spark.ExecutionStrategy.Types.SpreaderType;
namespace mbts.spark {
/// <summary>
@@ -47,6 +48,7 @@ namespace mbts.spark {
int _integer_tick_size = 1;
ExecutionStrategyImmutable _strategy;
HedgePricing _hedge_pricing;
SpreaderType _spreader_type;
/// <summary>
/// Inititalizes a new instance of SyntheticEditorVM class. This constructor may only be used for already-existing synthetic securities (id must be non-zero).
@@ -198,6 +200,8 @@ namespace mbts.spark {
} else {
_can_add_leg = backing(nameof(can_add_leg), false);
}
var sniper_ticks_model = new SniperTicksModel();
var reverse_hedge_ticks_model = new ReverseHedgeTicksModel();
var show_quantity_quantity = new ShowQuantityQuantityModel();
var quote_throttle_outside_ticks = new QuoteThrottleOutsideTicksModel();
@@ -225,15 +229,18 @@ namespace mbts.spark {
var post_hedge_opposite_market_based_cross_threshold = new PostHedgeOppositeMarketVolumeBasedCrossThresholdModel();
var post_hedge_opposite_market_based_cross_cross_ticks = new PostHedgeOppositeMarketVolumeBasedCrossCrossTicksModel();
var is_quote_model = new IsQuoteModel();
rows = new IRowModel[] {
_quantity_model,
_price_mult_model,
is_quote_model,
//new EnableHedgeModel(), //TAHA_SPREADER
new HedgeRatioModel(),
new SniperEnabledModel(sniper_ticks_model),
sniper_ticks_model,
new ReverseHedgeEnabledModel(reverse_hedge_ticks_model),
reverse_hedge_ticks_model,
//new MinLeanQtyModel(), //TAHA_SPREADER
//new QuoteHiddenModel(),
new QuoteEnabledModel(),
new QuoteQueueHolderModel(),
new MarketDataTypeModel(),
new ShowQuantityEnabledModel(show_quantity_quantity),
@@ -337,6 +344,13 @@ namespace mbts.spark {
public HedgePricing hedge_pricing { get => _hedge_pricing; set => Set(ref _hedge_pricing, value); }
public IEnumerable<HedgePricing> hedge_pricing_options => EnumHelper.get_values<HedgePricing>();
// spreader type
public SpreaderType spreader_type {
get => _spreader_type;
set => Set(ref _spreader_type, value);
}
public IEnumerable<SpreaderType> spreader_type_options => EnumHelper.get_values<SpreaderType>();
public ExecutionStrategyImmutable get_strategy() {
var mutable_strategy = _strategy.CloneToMutable();
mutable_strategy.LegParamsMap.Clear();
@@ -352,6 +366,7 @@ namespace mbts.spark {
_synthetic.Exchange = Venue.Bts;
_strategy = strategy;
hedge_pricing = strategy.HedgePricing;
spreader_type = strategy.SpreaderType;
leg_tuples.SetItems(_get_legs(_synthetic, strategy));
if (synthetic.HasContractSpec) {
/// assign the fields <see cref="_use_fractional_ticks"/> and <see cref="_integer_tick_size"/>,
@@ -8,6 +8,7 @@ using mbts.ui.controls;
using ProtoBuf;
using Spark.Api.Data;
using Spark.Api.Orders;
using System;
@@ -91,7 +92,7 @@ namespace bts.shell.fills {
// PNL's Expiration group popups use this prop to filtered data by a particular exp (it isn't
// serialized bc it's not part of the main window's settings)
public DateTime? exp_date { get; set; }
public ExpirationDate? exp_date { get; set; }
}
static class FillsFilterStatesExtensions {
@@ -121,7 +122,7 @@ namespace bts.shell.fills {
pb.add_numeric(fs.price, ft => ft.fill.LastPrice);
if (fs.exp_date.HasValue) {
pb.add_date_time(fs.exp_date.Value, f => f.security?.earliest_expiration());
pb.add_exp_date(fs.exp_date.Value, f => f.security?.earliest_expiration());
}
return pb.to_predicate();
}
@@ -41,6 +41,8 @@ namespace mbts.spark {
using System.Windows.Input;
using System.Windows.Media;
using SpreaderType = bts.spark.ExecutionStrategy.Types.SpreaderType;
class LadderVM : DisposableViewModelBase, IWillLetYouKnowWhenIDispose, IKeyListener {
static readonly Brush _transparent_status_brush = BTSColors.UI.transparent;
@@ -103,6 +105,7 @@ namespace mbts.spark {
ulong _misc_time = 0;
ulong _last_trade_time = 0;
ulong _last_bbo_time = 0;
SpreaderType _spreader_type = SpreaderType.Requote;
IReadOnlyDictionary<uint, OrderRecordTuple> _cache = DictionaryHelper.empty<uint, OrderRecordTuple>();
ImmutableDictionary<AlgoKey, LadderFavoriteAlgoMetadata> _favorite_algo_metadata_map =
ImmutableDictionary<AlgoKey, LadderFavoriteAlgoMetadata>.Empty;
@@ -126,6 +129,9 @@ namespace mbts.spark {
size_calculator.display_settings = model.security_md.quantity_display;
_slot_price_vs_orders = new Dictionary<double, LadderOrderVM>(_equality);
this.order_properties_vm = order_properties_vm;
if (model.security.HasSynthetic) {
spreader_type_choices.AddRange(EnumHelper.get_values<SpreaderType>());
}
_session_high = backing(nameof(session_high), double.NaN);
_session_low = backing(nameof(session_low), double.NaN);
_day_change = backing(nameof(day_change), double.NaN);
@@ -277,9 +283,7 @@ namespace mbts.spark {
var orders_source = settings.Select(s => s.ticks_per_row).DistinctUntilChanged()
.SelectSeries(tpr => {
var spec = TickUtils.multiply_tick_size(security.regional_ticks(), tpr);
var price_comp = Comparer<double>.Create((a, b) =>
Comparer<long?>.Default.Compare(spec.count_ticks(a), spec.count_ticks(b)));
return _order_price_source(model.orders, price_comp, p => spec.round_to_tick(p, RoundingMode.AbsDown));
return _order_price_source(model.orders, p => spec.round_to_tick(p, RoundingMode.AbsDown));
});
_day_change.Changed += (o, n) => RaisePropertyChanged(
nameof(day_change_font_color), nameof(day_change_background_color));
@@ -580,6 +584,12 @@ namespace mbts.spark {
public ObservableCollection<FairValueLineVM> fair_value_lines { get; } =
new ObservableCollection<FairValueLineVM>();
public SpreaderType spreader_type {
get => _spreader_type;
set => Set(ref _spreader_type, value);
}
public ObservableCollection<SpreaderType> spreader_type_choices { get; } = new();
public uint orders_column_width {
get => _orders_column_width.Value;
set => _orders_column_width.set(value);
@@ -759,15 +769,21 @@ namespace mbts.spark {
}
var order = order_properties_vm.create(side);
if (order.CustomDef is not null) {
if (order.CustomDef.Stop is not null) {
order.CustomDef.Stop.StopPrice = price;
} else if (order.CustomDef.StopLimit is not null) {
var def = order.CustomDef;
if (def is not null) {
if (def.Stop is not null) {
def.Stop.StopPrice = price;
} else if (def.StopLimit is not null) {
// swap the prices here so that the level clicked
// in the ladder is the StopPrice (trigger) and the Price
// is what was specificed in the extra parameter box
(price, order.CustomDef.StopLimit.StopPrice) =
(order.CustomDef.StopLimit.StopPrice, price);
(price, def.StopLimit.StopPrice) =
(def.StopLimit.StopPrice, price);
} else if (def.Spreader is not null) {
var strategy = def.Spreader.ExecutionStrategy;
if (strategy is not null) {
strategy.SpreaderType = spreader_type;
}
}
}
@@ -806,10 +822,8 @@ namespace mbts.spark {
}
static IObservable<(ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordTuple>> price_index, IReadOnlyDictionary<uint, OrderRecordTuple> map)> _order_price_source(
IObservable<TableUpdate<OrderRecordTuple, uint>> source,
IComparer<double> price_comp,
Func<double, double> normalize_price
) {
IObservable<TableUpdate<OrderRecordTuple, uint>> source, Func<double, double> normalize_price) {
var price_comp = MathUtils.error_margin_comparer(1E-9);
return Observable.Create<(ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordTuple>>, IReadOnlyDictionary<uint, OrderRecordTuple>)>(o => {
var state = ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordTuple>>(price_comp);
var sub = source.Subscribe(table => {
@@ -853,15 +867,18 @@ namespace mbts.spark {
return sub;
});
bool price_changed(Change<OrderRecordTuple> ch) =>
price_comp.Compare(ch.New.order.ui_price(), ch.Old.order.ui_price()) != 0;
double get_price(OrderRecordTuple order) {
var p = order.order.ui_price();
if (!(normalize_price is null))
p = normalize_price(p);
return p;
}
bool price_changed(Change<OrderRecordTuple> ch) {
var oldp = get_price(ch.Old);
var newp = get_price(ch.New);
return price_comp.Compare(oldp, newp) != 0;
}
}
double _normalize_price(double price) =>
@@ -626,10 +626,30 @@ namespace mbts.spark {
void _send_parameterized_order(Point point, Side side) {
Disposable.dispose(ref _parameterized_order_popup);
if (side == Side.Undefined)
throw new ArgumentException("Side is undefined.", nameof(side));
if (!(_vm.size_calculator.value is double size) || size == 0 || !(_vm.order_properties_vm?.selected_algo_definition?.algo_root?.can_expand ?? false))
void on_cannot_send(string reason) {
Log.Ui.warn($"LadderGridHost cannot send parameterized order. {reason}");
}
if (_vm is null) {
on_cannot_send("ViewModel is null");
return;
}
switch (side) {
case Side.Buy:
case Side.Sell:
break;
default:
on_cannot_send($"Invalid side '{side}'");
return;
}
double size = _vm?.size_calculator?.value ?? 0;
if (size > 0) {
return;
}
bool can_expand = _vm?.order_properties_vm?.selected_algo_definition?.algo_root?.can_expand ?? false;
if (!can_expand) {
on_cannot_send("Selected algo not parameterized");
return;
}
double price = _price_for_row(_row_for_point(point));
var grid = new Grid { DataContext = _vm };
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
@@ -701,6 +721,10 @@ namespace mbts.spark {
}
async Task _show_mbo_popup(Point point) {
if (_vm is null) {
Log.Ui.warn("Cannot show mbo popup. ViewModel is null.");
return;
}
var query = MBOPopupModel.default_query.with(
security_id: _vm.model.security.Id,
price_level: price_for_point(point)
@@ -1424,7 +1448,7 @@ namespace mbts.spark {
double best_ask = _get_top_price(_asks);
var v = best_bid
.if_nan(_spec.tickdown(best_ask))
.if_nan(_vm.settlement);
.if_nan(_vm?.settlement ?? double.NaN);
return _spec.round_to_tick(v, RoundingMode.AbsDown);
}
@@ -1432,11 +1456,14 @@ namespace mbts.spark {
TickUtils.normalize_price(_spec, price);
double _get_ask_market_break() {
if (_vm is null) {
return double.NaN;
}
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
var v = _spec.tickdown(best_ask)
.if_nan(best_bid)
.if_nan(_vm.settlement);
.if_nan(_vm?.settlement ?? double.NaN);
return _spec.round_to_tick(v, RoundingMode.AbsDown);
}
@@ -1446,15 +1473,15 @@ namespace mbts.spark {
var v = TickUtils.mid_tick(_spec, best_bid, best_ask)
.if_nan(best_bid)
.if_nan(_spec?.tickdown(best_ask) ?? double.NaN)
.if_nan(_vm.settlement);
.if_nan(_vm?.settlement ?? double.NaN);
return _spec.round_to_tick(v, RoundingMode.AbsDown);
}
double _get_session_low() => _normalize_price(_get_session_low_raw());
double _get_session_high() => _normalize_price(_get_session_high_raw());
double _get_session_low_raw() => _vm.md.session_low;
double _get_session_high_raw() => _vm.md.session_high;
double _get_session_low_raw() => _vm?.md?.session_low ?? double.NaN;
double _get_session_high_raw() => _vm?.md?.session_high ?? double.NaN;
void _render_mkt_break(DrawingVisual vis, double price, uint color) =>
LadderRender.render_mkt_break(vis, _row_for_price(price), _settings.row_height, color);
@@ -1475,7 +1502,7 @@ namespace mbts.spark {
}
/// The viewmodel will always have the most up-to-date settings, so ignore <see cref="s"/> and just get it from <see cref="_vm"/>.
/// This is a workaround for the global settings-defaults issue: when defaults are updated, some properties are inadvertantly overwritten, such as column-widths, etc.
_settings = _vm.get_save_state();
_settings = _vm?.get_save_state() ?? s;
_pending_scroll_to_origin |= _settings == null;
_invalidate_spec();
if (_price_list != null) {
@@ -1551,15 +1578,17 @@ namespace mbts.spark {
_visual_host.AddVisualChild(_mid_mkt_break_vis);
_visual_host.AddVisualChild(_ask_mkt_break_vis);
_visual_host.AddVisualChild(_bid_mkt_break_vis);
_fair_value_lines_sub = ui.utility.ObservableAutoWrapper.connect(
_vm.fair_value_lines,
_mkt_break_lines.fair_value_line_views,
vm => {
var with_vis = new FairValueLineWithVis(vm);
_visual_host.AddVisualChild(with_vis.vis);
return with_vis;
},
with_vis => _visual_host.RemoveVisualChild(with_vis.vis));
if (_vm is not null) {
_fair_value_lines_sub = ui.utility.ObservableAutoWrapper.connect(
_vm.fair_value_lines,
_mkt_break_lines.fair_value_line_views,
vm => {
var with_vis = new FairValueLineWithVis(vm);
_visual_host.AddVisualChild(with_vis.vis);
return with_vis;
},
with_vis => _visual_host.RemoveVisualChild(with_vis.vis));
}
_visual_host.AddVisualChild(_session_low_vis);
_visual_host.AddVisualChild(_session_high_vis);
_render_grid_background();
@@ -956,6 +956,13 @@
</Style>
</uag:OrderDetailsPopupControl.Resources>
</uag:OrderDetailsPopupControl>
<ComboBox
Margin="2"
ItemsSource="{Binding spreader_type_choices}"
SelectedItem="{Binding spreader_type}"
Visibility="{Binding HasItems, Mode=OneWay, RelativeSource={RelativeSource Self}, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
<!-- Position into Qty Button -->
<Button
@@ -5,6 +5,7 @@ using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using Spark.Api.Data;
using Spark.Core.Data;
using Spark.Core.Disposables;
using Spark.Core.Reflection;
@@ -8,6 +8,7 @@ using bts.utils;
using mbts.ui;
using mbts.ui.controls;
using Spark.Api.Data;
using Spark.Api.Orders;
using Spark.Core.Collections;
using Spark.ViewModel.Commands;
@@ -150,7 +150,11 @@ namespace bts.shell.orderbook {
pb.add_discrete_values(fs.exchanges, o => o.order.security?.Exchange ?? Venue.Unset);
pb.add_discrete_values(fs.tifs, o => o.order.order.time_in_force());
pb.add_discrete_values(fs.order_types, o => AlgoID.from_custom_def(o.order.order.Def.CustomDef));
pb.add_discrete_values(fs.root_algo_types, o => AlgoID.from_algo_key(o.root.algo?.key ?? default));
pb.add_discrete_values(
Array.ConvertAll(fs.root_algo_types, a => (AlgoID?)a),
o => o.root.algo.HasValue
? AlgoID.from_algo_key(o.root.algo.Value.key)
: null);
pb.add_discrete_values(fs.order_sub_types, o => o.order.order.sub_type());
pb.add_discrete_values(fs.sides, o => o.order.order.side());
pb.add_discrete_values(fs.statuses, o => o.order.order.status());
@@ -1,527 +1,317 @@
using bts.api;
using bts.api.orderman;
using bts.api.position;
using bts.utils;
using mbts;
using mbts.analytics;
using mbts.spark;
using mbts.spark.utilities;
using mbts.ui;
using mbts.utils;
using Spark.Api.Orders;
using Spark.Api.Position;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using PNLKey = bts.AccountSecurityKey;
namespace bts.shell.pnl {
public enum PNLCalculationType {
None,
Bid,
Ask,
Opposite,
Same,
[Description("Cascade")]
Waterfall,
Last,
Mid,
Settle
}
readonly struct WorkingOrderData {
public readonly WorkingOrderSideData long_data, short_data;
WorkingOrderData(WorkingOrderSideData long_data, WorkingOrderSideData short_data) {
this.long_data = long_data;
this.short_data = short_data;
}
public static WorkingOrderData Empty =>
new WorkingOrderData(WorkingOrderSideData.EmptyLong, WorkingOrderSideData.EmptyShort);
public double top_bid =>
long_data.data.Count > 0 ?
long_data.data.First().Key :
double.NaN;
public double top_ask =>
short_data.data.Count > 0 ?
short_data.data.First().Key :
double.NaN;
public PositionPriceSize summary =>
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
public static WorkingOrderData Create(
IEnumerable<OrderRecordImmutable> orders, QuantityConversionData<double> qty_conversion) =>
Create(orders.Select(o => (o, qty_conversion)));
public static WorkingOrderData Create(
IEnumerable<(OrderRecordImmutable order, QuantityConversionData<double> qty_conversion)> orders) =>
orders.Aggregate(Empty, (seed, tup) => seed.Add(tup.order, tup.qty_conversion));
public WorkingOrderData Add(OrderRecordImmutable order, QuantityConversionData<double> qty_conversion) {
if (order.working_quantity() <= 0)
return new WorkingOrderData(long_data, short_data);
switch (order.side()) {
case Side.Buy: return new WorkingOrderData(long_data.Add(order, qty_conversion), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order, qty_conversion));
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
}
}
public WorkingOrderData Remove(OrderRecordImmutable order) {
if (order.working_quantity() <= 0)
return new WorkingOrderData(long_data, short_data);
switch (order.side()) {
case Side.Buy: return new WorkingOrderData(long_data.Remove(order.Key, order.price()), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Remove(order.Key, order.price()));
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
}
}
}
readonly struct WorkingOrderSideData {
public readonly double avg_price, display_avg_price;
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable order, QuantityConversionData<double> qty_conversion)>> data;
public readonly double working_total;
public readonly double display_working_total;
WorkingOrderSideData(
double avg_price,
double working_total,
double display_avg_price,
double display_working_total,
ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>> data
) {
this.avg_price = avg_price;
this.working_total = working_total;
this.display_avg_price = display_avg_price;
this.data = data;
this.display_working_total = display_working_total;
}
public static WorkingOrderSideData EmptyLong => new(
double.NaN, 0, double.NaN, 0,
ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>>(
new ReverseComparer<double>(new OrderPriceComparer())));
public static WorkingOrderSideData EmptyShort => new(
double.NaN, 0, double.NaN, 0,
ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>>(
new OrderPriceComparer()));
public double top_price =>
data.Count > 0 ?
data.First().Key :
double.NaN;
public double top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.order.working_quantity()) :
0;
public double display_top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.qty_conversion
.convert_numeric_type<double>()
.to_display(o.order.working_quantity())) :
0;
public WorkingOrderSideData Add(OrderRecordImmutable order_record) =>
Add(order_record, new());
public WorkingOrderSideData Add(
OrderRecordImmutable order_record, QuantityConversionData<double> qty_conversion) {
var working_qty = order_record.working_quantity();
if (working_qty <= 0)
throw new ArgumentException(nameof(working_qty));
uint order = order_record.Key;
var dworking_qty = qty_conversion.to_display(working_qty);
double price = order_record.price();
if (working_total == 0) {
Log.assert(display_working_total == 0);
return new WorkingOrderSideData(price, working_qty, display_avg_price, dworking_qty, data.Add(
price, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>.Empty.Add(order, (order_record, qty_conversion))));
} else return new WorkingOrderSideData(
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
working_total + working_qty,
display_avg_price + ((price - display_avg_price) * ((double)dworking_qty / (display_working_total + dworking_qty))),
display_working_total + dworking_qty,
data.SetItem(price, data.TryGetValue(price, out var curr) ?
curr.Add(order, (order_record, qty_conversion)) :
ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>.Empty.Add(order, (order_record, qty_conversion))));
}
public WorkingOrderSideData Remove(uint order, double price) {
var tmp_data = data;
if (!tmp_data.TryGetValue(price, out var wdata))
throw new KeyNotFoundException(nameof(price));
if (!wdata.TryGetValue(order, out var worder))
throw new KeyNotFoundException(nameof(order));
var wqty = worder.order.working_quantity();
var dwqty = worder.qty_conversion.to_display(wqty);
wdata = wdata.Remove(order);
if (wdata.Count == 0)
tmp_data = tmp_data.Remove(price);
if (tmp_data.Count == 0)
return new(
double.NaN, 0, double.NaN, 0,
ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>>(
data.KeyComparer));
return new(
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
working_total - wqty,
display_avg_price - ((price - display_avg_price) * (dwqty / (display_working_total - dwqty))),
display_working_total - dwqty,
tmp_data);
}
public override string ToString() {
if (data.Count > 0) {
var best_order = data.Values.First().Values.First();
return OrderUtils.order_collection_summary(
best_order.order.price(),
display_avg_price,
best_order.qty_conversion.to_display(best_order.order.working_quantity()),
display_working_total,
Security.Defaults.ContractSpecificationImmutable);
}
return string.Empty;
}
}
readonly struct PNLRowData : IEquatable<PNLRowData> {
public static readonly Func<PNLRowData, PNLKey> key_fn = d => d.key;
public static IComparer<PNLRowData> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowData>(d => d.key);
static readonly WaterfallPriceFlags[] _waterfall_preferences = new[] {
WaterfallPriceFlags.LastInsideSpread,
WaterfallPriceFlags.Mid,
WaterfallPriceFlags.LastGTEBid,
WaterfallPriceFlags.Bid,
WaterfallPriceFlags.LastLTEAsk,
WaterfallPriceFlags.Ask,
WaterfallPriceFlags.Last,
WaterfallPriceFlags.Settle
};
public readonly PNLKey key;
public readonly SecurityImmutable? security;
public readonly QuantityConversionData<double> qty_conversion;
public readonly uint account_id;
public readonly string account_name;
public readonly PNLCalculationType calc_type;
public readonly PositionPriceSizeWithGreeks sod;
public readonly PositionPriceSizeWithGreeks day;
public readonly WorkingOrderData working;
public readonly double
default_cpp,
bid_price,
ask_price,
last_price,
settlement_price;
public readonly double? override_cpp;
public PNLRowData(
PNLKey key,
double default_cpp = 1,
double? override_cpp = null,
double pnl_price = double.NaN,
double bid_price = 0,
double ask_price = 0,
double last_price = 0,
double settlement_price = 0,
double sod_pnl = 0,
PositionPriceSizeWithGreeks? sod = null,
double day_pnl = 0,
PositionPriceSizeWithGreeks? day = null,
WorkingOrderData? working = null,
SecurityImmutable? security = null,
uint account_id = 0,
string account_name = null,
PNLCalculationType calc_type = PNLCalculationType.Mid,
QuantityConversionData<double>? qty_conversion = null) {
//
this.key = key;
this.default_cpp = default_cpp;
this.override_cpp = override_cpp;
this.pnl_price = pnl_price;
this.bid_price = bid_price;
this.ask_price = ask_price;
this.last_price = last_price;
this.settlement_price = settlement_price;
this.sod_pnl = sod_pnl;
this.sod = sod ?? PositionPriceSizeWithGreeks.empty;
this.day_pnl = day_pnl;
this.day = day ?? PositionPriceSizeWithGreeks.empty;
this.working = working ?? WorkingOrderData.Empty;
this.security = security;
this.account_id = account_id;
this.account_name = account_name;
this.calc_type = calc_type;
this.qty_conversion = qty_conversion ?? new();
}
[Flags]
enum WaterfallPriceFlags {
None = 0,
Bid = 1,
Ask = 1 << 1,
Last = 1 << 2,
Settle = 1 << 3,
Mid = Bid | Ask,
LastGTEBid = Bid | Last | 1 << 4,
LastLTEAsk = Ask | Last | 1 << 5,
LastInsideSpread = LastGTEBid | LastLTEAsk,
BidGTESettle = Bid | Settle | 1 << 6,
AskLTESettle = Ask | Settle | 1 << 7
}
public AccountTuple account => new(account_id, account_name);
public string symbol => security?.pnl_symbol() ?? string.Empty;
public DateTime expiration =>
security?.normal_expirations()?.MinOrDefault(DateTime.MinValue) ?? DateTime.MinValue;
public double effective_cpp => override_cpp ?? default_cpp;
/// Market/Calc
public double mid_price => (bid_price + ask_price) / 2;
public double pnl_price { get; }
/// Sod
public double sod_pnl { get; }
public double sod_long_pos => sod.long_pos;
public double sod_long_avg_price => sod.long_avg;
public double sod_short_pos => sod.short_pos;
public double sod_short_avg_price => sod.short_avg;
public double sod_net_pos => sod.net_pos;
public double sod_net_avg_price => sod.net_avg;
public double sod_delta => sod.delta;
public double sod_gamma => sod.gamma;
public double sod_vega => sod.vega;
public double sod_theta => sod.theta;
public double display_sod_net_pos => qty_conversion.to_display(sod_net_pos);
public double display_sod_long_pos => qty_conversion.to_display(sod_long_pos);
public double display_sod_short_pos => qty_conversion.to_display(sod_short_pos);
/// Day
public double day_pnl { get; }
public double day_long_pos => day.long_pos;
public double day_long_avg_price => day.long_avg;
public double day_short_pos => day.short_pos;
public double day_short_avg_price => day.short_avg;
public double day_delta => day.delta;
public double day_gamma => day.gamma;
public double day_vega => day.vega;
public double day_theta => day.theta;
public bool has_day_trades => day.long_pos > 0 || day.short_pos > 0;
public double day_net_pos => day.net_pos;
public double day_net_avg => day.net_avg;
public double display_day_net_pos => qty_conversion.to_display(day_net_pos);
public double display_day_long_pos => qty_conversion.to_display(day_long_pos);
public double display_day_short_pos => qty_conversion.to_display(day_short_pos);
/// Total
public double total_pnl => new[] { sod_pnl, day_pnl }.sum_real();
public double total_long_pos => new[] { sod.long_pos, day.long_pos }.sum_real();
public double total_long_avg_price =>
MathUtils.weighted_average(
sod_long_avg_price.if_nan(0),
sod_long_pos.if_nan(0),
day_long_avg_price.if_nan(0),
day_long_pos.if_nan(0));
public double total_short_pos => new[] { sod.short_pos, day.short_pos }.sum_real();
public double total_short_avg_price =>
MathUtils.weighted_average(
sod_short_avg_price.if_nan(0),
sod_short_pos.if_nan(0),
day_short_avg_price.if_nan(0),
day_short_pos.if_nan(0));
public double total_net_pos => new[] { sod.long_pos, -sod.short_pos, day.long_pos, -day.short_pos }.Where(n => n.is_real()).Sum();
public double total_delta => sod.delta.add_real(day.delta);
public double total_gamma => sod.gamma.add_real(day.gamma);
public double total_vega => sod.vega.add_real(day.vega);
public double total_theta => sod.theta.add_real(day.theta);
public double display_total_net_pos => qty_conversion.to_display(total_net_pos);
public double display_total_long_pos => qty_conversion.to_display(total_long_pos);
public double display_total_short_pos => qty_conversion.to_display(total_short_pos);
/// Working
public double working_long_pos => working.long_data.working_total;
public double working_long_avg_price => effective_cpp * working.long_data.avg_price;
public double working_long_top_price => effective_cpp * working.long_data.top_price;
public double working_short_pos => working.short_data.working_total;
public double working_short_avg_price => effective_cpp * working.short_data.avg_price;
public double working_short_top_price => effective_cpp * working.short_data.top_price;
public double working_net_pos => working_long_pos - working_short_pos;
public double display_working_net_pos => qty_conversion.to_display(working_net_pos);
public double display_working_long_pos => qty_conversion.to_display(working_long_pos);
public double display_working_short_pos => qty_conversion.to_display(working_short_pos);
public static PNLRowData from_pnl_unit(IPNLUnit u) =>
new(
u.id(),
default_cpp: 1.0,
override_cpp: null,
pnl_price: u.theo,
bid_price: double.NaN,
ask_price: double.NaN,
last_price: double.NaN,
settlement_price: u.settle_price,
sod_pnl: u.start_pnl,
sod: new() {
price_size = new(
u.start_long_pos,
u.start_long_avg_cost,
u.start_short_pos,
u.start_short_avg_cost),
greeks = new() {
delta = u.start_delta,
gamma = u.start_gamma,
vega = u.start_vega,
theta = u.start_theta
}
},
day_pnl: u.day_pnl,
day: new() {
price_size = new(
u.day_long_pos,
u.day_long_avg_cost,
u.day_short_pos,
u.day_short_avg_cost),
greeks = new() {
delta = u.day_delta,
gamma = u.day_gamma,
vega = u.day_vega,
theta = u.day_theta
}
},
working: null,
security: u.security,
account_id: u.account_id,
account_name: u.account,
calc_type: PNLCalculationType.Mid,
qty_conversion: null);
public PNLRowData with(
SecurityImmutable? security = null,
uint? account_id = null,
string account_name = null,
WorkingOrderData? working = null,
PNLCalculationType? calc_type = null) =>
new PNLRowData(
key,
default_cpp,
override_cpp,
pnl_price,
bid_price,
ask_price,
last_price,
settlement_price,
sod_pnl: sod_pnl,
sod,
day_pnl: day_pnl,
day,
working ?? this.working,
security ?? this.security,
account_id ?? this.account_id,
account_name ?? this.account_name,
calc_type ?? this.calc_type,
qty_conversion: qty_conversion);
WaterfallPriceFlags _waterfall_flags() {
var flags = WaterfallPriceFlags.None;
if (!double.IsNaN(bid_price)) flags |= WaterfallPriceFlags.Bid;
if (!double.IsNaN(ask_price)) flags |= WaterfallPriceFlags.Ask;
if (!double.IsNaN(last_price)) flags |= WaterfallPriceFlags.Last;
if (last_price >= bid_price) flags |= WaterfallPriceFlags.LastGTEBid;
if (last_price <= ask_price) flags |= WaterfallPriceFlags.LastLTEAsk;
if (!double.IsNaN(settlement_price)) flags |= WaterfallPriceFlags.Settle;
if (bid_price >= settlement_price) flags |= WaterfallPriceFlags.BidGTESettle;
if (ask_price <= settlement_price) flags |= WaterfallPriceFlags.AskLTESettle;
return flags;
}
double _waterfall_price() {
var flags = _waterfall_flags();
// return price for the best match
for (int i = 0; i < _waterfall_preferences.Length; i++) {
if ((_waterfall_preferences[i] & flags) == _waterfall_preferences[i])
return _waterfall_price(_waterfall_preferences[i]);
}
return double.NaN;
}
double _waterfall_price(WaterfallPriceFlags ltp) {
switch (ltp) {
case WaterfallPriceFlags.Last:
case WaterfallPriceFlags.LastInsideSpread:
case WaterfallPriceFlags.LastGTEBid:
case WaterfallPriceFlags.LastLTEAsk: return last_price;
case WaterfallPriceFlags.Mid: return mid_price;
case WaterfallPriceFlags.Bid:
case WaterfallPriceFlags.BidGTESettle: return bid_price;
case WaterfallPriceFlags.Ask:
case WaterfallPriceFlags.AskLTESettle: return ask_price;
case WaterfallPriceFlags.Settle: return settlement_price;
default: return double.NaN;
}
}
public bool Equals(PNLRowData other) =>
key == other.key &&
default_cpp == other.default_cpp &&
override_cpp == other.override_cpp &&
security.HasValue == other.security.HasValue &&
account_name == other.account_name &&
sod == other.sod &&
day == other.day &&
bid_price.Equals(other.bid_price) &&
ask_price.Equals(other.ask_price) &&
last_price.Equals(other.last_price) &&
mid_price.Equals(other.mid_price) &&
settlement_price.Equals(other.settlement_price) &&
calc_type.Equals(other.calc_type) &&
working.summary == other.working.summary &&
qty_conversion == other.qty_conversion;
public override bool Equals(object obj) =>
obj is PNLRowData other && Equals(other);
// assuming callers will not combine different values with the same key,
// the other components shouldn't be included in the hash calculation.
public override int GetHashCode() =>
key.GetHashCode();
public static bool operator ==(PNLRowData a, PNLRowData b) => a.Equals(b);
public static bool operator !=(PNLRowData a, PNLRowData b) => !a.Equals(b);
}
static class PNLDataExtensions {
public static string pnl_symbol(this SecurityImmutable security) {
return bts.api.ExchangeSpecificUtilities.get_pnl_symbol(security);
}
// while override_cpp could use double.NaN to indicate the lack of an override,
// the nullable double makes it clear that it is an optional value.
public static (double default_cpp, double? override_cpp) pnl_cpp_tuple(this PNLUnitImmutable pnl_unit, SecurityImmutable? security) =>
security.HasValue ?
pnl_cpp_tuple(pnl_unit, security.Value) :
(pnl_unit.CashValue, pnl_unit.HasCppOverride ? pnl_unit.CppOverride.Value : (double?)null);
public static (double default_cpp, double? override_cpp) pnl_cpp_tuple(this PNLUnitImmutable pnl_unit, SecurityImmutable security) {
// TODO: actual impl
double calced_pnl = bts.api.ExchangeSpecificUtilities.get_cpp(pnl_unit, security);
return (calced_pnl, pnl_unit.HasCppOverride ? pnl_unit.CppOverride.Value : (double?)null);
}
}
}
using bts.api;
using bts.api.position;
using bts.utils;
using mbts;
using mbts.analytics;
using mbts.spark;
using mbts.utils;
using Spark.Api.Data;
using Spark.Api.Position;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using PNLKey = bts.AccountSecurityKey;
namespace bts.shell.pnl {
public enum PNLCalculationType {
None,
Bid,
Ask,
Opposite,
Same,
[Description("Cascade")]
Waterfall,
Last,
Mid,
Settle
}
readonly struct PNLRowData : IEquatable<PNLRowData> {
public static readonly Func<PNLRowData, PNLKey> key_fn = d => d.key;
public static IComparer<PNLRowData> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowData>(d => d.key);
public readonly PNLKey key;
public readonly SecurityImmutable? security;
public readonly QuantityConversionData<double> qty_conversion;
public readonly uint account_id;
public readonly string account_name;
public readonly PNLCalculationType calc_type;
public readonly PositionPriceSizeWithGreeks sod;
public readonly PositionPriceSizeWithGreeks day;
public readonly WorkingOrderData working;
public readonly double
default_cpp,
bid_price,
ask_price,
last_price,
settlement_price;
public readonly double? override_cpp;
public PNLRowData(
PNLKey key,
double default_cpp = 1,
double? override_cpp = null,
double pnl_price = double.NaN,
double bid_price = 0,
double ask_price = 0,
double last_price = 0,
double settlement_price = 0,
PositionPriceSizeWithGreeks? sod = null,
PositionPriceSizeWithGreeks? day = null,
WorkingOrderData? working = null,
SecurityImmutable? security = null,
uint account_id = 0,
string account_name = null,
PNLCalculationType calc_type = PNLCalculationType.Mid,
QuantityConversionData<double>? qty_conversion = null) {
//
this.key = key;
this.default_cpp = default_cpp;
this.override_cpp = override_cpp;
this.pnl_price = pnl_price;
this.bid_price = bid_price;
this.ask_price = ask_price;
this.last_price = last_price;
this.settlement_price = settlement_price;
this.sod = sod ?? PositionPriceSizeWithGreeks.empty;
this.day = day ?? PositionPriceSizeWithGreeks.empty;
this.working = working ?? WorkingOrderData.Empty;
this.security = security;
this.account_id = account_id;
this.account_name = account_name;
this.calc_type = calc_type;
this.qty_conversion = qty_conversion ?? new();
}
public AccountTuple account => new(account_id, account_name);
public string symbol => security?.pnl_symbol() ?? string.Empty;
public ExpirationDate expiration =>
security?.normal_expirations()?.MinOrDefault(ExpirationDate.None) ?? ExpirationDate.None;
public double effective_cpp => override_cpp ?? default_cpp;
/// Market/Calc
public double mid_price => (bid_price + ask_price) / 2;
public double pnl_price { get; }
/// Sod
public double sod_pnl => new[] { sod_realized_pnl, sod_unrealized_pnl }.sum_real();
public double sod_long_pos => sod.long_pos;
public double sod_long_avg_price => sod.long_avg;
public double sod_long_avg_pnl => bid_price - sod.long_avg;
public double sod_short_pos => sod.short_pos;
public double sod_short_avg_price => sod.short_avg;
public double sod_short_avg_pnl => sod.short_avg - ask_price;
public double sod_net_pos => sod.net_pos;
public double sod_net_avg_price => sod.net_avg;
public double sod_unrealized_pnl => sod_long_pos > sod_short_pos
? (sod_long_pos - sod_short_pos) * effective_cpp * (bid_price - sod_long_avg_price)
: (sod_short_pos - sod_long_pos) * effective_cpp * (sod_short_avg_price - ask_price);
public double sod_realized_pnl => (sod_short_avg_price - sod_long_avg_price) * effective_cpp * Math.Min(sod_long_pos, sod_short_pos);
public double sod_delta => sod.delta;
public double sod_gamma => sod.gamma;
public double sod_vega => sod.vega;
public double sod_theta => sod.theta;
public double display_sod_net_pos => qty_conversion.to_display(sod_net_pos);
public double display_sod_long_pos => qty_conversion.to_display(sod_long_pos);
public double display_sod_short_pos => qty_conversion.to_display(sod_short_pos);
/// Day
public double day_pnl => new[] { day_realized_pnl + day_unrealized_pnl }.sum_real();
public double day_long_pos => day.long_pos;
public double day_long_avg_price => day.long_avg;
public double day_long_avg_pnl => bid_price - day.long_avg;
public double day_short_pos => day.short_pos;
public double day_short_avg_price => day.short_avg;
public double day_short_avg_pnl => day.short_avg - ask_price;
public double day_delta => day.delta;
public double day_gamma => day.gamma;
public double day_vega => day.vega;
public double day_theta => day.theta;
public bool has_day_trades => day.long_pos > 0 || day.short_pos > 0;
public double day_net_pos => day.net_pos;
public double day_net_avg => day.net_avg;
public double day_unrealized_pnl => day_long_pos > day_short_pos
? (day_long_pos - day_short_pos) * effective_cpp * (bid_price - day_long_avg_price)
: (day_short_pos - day_long_pos) * effective_cpp * (day_short_avg_price - ask_price);
public double day_realized_pnl => (day_short_avg_price - day_long_avg_price) * effective_cpp * Math.Min(day_long_pos, day_short_pos);
public double display_day_net_pos => qty_conversion.to_display(day_net_pos);
public double display_day_long_pos => qty_conversion.to_display(day_long_pos);
public double display_day_short_pos => qty_conversion.to_display(day_short_pos);
/// Total
public double total_pnl => new[] { sod_pnl, day_pnl }.sum_real();
public double total_long_pos => new[] { sod.long_pos, day.long_pos }.sum_real();
public double total_long_avg_price =>
MathUtils.weighted_average(
sod_long_avg_price.if_nan(0),
sod_long_pos.if_nan(0),
day_long_avg_price.if_nan(0),
day_long_pos.if_nan(0));
public double total_long_avg_pnl => new[] { sod_long_avg_pnl, day_long_avg_pnl }.sum_real();
public double total_short_pos => new[] { sod.short_pos, day.short_pos }.sum_real();
public double total_short_avg_price =>
MathUtils.weighted_average(
sod_short_avg_price.if_nan(0),
sod_short_pos.if_nan(0),
day_short_avg_price.if_nan(0),
day_short_pos.if_nan(0));
public double total_short_avg_pnl => new[] { sod_short_avg_pnl, day_short_avg_pnl }.sum_real();
public double total_net_pos => new[] { sod.long_pos, -sod.short_pos, day.long_pos, -day.short_pos }.Where(n => n.is_real()).Sum();
public double total_unrealized_pnl => new[] { sod_unrealized_pnl, day_unrealized_pnl }.sum_real();
public double total_realized_pnl => new[] { sod_realized_pnl, day_realized_pnl }.sum_real();
public double total_delta => sod.delta.add_real(day.delta);
public double total_gamma => sod.gamma.add_real(day.gamma);
public double total_vega => sod.vega.add_real(day.vega);
public double total_theta => sod.theta.add_real(day.theta);
public double display_total_net_pos => qty_conversion.to_display(total_net_pos);
public double display_total_long_pos => qty_conversion.to_display(total_long_pos);
public double display_total_short_pos => qty_conversion.to_display(total_short_pos);
/// Working
public double working_long_pos => working.long_data.working_total;
public double working_long_avg_price => effective_cpp * working.long_data.avg_price;
public double working_long_top_price => effective_cpp * working.long_data.top_price;
public double working_short_pos => working.short_data.working_total;
public double working_short_avg_price => effective_cpp * working.short_data.avg_price;
public double working_short_top_price => effective_cpp * working.short_data.top_price;
public double working_net_pos => working_long_pos - working_short_pos;
public double display_working_net_pos => qty_conversion.to_display(working_net_pos);
public double display_working_long_pos => qty_conversion.to_display(working_long_pos);
public double display_working_short_pos => qty_conversion.to_display(working_short_pos);
public static PNLRowData from_pnl_unit(IPNLUnit u) =>
new(
u.id(),
default_cpp: u.security.spec_or_default().CashPerPoint,
override_cpp: null,
pnl_price: u.theo,
bid_price: double.NaN,
ask_price: double.NaN,
last_price: double.NaN,
settlement_price: u.settle_price,
// Now computed as the sum: sod_realized_pnl + sod_unrealized_pnl
// sod_pnl: u.start_pnl,
sod: new() {
price_size = new(
u.start_long_pos,
u.start_long_avg_cost,
u.start_short_pos,
-u.start_short_avg_cost),
greeks = new() {
delta = u.start_delta,
gamma = u.start_gamma,
vega = u.start_vega,
theta = u.start_theta
}
},
// Now computed as the sum: day_realized_pnl + day_unrealized_pnl
// day_pnl: u.day_pnl,
day: new() {
price_size = new(
u.day_long_pos,
u.day_long_avg_cost,
u.day_short_pos,
-u.day_short_avg_cost),
greeks = new() {
delta = u.day_delta,
gamma = u.day_gamma,
vega = u.day_vega,
theta = u.day_theta
}
},
working: null,
security: u.security,
account_id: u.account_id,
account_name: u.account,
calc_type: PNLCalculationType.Mid,
qty_conversion: null);
public PNLRowData with(
SecurityImmutable? security = null,
uint? account_id = null,
string account_name = null,
WorkingOrderData? working = null,
PNLCalculationType? calc_type = null,
double? bid_price = null,
double? ask_price = null) =>
new PNLRowData(
key,
default_cpp,
override_cpp,
pnl_price,
bid_price ?? this.bid_price,
ask_price ?? this.ask_price,
last_price,
settlement_price,
sod,
day,
working ?? this.working,
security ?? this.security,
account_id ?? this.account_id,
account_name ?? this.account_name,
calc_type ?? this.calc_type,
qty_conversion: qty_conversion);
public PNLRowData with(SecurityMD smd) {
ulong time = 0;
if (!smd.md.get_bbo(ref time, out var bid, out var ask)) {
return this;
}
return with(security: smd.security, bid_price: bid.price, ask_price: ask.price);
}
public bool Equals(PNLRowData other) =>
key == other.key &&
default_cpp == other.default_cpp &&
override_cpp == other.override_cpp &&
security.HasValue == other.security.HasValue &&
account_name == other.account_name &&
sod == other.sod &&
day == other.day &&
bid_price.Equals(other.bid_price) &&
ask_price.Equals(other.ask_price) &&
last_price.Equals(other.last_price) &&
mid_price.Equals(other.mid_price) &&
settlement_price.Equals(other.settlement_price) &&
calc_type.Equals(other.calc_type) &&
working.summary == other.working.summary &&
qty_conversion == other.qty_conversion;
public override bool Equals(object obj) =>
obj is PNLRowData other && Equals(other);
// assuming callers will not combine different values with the same key,
// the other components shouldn't be included in the hash calculation.
public override int GetHashCode() =>
key.GetHashCode();
public static bool operator ==(PNLRowData a, PNLRowData b) => a.Equals(b);
public static bool operator !=(PNLRowData a, PNLRowData b) => !a.Equals(b);
}
static class PNLDataExtensions {
public static string pnl_symbol(this SecurityImmutable security) {
return bts.api.ExchangeSpecificUtilities.get_pnl_symbol(security);
}
// while override_cpp could use double.NaN to indicate the lack of an override,
// the nullable double makes it clear that it is an optional value.
public static (double default_cpp, double? override_cpp) pnl_cpp_tuple(this PNLUnitImmutable pnl_unit, SecurityImmutable? security) =>
security.HasValue ?
pnl_cpp_tuple(pnl_unit, security.Value) :
(pnl_unit.CashValue, pnl_unit.HasCppOverride ? pnl_unit.CppOverride.Value : (double?)null);
public static (double default_cpp, double? override_cpp) pnl_cpp_tuple(this PNLUnitImmutable pnl_unit, SecurityImmutable security) {
// TODO: actual impl
double calced_pnl = bts.api.ExchangeSpecificUtilities.get_cpp(pnl_unit, security);
return (calced_pnl, pnl_unit.HasCppOverride ? pnl_unit.CppOverride.Value : (double?)null);
}
}
}
@@ -38,6 +38,8 @@ namespace bts.shell.pnl {
// sod
public decimal sod_pnl { get; init; }
public decimal sod_realized_pnl { get; init; }
public decimal sod_unrealized_pnl { get; init; }
public decimal sod_long_pos { get; init; }
public decimal sod_short_pos { get; init; }
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
@@ -51,6 +53,8 @@ namespace bts.shell.pnl {
// day
public decimal day_pnl { get; init; }
public decimal day_realized_pnl { get; init; }
public decimal day_unrealized_pnl { get; init; }
public decimal day_long_pos { get; init; }
public decimal day_short_pos { get; init; }
public decimal day_net_pos => day_long_pos - day_short_pos;
@@ -64,6 +68,8 @@ namespace bts.shell.pnl {
// total
public decimal total_pnl => sod_pnl + day_pnl;
public decimal total_realized_pnl => sod_realized_pnl + day_realized_pnl;
public decimal total_unrealized_pnl => sod_unrealized_pnl + day_unrealized_pnl;
public decimal total_net_pos => total_long_pos - total_short_pos;
public decimal total_long_pos => sod_long_pos + day_long_pos;
public decimal total_short_pos => sod_short_pos + day_short_pos;
@@ -86,6 +92,8 @@ namespace bts.shell.pnl {
public bool Equals(PNLNormalizedData other) =>
// sod
sod_pnl == other.sod_pnl &&
sod_realized_pnl == other.sod_realized_pnl &&
sod_unrealized_pnl == other.sod_unrealized_pnl &&
sod_long_pos == other.sod_long_pos &&
sod_short_pos == other.sod_short_pos &&
sod_delta == other.sod_delta &&
@@ -97,6 +105,8 @@ namespace bts.shell.pnl {
// day
day_pnl == other.day_pnl &&
day_realized_pnl == other.day_realized_pnl &&
day_unrealized_pnl == other.day_unrealized_pnl &&
day_long_pos == other.day_long_pos &&
day_short_pos == other.day_short_pos &&
day_delta == other.day_delta &&
@@ -154,6 +164,8 @@ namespace bts.shell.pnl {
new() {
// sod
sod_pnl = op(a.sod_pnl, b.sod_pnl),
sod_realized_pnl = op(a.sod_realized_pnl, b.sod_realized_pnl),
sod_unrealized_pnl = op(a.sod_unrealized_pnl, b.sod_unrealized_pnl),
sod_long_pos = op(a.sod_long_pos, b.sod_long_pos),
sod_short_pos = op(a.sod_short_pos, b.sod_short_pos),
sod_delta = op(a.sod_delta, b.sod_delta),
@@ -165,6 +177,8 @@ namespace bts.shell.pnl {
// day
day_pnl = op(a.day_pnl, b.day_pnl),
day_realized_pnl = op(a.day_realized_pnl, b.day_realized_pnl),
day_unrealized_pnl = op(a.day_unrealized_pnl, b.day_unrealized_pnl),
day_long_pos = op(a.day_long_pos, b.day_long_pos),
day_short_pos = op(a.day_short_pos, b.day_short_pos),
day_delta = op(a.day_delta, b.day_delta),
@@ -39,6 +39,9 @@ namespace bts.shell {
class PNLWindow : SparkWindow<PNLSaveState>, ISharedWindow {
public const string WINDOW_TYPE = "PNL";
public const string WINDOW_TITLE = "P&L Summary";
// Cpp overrides aren't respected in pnl calc. Hide the "Override Cpp"
// menu item until we fix it.
const bool CAN_OVERRIDE_CPP = false;
static int _window_id_counter = 0;
@@ -137,6 +140,10 @@ namespace bts.shell {
_cached_columns = ImmutableList<DIYGrid.ColumnBase>.Empty.AddRange(_view._grid.Columns);
_commands = new() {
SparkCommands.ShowMarketView.complete(() => {
var rows = _vm.selected_rows.null_if_empty() ?? _vm.rows;
return SecurityCriteria.from_keys(rows.Select(r => r.security_key.value).Distinct());
}),
new SparkCommandCommand(
"Collapse All", Icons.RightArrow, new Command(() => _vm.set_all_expanded(false))),
new SparkCommandCommand(
@@ -152,7 +159,7 @@ namespace bts.shell {
},
new SparkCommandCommand(
"Override CPP...", Icons.CurrencyExchange, new Command(_override_cpp)) {
show_in_context_menu = true,
show_in_context_menu = CAN_OVERRIDE_CPP,
show_in_toolbar = false
},
SparkCommands.window_settings_command(new DialogCommand(_mk_settings_win)),
@@ -211,13 +218,16 @@ namespace bts.shell {
.ToArray();
var calc_type = _vm.calculation_type;
var state = _settings_manager.instance_settings;
var menu_display = _menu_display_vm?.create();
if (state is null) {
// don't use defaults if the user manipulated the window state outside of the
// settings dialog (e.g., resized column(s) via drag, or changed sort/filter)
var defaults = _settings_manager.default_settings;
if (calc_type == defaults.calculation_type &&
defaults.sorting_criteria.SequenceEqual(sorting_criteria) &&
defaults.grid.columns.SequenceEqual(cols) && defaults.filters.safe_equals(filters)) {
defaults.grid.columns.SequenceEqual(cols) &&
defaults.filters.safe_equals(filters) &&
(!menu_display.HasValue || menu_display.Value.Equals(defaults.menu_display))) {
// the user's current state still matches the defaults (i.e., no outside
// manipulation was performed); return null so this window will use the latest
// default settings when restored
@@ -234,7 +244,7 @@ namespace bts.shell {
state.grid.with(columns: cols, background: _vm.grid_background_color.to_uint()),
calc_type,
_vm.header_row_display,
menu_display: _menu_display_vm?.create());
menu_display: menu_display);
return new PNLSaveState { use_defaults = false, settings = state };
}
@@ -98,7 +98,7 @@ namespace bts.shell {
static bool _apply(ref PNLSettings settings, IEnumerable<PNLAnimationTriggerData> actual_triggers, IEnumerable<PNLAnimationTriggerData> default_triggers, Func<PNLSettings, PNLAnimationTriggerData[], PNLSettings> set_triggers) {
if (_apply(actual_triggers, default_triggers, out var result)) {
settings = set_triggers(settings, actual_triggers.Concat(result).ToArray());
settings = set_triggers(settings, result.ToArray());
return true;
}
return false;
@@ -9,6 +9,8 @@ namespace bts.shell.pnl {
using ProtoBuf;
using Spark.Api.Data;
using System;
using System.Linq;
@@ -33,8 +35,8 @@ namespace bts.shell.pnl {
// expiration
[ProtoMember(53)]
DateTime[] _expiration;
public DateTime[] expiration {
ExpirationDate[] _expiration;
public ExpirationDate[] expiration {
get => _expiration.empty_if_null();
set => _expiration = value;
}
@@ -65,16 +67,24 @@ namespace bts.shell.pnl {
/// sod
[ProtoMember(8)]
public DoubleMinMaxState sod_pnl { get; set; }
[ProtoMember(54)]
public DoubleMinMaxState sod_realized_pnl { get; set; }
[ProtoMember(55)]
public DoubleMinMaxState sod_unrealized_pnl { get; set; }
// sod long
[ProtoMember(9)]
public DoubleMinMaxState sod_long_pos { get; set; }
[ProtoMember(10)]
public DoubleMinMaxState sod_long_avg_price { get; set; }
[ProtoMember(60)]
public DoubleMinMaxState sod_long_avg_pnl { get; set; }
// sod short
[ProtoMember(11)]
public DoubleMinMaxState sod_short_pos { get; set; }
[ProtoMember(12)]
public DoubleMinMaxState sod_short_avg_price { get; set; }
[ProtoMember(61)]
public DoubleMinMaxState sod_short_avg_pnl { get; set; }
// sod net
[ProtoMember(13)]
public DoubleMinMaxState sod_net_pos { get; set; }
@@ -92,16 +102,24 @@ namespace bts.shell.pnl {
/// day
[ProtoMember(14)]
public DoubleMinMaxState day_pnl { get; set; }
[ProtoMember(56)]
public DoubleMinMaxState day_realized_pnl { get; set; }
[ProtoMember(57)]
public DoubleMinMaxState day_unrealized_pnl { get; set; }
// day long
[ProtoMember(15)]
public DoubleMinMaxState day_long_pos { get; set; }
[ProtoMember(16)]
public DoubleMinMaxState day_long_avg_price { get; set; }
[ProtoMember(62)]
public DoubleMinMaxState day_long_avg_pnl { get; set; }
// day short
[ProtoMember(17)]
public DoubleMinMaxState day_short_pos { get; set; }
[ProtoMember(18)]
public DoubleMinMaxState day_short_avg_price { get; set; }
[ProtoMember(63)]
public DoubleMinMaxState day_short_avg_pnl { get; set; }
// day net
[ProtoMember(19)]
public DoubleMinMaxState day_net_pos { get; set; }
@@ -149,16 +167,24 @@ namespace bts.shell.pnl {
/// total
[ProtoMember(29)]
public DoubleMinMaxState total_pnl { get; set; }
[ProtoMember(58)]
public DoubleMinMaxState total_realized_pnl { get; set; }
[ProtoMember(59)]
public DoubleMinMaxState total_unrealized_pnl { get; set; }
// total long
[ProtoMember(30)]
public DoubleMinMaxState total_long_pos { get; set; }
[ProtoMember(31)]
public DoubleMinMaxState total_long_avg_price { get; set; }
[ProtoMember(64)]
public DoubleMinMaxState total_long_avg_pnl { get; set; }
// total short
[ProtoMember(32)]
public DoubleMinMaxState total_short_pos { get; set; }
[ProtoMember(33)]
public DoubleMinMaxState total_short_avg_price { get; set; }
[ProtoMember(65)]
public DoubleMinMaxState total_short_avg_pnl { get; set; }
// total net
[ProtoMember(34)]
public DoubleMinMaxState total_net_pos { get; set; }
@@ -199,6 +225,8 @@ namespace bts.shell.pnl {
cpp.safe_equals(other.cpp) &&
/// sod
sod_pnl.safe_equals(other.sod_pnl) &&
sod_realized_pnl.safe_equals(other.sod_realized_pnl) &&
sod_unrealized_pnl.safe_equals(other.sod_unrealized_pnl) &&
sod_long_pos.safe_equals(other.sod_long_pos) &&
sod_long_avg_price.safe_equals(other.sod_long_avg_price) &&
sod_short_pos.safe_equals(other.sod_short_pos) &&
@@ -210,6 +238,8 @@ namespace bts.shell.pnl {
sod_theta.safe_equals(other.sod_theta) &&
/// day
day_pnl.safe_equals(other.day_pnl) &&
day_realized_pnl.safe_equals(other.day_realized_pnl) &&
day_unrealized_pnl.safe_equals(other.day_unrealized_pnl) &&
day_long_pos.safe_equals(other.day_long_pos) &&
day_long_avg_price.safe_equals(other.day_long_avg_price) &&
day_short_pos.safe_equals(other.day_short_pos) &&
@@ -232,6 +262,8 @@ namespace bts.shell.pnl {
working_net_pos.safe_equals(other.working_net_pos) &&
/// total
total_pnl.safe_equals(other.total_pnl) &&
total_realized_pnl.safe_equals(other.total_realized_pnl) &&
total_unrealized_pnl.safe_equals(other.total_unrealized_pnl) &&
total_long_pos.safe_equals(other.total_long_pos) &&
total_long_avg_price.safe_equals(other.total_long_avg_price) &&
total_short_pos.safe_equals(other.total_short_pos) &&
@@ -270,6 +302,8 @@ namespace bts.shell.pnl {
bd.add_numeric(fs.cpp, d => d.effective_cpp);
/// sod
bd.add_numeric(fs.sod_pnl, d => d.sod_pnl);
bd.add_numeric(fs.sod_realized_pnl, d => d.sod_realized_pnl);
bd.add_numeric(fs.sod_unrealized_pnl, d => d.sod_unrealized_pnl);
bd.add_numeric(fs.sod_long_pos, d => d.sod_long_pos);
bd.add_numeric(fs.sod_long_avg_price, d => d.sod_long_avg_price);
bd.add_numeric(fs.sod_short_pos, d => d.sod_short_pos);
@@ -281,6 +315,8 @@ namespace bts.shell.pnl {
bd.add_numeric(fs.sod_theta, d => d.sod_theta);
/// day
bd.add_numeric(fs.day_pnl, d => d.day_pnl);
bd.add_numeric(fs.day_realized_pnl, d => d.day_realized_pnl);
bd.add_numeric(fs.day_unrealized_pnl, d => d.day_unrealized_pnl);
bd.add_numeric(fs.day_long_pos, d => d.day_long_pos);
bd.add_numeric(fs.day_long_avg_price, d => d.day_long_avg_price);
bd.add_numeric(fs.day_short_pos, d => d.day_short_pos);
@@ -299,6 +335,8 @@ namespace bts.shell.pnl {
bd.add_numeric(fs.working_net_pos, d => d.working_net_pos);
/// total
bd.add_numeric(fs.total_pnl, d => d.total_pnl);
bd.add_numeric(fs.total_realized_pnl, d => d.total_realized_pnl);
bd.add_numeric(fs.total_unrealized_pnl, d => d.total_unrealized_pnl);
bd.add_numeric(fs.total_long_pos, d => d.total_long_pos);
bd.add_numeric(fs.total_long_avg_price, d => d.total_long_avg_price);
bd.add_numeric(fs.total_short_pos, d => d.total_short_pos);
@@ -9,9 +9,13 @@ using mbts.user.settings;
using ProtoBuf;
using Spark.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Media;
@@ -110,7 +114,22 @@ namespace bts.shell.pnl {
GridSettings _grid_backing;
PNLCellAppearanceProto _grid_cell_appearance_backing;
public static PNLSettings Default =>
static LazyOnce<PNLSettings> defaultProfile;
static PNLSettings() {
defaultProfile = new(() => {
var binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var store = ShellWindowSettingsDirectoryStore.Create<PNLSettings>(
Path.Combine(binDir, "ShellWindowSettings", "Pnl"), "*." + mbts.spark.UserSettings.FileExtensions.PNL);
return store.Profiles.GetValueOrDefault("Default")
?? store.Profiles.Values.FirstOrDefault()
?? historicDefault;
});
}
public static PNLSettings Default => defaultProfile.Value;
static PNLSettings historicDefault =>
new PNLSettings {
_sorting_criteria = new[] {
new SortingCriteria(nameof(Columns.Account), true),
@@ -192,6 +211,10 @@ namespace bts.shell.pnl {
yield return _trigger($"{nameof(PNLRowData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, font_weight: FontWeights.Bold, column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.TotalNetPos));
yield return _trigger($"{nameof(PNLRowData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.TotalNetPos));
yield return _trigger($"{nameof(PNLRowData.total_realized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, font_weight: FontWeights.Bold, column: Columns.TotalRealizedPNL));
yield return _trigger($"{nameof(PNLRowData.total_realized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, font_weight: FontWeights.Bold, column: Columns.TotalRealizedPNL));
yield return _trigger($"{nameof(PNLRowData.total_unrealized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, font_weight: FontWeights.Bold, column: Columns.TotalUnrealizedPNL));
yield return _trigger($"{nameof(PNLRowData.total_unrealized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, font_weight: FontWeights.Bold, column: Columns.TotalUnrealizedPNL));
// working
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.WorkingNetPos));
yield return _trigger($"{nameof(PNLRowData.working_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.WorkingNetPos));
@@ -202,16 +225,28 @@ namespace bts.shell.pnl {
get {
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_realized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.SODRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_realized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.SODRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_unrealized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.SODUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_unrealized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.SODUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.SODNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.SODNetPos));
// day
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_realized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.DayRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_realized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.DayRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_unrealized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.DayUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_unrealized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.DayUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.DayNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.DayNetPos));
// total
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_realized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.TotalRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_realized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.TotalRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_unrealized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.TotalUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_unrealized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.TotalUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.TotalNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.TotalNetPos));
// working
@@ -224,16 +259,28 @@ namespace bts.shell.pnl {
get {
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.SODPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_realized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.SODRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_realized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.SODRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_unrealized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.SODUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_unrealized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.SODUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.SODNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.sod_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.SODNetPos));
// day
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.DayPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_realized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.DayRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_realized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.DayRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_unrealized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.DayUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_unrealized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.DayUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.DayNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.day_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.DayNetPos));
// total
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.TotalPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_realized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.TotalRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_realized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.TotalRealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_unrealized_pnl)} > 0", _animation(foreground: BTSColors.PNL.profit, column: Columns.TotalUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_unrealized_pnl)} < 0", _animation(foreground: BTSColors.PNL.loss, column: Columns.TotalUnrealizedPNL));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} > 0", _animation(foreground: BTSColors.MarketData.bid, column: Columns.TotalNetPos));
yield return _trigger($"{nameof(PNLNormalizedData.total_net_pos)} < 0", _animation(foreground: BTSColors.MarketData.ask, column: Columns.TotalNetPos));
// working
@@ -392,10 +439,14 @@ namespace bts.shell.pnl {
public static GridColumnSettings cpp => new GridColumnSettings(nameof(Columns.CPP), "CPP", false, width: 50, horizontal_alignment: HorizontalAlignment.Right);
// sod
public static GridColumnSettings sod_pnl => new GridColumnSettings(nameof(Columns.SODPNL), Headers.SOD.PNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_realized_pnl => new GridColumnSettings(nameof(Columns.SODRealizedPNL), Headers.SOD.RealizedPNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_unrealized_pnl => new GridColumnSettings(nameof(Columns.SODUnrealizedPNL), Headers.SOD.UnrealizedPNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_long_pos => new GridColumnSettings(nameof(Columns.SODLongPos), Headers.SOD.Long, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_long_avg_price => new GridColumnSettings(nameof(Columns.SODLongAvgPrice), Headers.SOD.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_long_avg_pnl => new(nameof(Columns.SODLongAvgPNL), Headers.SOD.LongAvgPNL, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_short_pos => new GridColumnSettings(nameof(Columns.SODShortPos), Headers.SOD.Short, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_short_avg_price => new GridColumnSettings(nameof(Columns.SODShortAvgPrice), Headers.SOD.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_short_avg_pnl => new(nameof(Columns.SODShortAvgPNL), Headers.SOD.ShortAvgPNL, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_net_pos => new GridColumnSettings(nameof(Columns.SODNetPos), Headers.SOD.Net, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings sod_delta => new(
@@ -428,10 +479,14 @@ namespace bts.shell.pnl {
// day
public static GridColumnSettings day_pnl => new GridColumnSettings(nameof(Columns.DayPNL), Headers.Day.PNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_realized_pnl => new GridColumnSettings(nameof(Columns.DayRealizedPNL), Headers.Day.RealizedPNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_unrealized_pnl => new GridColumnSettings(nameof(Columns.DayUnrealizedPNL), Headers.Day.UnrealizedPNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_long_pos => new GridColumnSettings(nameof(Columns.DayLongPos), Headers.Day.Long, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_long_avg_price => new GridColumnSettings(nameof(Columns.DayLongAvgPrice), Headers.Day.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_long_avg_pnl => new GridColumnSettings(nameof(Columns.DayLongAvgPNL), Headers.Day.LongAvgPNL, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_short_pos => new GridColumnSettings(nameof(Columns.DayShortPos), Headers.Day.Short, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_short_avg_price => new GridColumnSettings(nameof(Columns.DayShortAvgPrice), Headers.Day.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_short_avg_pnl => new GridColumnSettings(nameof(Columns.DayShortAvgPNL), Headers.Day.ShortAvgPNL, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_net_pos => new GridColumnSettings(nameof(Columns.DayNetPos), Headers.Day.Net, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings day_delta => new(
@@ -471,10 +526,14 @@ namespace bts.shell.pnl {
public static GridColumnSettings working_net_pos => new GridColumnSettings(nameof(Columns.WorkingNetPos), Headers.Working.Net, width: Widths.WorkingQty, horizontal_alignment: HorizontalAlignment.Right);
// total
public static GridColumnSettings total_pnl => new GridColumnSettings(nameof(Columns.TotalPNL), Headers.Total.PNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_realized_pnl => new GridColumnSettings(nameof(Columns.TotalRealizedPNL), Headers.Total.RealizedPNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_unrealized_pnl => new GridColumnSettings(nameof(Columns.TotalUnrealizedPNL), Headers.Total.UnrealizedPNL, width: Widths.PriceBig, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_long_pos => new GridColumnSettings(nameof(Columns.TotalLongPos), Headers.Total.Long, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_long_avg_price => new GridColumnSettings(nameof(Columns.TotalLongAvgPrice), Headers.Total.LongAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_long_avg_pnl => new(nameof(Columns.TotalLongAvgPNL), Headers.Total.LongAvgPNL, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_short_pos => new GridColumnSettings(nameof(Columns.TotalShortPos), Headers.Total.Short, false, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_short_avg_price => new GridColumnSettings(nameof(Columns.TotalShortAvgPrice), Headers.Total.ShortAvg, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_short_avg_pnl => new(nameof(Columns.TotalShortAvgPNL), Headers.Total.ShortAvgPNL, false, width: Widths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_net_pos => new GridColumnSettings(nameof(Columns.TotalNetPos), Headers.Total.Net, width: Widths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings total_delta => new(
@@ -528,28 +587,40 @@ namespace bts.shell.pnl {
// pnl
yield return sod_pnl;
yield return sod_realized_pnl;
yield return sod_unrealized_pnl;
yield return day_pnl;
yield return day_realized_pnl;
yield return day_unrealized_pnl;
yield return total_pnl;
yield return total_realized_pnl;
yield return total_unrealized_pnl;
// sod pos, price
yield return sod_long_pos; // hidden
yield return sod_long_avg_price; // hidden
yield return sod_long_avg_pnl; // hidden
yield return sod_short_pos; // hidden
yield return sod_short_avg_price; // hidden
yield return sod_short_avg_pnl; // hidden
yield return sod_net_pos;
// day pos, price
yield return day_long_pos; // hidden
yield return day_long_avg_price; // hidden
yield return day_long_avg_pnl; // hidden
yield return day_short_pos; // hidden
yield return day_short_avg_price; // hidden
yield return day_short_avg_pnl; // hidden
yield return day_net_pos;
// total pos, price
yield return total_long_pos; // hidden
yield return total_long_avg_price; // hidden
yield return total_long_avg_pnl; // hidden
yield return total_short_pos; // hidden
yield return total_short_avg_price; // hidden
yield return total_short_avg_pnl; // hidden
yield return total_net_pos;
// sod greeks
@@ -34,6 +34,8 @@ namespace bts.shell.pnl {
_cell_map.set_appearances(default_appearances);
// sod
sod_pnl = _cell_map.add_pnl_decimal(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_realized_pnl = _cell_map.add_pnl_decimal(ColumnKeys.SODRealizedPNL, d => d.sod_realized_pnl);
sod_unrealized_pnl = _cell_map.add_pnl_decimal(ColumnKeys.SODUnrealizedPNL, d => d.sod_unrealized_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
@@ -44,6 +46,8 @@ namespace bts.shell.pnl {
// day
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
day_realized_pnl = _cell_map.add_pnl_decimal(ColumnKeys.DayRealizedPNL, d => d.day_realized_pnl);
day_unrealized_pnl = _cell_map.add_pnl_decimal(ColumnKeys.DayUnrealizedPNL, d => d.day_unrealized_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
@@ -58,6 +62,8 @@ namespace bts.shell.pnl {
working_short_pos = _cell_map.add_short_pos(ColumnKeys.WorkingShortPos, r => r.working_short_pos);
// total
total_pnl = _cell_map.add_pnl_decimal(ColumnKeys.TotalPNL, d => d.total_pnl);
total_realized_pnl = _cell_map.add_pnl_decimal(ColumnKeys.TotalRealizedPNL, d => d.total_realized_pnl);
total_unrealized_pnl = _cell_map.add_pnl_decimal(ColumnKeys.TotalUnrealizedPNL, d => d.total_unrealized_pnl);
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
@@ -88,6 +94,8 @@ namespace bts.shell.pnl {
// day
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_realized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_unrealized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
@@ -98,6 +106,8 @@ namespace bts.shell.pnl {
// sod
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_realized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_unrealized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
@@ -108,6 +118,8 @@ namespace bts.shell.pnl {
// total
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_realized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_unrealized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
@@ -6,6 +6,7 @@ using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using Spark.Api.Data;
using Spark.Core.Collections;
using Spark.Core.Data;
@@ -49,9 +50,7 @@ namespace bts.shell.pnl {
expiration_cell = _cell_map.add(
ColumnKeys.Expiration,
d => d.expiration,
e => e == DateTime.MinValue
? "N/A"
: SecurityFormatter.format_expiration(e));
e => e.ToString());
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.symbol);
security_cell = _cell_map.add(
ColumnKeys.Security,
@@ -63,14 +62,18 @@ namespace bts.shell.pnl {
cpp = _cell_map.add(ColumnKeys.CPP, d => d.effective_cpp);
// sod
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_realized_pnl = _cell_map.add_pnl_double(ColumnKeys.SODRealizedPNL, d => d.sod_realized_pnl);
sod_unrealized_pnl = _cell_map.add_pnl_double(ColumnKeys.SODUnrealizedPNL, d => d.sod_unrealized_pnl);
sod_net_pos = _cell_map.add_net_pos(
ColumnKeys.SODNetPos, d => d.display_sod_net_pos, fmt_qty);
sod_long_pos = _cell_map.add_long_pos(
ColumnKeys.SODLongPos, d => d.display_sod_long_pos, fmt_qty);
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price);
sod_long_avg_pnl = _cell_map.add_long_price(ColumnKeys.SODLongAvgPNL, d => d.sod_long_avg_pnl);
sod_short_pos = _cell_map.add_short_pos(
ColumnKeys.SODShortPos, d => d.display_sod_short_pos, fmt_qty);
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price);
sod_short_avg_pnl = _cell_map.add_short_price(ColumnKeys.SODShortAvgPNL, d => d.sod_short_avg_pnl);
sod_delta = _cell_map.add_pnl_double(ColumnKeys.SODDelta, d => d.sod_delta, fmt_greek);
sod_gamma = _cell_map.add_pnl_double(ColumnKeys.SODGamma, d => d.sod_gamma, fmt_greek);
sod_vega = _cell_map.add_pnl_double(ColumnKeys.SODVega, d => d.sod_vega, fmt_greek);
@@ -78,14 +81,18 @@ namespace bts.shell.pnl {
// day
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
day_realized_pnl = _cell_map.add_pnl_double(ColumnKeys.DayRealizedPNL, d => d.day_realized_pnl);
day_unrealized_pnl = _cell_map.add_pnl_double(ColumnKeys.DayUnrealizedPNL, d => d.day_unrealized_pnl);
day_net_pos = _cell_map.add_net_pos(
ColumnKeys.DayNetPos, d => d.display_day_net_pos, fmt_qty);
day_long_pos = _cell_map.add_long_pos(
ColumnKeys.DayLongPos, d => d.display_day_long_pos, fmt_qty);
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price);
day_long_avg_pnl = _cell_map.add_long_price(ColumnKeys.DayLongAvgPNL, d => d.day_long_avg_pnl);
day_short_pos = _cell_map.add_short_pos(
ColumnKeys.DayShortPos, d => d.display_day_short_pos, fmt_qty);
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price);
day_short_avg_pnl = _cell_map.add_short_price(ColumnKeys.DayShortAvgPNL, d => d.day_short_avg_pnl);
day_delta = _cell_map.add_pnl_double(ColumnKeys.DayDelta, d => d.day_delta, fmt_greek);
day_gamma = _cell_map.add_pnl_double(ColumnKeys.DayGamma, d => d.day_gamma, fmt_greek);
day_vega = _cell_map.add_pnl_double(ColumnKeys.DayVega, d => d.day_vega, fmt_greek);
@@ -106,14 +113,18 @@ namespace bts.shell.pnl {
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price);
// total
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
total_realized_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalRealizedPNL, d => d.total_realized_pnl);
total_unrealized_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalUnrealizedPNL, d => d.total_unrealized_pnl);
total_net_pos = _cell_map.add_net_pos(
ColumnKeys.TotalNetPos, d => d.display_total_net_pos, fmt_qty);
total_long_pos = _cell_map.add_long_pos(
ColumnKeys.TotalLongPos, d => d.display_total_long_pos, fmt_qty);
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price);
total_long_avg_pnl = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPNL, d => d.total_long_avg_pnl);
total_short_pos = _cell_map.add_short_pos(
ColumnKeys.TotalShortPos, d => d.display_total_short_pos, fmt_qty);
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price);
total_short_avg_pnl = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPNL, d => d.total_short_avg_pnl);
total_delta = _cell_map.add_pnl_double(
ColumnKeys.TotalDelta, d => d.total_delta, fmt_greek);
total_gamma = _cell_map.add_pnl_double(
@@ -192,6 +203,8 @@ namespace bts.shell.pnl {
var nd = new PNLNormalizedData() {
// sod
sod_pnl = sod_pnl.value.round_to_decimal(),
sod_realized_pnl = sod_realized_pnl.value.round_to_decimal(),
sod_unrealized_pnl = sod_unrealized_pnl.value.round_to_decimal(),
sod_long_pos = sod_long,
sod_short_pos = sod_short,
sod_delta = sod_delta,
@@ -203,6 +216,8 @@ namespace bts.shell.pnl {
// day
day_pnl = day_pnl.value.round_to_decimal(),
day_realized_pnl = day_realized_pnl.value.round_to_decimal(),
day_unrealized_pnl = day_unrealized_pnl.value.round_to_decimal(),
day_long_pos = day_long,
day_short_pos = day_short,
day_delta = day_delta,
@@ -230,13 +245,13 @@ namespace bts.shell.pnl {
public string account_name => data.account_name;
public Venue exchange => security?.Exchange ?? Venue.Unset;
public string symbol => security.symbol_or_default();
public DateTime expiration => data.expiration;
public ExpirationDate expiration => data.expiration;
public Security.ContractOneofCase security_type => security?.ContractCase ?? Security.ContractOneofCase.None;
public SecurityImmutable? security => data.security;
// cells
public PNLCellVM<PNLRowData, AccountTuple> account_cell { get; }
public PNLCellVM<PNLRowData, Venue> exchange_cell { get; }
public PNLCellVM<PNLRowData, DateTime> expiration_cell { get; }
public PNLCellVM<PNLRowData, ExpirationDate> expiration_cell { get; }
public PNLCellVM<PNLRowData, string> symbol_cell { get; }
public PNLCellVM<PNLRowData, Security.ContractOneofCase> security_type_cell { get; }
public PNLCellVM<PNLRowData, SecurityImmutable?> security_cell { get; }
@@ -244,11 +259,15 @@ namespace bts.shell.pnl {
public PNLCellVM<PNLRowData, double> cpp { get; }
// sod
public PNLCellVM<PNLRowData, double> sod_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_realized_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_unrealized_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_net_pos { get; }
public PNLCellVM<PNLRowData, double> sod_long_pos { get; }
public PNLCellVM<PNLRowData, double> sod_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_long_avg_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_short_pos { get; }
public PNLCellVM<PNLRowData, double> sod_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_short_avg_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_delta { get; }
public PNLCellVM<PNLRowData, double> sod_gamma { get; }
public PNLCellVM<PNLRowData, double> sod_vega { get; }
@@ -256,11 +275,15 @@ namespace bts.shell.pnl {
// day
public PNLCellVM<PNLRowData, double> day_pnl { get; }
public PNLCellVM<PNLRowData, double> day_realized_pnl { get; }
public PNLCellVM<PNLRowData, double> day_unrealized_pnl { get; }
public PNLCellVM<PNLRowData, double> day_net_pos { get; }
public PNLCellVM<PNLRowData, double> day_long_pos { get; }
public PNLCellVM<PNLRowData, double> day_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_long_avg_pnl { get; }
public PNLCellVM<PNLRowData, double> day_short_pos { get; }
public PNLCellVM<PNLRowData, double> day_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_short_avg_pnl { get; }
public PNLCellVM<PNLRowData, double> day_delta { get; }
public PNLCellVM<PNLRowData, double> day_gamma { get; }
public PNLCellVM<PNLRowData, double> day_vega { get; }
@@ -274,11 +297,15 @@ namespace bts.shell.pnl {
public PNLCellVM<PNLRowData, double> working_short_avg_price { get; }
// total
public PNLCellVM<PNLRowData, double> total_pnl { get; }
public PNLCellVM<PNLRowData, double> total_realized_pnl { get; }
public PNLCellVM<PNLRowData, double> total_unrealized_pnl { get; }
public PNLCellVM<PNLRowData, double> total_net_pos { get; }
public PNLCellVM<PNLRowData, double> total_long_pos { get; }
public PNLCellVM<PNLRowData, double> total_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_long_avg_pnl { get; }
public PNLCellVM<PNLRowData, double> total_short_pos { get; }
public PNLCellVM<PNLRowData, double> total_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_short_avg_pnl { get; }
public PNLCellVM<PNLRowData, double> total_delta { get; }
public PNLCellVM<PNLRowData, double> total_gamma { get; }
public PNLCellVM<PNLRowData, double> total_vega { get; }
@@ -17,6 +17,7 @@ namespace bts.shell.pnl {
using mbts.ui.utility;
using mbts.utils;
using Spark.Api.Data;
using Spark.Api.Orders;
using Spark.Core;
using Spark.Core.Collections;
@@ -119,7 +120,7 @@ namespace bts.shell.pnl {
out var symbol_filter_items_observer,
out var symbol_filter_items_view);
symbol_filter_items_view.CustomSort = Comparer<string>.Default.to_safe().to_non_generic();
expiration_filter = DiscreteValuesFilterVM.CreateTableUpdate<PNLRowData, DateTime, DateTime>(
expiration_filter = DiscreteValuesFilterVM.CreateTableUpdate<PNLRowData, ExpirationDate, ExpirationDate>(
r => r.expiration,
exp => exp,
out var expiration_filter_items_observer,
@@ -226,7 +227,7 @@ namespace bts.shell.pnl {
/// security
public IDataGridColumnPersistentFilter<PNLRowData, uint[]> account_filter { get; }
public IDataGridColumnPersistentFilter<PNLRowData, Venue[]> exchange_filter { get; } = DiscreteValuesFilterVM.Common.exchange<PNLRowData>(r => r.security.exchange_or_default(), nameof(PNLRowVM.exchange));
public IDataGridColumnPersistentFilter<PNLRowData, DateTime[]> expiration_filter { get; }
public IDataGridColumnPersistentFilter<PNLRowData, ExpirationDate[]> expiration_filter { get; }
public IDataGridColumnPersistentFilter<PNLRowData, string[]> symbol_filter { get; }
public IDataGridColumnPersistentFilter<PNLRowData, string> security_filter { get; } = new RegexFilterVM<PNLRowData>(r => r.security?.ToString(), nameof(PNLRowData.security));
public IDataGridColumnPersistentFilter<PNLRowData, string> security_key_filter { get; } = new RegexFilterVM<PNLRowData>(r => r.key.security_id.ToString());
@@ -237,10 +238,14 @@ namespace bts.shell.pnl {
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> cpp_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.effective_cpp);
/// sod
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_realized_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_realized_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_unrealized_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_unrealized_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.long_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_long_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_long_avg_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_long_avg_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.short_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_short_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_short_avg_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod_short_avg_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.net_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> sod_gamma_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.sod.gamma);
@@ -249,10 +254,14 @@ namespace bts.shell.pnl {
/// day
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_realized_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_realized_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_unrealized_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_unrealized_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_long_avg_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_long_avg_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_short_avg_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_short_avg_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day_net_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> day_gamma_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.day.gamma);
@@ -267,10 +276,14 @@ namespace bts.shell.pnl {
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> working_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.working_net_pos);
/// total
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_realized_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_realized_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_unrealized_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_unrealized_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_long_avg_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_long_avg_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_price_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_price);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_short_avg_pnl_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_short_avg_pnl);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_net_pos_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_net_pos);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_delta_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_delta);
public IDataGridColumnPersistentFilter<PNLRowData, DoubleMinMaxState> total_gamma_filter { get; } = new DoubleMinMax<PNLRowData>(r => r.total_gamma);
@@ -310,6 +323,8 @@ namespace bts.shell.pnl {
cpp = cpp_filter.get_state(),
/// sod
sod_pnl = sod_pnl_filter.get_state(),
sod_realized_pnl = sod_realized_pnl_filter.get_state(),
sod_unrealized_pnl = sod_unrealized_pnl_filter.get_state(),
sod_long_pos = sod_long_pos_filter.get_state(),
sod_long_avg_price = sod_long_avg_price_filter.get_state(),
sod_short_pos = sod_short_pos_filter.get_state(),
@@ -321,10 +336,14 @@ namespace bts.shell.pnl {
sod_theta = sod_theta_filter.get_state(),
/// day
day_pnl = day_pnl_filter.get_state(),
day_realized_pnl = day_realized_pnl_filter.get_state(),
day_unrealized_pnl = day_unrealized_pnl_filter.get_state(),
day_long_pos = day_long_pos_filter.get_state(),
day_long_avg_price = day_long_avg_price_filter.get_state(),
day_long_avg_pnl = day_long_avg_price_filter.get_state(),
day_short_pos = day_short_pos_filter.get_state(),
day_short_avg_price = day_short_avg_price_filter.get_state(),
day_short_avg_pnl = day_short_avg_pnl_filter.get_state(),
day_net_pos = day_net_pos_filter.get_state(),
day_delta = day_delta_filter.get_state(),
day_gamma = day_gamma_filter.get_state(),
@@ -333,6 +352,8 @@ namespace bts.shell.pnl {
has_day_trades = has_day_trades_filter.get_state(),
/// total
total_pnl = total_pnl_filter.get_state(),
total_realized_pnl = total_realized_pnl_filter.get_state(),
total_unrealized_pnl = total_unrealized_pnl_filter.get_state(),
total_long_pos = total_long_pos_filter.get_state(),
total_long_avg_price = total_long_avg_price_filter.get_state(),
total_short_pos = total_short_pos_filter.get_state(),
@@ -375,6 +396,8 @@ namespace bts.shell.pnl {
cpp_filter.load_state(f.cpp);
/// sod
sod_pnl_filter.load_state(f.sod_pnl);
sod_realized_pnl_filter.load_state(f.sod_realized_pnl);
sod_unrealized_pnl_filter.load_state(f.sod_unrealized_pnl);
sod_long_pos_filter.load_state(f.sod_long_pos);
sod_long_avg_price_filter.load_state(f.sod_long_avg_price);
sod_short_pos_filter.load_state(f.sod_short_pos);
@@ -383,6 +406,8 @@ namespace bts.shell.pnl {
sod_delta_filter.load_state(f.sod_delta);
/// day
day_pnl_filter.load_state(f.day_pnl);
day_realized_pnl_filter.load_state(f.day_realized_pnl);
day_unrealized_pnl_filter.load_state(f.day_unrealized_pnl);
day_long_pos_filter.load_state(f.day_long_pos);
day_long_avg_price_filter.load_state(f.day_long_avg_price);
day_short_pos_filter.load_state(f.day_short_pos);
@@ -391,6 +416,8 @@ namespace bts.shell.pnl {
has_day_trades_filter.load_state(f.has_day_trades);
/// total
total_pnl_filter.load_state(f.total_pnl);
total_realized_pnl_filter.load_state(f.total_realized_pnl);
total_unrealized_pnl_filter.load_state(f.total_unrealized_pnl);
total_long_pos_filter.load_state(f.total_long_pos);
total_long_avg_price_filter.load_state(f.total_long_avg_price);
total_short_pos_filter.load_state(f.total_short_pos);
@@ -566,10 +593,14 @@ namespace bts.shell.pnl {
case ColumnKeys.CPP: return wrap(r => r.cpp);
/// SOD
case ColumnKeys.SODPNL: return wrap(r => r.sod_pnl);
case ColumnKeys.SODRealizedPNL: return wrap(r => r.sod_realized_pnl);
case ColumnKeys.SODUnrealizedPNL: return wrap(r => r.sod_unrealized_pnl);
case ColumnKeys.SODLongPos: return wrap(r => r.sod_long_pos);
case ColumnKeys.SODLongAvgPrice: return wrap(r => r.sod_long_avg_price);
case ColumnKeys.SODLongAvgPNL: return wrap(r => r.sod_long_avg_pnl);
case ColumnKeys.SODShortPos: return wrap(r => r.sod_short_pos);
case ColumnKeys.SODShortAvgPrice: return wrap(r => r.sod_short_avg_price);
case ColumnKeys.SODShortAvgPNL: return wrap(r => r.sod_short_avg_pnl);
case ColumnKeys.SODNetPos: return wrap(r => r.sod_net_pos);
case ColumnKeys.SODDelta: return wrap(r => r.sod_delta);
case ColumnKeys.SODGamma: return wrap(r => r.sod_gamma);
@@ -577,10 +608,14 @@ namespace bts.shell.pnl {
case ColumnKeys.SODTheta: return wrap(r => r.sod_theta);
/// Day
case ColumnKeys.DayPNL: return wrap(r => r.day_pnl);
case ColumnKeys.DayRealizedPNL: return wrap(r => r.day_realized_pnl);
case ColumnKeys.DayUnrealizedPNL: return wrap(r => r.day_unrealized_pnl);
case ColumnKeys.DayLongPos: return wrap(r => r.day_long_pos);
case ColumnKeys.DayLongAvgPrice: return wrap(r => r.day_long_avg_price);
case ColumnKeys.DayLongAvgPNL: return wrap(r => r.day_long_avg_pnl);
case ColumnKeys.DayShortPos: return wrap(r => r.day_short_pos);
case ColumnKeys.DayShortAvgPrice: return wrap(r => r.day_short_avg_price);
case ColumnKeys.DayShortAvgPNL: return wrap(r => r.day_short_avg_pnl);
case ColumnKeys.DayNetPos: return wrap(r => r.day_net_pos);
case ColumnKeys.DayDelta: return wrap(r => r.day_delta);
case ColumnKeys.DayGamma: return wrap(r => r.day_gamma);
@@ -589,10 +624,14 @@ namespace bts.shell.pnl {
case ColumnKeys.HasDayTrades: return wrap(r => r.has_day_trades);
/// Total
case ColumnKeys.TotalPNL: return wrap(r => r.total_pnl);
case ColumnKeys.TotalRealizedPNL: return wrap(r => r.total_realized_pnl);
case ColumnKeys.TotalUnrealizedPNL: return wrap(r => r.total_unrealized_pnl);
case ColumnKeys.TotalLongPos: return wrap(r => r.total_long_pos);
case ColumnKeys.TotalLongAvgPrice: return wrap(r => r.total_long_avg_price);
case ColumnKeys.TotalLongAvgPNL: return wrap(r => r.total_long_avg_pnl);
case ColumnKeys.TotalShortPos: return wrap(r => r.total_short_pos);
case ColumnKeys.TotalShortAvgPrice: return wrap(r => r.total_short_avg_price);
case ColumnKeys.TotalShortAvgPNL: return wrap(r => r.total_short_avg_pnl);
case ColumnKeys.TotalNetPos: return wrap(r => r.total_net_pos);
case ColumnKeys.TotalDelta: return wrap(r => r.total_delta);
case ColumnKeys.TotalGamma: return wrap(r => r.total_gamma);
@@ -627,13 +666,13 @@ namespace bts.shell.pnl {
yield return col_hash.Contains(c.id) ? c.with(is_visible: false) : c;
}
static IObservable<TableUpdate<DateTime, DateTime>> _expirations_source(
static IObservable<TableUpdate<ExpirationDate, ExpirationDate>> _expirations_source(
IObservable<TableUpdate<PNLRowData, PNLKey>> pnl_source) =>
pnl_source
.StartWith(TableUpdate<PNLRowData, PNLKey>.mk_empty(PNLRowData.key_fn))
.distinct_on(
PNLRowData.key_fn, r => r.expiration, e => e, EqualityComparer<DateTime>.Default)
.Filter(e => e != DateTime.MinValue);
PNLRowData.key_fn, r => r.expiration, e => e, EqualityComparer<ExpirationDate>.Default)
.Filter(e => e != ExpirationDate.None);
// combines the known system symbols
static IObservable<TableUpdate<string, string>> _symbols_source(IObservable<TableUpdate<PNLRowData, PNLKey>> pnl_source) =>
@@ -707,7 +746,7 @@ namespace bts.shell.pnl {
filter.security = SecurityCriteria.from_securities(sec));
break;
case ColumnKeys.Expiration:
try_set_filter_prop<DateTime>(exp_date => filter.exp_date = exp_date);
try_set_filter_prop<ExpirationDate>(exp_date => filter.exp_date = exp_date);
break;
default:
throw new NotImplementedException();
@@ -774,6 +813,17 @@ namespace bts.shell.pnl {
case ColumnKeys.Account: filter.accounts = new[] { g.Name.ToString() }; break;
case ColumnKeys.Symbol: filter.symbols = new[] { g.Name.ToString() }; break;
case ColumnKeys.Exchange: filter.exchanges = new[] { (Venue)g.Name }; break;
case ColumnKeys.Expiration: {
if (g.Name is ExpirationDate exp) {
if (filter.security_criteria is null) {
filter.security_criteria = SecurityCriteria.from_expirations(exp);
} else {
filter.security_criteria.clauses = ArrayHelper.add(
filter.security_criteria.clauses, SecurityCriteriaClause.from_expirations(exp));
}
}
break;
}
case ColumnKeys.Security:
if (g.Name is SecurityImmutable sec)
filter.security_criteria = SecurityCriteria.from_securities(sec);
@@ -118,6 +118,8 @@ namespace bts.shell.pnl {
});
// sod
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_realized_pnl = _cell_map.add_pnl(ColumnKeys.SODRealizedPNL, d => d.sod_realized_pnl);
sod_unrealized_pnl = _cell_map.add_pnl(ColumnKeys.SODUnrealizedPNL, d => d.sod_unrealized_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.display_sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.display_sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.display_sod_short_pos);
@@ -128,6 +130,8 @@ namespace bts.shell.pnl {
// day
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
day_realized_pnl = _cell_map.add_pnl(ColumnKeys.DayRealizedPNL, d => d.day_realized_pnl);
day_unrealized_pnl = _cell_map.add_pnl(ColumnKeys.DayUnrealizedPNL, d => d.day_unrealized_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.display_day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.display_day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.display_day_short_pos);
@@ -138,6 +142,8 @@ namespace bts.shell.pnl {
// total
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
total_realized_pnl = _cell_map.add_pnl(ColumnKeys.TotalRealizedPNL, d => d.total_realized_pnl);
total_unrealized_pnl = _cell_map.add_pnl(ColumnKeys.TotalUnrealizedPNL, d => d.total_unrealized_pnl);
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.display_total_net_pos);
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.display_total_long_pos);
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.display_total_short_pos);
@@ -181,6 +187,8 @@ namespace bts.shell.pnl {
// sod
public PNLCellVM<PNLNormalizedData, decimal> sod_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_realized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_unrealized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> sod_short_pos { get; }
@@ -191,6 +199,8 @@ namespace bts.shell.pnl {
// day
public PNLCellVM<PNLNormalizedData, decimal> day_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_realized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_unrealized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> day_short_pos { get; }
@@ -201,6 +211,8 @@ namespace bts.shell.pnl {
// total
public PNLCellVM<PNLNormalizedData, decimal> total_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_realized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_unrealized_pnl { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_net_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_long_pos { get; }
public PNLCellVM<PNLNormalizedData, decimal> total_short_pos { get; }
@@ -266,6 +266,7 @@
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="4,0,0,0"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="{u:BTSColor UI.default_text}"/>
</Style>
<Style x:Key="HeaderRowCellText" TargetType="TextBlock">
@@ -570,6 +571,8 @@
</TextBlock>
<!--SOD-->
<c:TextCell c:DIYGrid.ColumnKey="SODPNL" DataContext="{Binding total_aggregate.sod_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODRealizedPNL" DataContext="{Binding total_aggregate.sod_realized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODUnrealizedPNL" DataContext="{Binding total_aggregate.sod_unrealized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODNetPos" DataContext="{Binding total_aggregate.sod_net_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODLongPos" DataContext="{Binding total_aggregate.sod_long_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODShortPos" DataContext="{Binding total_aggregate.sod_short_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
@@ -580,6 +583,8 @@
<!--Day-->
<c:TextCell c:DIYGrid.ColumnKey="DayPNL" DataContext="{Binding total_aggregate.day_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayRealizedPNL" DataContext="{Binding total_aggregate.day_realized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayUnrealizedPNL" DataContext="{Binding total_aggregate.day_unrealized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayNetPos" DataContext="{Binding total_aggregate.day_net_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayLongPos" DataContext="{Binding total_aggregate.day_long_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayShortPos" DataContext="{Binding total_aggregate.day_short_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
@@ -598,6 +603,8 @@
<c:HyperlinkCell c:DIYGrid.ColumnKey="WorkingShortPos" DataContext="{Binding total_aggregate.working_short_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
<!--Total-->
<c:TextCell c:DIYGrid.ColumnKey="TotalPNL" DataContext="{Binding total_aggregate.total_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalRealizedPNL" DataContext="{Binding total_aggregate.total_realized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalUnrealizedPNL" DataContext="{Binding total_aggregate.total_unrealized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalNetPos" DataContext="{Binding total_aggregate.total_net_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalLongPos" DataContext="{Binding total_aggregate.total_long_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalShortPos" DataContext="{Binding total_aggregate.total_short_pos}" Style="{StaticResource TotalRowHyperlinkCell}"/>
@@ -632,6 +639,8 @@
<TextBlock Text="{Binding selected_rows.Count, StringFormat='Selected ({0})'}" Style="{StaticResource HeaderRowHeaderText}"/>
<!--SOD-->
<c:TextCell c:DIYGrid.ColumnKey="SODPNL" DataContext="{Binding selected_aggregate.sod_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODRealizedPNL" DataContext="{Binding selected_aggregate.sod_realized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODUnrealizedPNL" DataContext="{Binding selected_aggregate.sod_unrealized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODNetPos" DataContext="{Binding selected_aggregate.sod_net_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODLongPos" DataContext="{Binding selected_aggregate.sod_long_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODShortPos" DataContext="{Binding selected_aggregate.sod_short_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
@@ -642,6 +651,8 @@
<!--Day-->
<c:TextCell c:DIYGrid.ColumnKey="DayPNL" DataContext="{Binding selected_aggregate.day_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayRealizedPNL" DataContext="{Binding selected_aggregate.day_realized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayUnrealizedPNL" DataContext="{Binding selected_aggregate.day_unrealized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayNetPos" DataContext="{Binding selected_aggregate.day_net_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayLongPos" DataContext="{Binding selected_aggregate.day_long_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayShortPos" DataContext="{Binding selected_aggregate.day_short_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
@@ -660,6 +671,8 @@
<c:HyperlinkCell c:DIYGrid.ColumnKey="WorkingShortPos" DataContext="{Binding selected_aggregate.working_short_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
<!--Total-->
<c:TextCell c:DIYGrid.ColumnKey="TotalPNL" DataContext="{Binding selected_aggregate.total_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalRealizedPNL" DataContext="{Binding selected_aggregate.total_realized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalUnrealizedPNL" DataContext="{Binding selected_aggregate.total_unrealized_pnl}" Style="{StaticResource HeaderRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalNetPos" DataContext="{Binding selected_aggregate.total_net_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalLongPos" DataContext="{Binding selected_aggregate.total_long_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalShortPos" DataContext="{Binding selected_aggregate.total_short_pos}" Style="{StaticResource SelectedTotalRowHyperlinkCell}"/>
@@ -791,6 +804,26 @@
data_member_path="sod_pnl"
Header="SOD PnL"
/>
<!--SOD Realized PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.sod_realized_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.sod_realized_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.sod_realized_pnl_filter.open, Source={u:DirectReference _grid}}"
key="SODRealizedPNL"
data_member_path="sod_realized_pnl"
Header="SOD Realized PnL"
/>
<!--SOD Unrealized PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.sod_unrealized_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.sod_unrealized_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.sod_unrealized_pnl_filter.open, Source={u:DirectReference _grid}}"
key="SODUnrealizedPNL"
data_member_path="sod_unrealized_pnl"
Header="SOD Unrealized PnL"
/>
<!--SOD Net Pos-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.sod_net_pos_filter, Source={u:DirectReference _grid}}"
@@ -819,6 +852,15 @@
key="SODLongAvgPrice"
data_member_path="sod_long_avg_price"
/>
<!--SOD Long Avg PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.sod_long_avg_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.sod_long_avg_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.sod_long_avg_pnl_filter.open, Source={u:DirectReference _grid}}"
key="SODLongAvgPNL"
data_member_path="sod_long_avg_pnl"
/>
<!--SOD Short Pos-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.sod_short_pos_filter, Source={u:DirectReference _grid}}"
@@ -837,6 +879,15 @@
key="SODShortAvgPrice"
data_member_path="sod_short_avg_price"
/>
<!--SOD Short Avg PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.sod_short_avg_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.sod_short_avg_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.sod_short_avg_pnl_filter.open, Source={u:DirectReference _grid}}"
key="SODShortAvgPNL"
data_member_path="sod_short_avg_pnl"
/>
<!--SOD Delta-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.sod_delta_filter, Source={u:DirectReference _grid}}"
@@ -880,6 +931,26 @@
data_member_path="day_pnl"
sort_member_path="day_pnl"
/>
<!--Day Realized PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.day_realized_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.day_realized_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.day_realized_pnl_filter.open, Source={u:DirectReference _grid}}"
key="DayRealizedPNL"
data_member_path="day_realized_pnl"
Header="Day Realized PnL"
/>
<!--Day Unrealized PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.day_unrealized_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.day_unrealized_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.day_unrealized_pnl_filter.open, Source={u:DirectReference _grid}}"
key="DayUnrealizedPNL"
data_member_path="day_unrealized_pnl"
Header="Day Unrealized PnL"
/>
<!--Day Net Pos-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.day_net_pos_filter, Source={u:DirectReference _grid}}"
@@ -910,6 +981,16 @@
data_member_path="day_long_avg_price"
sort_member_path="day_long_avg_price"
/>
<!--Day Long Avg PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.day_long_avg_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.day_long_avg_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.day_long_avg_pnl_filter.open, Source={u:DirectReference _grid}}"
key="DayLongAvgPNL"
data_member_path="day_long_avg_pnl"
sort_member_path="day_long_avg_pnl"
/>
<!--Day Delta-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.day_delta_filter, Source={u:DirectReference _grid}}"
@@ -1012,6 +1093,16 @@
data_member_path="day_short_avg_price"
sort_member_path="day_short_avg_price"
/>
<!--Day Short Avg PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.day_short_avg_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.day_short_avg_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.day_short_avg_pnl_filter.open, Source={u:DirectReference _grid}}"
key="DayShortAvgPNL"
data_member_path="day_short_avg_pnl"
sort_member_path="day_short_avg_pnl"
/>
<!--Day Short Pos-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.day_short_pos_filter, Source={u:DirectReference _grid}}"
@@ -1090,6 +1181,26 @@
key="TotalPNL"
data_member_path="total_pnl"
/>
<!--Total Realized PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.total_realized_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.total_realized_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.total_realized_pnl_filter.open, Source={u:DirectReference _grid}}"
key="TotalRealizedPNL"
data_member_path="total_realized_pnl"
Header="Total Realized PnL"
/>
<!--Total Unrealized PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.total_unrealized_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.total_unrealized_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.total_unrealized_pnl_filter.open, Source={u:DirectReference _grid}}"
key="TotalUnrealizedPNL"
data_member_path="total_unrealized_pnl"
Header="Total Unrealized PnL"
/>
<!--Total Net Pos-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.total_net_pos_filter, Source={u:DirectReference _grid}}"
@@ -1120,6 +1231,16 @@
data_member_path="total_long_avg_price"
sort_member_path="total_long_avg_price"
/>
<!--Total Long Avg PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.total_long_avg_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.total_long_avg_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.total_long_avg_pnl_filter.open, Source={u:DirectReference _grid}}"
key="TotalLongAvgPNL"
data_member_path="total_long_avg_pnl"
sort_member_path="total_long_avg_pnl"
/>
<!--Total Short Pos-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.total_short_pos_filter, Source={u:DirectReference _grid}}"
@@ -1140,6 +1261,16 @@
data_member_path="total_short_avg_price"
sort_member_path="total_short_avg_price"
/>
<!--Total Short Avg PNL-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.total_short_avg_pnl_filter, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterTemplate="{StaticResource {x:Static ft:DataGridFilterKnownTemplates.DoubleMinMax}}"
c:DataGridFilterProperties.FilterActive="{Binding DataContext.total_short_avg_pnl_filter.active, Source={u:DirectReference _grid}}"
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.total_short_avg_pnl_filter.open, Source={u:DirectReference _grid}}"
key="TotalShortAvgPNL"
data_member_path="total_short_avg_pnl"
sort_member_path="total_short_avg_pnl"
/>
<!--Total Delta-->
<pnl:PNLColumn
c:DataGridFilterProperties.Filter="{Binding DataContext.total_delta_filter, Source={u:DirectReference _grid}}"
@@ -1198,11 +1329,15 @@
<c:TextCell c:DIYGrid.ColumnKey="CPP" DataContext="{Binding cpp}" Style="{StaticResource RowTextCell}"/>
<!--SOD-->
<c:TextCell c:DIYGrid.ColumnKey="SODPNL" DataContext="{Binding sod_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODRealizedPNL" DataContext="{Binding sod_realized_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODUnrealizedPNL" DataContext="{Binding sod_unrealized_pnl}" Style="{StaticResource RowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODNetPos" DataContext="{Binding sod_net_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODLongPos" DataContext="{Binding sod_long_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODLongAvgPrice" DataContext="{Binding sod_long_avg_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODLongAvgPNL" DataContext="{Binding sod_long_avg_pnl}" Style="{StaticResource RowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODShortPos" DataContext="{Binding sod_short_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODShortAvgPrice" DataContext="{Binding sod_short_avg_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODShortAvgPNL" DataContext="{Binding sod_short_avg_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell
c:DIYGrid.ColumnKey="SODDelta"
DataContext="{Binding sod_delta}"
@@ -1222,11 +1357,15 @@
<!--Day-->
<c:TextCell c:DIYGrid.ColumnKey="DayPNL" DataContext="{Binding day_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayRealizedPNL" DataContext="{Binding day_realized_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayUnrealizedPNL" DataContext="{Binding day_unrealized_pnl}" Style="{StaticResource RowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayNetPos" DataContext="{Binding day_net_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayLongPos" DataContext="{Binding day_long_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayLongAvgPrice" DataContext="{Binding day_long_avg_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayLongAvgPNL" DataContext="{Binding day_long_avg_pnl}" Style="{StaticResource RowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayShortPos" DataContext="{Binding day_short_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayShortAvgPrice" DataContext="{Binding day_short_avg_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayShortAvgPNL" DataContext="{Binding day_short_avg_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell
c:DIYGrid.ColumnKey="DayDelta"
DataContext="{Binding day_delta}"
@@ -1257,6 +1396,9 @@
<c:TextCell c:DIYGrid.ColumnKey="LastPrice" DataContext="{Binding last_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SettlementPrice" DataContext="{Binding settlement_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="PNLPrice" DataContext="{Binding pnl_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="BidPrice" DataContext="{Binding bid_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="AskPrice" DataContext="{Binding ask_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="MidPrice" DataContext="{Binding mid_price}" Style="{StaticResource RowTextCell}"/>
<!--Working-->
<c:HyperlinkCell c:DIYGrid.ColumnKey="WorkingNetPos" DataContext="{Binding working_net_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="WorkingLongPos" DataContext="{Binding working_long_pos}" Style="{StaticResource RowHyperlinkCell}"/>
@@ -1265,11 +1407,15 @@
<c:TextCell c:DIYGrid.ColumnKey="WorkingShortAvgPrice" DataContext="{Binding working_short_avg_price}" Style="{StaticResource RowTextCell}"/>
<!--Total-->
<c:TextCell c:DIYGrid.ColumnKey="TotalPNL" DataContext="{Binding total_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalRealizedPNL" DataContext="{Binding total_realized_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalUnrealizedPNL" DataContext="{Binding total_unrealized_pnl}" Style="{StaticResource RowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalNetPos" DataContext="{Binding total_net_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalLongPos" DataContext="{Binding total_long_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalLongAvgPrice" DataContext="{Binding total_long_avg_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalLongAvgPNL" DataContext="{Binding total_long_avg_pnl}" Style="{StaticResource RowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalShortPos" DataContext="{Binding total_short_pos}" Style="{StaticResource RowHyperlinkCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalShortAvgPrice" DataContext="{Binding total_short_avg_price}" Style="{StaticResource RowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalShortAvgPNL" DataContext="{Binding total_short_avg_pnl}" Style="{StaticResource RowTextCell}"/>
<c:TextCell
c:DIYGrid.ColumnKey="TotalDelta"
DataContext="{Binding total_delta}"
@@ -1360,6 +1506,8 @@
</Grid>
<!--SOD-->
<c:TextCell c:DIYGrid.ColumnKey="SODPNL" DataContext="{Binding sod_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODRealizedPNL" DataContext="{Binding sod_realized_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="SODUnrealizedPNL" DataContext="{Binding sod_unrealized_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODNetPos" DataContext="{Binding sod_net_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODLongPos" DataContext="{Binding sod_long_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="SODShortPos" DataContext="{Binding sod_short_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
@@ -1370,6 +1518,8 @@
<!--Day-->
<c:TextCell c:DIYGrid.ColumnKey="DayPNL" DataContext="{Binding day_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayRealizedPNL" DataContext="{Binding day_realized_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="DayUnrealizedPNL" DataContext="{Binding day_unrealized_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayNetPos" DataContext="{Binding day_net_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayLongPos" DataContext="{Binding day_long_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="DayShortPos" DataContext="{Binding day_short_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
@@ -1384,6 +1534,8 @@
<c:HyperlinkCell c:DIYGrid.ColumnKey="WorkingShortPos" DataContext="{Binding working_short_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
<!--Total-->
<c:TextCell c:DIYGrid.ColumnKey="TotalPNL" DataContext="{Binding total_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalRealizedPNL" DataContext="{Binding total_realized_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:TextCell c:DIYGrid.ColumnKey="TotalUnrealizedPNL" DataContext="{Binding total_unrealized_pnl}" Style="{StaticResource GroupRowTextCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalNetPos" DataContext="{Binding total_net_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalLongPos" DataContext="{Binding total_long_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
<c:HyperlinkCell c:DIYGrid.ColumnKey="TotalShortPos" DataContext="{Binding total_short_pos}" Style="{StaticResource GroupRowHyperlinkCell}"/>
@@ -1,427 +1,439 @@
namespace bts.shell.pnl {
using bts.collections;
using mbts.collections;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.dpi;
using mbts.ui.extensions;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
enum PNLHeaderRowDisplay {
None,
Total,
SelectedTotal
}
partial class PNLView : UserControl {
public static readonly double
MinGroupRowHeight = 24.0.RoundLayoutValue(),
MinHeaderRowHeight = 24.0.RoundLayoutValue();
DIYGridPopupManager _popup_man_backing;
public enum Columns {
Account = 1,
// Security
Exchange,
Symbol,
Expiration,
SecurityType,
Security,
SecurityKey,
CPP,
// SOD
SODPNL,
SODLongPos,
SODLongAvgPrice,
SODShortPos,
SODShortAvgPrice,
SODNetPos,
SODDelta,
SODGamma,
SODVega,
SODTheta,
// Day
DayPNL,
DayLongPos,
DayLongAvgPrice,
DayShortAvgPrice,
DayShortPos,
DayDelta,
DayGamma,
DayVega,
DayTheta,
DayCalcType,
DayNetPos,
HasDayTrades,
// Raw Day
RawDayLongPos,
RawDayShortPos,
RawDayNetPos,
// Working
WorkingLongPos,
WorkingLongAvgPrice,
WorkingShortPos,
WorkingShortAvgPrice,
WorkingNetPos,
// Total
TotalPNL,
TotalLongPos,
TotalLongAvgPrice,
TotalShortPos,
TotalShortAvgPrice,
TotalNetPos,
TotalDelta,
TotalGamma,
TotalVega,
TotalTheta,
// Market
BidPrice,
MidPrice,
AskPrice,
LastPrice,
SettlementPrice,
PNLPrice
}
public static readonly IValueConverter
IncrementalGroupLevelConverter = ValueConverter.create<int, int>(level => level + 1),
GroupLevelMarginConverter = ValueConverter.create<int, Thickness>(level => new Thickness(24 * level, 0, 0, 0)),
HeaderRowHeightConverter = new RangeBlockConverter<double> { Minimum = MinHeaderRowHeight },
ExpandedVisualStateConverter = ValueConverter.create<bool, string>(is_expanded => is_expanded ? "Expanded" : "Collapsed");
public static readonly IMultiValueConverter
TotalHeaderRowDisplayTextConverter = ValueConverter.create<int, int, string>((filtered, total) => $"Total ({(filtered == total ? total.ToString() : $"{filtered}/{total}")})"),
CellParamsConverter = ValueConverter.create<object, Columns, PNLCell>((item, col) => new PNLCell(item, col));
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
static readonly string _node_path_separator = $" {SpecialCharacters.Dot} ";
IDisposable _anchors_hook, _animations_hook;
int _anchor_version;
public PNLView() {
SetCurrentValue(AnchorIndexProperty, new AnchorIndex());
InitializeComponent();
SetBinding(AnchorsProperty, new Binding(nameof(PNLVM.anchors)));
_grid.ConnectOnLoaded(() => {
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter)) {
presenter.GroupItemSelectionPivot += on_group_item_selection_pivot;
return Disposable.Create(() => presenter.GroupItemSelectionPivot -= on_group_item_selection_pivot);
}
return null;
});
void on_group_item_selection_pivot(object sender, GroupItemSelectionPivotEventArgs e) =>
e.Handled = e.group_item is PNLRowCollectionViewGroup pnl_group && pnl_group.is_expanded;
}
public new PNLVM DataContext {
get => base.DataContext as PNLVM;
set => base.DataContext = value;
}
public bool EnableAnimations {
get => _animations_hook != null;
set {
if (EnableAnimations != value) {
if (_animations_hook == null) {
_animations_hook = AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
} else {
_animations_hook.Dispose();
_animations_hook = null;
}
}
}
}
#region AnchorIndex
public static readonly DependencyProperty AnchorIndexProperty =
WPFHelper.create_dp<AnchorIndex, PNLView>(
nameof(AnchorIndex),
(o, v) => v?.reset(o?.Anchors));
public AnchorIndex AnchorIndex {
get => GetValue(AnchorIndexProperty) as AnchorIndex;
set => SetValue(AnchorIndexProperty, value);
}
#endregion
#region GroupLevel
public static readonly DependencyProperty GroupLevelProperty =
DependencyProperty.RegisterAttached(
"GroupLevel",
typeof(int),
typeof(PNLView),
new PropertyMetadata(0, (d, e) => {
if (d is GroupItem group && e.NewValue is int level)
group.SetBinding(DIYGrid.ColumnKeyProperty, new Binding($"{nameof(Anchors)}[{level}]") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(PNLView), 1) });
}));
public static int GetGroupLevel(DependencyObject d) => (int)d.GetValue(GroupLevelProperty);
public static void SetGroupLevel(DependencyObject d, int value) => d.SetValue(GroupLevelProperty, value);
void _grid_loaded(object sender, RoutedEventArgs e) {
// this is a hack to make room for the 'totals row' between the column headers and the regular rows.
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter rp)) {
rp.SetBinding(MarginProperty,
BindingHelper.create_multi(
ValueConverter.create<PNLHeaderRowDisplay, double, Thickness>((hrd, row_height) => {
double top = 0;
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
top += row_height;
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
top += row_height;
return new Thickness(0, top, 0, 0);
}),
new Binding(nameof(PNLVM.header_row_display)),
new Binding {
Path = new PropertyPath(DIYGrid.RowHeightProperty),
Converter = HeaderRowHeightConverter,
Source = _grid
}));
}
}
#endregion
#region Anchors
public static readonly DependencyProperty AnchorsProperty =
WPFHelper.create_dp<IEnumerable<Columns>, PNLView>(
nameof(Anchors),
(o, v) => {
if (o?._grid?.Columns is not ObservableCollection<DIYGrid.ColumnBase> cols ||
o?._grid?.Rows is not ICollectionView rows_view ||
v is not INotifyCollectionChanged anchor_notifier ||
v is not IReadOnlyList<Columns> anchor_keys) {
return;
}
int version = ++o._anchor_version;
bool busy = false;
var pnl_cols = cols.CastObservable<DIYGrid.ColumnBase, PNLColumn>();
o._anchors_hook?.Dispose();
o._anchors_hook = Disposable.Create(
anchor_notifier.Subscribe((_, __) => {
if (busy)
return;
// invoke async to avoid noisy consecutive resets due to batch changes
busy = true;
o.Dispatcher.InvokeAsync(() => {
sync_anchor_cols();
busy = false;
});
}));
o.AnchorIndex?.reset(anchor_keys);
sync_anchor_cols();
void sync_anchor_cols() {
// since reset is invoked asynchronously, need to check we are still the latest version.
if (o._anchor_version != version)
return;
o.AnchorIndex?.reset(anchor_keys);
// move all the anchor columns to the left, ordered by anchor position (grouping hierarchy).
for (int i = 0; i < anchor_keys.Count; i++) {
if (pnl_cols.TryGetFirst(c => c.key == anchor_keys[i], out var match, out int curr_pos)) {
match.is_visible = true;
if (curr_pos != i) {
cols.Move(curr_pos, i);
}
match.anchor_index = i + 1;
}
}
for (int i = anchor_keys.Count; i < pnl_cols.Count; i++) {
pnl_cols[i].anchor_index = null;
}
using (rows_view.DeferRefresh()) {
rows_view.GroupDescriptions.Clear();
var anchor_cols = from key in anchor_keys
join col in pnl_cols on key equals col.key
select new PNLGroupDescription(col);
rows_view.GroupDescriptions.AddRange(anchor_cols);
}
}
});
public IEnumerable<Columns> Anchors {
get => GetValue(AnchorsProperty) as IEnumerable<Columns>;
set => SetValue(AnchorsProperty, value);
}
#endregion
DIYGridPopupManager _popup_man {
get {
if (_popup_man_backing == null) {
_popup_man_backing = DIYGridPopupManager.create(this);
}
return _popup_man_backing;
}
}
void _cell_click(PNLCell cell) {
Log.Ui.info($"PNLCell Click: {cell}.");
try {
switch (cell.column) {
case Columns.SODNetPos:
case Columns.SODLongPos:
case Columns.SODShortPos:
case Columns.DayNetPos:
case Columns.DayLongPos:
case Columns.DayShortPos:
case Columns.RawDayNetPos:
case Columns.RawDayLongPos:
case Columns.RawDayShortPos:
case Columns.TotalNetPos:
case Columns.TotalLongPos:
case Columns.TotalShortPos: {
var oneof = cell.oneof();
if (oneof == null)
return;
var (vm, context, title, fg) = cell.oneof().Match(_row_fills, _group_fills, _header_fills);
_show_auxiliary_popup(vm, context, title, fg);
}
break;
case Columns.WorkingNetPos:
case Columns.WorkingLongPos:
case Columns.WorkingShortPos: {
var (vm, context, title, fg) = cell.oneof().Match(_orderbook_params, _orderbook_params, _orderbook_params);
_show_auxiliary_popup(vm, context, title, fg);
}
break;
}
} catch (Exception ex) {
Log.Ui.warn($"{nameof(PNLView)} error on cell click: {ex}.");
}
}
#region Fills
(FillsVM vm, string context, string title, Color fg) _row_fills(PNLRowCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
}
(FillsVM vm, string context, string title, Color fg) _group_fills(PNLGroupCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
}
(FillsVM vm, string context, string title, Color fg) _header_fills(PNLHeaderRowCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
}
static (string desc, Color fg) _fills_title(Columns key) {
switch (key) {
case Columns.SODLongPos: return ("Open Buy Fills", BTSColors.MarketData.bid);
case Columns.SODShortPos: return ("Open Sell Fills", BTSColors.MarketData.ask);
case Columns.SODNetPos: return ("Open Fills", BTSColors.UI.default_text);
case Columns.DayLongPos: return ("Day Buy Fills", BTSColors.MarketData.bid);
case Columns.DayShortPos: return ("Day Sell Fills", BTSColors.MarketData.ask);
case Columns.DayNetPos: return ("Day Fills", BTSColors.UI.default_text);
case Columns.RawDayLongPos: return ("Raw Day Buy Fills", BTSColors.MarketData.bid);
case Columns.RawDayShortPos: return ("Raw Day Sell Fills", BTSColors.MarketData.ask);
case Columns.RawDayNetPos: return ("Raw Day Fills", BTSColors.UI.default_text);
case Columns.TotalLongPos: return ("All Buy Fills", BTSColors.MarketData.bid);
case Columns.TotalShortPos: return ("All Sell Fills", BTSColors.MarketData.ask);
case Columns.TotalNetPos: return ("All Fills", BTSColors.UI.default_text);
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
}
}
#endregion
#region OrderBook
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLRowCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
}
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLGroupCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
}
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLHeaderRowCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
}
static (string desc, Color fg) _orderbook_title(Columns key) {
switch (key) {
case Columns.WorkingLongPos: return ("Working Buy Orders", BTSColors.MarketData.bid);
case Columns.WorkingShortPos: return ("Working Sell Orders", BTSColors.MarketData.ask);
case Columns.WorkingNetPos: return ("Working Orders", BTSColors.UI.default_text);
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
}
}
#endregion
void _cell_click_executed(object sender, ExecutedRoutedEventArgs e) {
if (e.Parameter is PNLCell cell) {
_cell_click(cell);
} else Log.Ui.warn($"{nameof(PNLView)} '{nameof(_cell_click_executed)}' parameter should be a {typeof(PNLCell)} (actually '{e.Parameter}').");
}
void _on_group_item_mouse_down(object sender, MouseButtonEventArgs e) {
if (!e.Handled && sender is FrameworkElement el && el.DataContext is PNLRowCollectionViewGroup group) {
if (e.ClickCount == 2) {
group.is_expanded = !group.is_expanded;
} else if (el.try_find_ancestor_of_type(out DIYGrid grid) && grid.SelectionMode != SelectionMode.Single) {
group.is_all_selected = !group.is_all_selected.GetValueOrDefault();
} else {
return;
}
e.Handled = true;
}
}
void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
var view = new HeaderedContentControl {
Content = content,
Header = new AuxiliaryDataHeader {
context = context_desc,
title = title_desc,
title_foreground = fg
},
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Resources = new AuxiliaryDataResources()
};
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
_popup_man.show(view);
}
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
public PNLGroupDescription(PNLColumn column) : base(column.data_member_path.Path) =>
key = column.key;
public Columns key { get; }
public bool Equals(PNLGroupDescription other) =>
other != null && key == other.key;
public override int GetHashCode() =>
key.GetHashCode();
public override bool Equals(object obj) =>
Equals(obj as PNLGroupDescription);
}
}
}
namespace bts.shell.pnl {
using bts.collections;
using mbts.collections;
using mbts.spark;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.dpi;
using mbts.ui.extensions;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
enum PNLHeaderRowDisplay {
None,
Total,
SelectedTotal
}
partial class PNLView : UserControl {
public static readonly double
MinGroupRowHeight = 24.0.RoundLayoutValue(),
MinHeaderRowHeight = 24.0.RoundLayoutValue();
DIYGridPopupManager _popup_man_backing;
public enum Columns {
Account = 1,
// Security
Exchange,
Symbol,
Expiration,
SecurityType,
Security,
SecurityKey,
CPP,
// SOD
SODPNL,
SODRealizedPNL,
SODUnrealizedPNL,
SODLongPos,
SODLongAvgPrice,
SODLongAvgPNL,
SODShortPos,
SODShortAvgPrice,
SODShortAvgPNL,
SODNetPos,
SODDelta,
SODGamma,
SODVega,
SODTheta,
// Day
DayPNL,
DayRealizedPNL,
DayUnrealizedPNL,
DayLongPos,
DayLongAvgPrice,
DayLongAvgPNL,
DayShortPos,
DayShortAvgPrice,
DayShortAvgPNL,
DayDelta,
DayGamma,
DayVega,
DayTheta,
DayCalcType,
DayNetPos,
HasDayTrades,
// Raw Day
RawDayLongPos,
RawDayShortPos,
RawDayNetPos,
// Working
WorkingLongPos,
WorkingLongAvgPrice,
WorkingShortPos,
WorkingShortAvgPrice,
WorkingNetPos,
// Total
TotalPNL,
TotalRealizedPNL,
TotalUnrealizedPNL,
TotalLongPos,
TotalLongAvgPrice,
TotalLongAvgPNL,
TotalShortPos,
TotalShortAvgPrice,
TotalShortAvgPNL,
TotalNetPos,
TotalDelta,
TotalGamma,
TotalVega,
TotalTheta,
// Market
BidPrice,
MidPrice,
AskPrice,
LastPrice,
SettlementPrice,
PNLPrice
}
public static readonly IValueConverter
IncrementalGroupLevelConverter = ValueConverter.create<int, int>(level => level + 1),
GroupLevelMarginConverter = ValueConverter.create<int, Thickness>(level => new Thickness(24 * level, 0, 0, 0)),
HeaderRowHeightConverter = new RangeBlockConverter<double> { Minimum = MinHeaderRowHeight },
ExpandedVisualStateConverter = ValueConverter.create<bool, string>(is_expanded => is_expanded ? "Expanded" : "Collapsed");
public static readonly IMultiValueConverter
TotalHeaderRowDisplayTextConverter = ValueConverter.create<int, int, string>((filtered, total) => $"Total ({(filtered == total ? total.ToString() : $"{filtered}/{total}")})"),
CellParamsConverter = ValueConverter.create<object, Columns, PNLCell>((item, col) => new PNLCell(item, col));
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
static readonly string _node_path_separator = $" {SpecialCharacters.Dot} ";
IDisposable _anchors_hook, _animations_hook;
int _anchor_version;
public PNLView() {
SetCurrentValue(AnchorIndexProperty, new AnchorIndex());
InitializeComponent();
SetBinding(AnchorsProperty, new Binding(nameof(PNLVM.anchors)));
_grid.ConnectOnLoaded(() => {
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter presenter)) {
presenter.GroupItemSelectionPivot += on_group_item_selection_pivot;
return Disposable.Create(() => presenter.GroupItemSelectionPivot -= on_group_item_selection_pivot);
}
return null;
});
void on_group_item_selection_pivot(object sender, GroupItemSelectionPivotEventArgs e) =>
e.Handled = e.group_item is PNLRowCollectionViewGroup pnl_group && pnl_group.is_expanded;
}
public new PNLVM DataContext {
get => base.DataContext as PNLVM;
set => base.DataContext = value;
}
public bool EnableAnimations {
get => _animations_hook != null;
set {
if (EnableAnimations != value) {
if (_animations_hook == null) {
_animations_hook = AdornerHelper.attach_continuously_on_loaded(
() => new GridAnimation.DIYGridAnimationAdorner<Columns>(_grid, _grid.descendants_of_type<DIYGridRowsPresenter>().First()),
_grid);
} else {
_animations_hook.Dispose();
_animations_hook = null;
}
}
}
}
#region AnchorIndex
public static readonly DependencyProperty AnchorIndexProperty =
WPFHelper.create_dp<AnchorIndex, PNLView>(
nameof(AnchorIndex),
(o, v) => v?.reset(o?.Anchors));
public AnchorIndex AnchorIndex {
get => GetValue(AnchorIndexProperty) as AnchorIndex;
set => SetValue(AnchorIndexProperty, value);
}
#endregion
#region GroupLevel
public static readonly DependencyProperty GroupLevelProperty =
DependencyProperty.RegisterAttached(
"GroupLevel",
typeof(int),
typeof(PNLView),
new PropertyMetadata(0, (d, e) => {
if (d is GroupItem group && e.NewValue is int level)
group.SetBinding(DIYGrid.ColumnKeyProperty, new Binding($"{nameof(Anchors)}[{level}]") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(PNLView), 1) });
}));
public static int GetGroupLevel(DependencyObject d) => (int)d.GetValue(GroupLevelProperty);
public static void SetGroupLevel(DependencyObject d, int value) => d.SetValue(GroupLevelProperty, value);
void _grid_loaded(object sender, RoutedEventArgs e) {
// this is a hack to make room for the 'totals row' between the column headers and the regular rows.
if (_grid.try_find_descendent_of_type(out DIYGridRowsPresenter rp)) {
rp.SetBinding(MarginProperty,
BindingHelper.create_multi(
ValueConverter.create<PNLHeaderRowDisplay, double, Thickness>((hrd, row_height) => {
double top = 0;
if ((hrd & PNLHeaderRowDisplay.Total) != 0)
top += row_height;
if ((hrd & PNLHeaderRowDisplay.SelectedTotal) != 0)
top += row_height;
return new Thickness(0, top, 0, 0);
}),
new Binding(nameof(PNLVM.header_row_display)),
new Binding {
Path = new PropertyPath(DIYGrid.RowHeightProperty),
Converter = HeaderRowHeightConverter,
Source = _grid
}));
}
}
#endregion
#region Anchors
public static readonly DependencyProperty AnchorsProperty =
WPFHelper.create_dp<IEnumerable<Columns>, PNLView>(
nameof(Anchors),
(o, v) => {
if (o?._grid?.Columns is not ObservableCollection<DIYGrid.ColumnBase> cols ||
o?._grid?.Rows is not ICollectionView rows_view ||
v is not INotifyCollectionChanged anchor_notifier ||
v is not IReadOnlyList<Columns> anchor_keys) {
return;
}
int version = ++o._anchor_version;
bool busy = false;
var pnl_cols = cols.CastObservable<DIYGrid.ColumnBase, PNLColumn>();
o._anchors_hook?.Dispose();
o._anchors_hook = Disposable.Create(
anchor_notifier.Subscribe((_, __) => {
if (busy)
return;
// invoke async to avoid noisy consecutive resets due to batch changes
busy = true;
o.Dispatcher.InvokeAsync(() => {
sync_anchor_cols();
busy = false;
});
}));
o.AnchorIndex?.reset(anchor_keys);
sync_anchor_cols();
void sync_anchor_cols() {
// since reset is invoked asynchronously, need to check we are still the latest version.
if (o._anchor_version != version)
return;
o.AnchorIndex?.reset(anchor_keys);
// move all the anchor columns to the left, ordered by anchor position (grouping hierarchy).
for (int i = 0; i < anchor_keys.Count; i++) {
if (pnl_cols.TryGetFirst(c => c.key == anchor_keys[i], out var match, out int curr_pos)) {
match.is_visible = true;
if (curr_pos != i) {
cols.Move(curr_pos, i);
}
match.anchor_index = i + 1;
}
}
for (int i = anchor_keys.Count; i < pnl_cols.Count; i++) {
pnl_cols[i].anchor_index = null;
}
using (rows_view.DeferRefresh()) {
rows_view.GroupDescriptions.Clear();
var anchor_cols = from key in anchor_keys
join col in pnl_cols on key equals col.key
select new PNLGroupDescription(col);
rows_view.GroupDescriptions.AddRange(anchor_cols);
}
}
});
public IEnumerable<Columns> Anchors {
get => GetValue(AnchorsProperty) as IEnumerable<Columns>;
set => SetValue(AnchorsProperty, value);
}
#endregion
DIYGridPopupManager _popup_man {
get {
if (_popup_man_backing == null) {
_popup_man_backing = DIYGridPopupManager.create(this);
}
return _popup_man_backing;
}
}
void _cell_click(PNLCell cell) {
Log.Ui.info($"PNLCell Click: {cell}.");
try {
switch (cell.column) {
case Columns.SODNetPos:
case Columns.SODLongPos:
case Columns.SODShortPos:
case Columns.DayNetPos:
case Columns.DayLongPos:
case Columns.DayShortPos:
case Columns.RawDayNetPos:
case Columns.RawDayLongPos:
case Columns.RawDayShortPos:
case Columns.TotalNetPos:
case Columns.TotalLongPos:
case Columns.TotalShortPos: {
var oneof = cell.oneof();
if (oneof == null)
return;
var (vm, context, title, fg) = cell.oneof().Match(_row_fills, _group_fills, _header_fills);
_show_auxiliary_popup(vm, context, title, fg);
}
break;
case Columns.WorkingNetPos:
case Columns.WorkingLongPos:
case Columns.WorkingShortPos: {
var (vm, context, title, fg) = cell.oneof().Match(_orderbook_params, _orderbook_params, _orderbook_params);
_show_auxiliary_popup(vm, context, title, fg);
}
break;
}
} catch (Exception ex) {
Log.Ui.warn($"{nameof(PNLView)} error on cell click: {ex}.");
}
}
#region Fills
(FillsVM vm, string context, string title, Color fg) _row_fills(PNLRowCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
}
(FillsVM vm, string context, string title, Color fg) _group_fills(PNLGroupCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
}
(FillsVM vm, string context, string title, Color fg) _header_fills(PNLHeaderRowCell cell) {
var (title_desc, fg) = _fills_title(cell.column);
return (DataContext.mk_fills_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
}
static (string desc, Color fg) _fills_title(Columns key) {
switch (key) {
case Columns.SODLongPos: return ("Open Buy Fills", BTSColors.MarketData.bid);
case Columns.SODShortPos: return ("Open Sell Fills", BTSColors.MarketData.ask);
case Columns.SODNetPos: return ("Open Fills", BTSColors.UI.default_text);
case Columns.DayLongPos: return ("Day Buy Fills", BTSColors.MarketData.bid);
case Columns.DayShortPos: return ("Day Sell Fills", BTSColors.MarketData.ask);
case Columns.DayNetPos: return ("Day Fills", BTSColors.UI.default_text);
case Columns.RawDayLongPos: return ("Raw Day Buy Fills", BTSColors.MarketData.bid);
case Columns.RawDayShortPos: return ("Raw Day Sell Fills", BTSColors.MarketData.ask);
case Columns.RawDayNetPos: return ("Raw Day Fills", BTSColors.UI.default_text);
case Columns.TotalLongPos: return ("All Buy Fills", BTSColors.MarketData.bid);
case Columns.TotalShortPos: return ("All Sell Fills", BTSColors.MarketData.ask);
case Columns.TotalNetPos: return ("All Fills", BTSColors.UI.default_text);
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
}
}
#endregion
#region OrderBook
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLRowCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.row, cell.column), $"{cell.row.account_name} {cell.row.security}", title_desc, fg);
}
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLGroupCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.group, cell.column), string.Join(_node_path_separator, cell.group.GroupNamePath), title_desc, fg);
}
(OrderBookVM vm, string context, string title, Color fg) _orderbook_params(PNLHeaderRowCell cell) {
var (title_desc, fg) = _orderbook_title(cell.column);
return (DataContext.mk_orderbook_vm(cell.header_row, cell.column), EnumHelper.get_display_name(cell.header_row), title_desc, fg);
}
static (string desc, Color fg) _orderbook_title(Columns key) {
switch (key) {
case Columns.WorkingLongPos: return ("Working Buy Orders", BTSColors.MarketData.bid);
case Columns.WorkingShortPos: return ("Working Sell Orders", BTSColors.MarketData.ask);
case Columns.WorkingNetPos: return ("Working Orders", BTSColors.UI.default_text);
default: throw ExceptionBuilder.Argument.unexpected_case(key, nameof(key));
}
}
#endregion
void _cell_click_executed(object sender, ExecutedRoutedEventArgs e) {
if (e.Parameter is PNLCell cell) {
_cell_click(cell);
} else Log.Ui.warn($"{nameof(PNLView)} '{nameof(_cell_click_executed)}' parameter should be a {typeof(PNLCell)} (actually '{e.Parameter}').");
}
void _on_group_item_mouse_down(object sender, MouseButtonEventArgs e) {
if (!e.Handled && sender is FrameworkElement el && el.DataContext is PNLRowCollectionViewGroup group) {
if (e.ClickCount == 2) {
group.is_expanded = !group.is_expanded;
} else if (el.try_find_ancestor_of_type(out DIYGrid grid) && grid.SelectionMode != SelectionMode.Single) {
group.is_all_selected = !group.is_all_selected.GetValueOrDefault();
} else {
return;
}
e.Handled = true;
}
}
void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
var view = new HeaderedContentControl {
Content = content,
Header = new AuxiliaryDataHeader {
context = context_desc,
title = title_desc,
title_foreground = fg
},
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Resources = new AuxiliaryDataResources()
};
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
_popup_man.show(view);
}
class PNLGroupDescription : PropertyGroupDescription, IEquatable<PNLGroupDescription> {
public PNLGroupDescription(PNLColumn column) : base(column.data_member_path.Path) =>
key = column.key;
public Columns key { get; }
public bool Equals(PNLGroupDescription other) =>
other != null && key == other.key;
public override int GetHashCode() =>
key.GetHashCode();
public override bool Equals(object obj) =>
Equals(obj as PNLGroupDescription);
}
}
}
+244 -243
View File
@@ -1,243 +1,244 @@
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.shell;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui.controls;
using Spark.Api.Orders;
using Spark.Core;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using OrderPair = System.ValueTuple<bts.api.SecurityImmutable?, bts.api.orderman.OrderRecordImmutable>;
using TickDisplaySettingsImmutable = bts.api.domain.TickDisplaySettingsImmutable;
namespace bts.spark {
internal class Activity {
public SecurityImmutable? security;
public string message;
public ILogViewerCategory category;
}
class OrderActivitySummarizer : IDisposable {
readonly IBus _bus;
IReadOnlyDictionary<uint, SecurityMD> _security = new Dictionary<uint, SecurityMD>();
IDisposable _dispose;
static readonly IEqualityComparer<OrderRecordImmutable> _id_comparer =
bts.utils.EqualityComparer.Create<OrderRecordImmutable, uint>(a => a.Key);
public OrderActivitySummarizer(IBus bus, IObservable<TableUpdate<OrderRecordTuple, uint>> orders) {
_bus = bus;
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(sec => _security = sec),
orders.Subscribe(orders_updated));
}
public void Dispose() => Disposable.dispose(ref _dispose);
#region formatting
SecurityImmutable? try_get_security(uint security_key) {
if (_security.TryGetValue(security_key, out SecurityMD md)) {
return md.security;
}
return null;
}
public static string format_order_success(
uint key, double price, double size, Side side, ContractSpecificationImmutable? spec) =>
$"({key}) {side.ToString().ToLower()} {size} @ {spec.format(price)}";
string format_order_fail(
double price,
double size,
Side side,
ContractSpecificationImmutable? spec,
string error) =>
$"Failed to {side.ToString().ToLower()} {size} @ {spec.format(price)}. {error}";
string format_cancel_success(OrderRecordImmutable order, ContractSpecificationImmutable? spec) {
return $"({order.Key}) Canceled {order.short_description(spec)}, was working {order.working_quantity()}";
}
string format_cancel_error(OrderRecordImmutable order, ContractSpecificationImmutable? spec, string error) {
return $"({order.Key}) Failed to cancel {order.short_description(spec)}: {error}";
}
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after) =>
_format_fill_summary(_header(after.Key), before, after);
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after, SecurityImmutable? security) =>
_format_fill_summary(_header(after.Key, security), before, after);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status) =>
_format_status_change(_header(key), key, old_status, new_status);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status, SecurityImmutable? security) =>
_format_status_change(_header(key, security), key, old_status, new_status);
// since changes are naggled, transition changes aren't always detected (e.g. a new order changes before observers are notified of its existence), so old_status might be null.
public static string format_status_change(uint key, OrderStatus? old_status, OrderStatus new_status, SecurityImmutable? security) {
string header = _header(key, security);
return old_status.HasValue ?
_format_status_change(header, key, old_status.Value, new_status) :
_format_status_change(header, key, new_status);
}
public static string order_completion_debrief(
OrderRecordImmutable order,
SecurityImmutable? security,
TickDisplaySettingsImmutable qty_display) =>
_format_order_completion(
_header(order.Key, security), order, security?.ContractSpec, qty_display);
public static string modified_description(OrderRecordImmutable old_order, OrderRecordImmutable new_order, ContractSpecificationImmutable? spec) {
double old_price = old_order.State.Price, new_price = new_order.State.Price;
uint old_size = old_order.State.Quantity, new_size = new_order.State.Quantity;
uint old_attached_target_key = old_order.attached_target_order_key(), new_attached_target_key = new_order.attached_target_order_key();
return StringBuilderEx.join(prefix, change_descs());
//
string prefix() => $"({old_order.Key}) Changed";
IEnumerable<string> change_descs() {
if (old_price != new_price)
yield return $"price from {spec.format(old_price)} to {spec.format(new_price)}";
if (old_size != new_size)
yield return $"size from {old_size} to {new_size}, working {new_order.working_quantity()}";
if (old_attached_target_key != new_attached_target_key)
yield return $"attached target from {old_attached_target_key} to {new_attached_target_key}";
}
}
static string _header(uint key) => $"({key})";
static string _header(uint key, SecurityImmutable? security) => $"({key}) {security.display_name()}";
static string _format_fill_summary(string header, OrderRecordImmutable before, OrderRecordImmutable after) {
int fb = (int)before.State.FilledQuantity;
int fa = (int)after.State.FilledQuantity;
return $"{header} {after.Def.Side.past_tense()} {(fa - fb)} ({fa} total), still working {after.working_quantity()}";
}
static string _format_status_change(string header, uint key, OrderStatus new_status) =>
$"{header} Status changed to {new_status}";
static string _format_status_change(string header, uint key, OrderStatus old_status, OrderStatus new_status) =>
$"{header} Status changed from {old_status} to {new_status}";
static string _format_order_completion(
string header,
OrderRecordImmutable order,
ContractSpecificationImmutable? spec,
TickDisplaySettingsImmutable qty_display) {
var filled_qty_str = qty_display.format(order.filled_quantity());
return $"{header} Completed: {order.side().past_tense()} {filled_qty_str}, " +
$"@ {spec.spec_or_default().format(order.avg_price())} (avg)";
}
#endregion
void orders_updated(TableUpdate<OrderRecordTuple, uint> update) {
foreach (var o in update.added) {
if (!string.IsNullOrEmpty(o.order.State.LastMsg) && o.order.State.Created.ToDateTime().Date == DateTime.Today) {
if (o.security.HasValue) {
log_status(ActivityLogCategory.warn, o.security.Value, o.order.State.LastMsg);
}
}
}
foreach (var order in update.changes) {
var neworder = order.New.order;
var oldorder = order.Old.order;
var security = order.New.security ?? order.Old.security;
//
// description
var mod_desc = modified_description(oldorder, neworder, security?.ContractSpec);
if (!string.IsNullOrEmpty(mod_desc)) {
log_status(ActivityLogCategory.modify, security, mod_desc);
}
//
// filled quantity
if (oldorder.State.FilledQuantity != neworder.State.FilledQuantity && !neworder.has_parent()) {
log_status(ActivityLogCategory.fill, security, format_fill_summary(oldorder, neworder));
}
//
// status
if (oldorder.State.Status != neworder.State.Status && !neworder.has_parent()) {
log_status(ActivityLogCategory.modify, security,
format_status_change(neworder.Key, oldorder.State.Status, neworder.State.Status));
}
// last msg
if (oldorder.State.LastMsg != neworder.State.LastMsg) {
if (security.HasValue) {
log_status(ActivityLogCategory.warn, security, neworder.State.LastMsg);
}
}
}
}
public void log_status(ILogViewerCategory category, SecurityImmutable? security, string msg) {
_bus.post(SparkTags.LogActivity, new Activity { security = security, message = msg, category = category });
}
internal void on_send_order(
OrderDefinitionImmutable order, double price, double size, uint? key, string error) {
var security = try_get_security(order.SecurityKey);
log_status(ActivityLogCategory.order, security, key.HasValue ?
format_order_success(key.Value, price, size, order.Side, security?.ContractSpec) :
format_order_fail(price, size, order.Side, security?.ContractSpec, error));
}
internal void on_cancel_order(OrderRecordImmutable order, bool success, string error) {
var security = try_get_security(order.Def.SecurityKey);
log_status(ActivityLogCategory.cancel, security, success ?
format_cancel_success(order, security?.ContractSpec) :
format_cancel_error(order, security?.ContractSpec, error));
}
internal void update_order(
OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "change from");
internal void update_order_failed(OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "failed to change from");
void _log_order_update(
OrderRecordImmutable order,
double price,
double size,
bool algo_changed,
string change_desc_prefix
) {
if (price == order.price() && size == order.working_quantity() && !algo_changed) {
// nothing actually changed
return;
}
const string algo_change_text = "change algo parameters";
var security = try_get_security(order.Def.SecurityKey);
var change_descs = new List<string>(1);
var price_display = TickUtils.get_tick(security, price).to_api_tick_display_settingsi();
var price_qty_change_desc = OrdersExtensions.update_description(
order, price, size, price_display);
if (!price_qty_change_desc.is_null_or_empty()) {
change_descs.Add(price_qty_change_desc);
}
if (algo_changed) {
change_descs.Add(algo_change_text);
}
var desc = $"({order.Key}) {change_desc_prefix} {"; ".join(change_descs)}";
log_status(ActivityLogCategory.modify, security, desc);
}
}
}
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.shell;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui.controls;
using Spark.Api.Orders;
using Spark.Core;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using OrderPair = System.ValueTuple<bts.api.SecurityImmutable?, bts.api.orderman.OrderRecordImmutable>;
using TickDisplaySettingsImmutable = bts.api.domain.TickDisplaySettingsImmutable;
namespace bts.spark {
internal class Activity {
public SecurityImmutable? security;
public string message;
public ILogViewerCategory category;
}
class OrderActivitySummarizer : IDisposable {
readonly IBus _bus;
IReadOnlyDictionary<uint, SecurityMD> _security = new Dictionary<uint, SecurityMD>();
IDisposable _dispose;
static readonly IEqualityComparer<OrderRecordImmutable> _id_comparer =
bts.utils.EqualityComparer.Create<OrderRecordImmutable, uint>(a => a.Key);
public OrderActivitySummarizer(IBus bus, IObservable<TableUpdate<OrderRecordTuple, uint>> orders) {
_bus = bus;
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(sec => _security = sec),
orders.Subscribe(orders_updated));
}
public void Dispose() => Disposable.dispose(ref _dispose);
#region formatting
SecurityImmutable? try_get_security(uint security_key) {
if (_security.TryGetValue(security_key, out SecurityMD md)) {
return md.security;
}
return null;
}
public static string format_order_success(
uint key, double price, double size, Side side, ContractSpecificationImmutable? spec) =>
$"({key}) {side.ToString().ToLower()} {size} @ {spec.format(price)}";
string format_order_fail(
double price,
double size,
Side side,
ContractSpecificationImmutable? spec,
string error) =>
$"Failed to {side.ToString().ToLower()} {size} @ {spec.format(price)}. {error}";
string format_cancel_success(OrderRecordImmutable order, ContractSpecificationImmutable? spec) {
return $"({order.Key}) Canceled {order.short_description(spec)}, was working {order.working_quantity()}";
}
string format_cancel_error(OrderRecordImmutable order, ContractSpecificationImmutable? spec, string error) {
return $"({order.Key}) Failed to cancel {order.short_description(spec)}: {error}";
}
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after) =>
_format_fill_summary(_header(after.Key), before, after);
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after, SecurityImmutable? security) =>
_format_fill_summary(_header(after.Key, security), before, after);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status) =>
_format_status_change(_header(key), key, old_status, new_status);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status, SecurityImmutable? security) =>
_format_status_change(_header(key, security), key, old_status, new_status);
// since changes are naggled, transition changes aren't always detected (e.g. a new order changes before observers are notified of its existence), so old_status might be null.
public static string format_status_change(uint key, OrderStatus? old_status, OrderStatus new_status, SecurityImmutable? security) {
string header = _header(key, security);
return old_status.HasValue ?
_format_status_change(header, key, old_status.Value, new_status) :
_format_status_change(header, key, new_status);
}
public static string order_completion_debrief(
OrderRecordImmutable order,
SecurityImmutable? security,
TickDisplaySettingsImmutable qty_display) =>
_format_order_completion(
_header(order.Key, security), order, security?.ContractSpec, qty_display);
public static string modified_description(OrderRecordImmutable old_order, OrderRecordImmutable new_order, ContractSpecificationImmutable? spec) {
double old_price = old_order.State.Price, new_price = new_order.State.Price;
double old_size = old_order.State.quantity();
double new_size = new_order.State.quantity();
uint old_attached_target_key = old_order.attached_target_order_key(), new_attached_target_key = new_order.attached_target_order_key();
return StringBuilderEx.join(prefix, change_descs());
//
string prefix() => $"({old_order.Key}) Changed";
IEnumerable<string> change_descs() {
if (old_price != new_price)
yield return $"price from {spec.format(old_price)} to {spec.format(new_price)}";
if (old_size != new_size)
yield return $"size from {old_size} to {new_size}, working {new_order.working_quantity()}";
if (old_attached_target_key != new_attached_target_key)
yield return $"attached target from {old_attached_target_key} to {new_attached_target_key}";
}
}
static string _header(uint key) => $"({key})";
static string _header(uint key, SecurityImmutable? security) => $"({key}) {security.display_name()}";
static string _format_fill_summary(string header, OrderRecordImmutable before, OrderRecordImmutable after) {
int fb = (int)before.State.FilledQuantity;
int fa = (int)after.State.FilledQuantity;
return $"{header} {after.Def.Side.past_tense()} {(fa - fb)} ({fa} total), still working {after.working_quantity()}";
}
static string _format_status_change(string header, uint key, OrderStatus new_status) =>
$"{header} Status changed to {new_status}";
static string _format_status_change(string header, uint key, OrderStatus old_status, OrderStatus new_status) =>
$"{header} Status changed from {old_status} to {new_status}";
static string _format_order_completion(
string header,
OrderRecordImmutable order,
ContractSpecificationImmutable? spec,
TickDisplaySettingsImmutable qty_display) {
var filled_qty_str = qty_display.format(order.filled_quantity());
return $"{header} Completed: {order.side().past_tense()} {filled_qty_str}, " +
$"@ {spec.spec_or_default().format(order.avg_price())} (avg)";
}
#endregion
void orders_updated(TableUpdate<OrderRecordTuple, uint> update) {
foreach (var o in update.added) {
if (!string.IsNullOrEmpty(o.order.State.LastMsg) && o.order.State.Created.ToDateTime().Date == DateTime.Today) {
if (o.security.HasValue) {
log_status(ActivityLogCategory.warn, o.security.Value, o.order.State.LastMsg);
}
}
}
foreach (var order in update.changes) {
var neworder = order.New.order;
var oldorder = order.Old.order;
var security = order.New.security ?? order.Old.security;
//
// description
var mod_desc = modified_description(oldorder, neworder, security?.ContractSpec);
if (!string.IsNullOrEmpty(mod_desc)) {
log_status(ActivityLogCategory.modify, security, mod_desc);
}
//
// filled quantity
if (oldorder.State.FilledQuantity != neworder.State.FilledQuantity && !neworder.has_parent()) {
log_status(ActivityLogCategory.fill, security, format_fill_summary(oldorder, neworder));
}
//
// status
if (oldorder.State.Status != neworder.State.Status && !neworder.has_parent()) {
log_status(ActivityLogCategory.modify, security,
format_status_change(neworder.Key, oldorder.State.Status, neworder.State.Status));
}
// last msg
if (oldorder.State.LastMsg != neworder.State.LastMsg) {
if (security.HasValue) {
log_status(ActivityLogCategory.warn, security, neworder.State.LastMsg);
}
}
}
}
public void log_status(ILogViewerCategory category, SecurityImmutable? security, string msg) {
_bus.post(SparkTags.LogActivity, new Activity { security = security, message = msg, category = category });
}
internal void on_send_order(
OrderDefinitionImmutable order, double price, double size, uint? key, string error) {
var security = try_get_security(order.SecurityKey);
log_status(ActivityLogCategory.order, security, key.HasValue ?
format_order_success(key.Value, price, size, order.Side, security?.ContractSpec) :
format_order_fail(price, size, order.Side, security?.ContractSpec, error));
}
internal void on_cancel_order(OrderRecordImmutable order, bool success, string error) {
var security = try_get_security(order.Def.SecurityKey);
log_status(ActivityLogCategory.cancel, security, success ?
format_cancel_success(order, security?.ContractSpec) :
format_cancel_error(order, security?.ContractSpec, error));
}
internal void update_order(
OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "change from");
internal void update_order_failed(OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "failed to change from");
void _log_order_update(
OrderRecordImmutable order,
double price,
double size,
bool algo_changed,
string change_desc_prefix
) {
if (price == order.price() && size == order.working_quantity() && !algo_changed) {
// nothing actually changed
return;
}
const string algo_change_text = "change algo parameters";
var security = try_get_security(order.Def.SecurityKey);
var change_descs = new List<string>(1);
var price_display = TickUtils.get_tick(security, price).to_api_tick_display_settingsi();
var price_qty_change_desc = OrdersExtensions.update_description(
order, price, size, price_display);
if (!price_qty_change_desc.is_null_or_empty()) {
change_descs.Add(price_qty_change_desc);
}
if (algo_changed) {
change_descs.Add(algo_change_text);
}
var desc = $"({order.Key}) {change_desc_prefix} {"; ".join(change_descs)}";
log_status(ActivityLogCategory.modify, security, desc);
}
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
namespace mbts.spark {
namespace mbts.spark {
using bts;
using bts.api;
using bts.api.core;
@@ -662,7 +662,7 @@
if (o.State.Price == price) return true;
var wq = o.working_quantity();
if (wq > 0) {
return await orders_model.update_order(o, price, o.State.Quantity);
return await orders_model.update_order(o, price, o.State.quantity());
}
}
return false;
@@ -64,9 +64,5 @@ namespace System.Reactive.Linq {
}
});
}
public static IObservable<TResult> SelectInline<T, TResult>(
this IObservable<T> source, Func<T, TResult> selector) =>
Observable.Create<TResult>(o => source.Subscribe(v => o.OnNext(selector(v))));
}
}
Binary file not shown.
@@ -0,0 +1,59 @@
#nullable enable
using bts.collections;
using Spark.Core;
using Spark.Core.Protobuf;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace bts.shell;
static class ShellWindowSettingsDirectoryStore {
public static ShellWindowSettingsDirectoryStore<T> Create<T>(string path, string searchPattern) =>
new(path, searchPattern);
}
class ShellWindowSettingsDirectoryStore<T> {
readonly LazyOnce<Dictionary<string, SingleFileStore>> profiles;
public ShellWindowSettingsDirectoryStore(string path, string searchPattern) =>
profiles = new(() => {
try {
return Directory.EnumerateFiles(path, searchPattern).Select(SingleFileStore.FromPath).ToDictionary(sfs => sfs.Name);
} catch (Exception ex) {
Log.Ui.error($"Failed to enumerate {typeof(T)} settings files in '{path}'. {ex}");
}
return new(0);
});
public IReadOnlyDictionary<string, T?> Profiles =>
MappedDictionary.create(profiles.Value, sfs => sfs.Value);
class SingleFileStore {
LazyOnce<T?> value;
public SingleFileStore(string path) {
Name = Path.GetFileNameWithoutExtension(path);
value = new(LoadValue);
T? LoadValue() {
try {
using var f = File.OpenRead(path);
return ProtobufSerializationHelper.deserialize<T>(f);
} catch (Exception ex) {
Log.Ui.error($"Failed to load {typeof(T)} file '{path}'. {ex}");
}
return default;
}
}
public static SingleFileStore FromPath(string path) =>
new(path);
public string Name { get; }
public T? Value => value.Value;
}
}
+4
View File
@@ -77,6 +77,10 @@
<EmbeddedResource Include="Sounds/whistle.wav" />
</ItemGroup>
<ItemGroup>
<None Include="ShellWindowSettings/**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Target Name="diagnostic" AfterTargets="PreBuildEvent" />
</Project>
@@ -254,9 +254,9 @@ namespace mbts.ui {
public static class PNL {
public static BTSColor foreground => UI.default_foreground;
public static BTSColor anchor => Colors.Gold;
public static BTSColor profit => Colors.Green;
public static BTSColor profit => BTSCoreColors.spring_green;
public static BTSColor net_zero => BTSCoreColors.gray;
public static BTSColor loss => BTSCoreColors.venetian_red;
public static BTSColor loss => BTSCoreColors.fire_brick;
public static BTSColor header_row_background => 0xffaaaaaa;
// group background colors
public static BTSColor group_row_background1 => 0x19141420;
@@ -276,8 +276,8 @@ namespace mbts.ui {
}
public static Color anchor_color => Colors.Gold;
public static Color profit_color => Colors.Green;
public static Color loss_color => BTSCoreColors.venetian_red;
public static Color profit_color => profit;
public static Color loss_color => loss;
public static Color header_row_background_color => 0xffaaaaaa.argb_color();
// group background colors
public static Color group_row_background_color1 => group_row_background1;
@@ -1040,6 +1040,7 @@ namespace mbts.ui {
}
public static class DayFills {
public static BTSColor background => UI.common_background;
public static BTSColor foreground => UI.default_foreground;
}
public static class ComboReport {
public static BTSColor foreground => UI.default_text;
@@ -83,6 +83,7 @@
public static BTSColor green => 0xff00c000;
public static BTSColor green_p_light => 0xff99ee90;
public static BTSColor green_dark_light => 0xff255d25;
public static BTSColor spring_green => 0xff00fb76;
public static BTSColor light_sea_green => 0xff17b794;
public static BTSColor very_light_malachite_green => 0xff52e887;
public static BTSColor may_green => 0xff43a23c;
@@ -100,6 +101,7 @@
public static BTSColor red_light_dark => 0xffd69a9a;
public static BTSColor red_light_dark_saturated => 0xffd67e7e;
public static BTSColor permanent_geranium_lake => 0xffde3429;
public static BTSColor fire_brick => 0xffb1373a;
public static BTSColor fire_opal => 0xfff05454;
public static BTSColor paradise_pink => 0xffe94560;
public static BTSColor tulip => 0xffff878d;
@@ -209,11 +209,15 @@ namespace mbts.ui.controls {
this.prefix = prefix;
public string PNL => $"{prefix} PNL";
public string RealizedPNL => $"{prefix} R PNL";
public string UnrealizedPNL => $"{prefix} U PNL";
public string Net => $"{prefix} {PNLHeaders.Net}";
public string Long => $"{prefix} {PNLHeaders.Long}";
public string LongAvg => $"{Long} {AvgPrice}";
public string LongAvgPNL => $"{Long} {AvgPrice} PNL";
public string Short => $"{prefix} {PNLHeaders.Short}";
public string ShortAvg => $"{Short} {AvgPrice}";
public string ShortAvgPNL => $"{Short} {AvgPrice} PNL";
public string Delta => $"{prefix} {PNLHeaders.Delta}";
public string Gamma => $"{prefix} {PNLHeaders.Gamma}";
public string Vega => $"{prefix} {PNLHeaders.Vega}";
@@ -2,6 +2,8 @@
using mbts.ui;
using Spark.Api.Data;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -134,6 +136,20 @@ namespace mbts.spark.filters {
}
}
public void add_exp_date(
ExpirationDate dt, Func<TRow, ExpirationDate?> selector, bool null_passes_filter = false) {
// Use local fns to provide context in stack trace
_predicates.Add(null_passes_filter ? null_pass_impl : null_fail_impl);
bool null_fail_impl(TRow r) =>
selector(r) == dt;
bool null_pass_impl(TRow r) {
var rdt = selector(r);
return !rdt.HasValue || rdt == dt;
}
}
public Func<TRow, bool> to_predicate() =>
_predicates.Count == 0 ? null : new Func<TRow, bool>(o => _predicates.All(p => p(o)));
}
@@ -8,7 +8,8 @@
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:usp="clr-namespace:mbts.ui.spark"
d:DesignHeight="100" d:DesignWidth="800" x:Name="_me_price">
d:DesignHeight="100" d:DesignWidth="800" x:Name="_me_price"
Foreground="{u:BTSColor UI.input_text}">
<UserControl.Resources>
<cv:NullableDoubleConverter x:Key="TickDefaultPriceConverter" DefaultValue="0"/>
<cv:TickDisplayConverter x:Key="TickDisplayConverter" x:Shared="False"/>
@@ -6,6 +6,7 @@
using mbts.ui;
using mbts.ui.shell;
using Spark.Api.Data;
using Spark.Core.Disposables;
using System;
@@ -0,0 +1,27 @@
namespace mbts.ui;
using mbts.ui.shell;
using Spark.Api.Data;
using Spark.Core.Disposables;
using System;
static class DisplayTranslatorExtensions {
public static IDisposable connect_owner<T>(
this IDisplayTranslator<T> display_translator,
IRaiseNotifyPropertyChanged owner,
params string[] properties
) {
if (display_translator is null)
throw new ArgumentNullException(nameof(display_translator));
if (owner is null)
throw new ArgumentNullException(nameof(owner));
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
for (int i = 0; i < properties.Length; i++)
owner.raise_notify_property_changed(properties[i]);
}
}
}
@@ -1,215 +0,0 @@
using bts.utils;
using mbts.ui.shell;
using Spark.Core;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reactive.Linq;
namespace mbts.ui {
interface IDisplayTranslator<T> {
T display_value { get; set; }
T actual_value { get; set; }
event Action value_changed;
}
enum QuantityConversionOpType {
None,
MultiplyActual,
DivideActual
}
readonly struct QuantityConversionData<T> : IEquatable<QuantityConversionData<T>>
where T : INumber<T> {
public QuantityConversionData(T value, QuantityConversionOpType op) {
this.value = value;
this.op = op;
}
public enum OpType {
None,
MultiplyActual,
DivideActual
}
public T value { get; }
public QuantityConversionOpType op { get; }
public QuantityConversionData<TResult> convert_numeric_type<TResult>()
where TResult : INumber<TResult> =>
new(NumericCastConverter<T, TResult>.convert(value), op);
public bool Equals(QuantityConversionData<T> other) =>
value == other.value &&
op == other.op;
public override bool Equals(object obj) =>
obj is QuantityConversionData<T> other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.auto_phase(value.GetHashCode(), op.GetHashCode());
public static bool operator ==(QuantityConversionData<T> x, QuantityConversionData<T> y) =>
x.Equals(y);
public static bool operator !=(QuantityConversionData<T> x, QuantityConversionData<T> y) =>
!x.Equals(y);
}
static class DisplayQuantityMultiplier {
public static DisplayQuantityMultiplier<uint> create_uint() => new(
DisplayTranslatorExtensions.to_display,
DisplayTranslatorExtensions.from_display);
public static DisplayQuantityMultiplier<double> create_double() => new(
DisplayTranslatorExtensions.to_display,
DisplayTranslatorExtensions.from_display);
}
class DisplayQuantityMultiplier<T> : IDisplayTranslator<T> where T : struct, INumber<T> {
readonly Func<QuantityConversionData<T>, T, T> _to_display, _from_display;
readonly IEqualityComparer<T> _comparer;
T _actual_value;
QuantityConversionData<T> _conversion_data;
public DisplayQuantityMultiplier(
Func<QuantityConversionData<T>, T, T> to_display,
Func<QuantityConversionData<T>, T, T> from_display,
IEqualityComparer<T> comparer = null,
T actual_value = default,
QuantityConversionData<T> conversion_data = default) {
//
_to_display = to_display ?? throw new ArgumentNullException(nameof(to_display));
_from_display = from_display ?? throw new ArgumentNullException(nameof(from_display));
_comparer = comparer ?? EqualityComparer<T>.Default;
_actual_value = actual_value;
_conversion_data = conversion_data;
}
public event Action value_changed;
public T actual_value {
get => _actual_value;
set {
if (!_comparer.Equals(_actual_value, value)) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public T display_value {
get => _to_display(_conversion_data, _actual_value);
set => actual_value = _from_display(_conversion_data, value);
}
public QuantityConversionData<T> conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
class OptionalDisplayQuantityMultiplier<T> : IDisplayTranslator<T?> where T : struct, INumber<T> {
T? _actual_value;
QuantityConversionData<T> _conversion_data;
public event Action value_changed;
public T? actual_value {
get => _actual_value;
set {
if (_actual_value != value) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public T? display_value {
get => _actual_value.HasValue ?
_conversion_data.to_display(_actual_value.Value) : _actual_value;
set => actual_value = value.HasValue ?
_conversion_data.from_display(value.Value) : value;
}
public QuantityConversionData<T> conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
static class DisplayTranslatorExtensions {
public static IDisposable connect_owner<T>(
this IDisplayTranslator<T> display_translator,
IRaiseNotifyPropertyChanged owner,
params string[] properties
) {
if (display_translator is null)
throw new ArgumentNullException(nameof(display_translator));
if (owner is null)
throw new ArgumentNullException(nameof(owner));
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
for (int i = 0; i < properties.Length; i++)
owner.raise_notify_property_changed(properties[i]);
}
}
public static IObservable<T> subscribe_actual_value<T>(
this IDisplayTranslator<T> display_translator) =>
subscribe_actual_value(display_translator, EqualityComparer<T>.Default);
public static IObservable<T> subscribe_actual_value<T>(
this IDisplayTranslator<T> display_translator, IEqualityComparer<T> comparer) {
if (comparer is null)
throw new ArgumentNullException(nameof(comparer));
return Observable.Create<T>(o => {
var curr = display_translator.actual_value;
o.OnNext(curr);
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
var next = display_translator.actual_value;
if (!comparer.Equals(curr, next))
o.OnNext(curr = next);
}
});
}
public static T to_display<T>(
this QuantityConversionData<T> quantity_conversion_data, T actual_value)
where T : INumber<T> =>
quantity_conversion_data.op switch {
QuantityConversionOpType.DivideActual =>
actual_value.safe_divide(quantity_conversion_data.value),
QuantityConversionOpType.MultiplyActual =>
actual_value * quantity_conversion_data.value,
_ => actual_value,
};
public static T from_display<T>(
this QuantityConversionData<T> quantity_conversion_data, T display_value)
where T : INumber<T> =>
quantity_conversion_data.op switch {
QuantityConversionOpType.DivideActual => display_value * quantity_conversion_data.value,
QuantityConversionOpType.MultiplyActual =>
display_value.safe_divide(quantity_conversion_data.value),
_ => display_value,
};
}
}
+47
View File
@@ -0,0 +1,47 @@
#nullable enable
namespace Spark.Api.MarketData;
using System;
public readonly struct Bbo : IEquatable<Bbo> {
public readonly double BidPrice;
public readonly double BidQuantity;
public readonly double AskPrice;
public readonly double AskQuantity;
public Bbo(double bidPrice, double bidQuantity, double askPrice, double askQuantity) {
BidPrice = bidPrice;
BidQuantity = bidQuantity;
AskPrice = askPrice;
AskQuantity = askQuantity;
}
public static Bbo Empty => new(double.NaN, double.NaN, double.NaN, double.NaN);
public bool Equals(Bbo other) =>
BidPrice.Equals(other.BidPrice) &&
BidQuantity.Equals(other.BidQuantity) &&
AskPrice.Equals(other.AskPrice) &&
AskQuantity.Equals(other.AskQuantity);
public override bool Equals(object? obj) =>
obj is Bbo other && Equals(other);
public override int GetHashCode() =>
HashCode.Combine(BidPrice, BidQuantity, AskPrice, AskQuantity);
public override string ToString() =>
$"({BidQuantity}) {BidPrice} {AskPrice} ({AskQuantity})";
public static bool operator ==(Bbo left, Bbo right) =>
left.Equals(right);
public static bool operator !=(Bbo left, Bbo right) =>
!left.Equals(right);
public static implicit operator (double, double, double, double)(Bbo bbo) =>
(bbo.BidPrice, bbo.BidQuantity, bbo.AskPrice, bbo.AskQuantity);
public static implicit operator Bbo((double, double, double, double) bboTup) =>
(bboTup.Item1, bboTup.Item2, bboTup.Item3, bboTup.Item4);
}
@@ -0,0 +1,44 @@
#nullable enable
namespace Spark.Api.MarketData;
using mbts;
using mbts.co2;
using System.Collections.Generic;
class BookStore {
readonly uint numDepthLevels;
readonly Dictionary<uint, PollableBook> books = new(0);
IReadOnlyDictionary<uint, SecurityMD> securityMds = new Dictionary<uint, SecurityMD>(0);
public BookStore(uint numDepthLevels = NativePriceHelpers.COCONUT_MAX_DEPTH) =>
this.numDepthLevels = numDepthLevels;
public BookStore(IReadOnlyDictionary<uint, SecurityMD> securityMds, uint numDepthLevels = NativePriceHelpers.COCONUT_MAX_DEPTH) {
this.securityMds = securityMds;
this.numDepthLevels = numDepthLevels;
}
public void OnSecurityMds(IReadOnlyDictionary<uint, SecurityMD> securityMds) {
this.securityMds = securityMds;
books.Clear();
}
public PollableBook? GetPollableBook(uint id) {
if (!books.TryGetValue(id, out var book)) {
if (securityMds.TryGetValue(id, out var smd)) {
book = new(smd, numDepthLevels);
books.Add(id, book);
}
}
return book;
}
public BookTuple? GetBook(uint id, bool poll = true) {
var pb = GetPollableBook(id);
if (poll) {
pb?.poll_changes();
}
return pb?.to_book_tuple();
}
}
@@ -22,6 +22,13 @@ namespace Spark.Api.MarketData {
public SecurityImmutable security { get; }
public uint security_id => security.Id;
public Book book { get; }
public Bbo bbo {
get {
(double bidp, double bidq) = book.bids.Count > 0 ? (book.bids[0].price, book.bids[0].size) : (double.NaN, double.NaN);
(double askp, double askq) = book.asks.Count > 0 ? (book.asks[0].price, book.asks[0].size) : (double.NaN, double.NaN);
return new(bidp, bidq, askp, askq);
}
}
public TickDisplaySettingsImmutable quantity_display =>
_quantity_display ?? Security.Defaults.quantity_display_settingsi;
@@ -180,38 +180,6 @@ namespace bts.md {
}
}
class PollableBook {
readonly SecurityMD _security_md;
readonly uint _num_depth_levels;
ulong _last_update_time = 0;
// Reuse the same arrays as an optimization to avoid reallocating memory on each poll.
// Note: this will break if the code isn't called in a thread-safe manner.
NativePriceSize[] _bids;
NativePriceSize[] _asks;
public PollableBook(SecurityMD security_md, uint num_depth_levels) {
_security_md = security_md;
_num_depth_levels = num_depth_levels;
_bids = new NativePriceSize[_num_depth_levels];
_asks = new NativePriceSize[_num_depth_levels];
}
public BookTuple to_book_tuple() {
var book = new Book(
_security_md.security.Id, _to_book_side(_bids), _to_book_side(_asks));
return new(_security_md.security, book, _security_md.quantity_display);
}
public bool poll_changes() =>
_security_md.md.get_depth(
ref _last_update_time, ref _bids, ref _asks, _num_depth_levels);
static List<PriceSize> _to_book_side(NativePriceSize[] native_book_side) =>
native_book_side
.Select(nps => new PriceSize(nps.size, nps.direct_size, nps.implied_size, nps.price))
.ToList();
}
class PollablePriceTicker {
readonly SecurityMD _security_md;
ulong _bbo_poll_time = 0;
@@ -0,0 +1,102 @@
#nullable enable
namespace Spark.Api.MarketData;
using bts;
using bts.api.core;
using mbts;
using Spark.Core.Reactive;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
public static class MarketDataExtensions {
public static IObservable<TableUpdate<TResult, TKey>>? JoinBbo<T, TResult, TKey>(
this IObservable<TableUpdate<T, TKey>> source,
IObservable<IReadOnlyDictionary<uint, SecurityMD>> securities,
Func<T, uint> getSecId,
Func<T, Bbo, TResult> getResult,
Func<TResult, TKey> getResultKey,
TimeSpan? pollInterval = null
) where TKey : IEquatable<TKey> {
return source.JoinBooks(securities, getSecId, GetResult, getResultKey, pollInterval, 1);
TResult GetResult(T value, BookTuple book) =>
getResult(value, book.bbo);
}
public static IObservable<TableUpdate<(T, BookTuple), TKey>> JoinBooks<T, TKey>(
this IObservable<TableUpdate<T, TKey>> source,
IObservable<IReadOnlyDictionary<uint, SecurityMD>> securities,
Func<T, uint> getSecId,
TimeSpan? pollInterval = null,
uint? numDepthLevels = null
) where TKey : IEquatable<TKey> {
// Get key function from the first update. This is for convenience (one
// less arg), and guarentees the key is consistent with the source data.
bool isKeyInitialized = false;
Func<T, TKey> keyFn = _ => throw new NotImplementedException(
"Key function expected from first update in source sequence");
return source.SelectInline(
s => {
if (!isKeyInitialized) {
isKeyInitialized = true;
Interlocked.Exchange(ref keyFn, Log.notnull(s.key_extractor));
}
return s;
})
.JoinBooks(securities, getSecId, (v, b) => (v, b), getKey, pollInterval, numDepthLevels);
TKey getKey((T v, BookTuple) tup) =>
keyFn(tup.v);
}
public static IObservable<TableUpdate<TResult, TKey>> JoinBooks<T, TResult, TKey>(
this IObservable<TableUpdate<T, TKey>> source,
IObservable<IReadOnlyDictionary<uint, SecurityMD>> securities,
Func<T, uint> getSecId,
Func<T, BookTuple, TResult> getResult,
Func<TResult, TKey> getResultKey,
TimeSpan? pollInterval = null,
uint? numDepthLevels = null
) where TKey : IEquatable<TKey> {
pollInterval ??= TimeSpan.FromSeconds(0.1);
numDepthLevels ??= 5;
return source.CombineLatest(securities.Select(smds => new BookStore(smds)))
.Sample(pollInterval.Value)
.Scan(TableUpdate<TResult, TKey>.mk_empty(getResultKey), Accumulate);
TableUpdate<TResult, TKey> Accumulate(TableUpdate<TResult, TKey> lastOut, (TableUpdate<T, TKey>, BookStore) lastIn) {
var (table, bookStore) = lastIn;
if (table is null || bookStore is null) {
return lastOut;
}
var bd = lastOut.to_builder();
var getKey = Log.notnull(table.key_extractor);
var dirtyKeys = table.added.Concat(table.changes.New()).ToHashSet(getKey);
lock (bookStore) {
foreach (var secGroup in table.state.Values.GroupBy(getSecId)) {
var pbook = bookStore.GetPollableBook(secGroup.Key);
if (pbook is null) {
continue;
}
bool isBookDirty = pbook.poll_changes();
var book = pbook.to_book_tuple();
var dirtyVals = secGroup.AsEnumerable();
if (!isBookDirty) {
dirtyVals = dirtyVals.Where(v => dirtyKeys.Contains(getKey(v)));
}
foreach (var v in dirtyVals) {
var result = getResult(v, book);
bd.add_or_update(result);
}
}
return bd.to_table();
}
}
}
}
@@ -0,0 +1,45 @@
#nullable enable
namespace Spark.Api.MarketData;
using bts.md;
using mbts;
using mbts.co2;
using System.Collections.Generic;
using System.Linq;
class PollableBook {
readonly SecurityMD _security_md;
readonly uint _num_depth_levels;
ulong _last_update_time = 0;
// Reuse the same arrays as an optimization to avoid reallocating memory on each poll.
// Note: this will break if the code isn't called in a thread-safe manner.
NativePriceSize[] _bids;
NativePriceSize[] _asks;
public PollableBook(SecurityMD security_md, uint num_depth_levels = NativePriceHelpers.COCONUT_MAX_DEPTH) {
_security_md = security_md;
_num_depth_levels = num_depth_levels;
_bids = new NativePriceSize[_num_depth_levels];
_asks = new NativePriceSize[_num_depth_levels];
}
public ulong last_update_time => _last_update_time;
public BookTuple to_book_tuple() {
var book = new Book(
_security_md.security.Id, _to_book_side(_bids), _to_book_side(_asks));
return new(_security_md.security, book, _security_md.quantity_display);
}
public bool poll_changes() =>
_security_md.md.get_depth(
ref _last_update_time, ref _bids, ref _asks, _num_depth_levels);
static List<PriceSize> _to_book_side(NativePriceSize[] native_book_side) =>
native_book_side
.Select(nps => new PriceSize(nps.size, nps.direct_size, nps.implied_size, nps.price))
.ToList();
}
-19
View File
@@ -1,19 +0,0 @@
namespace bts.api.orderman {
public static class OrderUtilities {
public static NativeOrderStateImmutable? native_order_state(this OrderStateImmutable order) {
switch(order.CustomCase) {
case OrderState.CustomOneofCase.Market:
return order.Market.NativeState;
case OrderState.CustomOneofCase.Limit:
return order.Limit.NativeState;
case OrderState.CustomOneofCase.Stop:
return order.Stop.NativeState;
case OrderState.CustomOneofCase.StopLimit:
return order.StopLimit.NativeState;
default:
return null;
}
}
}
}
@@ -37,6 +37,20 @@ namespace Spark.Api.Orders {
public static double fill_ratio(this OrderStateImmutable state) =>
state.filled_quantity() / state.quantity();
public static NativeOrderStateImmutable? native_order_state(this OrderStateImmutable order) {
switch(order.CustomCase) {
case OrderState.CustomOneofCase.Market:
return order.Market.NativeState;
case OrderState.CustomOneofCase.Limit:
return order.Limit.NativeState;
case OrderState.CustomOneofCase.Stop:
return order.Stop.NativeState;
case OrderState.CustomOneofCase.StopLimit:
return order.StopLimit.NativeState;
default:
return null;
}
}
#endregion
public static double filled_quantity(this OrderRecordImmutable order) =>
@@ -255,7 +269,7 @@ namespace Spark.Api.Orders {
}
public static string short_description(this OrderRecordImmutable order, ContractSpecificationImmutable? spec) =>
$"{(order.HasDef ? order.Def.Side.ToString() : "(unknown-side)")} {(order.HasState ? ($@"{spec.format(order.State.Price)} @ {order.State.Quantity}") : "(unknown-price) @ (unknown-quantity)")}";
$"{(order.HasDef ? order.Def.Side.ToString() : "(unknown-side)")} {(order.HasState ? $@"{order.State.quantity()} @ {spec.format(order.State.Price)}" : "(unknown-price) @ (unknown-quantity)")}";
public static string short_description(
SecurityImmutable security, Side side, double price, double quantity) {
@@ -0,0 +1,69 @@
namespace bts.shell.pnl;
using bts.api;
using bts.api.orderman;
using mbts.spark;
using mbts.utils;
using Spark.Api.Data;
using Spark.Api.Orders;
using Spark.Api.Position;
using System.Collections.Generic;
using System.Linq;
readonly struct WorkingOrderData {
public readonly WorkingOrderSideData long_data, short_data;
WorkingOrderData(WorkingOrderSideData long_data, WorkingOrderSideData short_data) {
this.long_data = long_data;
this.short_data = short_data;
}
public static WorkingOrderData Empty =>
new(WorkingOrderSideData.EmptyLong, WorkingOrderSideData.EmptyShort);
public double top_bid =>
long_data.data.Count > 0 ?
long_data.data.First().Key :
double.NaN;
public double top_ask =>
short_data.data.Count > 0 ?
short_data.data.First().Key :
double.NaN;
public PositionPriceSize summary =>
new(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
public static WorkingOrderData Create(
IEnumerable<OrderRecordImmutable> orders, QuantityConversionData<double> qty_conversion) =>
Create(orders.Select(o => (o, qty_conversion)));
public static WorkingOrderData Create(
IEnumerable<(OrderRecordImmutable order, QuantityConversionData<double> qty_conversion)> orders) =>
orders.Aggregate(Empty, (seed, tup) => seed.Add(tup.order, tup.qty_conversion));
public WorkingOrderData Add(OrderRecordImmutable order, QuantityConversionData<double> qty_conversion) {
if (order.working_quantity() <= 0) {
return new(long_data, short_data);
}
return order.side() switch {
Side.Buy => new(long_data.Add(order, qty_conversion), short_data),
Side.Sell => new(long_data, short_data.Add(order, qty_conversion)),
_ => throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side"),
};
}
public WorkingOrderData Remove(OrderRecordImmutable order) {
if (order.working_quantity() <= 0) {
return new(long_data, short_data);
}
return order.side() switch {
Side.Buy => new(long_data.Remove(order.Key, order.price()), short_data),
Side.Sell => new(long_data, short_data.Remove(order.Key, order.price())),
_ => throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side"),
};
}
}
@@ -0,0 +1,128 @@
namespace bts.shell.pnl;
using bts.api;
using bts.api.orderman;
using mbts.spark;
using mbts.utils;
using Spark.Api.Data;
using Spark.Api.Orders;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
public readonly struct WorkingOrderSideData {
public readonly double avg_price, display_avg_price;
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable order, QuantityConversionData<double> qty_conversion)>> data;
public readonly double working_total;
public readonly double display_working_total;
WorkingOrderSideData(
double avg_price,
double working_total,
double display_avg_price,
double display_working_total,
ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>> data
) {
this.avg_price = avg_price;
this.working_total = working_total;
this.display_avg_price = display_avg_price;
this.data = data;
this.display_working_total = display_working_total;
}
public static WorkingOrderSideData EmptyLong => new(
double.NaN, 0, double.NaN, 0,
ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>>(
new ReverseComparer<double>(new OrderPriceComparer())));
public static WorkingOrderSideData EmptyShort => new(
double.NaN, 0, double.NaN, 0,
ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>>(
new OrderPriceComparer()));
public double top_price =>
data.Count > 0 ?
data.First().Key :
double.NaN;
public double top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.order.working_quantity()) :
0;
public double display_top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.qty_conversion
.convert_numeric_type<double>()
.to_display(o.order.working_quantity())) :
0;
public WorkingOrderSideData Add(OrderRecordImmutable order_record) =>
Add(order_record, new());
public WorkingOrderSideData Add(
OrderRecordImmutable order_record, QuantityConversionData<double> qty_conversion) {
var working_qty = order_record.working_quantity();
if (working_qty <= 0)
throw new ArgumentException(nameof(working_qty));
uint order = order_record.Key;
var dworking_qty = qty_conversion.to_display(working_qty);
double price = order_record.price();
if (working_total == 0) {
Log.assert(display_working_total == 0);
return new WorkingOrderSideData(price, working_qty, display_avg_price, dworking_qty, data.Add(
price, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>.Empty.Add(order, (order_record, qty_conversion))));
} else return new WorkingOrderSideData(
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
working_total + working_qty,
display_avg_price + ((price - display_avg_price) * ((double)dworking_qty / (display_working_total + dworking_qty))),
display_working_total + dworking_qty,
data.SetItem(price, data.TryGetValue(price, out var curr) ?
curr.Add(order, (order_record, qty_conversion)) :
ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>.Empty.Add(order, (order_record, qty_conversion))));
}
public WorkingOrderSideData Remove(uint order, double price) {
var tmp_data = data;
if (!tmp_data.TryGetValue(price, out var wdata))
throw new KeyNotFoundException(nameof(price));
if (!wdata.TryGetValue(order, out var worder))
throw new KeyNotFoundException(nameof(order));
var wqty = worder.order.working_quantity();
var dwqty = worder.qty_conversion.to_display(wqty);
wdata = wdata.Remove(order);
if (wdata.Count == 0)
tmp_data = tmp_data.Remove(price);
if (tmp_data.Count == 0)
return new(
double.NaN, 0, double.NaN, 0,
ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData<double>)>>(
data.KeyComparer));
return new(
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
working_total - wqty,
display_avg_price - ((price - display_avg_price) * (dwqty / (display_working_total - dwqty))),
display_working_total - dwqty,
tmp_data);
}
public override string ToString() {
if (data.Count > 0) {
if (display_working_total == 0) {
return string.Empty;
}
var spec = Security.Defaults.ContractSpecificationImmutable;
var best_order = data.Values.First().Values.First();
var best_price_volume = best_order.qty_conversion.to_display(best_order.order.working_quantity());
var best_price = best_order.order.price();
return display_working_total == best_price_volume ?
$"{best_price_volume} @ {spec.format(best_price)}" :
$"{best_price_volume} ({display_working_total}) @ {spec.format(best_price)}";
}
return string.Empty;
}
}
@@ -0,0 +1,99 @@
namespace Spark.Api.Data;
using mbts.ui;
using System;
using System.Collections.Generic;
using System.Numerics;
public class DisplayQuantityMultiplier<T> : IDisplayTranslator<T> where T : struct, INumber<T> {
readonly Func<QuantityConversionData<T>, T, T> _to_display, _from_display;
readonly IEqualityComparer<T> _comparer;
T _actual_value;
QuantityConversionData<T> _conversion_data;
public DisplayQuantityMultiplier(
Func<QuantityConversionData<T>, T, T> to_display,
Func<QuantityConversionData<T>, T, T> from_display,
IEqualityComparer<T> comparer = null,
T actual_value = default,
QuantityConversionData<T> conversion_data = default) {
//
_to_display = to_display ?? throw new ArgumentNullException(nameof(to_display));
_from_display = from_display ?? throw new ArgumentNullException(nameof(from_display));
_comparer = comparer ?? EqualityComparer<T>.Default;
_actual_value = actual_value;
_conversion_data = conversion_data;
}
public event Action value_changed;
public T actual_value {
get => _actual_value;
set {
if (!_comparer.Equals(_actual_value, value)) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public T display_value {
get => _to_display(_conversion_data, _actual_value);
set => actual_value = _from_display(_conversion_data, value);
}
public QuantityConversionData<T> conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
public class OptionalDisplayQuantityMultiplier<T> : IDisplayTranslator<T?> where T : struct, INumber<T> {
T? _actual_value;
QuantityConversionData<T> _conversion_data;
public event Action value_changed;
public T? actual_value {
get => _actual_value;
set {
if (_actual_value != value) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public T? display_value {
get => _actual_value.HasValue ?
_conversion_data.to_display(_actual_value.Value) : _actual_value;
set => actual_value = value.HasValue ?
_conversion_data.from_display(value.Value) : value;
}
public QuantityConversionData<T> conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
public static class DisplayQuantityMultiplier {
public static DisplayQuantityMultiplier<uint> create_uint() => new(
DisplayTranslatorExtensions.to_display,
DisplayTranslatorExtensions.from_display);
public static DisplayQuantityMultiplier<double> create_double() => new(
DisplayTranslatorExtensions.to_display,
DisplayTranslatorExtensions.from_display);
}
@@ -0,0 +1,67 @@
namespace Spark.Api.Data;
using bts.api;
using mbts.utils;
using System;
public enum ExpirationDateType {
None = 0,
FixedDate = 1,
Perpetual = 2
}
public readonly struct ExpirationDate : IEquatable<ExpirationDate>, IComparable<ExpirationDate> {
public ExpirationDate(DateTime value, ExpirationDateType type) {
Value = value;
Type = type;
}
public static ExpirationDate None => new(default, ExpirationDateType.None);
public static ExpirationDate Perpetual => new(default, ExpirationDateType.Perpetual);
public static ExpirationDate FixedDate(DateTime dateTime) => new(dateTime, ExpirationDateType.FixedDate);
public DateTime Value { get; }
public ExpirationDateType Type { get; }
static ComparerBuilder<ExpirationDate> comparer = new ComparerBuilder<ExpirationDate>() {
ed => ed.Type,
ed => ed.Value
};
public int CompareTo(ExpirationDate other) =>
comparer.Compare(this, other);
public bool Equals(ExpirationDate other) =>
Value == other.Value && Type == other.Type;
public override bool Equals(object obj) =>
obj is ExpirationDate other && Equals(other);
public override int GetHashCode() =>
HashCode.Combine(Value.GetHashCode(), (int)Type);
public override string ToString() =>
ToString(DateFormat.DayMonthAbvDiff2000);
public string ToString(DateFormat dateFormat) =>
Type switch {
ExpirationDateType.None => "None",
ExpirationDateType.FixedDate => DateFormatter.Format(Value, dateFormat),
ExpirationDateType.Perpetual => "Perp",
_ => "Invalid",
};
public static bool operator ==(ExpirationDate left, ExpirationDate right) =>
left.Equals(right);
public static bool operator !=(ExpirationDate left, ExpirationDate right) =>
!left.Equals(right);
public static bool operator <(ExpirationDate left, ExpirationDate right) =>
comparer.Compare(left, right) < 0;
public static bool operator >(ExpirationDate left, ExpirationDate right) =>
comparer.Compare(left, right) > 0;
}
@@ -0,0 +1,60 @@
using bts.utils;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reactive.Linq;
namespace Spark.Api.Data;
public interface IDisplayTranslator<T> {
T display_value { get; set; }
T actual_value { get; set; }
event Action value_changed;
}
public static class DisplayTranslatorExtensions {
public static IObservable<T> subscribe_actual_value<T>(
this IDisplayTranslator<T> display_translator) =>
subscribe_actual_value(display_translator, EqualityComparer<T>.Default);
public static IObservable<T> subscribe_actual_value<T>(
this IDisplayTranslator<T> display_translator, IEqualityComparer<T> comparer) {
if (comparer is null)
throw new ArgumentNullException(nameof(comparer));
return Observable.Create<T>(o => {
var curr = display_translator.actual_value;
o.OnNext(curr);
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
var next = display_translator.actual_value;
if (!comparer.Equals(curr, next))
o.OnNext(curr = next);
}
});
}
public static T to_display<T>(
this QuantityConversionData<T> quantity_conversion_data, T actual_value)
where T : INumber<T> =>
quantity_conversion_data.op switch {
QuantityConversionOpType.DivideActual =>
actual_value.safe_divide(quantity_conversion_data.value),
QuantityConversionOpType.MultiplyActual =>
actual_value * quantity_conversion_data.value,
_ => actual_value,
};
public static T from_display<T>(
this QuantityConversionData<T> quantity_conversion_data, T display_value)
where T : INumber<T> =>
quantity_conversion_data.op switch {
QuantityConversionOpType.DivideActual => display_value * quantity_conversion_data.value,
QuantityConversionOpType.MultiplyActual =>
display_value.safe_divide(quantity_conversion_data.value),
_ => display_value,
};
}
@@ -0,0 +1,43 @@
namespace Spark.Api.Data;
using Spark.Core;
using System;
using System.Numerics;
public enum QuantityConversionOpType {
None,
MultiplyActual,
DivideActual
}
public readonly struct QuantityConversionData<T> : IEquatable<QuantityConversionData<T>>
where T : INumber<T> {
public QuantityConversionData(T value, QuantityConversionOpType op) {
this.value = value;
this.op = op;
}
public T value { get; }
public QuantityConversionOpType op { get; }
public QuantityConversionData<TResult> convert_numeric_type<TResult>()
where TResult : INumber<TResult> =>
new(NumericCastConverter<T, TResult>.convert(value), op);
public bool Equals(QuantityConversionData<T> other) =>
value == other.value &&
op == other.op;
public override bool Equals(object obj) =>
obj is QuantityConversionData<T> other && Equals(other);
public override int GetHashCode() =>
HashCode.Combine(value, op);
public static bool operator ==(QuantityConversionData<T> x, QuantityConversionData<T> y) =>
x.Equals(y);
public static bool operator !=(QuantityConversionData<T> x, QuantityConversionData<T> y) =>
!x.Equals(y);
}
@@ -5,6 +5,7 @@ namespace mbts.spark {
using ProtoBuf;
using Spark.Api.Data;
using Spark.Core.Collections;
using Spark.Core.Protobuf;
@@ -27,6 +28,15 @@ namespace mbts.spark {
internal static SecurityCriteria from_keys(IEnumerable<uint> keys) =>
new(new[] { new SecurityCriteriaClause(security_keys: keys.ToArray()) });
internal static SecurityCriteria from_expirations(params ExpirationDate[] exp_dates) =>
new(new[] { new SecurityCriteriaClause(expirations: exp_dates) });
internal static SecurityCriteria from_expirations(params DateTime[] exp_dates) =>
from_expirations(exp_dates.AsEnumerable());
internal static SecurityCriteria from_expirations(IEnumerable<DateTime> expirations) =>
new(new[] { SecurityCriteriaClause.from_expirations(expirations) });
internal static SecurityCriteria from_securities(params SecurityImmutable[] securities) =>
from_securities(securities.AsEnumerable());
@@ -4,6 +4,7 @@ namespace mbts.spark {
using ProtoBuf;
using Spark.Api.Data;
using Spark.Core.Collections;
using Spark.Core.Protobuf;
@@ -17,12 +18,12 @@ namespace mbts.spark {
Venue[]? exchanges = null,
SecurityType[]? security_types = null,
string[]? symbols = null,
DateTime[]? expirations = null,
ExpirationDate[]? expirations = null,
uint[]? security_keys = null,
string? pretty_name = null,
double[]? strikes = null,
PutOrCall[]? callputs = null,
string[]? names= null
string[]? names = null
) {
this.exchanges = exchanges;
this.security_types = security_types;
@@ -32,7 +33,7 @@ namespace mbts.spark {
this.pretty_name = pretty_name;
this.strikes = strikes;
this.callputs = callputs;
this.names=names;
this.names = names;
}
public static SecurityCriteriaClause from_securities(
@@ -58,6 +59,20 @@ namespace mbts.spark {
return new SecurityCriteriaClause(security_keys: keys, pretty_name: pretty_name);
}
public static SecurityCriteriaClause from_expirations(IEnumerable<DateTime> expirations) {
return expirations is null
? new()
: from_expirations(expirations.Select(convert).ToArray());
static ExpirationDate convert(DateTime e) =>
Security.is_perpetual_exp(e)
? ExpirationDate.Perpetual
: ExpirationDate.FixedDate(e);
}
public static SecurityCriteriaClause from_expirations(params ExpirationDate[] expirations) =>
new(expirations: expirations);
[ProtoMember(1)]
public string? pretty_name { get; }
@@ -71,7 +86,7 @@ namespace mbts.spark {
public string[]? symbols { get; }
// Not stored in protobuf (yet)
public DateTime[]? expirations { get; }
public ExpirationDate[]? expirations { get; }
[ProtoMember(5)]
public uint[]? security_keys { get; }
@@ -3,6 +3,8 @@ namespace mbts.spark {
using bts.api;
using bts.collections;
using Spark.Api.Data;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -180,7 +182,7 @@ namespace mbts.spark {
Venue[]? exchanges = null,
SecurityType[]? security_types = null,
string[]? symbols = null,
DateTime[]? expirations = null,
ExpirationDate[]? expirations = null,
uint[]? security_keys = null,
string? pretty_name = null,
double[]? strikes = null,
@@ -567,6 +567,9 @@ namespace mbts {
public static double round(this bts.api.domain.TickFactorsImmutable tick, double input) =>
_round_tick(tick.tick_unit_frac(), input);
public static void round(this bts.api.domain.TickFactorsImmutable tick, ref double input) =>
input = _round_tick(tick.tick_unit_frac(), input);
public static double round(this RegionalTickFactors regional_tick, double input) {
if (DoubleHelper.IsNaNOrInfinity(input)) {
return input;
+1 -1
View File
@@ -13,7 +13,7 @@ namespace bts.api {
public static security_key key(Security s) { return s.Id; }
public static bool is_perpetual_exp(DateTime exp) =>
exp.Year < 2000 || exp.Year > 2999;
exp.Year < 2000 || exp.Year > 2099;
public static class Defaults {
public static ContractSpecification ContractSpecification =>
+76 -175
View File
@@ -1,9 +1,8 @@
namespace bts.api {
using mbts.utils;
using Spark.Core.Collections;
using System;
using System.Collections.Generic;
using System.Linq;
class SecurityComparer : IComparer<Security>, IComparer<SecurityImmutable> {
static readonly SecurityComparer _instance = new();
@@ -25,182 +24,24 @@ namespace bts.api {
}
public static int compare(SecurityImmutable x, SecurityImmutable y) {
int c = non_deterministic_impl();
if (c == 0) {
c = x.ToString().CompareTo(y.ToString());
if (c == 0) {
c = x.Id.CompareTo(y.Id);
var xlegs = new ContractLegEnumerable(x);
var ylegs = new ContractLegEnumerable(y);
using var xen = xlegs.GetEnumerator();
using var yen = ylegs.GetEnumerator();
int c = 0;
while (xen.MoveNext()) {
if (!yen.MoveNext()) {
return 1;
}
c = new ComparisonCriteria(xen.Current).CompareTo(new(yen.Current));
if (c != 0) {
return c;
}
}
return c;
// this has been observed to be non-determinisitc with certain ICE securities
// (it is determenistic with other exchanges..as far as we know).
int non_deterministic_impl() {
// | Type 1 | Type 2 | Comp Hierarchy |
// |------------------------------------------|
// | SPOT | SPOT | sym, curr |
// | SPOT | FUT | sym, curr |
// | SPOT | OPT | SPOT < OPT |
// | SPOT | SPREAD | * SPOT < SPREAD |
// | SPOT | SYNTH | * SPOT < SYNTH |
// | FUT | FUT | sym, exp, curr |
// | FUT | OPT | * FUT < OPT |
// | FUT | SPREAD | * FUT < SPREAD |
// | FUT | SYNTH | * FUT < SYNTH |
// | OPT | OPT | sym, exp, curr, k, c/p |
// | OPT | SPREAD | OPT <=> first leg |
// | OPT | SYNTH | OPT <=> first leg |
// | SPREAD | SPREAD | legs <=> legs |
// | SPREAD | SYNTH | legs <=> legs |
//
// * comp result is constant (there is no comp hierarchy)
return x.ContractCase switch {
Security.ContractOneofCase.Future =>
y.ContractCase switch {
Security.ContractOneofCase.Spot =>
new ComparerBuilder<SecurityImmutable> {
s => s.Symbol,
s => s.Currency
}.Compare(x, y),
Security.ContractOneofCase.Future =>
new ComparerBuilder<SecurityImmutable> {
s => s.Symbol,
s => s.Future.expiration_date()
}.Compare(x, y),
Security.ContractOneofCase.Option or
Security.ContractOneofCase.Spread or
Security.ContractOneofCase.Synthetic => -1,
_ => 1,
},
Security.ContractOneofCase.Spot =>
y.ContractCase switch {
Security.ContractOneofCase.Spot or
Security.ContractOneofCase.Future =>
new ComparerBuilder<SecurityImmutable> {
s => s.Symbol,
s => s.Currency
}.Compare(x, y),
_ => -1,
},
Security.ContractOneofCase.Option or
Security.ContractOneofCase.Spread or
Security.ContractOneofCase.Synthetic =>
y.ContractCase switch {
Security.ContractOneofCase.Spread or
Security.ContractOneofCase.Synthetic or
Security.ContractOneofCase.Option => compare_flat_legs(),
_ => 1,
},
Security.ContractOneofCase.None =>
y.ContractCase == Security.ContractOneofCase.None ? 0 : -1,
_ => throw new InvalidOperationException(),
};
if (yen.MoveNext()) {
return -1;
}
int compare_flat_legs() {
var xlegs = new ContractLegEnumerable(x).ToList();
var ylegs = new ContractLegEnumerable(y).ToList();
int max = Math.Min(xlegs.Count, ylegs.Count);
for (int i = 0; i < max; i++) {
var xleg = xlegs[i];
var yleg = ylegs[i];
int comp = _compare(
xleg.Security, xleg.signed_quantity(),
yleg.Security, yleg.signed_quantity());
if (comp != 0) {
return comp;
}
}
return xlegs.Count.CompareTo(ylegs.Count);
}
}
public static int compare(string x_sym, FutureContract x, string y_sym, FutureContract y) {
int sym_comp = x_sym.CompareTo(y_sym);
return sym_comp == 0 ? x.expiration_date().CompareTo(y.expiration_date()) : sym_comp;
}
public static int compare(string x_sym, FutureContractImmutable x, string y_sym, FutureContractImmutable y) {
int sym_comp = x_sym.CompareTo(y_sym);
return sym_comp == 0 ? x.expiration_date().CompareTo(y.expiration_date()) : sym_comp;
}
/// <summary>
/// Compares option/future securities
/// </summary>
/// <exception cref="ArgumentException"/>
static int _compare(SecurityImmutable x, double xqty, SecurityImmutable y, double yqty) {
static bool is_valid_sec_type(SecurityImmutable sec) =>
sec.ContractCase switch {
Security.ContractOneofCase.Spot or
Security.ContractOneofCase.Future or
Security.ContractOneofCase.Option => true,
_ => false
};
if (!is_valid_sec_type(x)) {
throw new ArgumentException("Security not of type Option or Future", nameof(x));
}
if (!is_valid_sec_type(y)) {
throw new ArgumentException("Security not of type Option or Future", nameof(y));
}
// symbol
int comp = string.Compare(x.Symbol, y.Symbol);
if (comp != 0) {
return comp;
}
// expiration
var xexp = x.security_expiration();
var yexp = y.security_expiration();
if (xexp.HasValue) {
if (!yexp.HasValue) {
return -1;
}
comp = xexp.Value.CompareTo(yexp.Value);
if (comp != 0) {
return comp;
}
} else if (yexp.HasValue) {
return 1;
}
// strike
var xk = x.strike();
var yk = y.strike();
if (double.IsNaN(xk) != double.IsNaN(yk)) {
return double.IsNaN(xk) ? -1 : 1;
}
if (!double.IsNaN(xk)) {
comp = xk.CompareTo(yk);
if (comp != 0) {
return comp;
}
}
// security type
comp = x.ContractCase.CompareTo(y.ContractCase);
if (comp != 0) {
return comp;
}
// quantity
comp = xqty.CompareTo(yqty);
if (comp != 0) {
return comp;
}
// venue
comp = x.Exchange.CompareTo(y.Exchange);
if (comp != 0) {
return comp;
}
// id
comp = x.Id.CompareTo(y.Id);
return comp;
return 0;
}
int IComparer<Security>.Compare(Security x, Security y) =>
@@ -208,5 +49,65 @@ namespace bts.api {
int IComparer<SecurityImmutable>.Compare(SecurityImmutable x, SecurityImmutable y) =>
compare(x, y);
readonly struct ComparisonCriteria : IComparable<ComparisonCriteria> {
readonly SecurityImmutable security;
readonly double legQty;
public ComparisonCriteria(SecurityImmutable security) {
this.security = security;
legQty = 1;
}
public ComparisonCriteria(ContractLegImmutable leg) {
security = leg.Security;
legQty = leg.signed_quantity();
}
public string Symbol => security.Symbol;
public DateTime? Expiration => security.ContractCase switch {
Security.ContractOneofCase.Future => security.Future.expiration_date(),
Security.ContractOneofCase.Option => security.Option.expiration_date(),
_ => null,
};
public string Currency => security.Currency;
public double? Strike => security.HasOption ? security.Option.Strike : null;
public PutOrCall? CallPut => security.HasOption ? security.Option.CallPut : null;
public Venue Exchange => security.Exchange;
public uint Id => security.Id;
public int CompareTo(ComparisonCriteria other) {
var c = string.Compare(Symbol, other.Symbol);
if (c != 0) {
return c;
}
c = NullCheckComparer.Compare(Expiration, other.Expiration);
if (c != 0) {
return c;
}
c = string.Compare(Currency, other.Currency);
if (c != 0) {
return c;
}
c = NullCheckComparer.Compare(Strike, other.Strike);
if (c != 0) {
return c;
}
c = NullCheckComparer.Compare(CallPut, other.CallPut);
if (c != 0) {
return c;
}
c = NullCheckComparer.Compare(Exchange, other.Exchange);
if (c != 0) {
return c;
}
c = legQty.CompareTo(other.legQty);
if (c != 0) {
return c;
}
c = Id.CompareTo(other.Id);
return c;
}
}
}
}
@@ -1,4 +1,6 @@
namespace bts.api {
using Spark.Api.Data;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -37,15 +39,20 @@ 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() },
public static IEnumerable<ExpirationDate> normal_expirations(this SecurityImmutable security) {
return security.ContractCase switch {
Security.ContractOneofCase.Spot => new[] { ExpirationDate.None },
Security.ContractOneofCase.Future => new[] { future_exp(security.Future.expiration_date()) },
Security.ContractOneofCase.Option => new[] { ExpirationDate.FixedDate(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>()
_ => Enumerable.Empty<ExpirationDate>()
};
static ExpirationDate future_exp(DateTime exp_date) =>
Security.is_perpetual_exp(exp_date) ? ExpirationDate.Perpetual : ExpirationDate.FixedDate(exp_date);
}
public static ProductImmutable product(this SecurityImmutable security) =>
new(new() {
Exchange = security.Exchange,
@@ -68,7 +75,7 @@ namespace bts.api {
public static DateTime expiration_date(this FutureContractImmutable future) =>
new((int)future.ExpirationYear, (int)future.ExpirationMonth, (int)future.ExpirationDay);
internal static DateTime earliest_expiration(this SecurityImmutable security) =>
internal static ExpirationDate earliest_expiration(this SecurityImmutable security) =>
security.normal_expirations().Min();
public static double strike(this SecurityImmutable security) =>
@@ -135,6 +142,7 @@ namespace bts.api {
domain.SecurityType.Future => SecurityType.Future,
domain.SecurityType.Option => SecurityType.Option,
domain.SecurityType.Spread => SecurityType.Spread,
domain.SecurityType.Synthetic => SecurityType.Synthetic,
domain.SecurityType.Unspecified => SecurityType.Unset,
_ => throw new NotImplementedException(),
};
@@ -145,6 +153,7 @@ namespace bts.api {
SecurityType.Future => domain.SecurityType.Future,
SecurityType.Option => domain.SecurityType.Option,
SecurityType.Spread => domain.SecurityType.Spread,
SecurityType.Synthetic => domain.SecurityType.Synthetic,
SecurityType.Unset => domain.SecurityType.Unspecified,
_ => throw new NotImplementedException(),
};
@@ -1,4 +1,6 @@
namespace bts.api {
using Spark.Api.Data;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -106,8 +108,10 @@
string? symbol = null,
Security.ContractOneofCase? type = null,
DateFormat date_format = DateFormat.DayMonthAbvDiff2000) {
var result = Security.is_perpetual_exp(exp_date) ?
"Perp" : DateFormatter.Format(exp_date, date_format);
var exp_type = Security.is_perpetual_exp(exp_date)
? ExpirationDateType.Perpetual
: ExpirationDateType.FixedDate;
var result = new ExpirationDate(exp_date, exp_type).ToString(date_format);
if (symbol is not null) {
result = symbol + " " + result;
}
@@ -1,149 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace bts.collections {
class OperableHierarchicalCollectionModel<TRow, T, TKey> where TKey : IEquatable<TKey> where TRow : IHierarchicalRow<T> {
readonly Func<T, TRow> _row_factory;
readonly Func<T, TKey> _key;
SortedTree<TRow, TKey> _tree;
IComparer<TRow> _comparer;
Func<TRow, bool> _filter;
List<INode<TRow>> _cached_filtered = new List<INode<TRow>>();
public OperableHierarchicalCollectionModel(Func<T, TRow> row_factory, Func<T, TKey> key, Func<T, TKey> parent_key, TKey no_parent_key, IComparer<TRow> comparer) {
_row_factory = row_factory;
_tree = new SortedTree<TRow, TKey>(_comparer = comparer, el => key(el.value), el => parent_key(el.value), no_parent_key);
_key = key;
}
public IComparer<TRow> comparer {
get => _comparer;
set {
_tree = _tree.Resort(_comparer = value);
_cached_filtered = null;
}
}
public Func<TRow, bool> filter {
get => _filter;
set {
_filter = value;
_cached_filtered = null;
_refresh_hierarchical_props();
}
}
public IEnumerable<TRow> rows {
get {
return _root_nodes.SelectMany(impl);
IEnumerable<TRow> impl(INode<TRow> n) {
yield return n.value;
if (n.value.is_expanded) {
foreach (var _n in n.inner.SelectMany(impl))
yield return _n;
}
}
}
}
public IEnumerable<TRow> all_rows {
get {
return _root_nodes.SelectMany(impl);
IEnumerable<TRow> impl(INode<TRow> n) {
yield return n.value;
foreach (var _n in n.inner.SelectMany(impl))
yield return _n;
}
}
}
public int total_row_count { get; private set; }
IEnumerable<INode<TRow>> _root_nodes =>
_filter == null ? _tree.RootNodes : _cached_filtered ?? (_cached_filtered = _tree.RootNodes.filtered(_filter).sorted_hard(_comparer));
public IEnumerable<TRow> GetAllRows(bool excludeFiltered = false) =>
excludeFiltered ? all_rows : _tree;
// collapse
public void collapse_row(TKey key) => _toggle_is_expanded(key, false);
public void collapse_all() => _toggle_is_expanded(false);
// expand
public void expand_row(TKey key) => _toggle_is_expanded(key, true);
public void expand_all() => _toggle_is_expanded(true);
// lookups
public bool try_get_row(TKey key, out TRow row) {
if (_tree.TryGetNode(key, out var node)) {
row = node.value;
return true;
}
row = default;
return false;
}
public TRow get_parent(TKey key) {
if (try_get_row(key, out TRow r)) {
return _tree.GetParent(r).value;
}
return default;
}
public IEnumerable<TRow> get_descendent_rows(TKey key) {
if (_tree.TryGetNode(key, out var n)) {
yield return n.value;
foreach (var v in n.inner_values())
yield return v;
}
}
// update
public void update(IEnumerable<T> added, IEnumerable<T> changed, IEnumerable<TKey> removed) {
foreach (var k in removed) {
if (_tree.Remove(k))
total_row_count--;
}
foreach (var v in changed) {
_tree.TryReplace(_key(v), r => { r.on_value_changed(v); return r; });
}
foreach (var v in added) {
if (_tree.Insert(_row_factory(v)))
total_row_count++;
}
_cached_filtered = null;
_refresh_hierarchical_props();
}
void _refresh_hierarchical_props() {
foreach (var n in _root_nodes)
impl(n, 0);
void impl(INode<TRow> n, int depth_level) {
if (n.value.depth_level != depth_level) {
n.value.on_depth_level_changed(depth_level);
}
using (var en = n.inner.GetEnumerator()) {
if (en.MoveNext()) {
if (!n.value.is_parent)
n.value.on_is_parent_changed(true);
depth_level++;
do impl(en.Current, depth_level);
while (en.MoveNext());
} else if (n.value.is_parent)
n.value.on_is_parent_changed(false);
}
}
}
void _toggle_is_expanded(TKey key, bool is_expanded) =>
_tree.TryReplace(key, r => {
if (r.is_expanded != is_expanded)
r.on_is_expanded_changed(is_expanded);
return r;
});
void _toggle_is_expanded(bool is_expanded) {
foreach (var row in _tree)
if (row.is_expanded != is_expanded)
row.on_is_expanded_changed(is_expanded);
}
T _value_selector(TRow row) => row.value;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace bts.collections {
class OperableHierarchicalCollectionModel<TRow, T, TKey> where TKey : IEquatable<TKey> where TRow : IHierarchicalRow<T> {
readonly Func<T, TRow> _row_factory;
readonly Func<T, TKey> _key;
SortedTree<TRow, TKey> _tree;
IComparer<TRow> _comparer;
Func<TRow, bool> _filter;
IEnumerable<INode<TRow>> _cached_filtered = Enumerable.Empty<INode<TRow>>();
public OperableHierarchicalCollectionModel(Func<T, TRow> row_factory, Func<T, TKey> key, Func<T, TKey> parent_key, TKey no_parent_key, IComparer<TRow> comparer) {
_row_factory = row_factory;
_tree = new SortedTree<TRow, TKey>(_comparer = comparer, el => key(el.value), el => parent_key(el.value), no_parent_key);
_key = key;
}
public IComparer<TRow> comparer {
get => _comparer;
set {
_tree = _tree.Resort(_comparer = value);
_cached_filtered = null;
}
}
public Func<TRow, bool> filter {
get => _filter;
set {
_filter = value;
_cached_filtered = null;
_refresh_hierarchical_props();
}
}
public IEnumerable<TRow> rows {
get {
return _root_nodes.SelectMany(impl);
IEnumerable<TRow> impl(INode<TRow> n) {
yield return n.value;
if (n.value.is_expanded) {
foreach (var _n in n.inner.SelectMany(impl))
yield return _n;
}
}
}
}
public IEnumerable<TRow> all_rows {
get {
return _root_nodes.SelectMany(impl);
IEnumerable<TRow> impl(INode<TRow> n) {
yield return n.value;
foreach (var _n in n.inner.SelectMany(impl))
yield return _n;
}
}
}
public int total_row_count { get; private set; }
IEnumerable<INode<TRow>> _root_nodes =>
_filter == null ? _tree.RootNodes : (_cached_filtered ??= _tree.RootNodes.filtered(_filter));
public IEnumerable<TRow> GetAllRows(bool excludeFiltered = false) =>
excludeFiltered ? all_rows : _tree;
// collapse
public void collapse_row(TKey key) => _toggle_is_expanded(key, false);
public void collapse_all() => _toggle_is_expanded(false);
// expand
public void expand_row(TKey key) => _toggle_is_expanded(key, true);
public void expand_all() => _toggle_is_expanded(true);
// lookups
public bool try_get_row(TKey key, out TRow row) {
if (_tree.TryGetNode(key, out var node)) {
row = node.value;
return true;
}
row = default;
return false;
}
public TRow get_parent(TKey key) {
if (try_get_row(key, out TRow r)) {
return _tree.GetParent(r).value;
}
return default;
}
public IEnumerable<TRow> get_descendent_rows(TKey key) {
if (_tree.TryGetNode(key, out var n)) {
yield return n.value;
foreach (var v in n.inner_values())
yield return v;
}
}
// update
public void update(IEnumerable<T> added, IEnumerable<T> changed, IEnumerable<TKey> removed) {
foreach (var k in removed) {
if (_tree.Remove(k))
total_row_count--;
}
foreach (var v in changed) {
_tree.TryReplace(_key(v), r => { r.on_value_changed(v); return r; });
}
foreach (var v in added) {
if (_tree.Insert(_row_factory(v)))
total_row_count++;
}
_cached_filtered = null;
_refresh_hierarchical_props();
}
void _refresh_hierarchical_props() {
foreach (var n in _root_nodes)
impl(n, 0);
void impl(INode<TRow> n, int depth_level) {
if (n.value.depth_level != depth_level) {
n.value.on_depth_level_changed(depth_level);
}
using (var en = n.inner.GetEnumerator()) {
if (en.MoveNext()) {
if (!n.value.is_parent)
n.value.on_is_parent_changed(true);
depth_level++;
do impl(en.Current, depth_level);
while (en.MoveNext());
} else if (n.value.is_parent)
n.value.on_is_parent_changed(false);
}
}
}
void _toggle_is_expanded(TKey key, bool is_expanded) =>
_tree.TryReplace(key, r => {
if (r.is_expanded != is_expanded)
r.on_is_expanded_changed(is_expanded);
return r;
});
void _toggle_is_expanded(bool is_expanded) {
foreach (var row in _tree)
if (row.is_expanded != is_expanded)
row.on_is_expanded_changed(is_expanded);
}
T _value_selector(TRow row) => row.value;
}
}
@@ -1,20 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.utils {
public class ReverseComparer<T> : IComparer<T> {
private readonly IComparer<T> _wrapped;
public ReverseComparer(IComparer<T> wrapped) {
_wrapped = wrapped;
}
public int Compare(T x, T y) {
// reverse operands
return _wrapped.Compare(y, x);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.utils {
public class ReverseComparer<T> : IComparer<T> {
private readonly IComparer<T> _wrapped;
public ReverseComparer(IComparer<T> wrapped) {
_wrapped = wrapped;
}
public int Compare(T x, T y) {
// reverse operands
return _wrapped.Compare(y, x);
}
}
}
@@ -35,6 +35,7 @@
this IObservable<T> source, Func<T, TResult> bootstrap_selector, Func<T, TResult> selector) =>
Observable.Create<TResult>(o => {
object locker = new();
var local_bootstrap_selector = bootstrap_selector;
return source.Subscribe(v => {
// The preliminary null-check for `bootstrapper` is performed outside of the lock.
//
@@ -45,13 +46,13 @@
//
// If `bootstrapper is not null`, the use of a lock becomes neccessary to ensure that
// the observer is only bootstrapped once.
if (bootstrap_selector is null) {
if (local_bootstrap_selector is null) {
o.OnNext(selector(v));
return;
}
lock (locker) {
o.OnNext((bootstrap_selector ?? selector)(v));
bootstrap_selector = null;
o.OnNext((local_bootstrap_selector ?? selector)(v));
local_bootstrap_selector = null;
}
});
});
@@ -210,6 +211,10 @@
});
}
public static IObservable<TResult> SelectInline<T, TResult>(
this IObservable<T> source, Func<T, TResult> selector) =>
Observable.Create<TResult>(o => source.Subscribe(v => o.OnNext(selector(v))));
/// <summary>
/// Uses each element of a source observable to create an inner observable of <typeparamref name="TResult"/>, unlike <see cref="Observable.SelectMany{TSource, TResult}(IObservable{TSource}, Func{TSource, IObservable{TResult}})"/>,
/// each inner sequence is terminated when the next one begins.
@@ -322,7 +322,7 @@ namespace bts.api.core {
Log.DataFlow.diag($"Create Filter {typeof(T).Name}");
return source.CombineLatest(predicate, (upd, pred) => (upd, pred)).Subscribe(tup => {
bool pred_changed = tup.pred == pred_cache && pred_cache is not null;
bool pred_changed = tup.pred != pred_cache && pred_cache is not null;
pred_cache = tup.pred;
unfiltered_cache = tup.upd;
if (pred_changed && filtered_cache is not null) {
@@ -13,6 +13,7 @@ namespace bts.websocket_core {
using Newtonsoft.Json.Linq;
using Spark.Api.Data;
using Spark.Api.MarketData;
using Spark.Api.Orders;
using Spark.Core.Collections;
@@ -252,7 +253,9 @@ namespace bts.websocket_core {
exchanges,
types,
symbols?.getuprarr(),
expiration,
expiration is null
? Array.Empty<ExpirationDate>()
: Array.ConvertAll(expiration, ExpirationDate.FixedDate),
null,
null,
strikes,
+5
View File
@@ -7,6 +7,11 @@ include(${CMAKE_SOURCE_DIR}/cmake/Protobuf.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateDefaultConfig.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/PrintList.cmake)
configure_file(
"${CMAKE_SOURCE_DIR}/cmake/version.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/version.h"
)
if (DEFINED DOTNET_BINARY_DIR OR DEFINED DOTNET_WINDOWS_BINARY_DIR)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AddCopyBinariesTarget.cmake)
endif()
@@ -770,14 +770,14 @@ bts::api::orderman::RoutingRecord AdminServiceUtils::get_routing_record(
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());
rv.set_operator_id(user_ptr->name());
out_result.set_is_success(true);
return rv;
}
case api::VENUE_BINANCE: {
rv.set_session("binance_1");
rv.set_account(account_ptr->name());
rv.set_account(user_ptr->name());
rv.set_operator_id(user_ptr->name());
out_result.set_is_success(true);
return rv;
}
+21 -2
View File
@@ -1,13 +1,29 @@
#include "synthetic_book_side.h"
#include "lib/domain/book_side_consumer.h"
#include <cmath>
namespace bts::domain {
Price apply_price_mult(Price price, Quantity qty, double price_mult) {
if (std::isnan(price_mult) || price_mult == 0) {
return price * qty.value();
}
if ((price_mult < 0) != (qty < 0)) {
// Price mult and leg qty should have the same sign. If they don't, "fix"
// it (presumably the user made a mistake when they created the spread).
return price * -price_mult;
} else {
return price * price_mult;
}
}
void SyntheticBookSide::copy_(const SyntheticBookSide &other) noexcept {
if (this == &other) {
return;
}
leg_qty_.value() = other.leg_qty_.value();
price_mult_ = other.price_mult_;
// Copy 'data_'
if (other.data_ != nullptr) {
// Clear 'own_data_'
@@ -45,6 +61,7 @@ void SyntheticBookSide::move_(SyntheticBookSide &&other) noexcept {
delete[] own_data_;
}
leg_qty_.value() = other.leg_qty_.value();
price_mult_ = other.price_mult_;
data_ = other.data_;
size_ = other.size_;
own_data_ = other.own_data_;
@@ -99,8 +116,10 @@ SyntheticBookSide SyntheticBookSide::add_leg(
}
const auto [qty1, cost1] = l1.consume(take1);
const auto [qty2, cost2] = l2.consume(take2);
Price price1 = (cost1 / qty1.value()) * l1_qty.value();
Price price2 = (cost2 / qty2.value()) * l2_qty.value();
Price price1 = (cost1 / qty1.value());
price1 = apply_price_mult(price1, l1_qty, price_mult_);
Price price2 = (cost2 / qty2.value());
price2 = apply_price_mult(price2, l2_qty, other_leg.price_mult_);
auto &synth_lvl = synth_bk[i];
synth_lvl.price = price1 + price2;
synth_lvl.total_quantity = qty1;
+12 -3
View File
@@ -5,6 +5,7 @@
#include <cstddef>
#include <cstring>
#include <limits>
namespace bts::domain {
@@ -15,6 +16,7 @@ class SyntheticBookSide {
const BookLevel *data_{nullptr};
BookLevel *own_data_{nullptr};
std::size_t size_{0};
double price_mult_{std::numeric_limits<double>::quiet_NaN()};
void copy_(const SyntheticBookSide &other) noexcept;
void move_(SyntheticBookSide &&other) noexcept;
@@ -22,7 +24,10 @@ class SyntheticBookSide {
public:
constexpr SyntheticBookSide() = default;
constexpr SyntheticBookSide(
Quantity leg_qty, const BookLevel *book_side, std::size_t size);
Quantity leg_qty,
const BookLevel *book_side,
std::size_t size,
double price_mult = std::numeric_limits<double>::quiet_NaN());
constexpr ~SyntheticBookSide();
SyntheticBookSide(const SyntheticBookSide &other);
@@ -57,8 +62,12 @@ class SyntheticBookSide {
//////////////////////////////////////////////////
constexpr SyntheticBookSide::SyntheticBookSide(
Quantity leg_qty, const BookLevel *data, std::size_t size)
: leg_qty_(leg_qty), data_(data), own_data_(nullptr), size_(size) {}
Quantity leg_qty,
const BookLevel *data,
std::size_t size,
double price_mult)
: leg_qty_(leg_qty), data_(data), own_data_(nullptr), size_(size),
price_mult_(price_mult) {}
constexpr SyntheticBookSide::~SyntheticBookSide() {
leg_qty_.value() = 0;
+34
View File
@@ -0,0 +1,34 @@
#include "common.h"
#include "lib/utils/bts_assert.h"
namespace bts {
double CQuantity::round_to_decimal_places(double value, int decimal_places) {
if (std::isnan(value) || std::isinf(value)) {
return value;
}
const bool negate = value < 0;
if (negate) {
value = -value;
}
// Prevent arithmetic overflow (it could have nasty, unpredictable side effects).
bts_assert(
value < static_cast<double>(std::numeric_limits<uint32_t>::max()));
const uint32_t int_part = static_cast<uint32_t>(value);
double frac_part = value - int_part;
if (frac_part > (1.0 - err)) {
value = int_part + 1;
} else if (frac_part < err) {
value = int_part;
} else {
const double factor = pow(10, decimal_places);
frac_part = std::round(frac_part * factor) / factor;
value = static_cast<double>(int_part) + frac_part;
}
if (negate) {
value = -value;
}
return value;
}
} // namespace bts
+8 -11
View File
@@ -14,8 +14,8 @@
namespace bts {
struct CQuantity {
double qty = 0;
constexpr static double precision_digits = 6;
constexpr static double err = 0.0000001;
constexpr static int precision_digits = 7;
constexpr static double err = 0.000001;
CQuantity() {}
CQuantity(uint32_t whole_qty) : qty(whole_qty) {}
CQuantity(uint32_t int_qty, double fractional_qty)
@@ -26,15 +26,12 @@ struct CQuantity {
return rv;
}
static double round_to_decimal_places(double value, int decimal_places) {
double factor = pow(10, decimal_places);
return std::round(value * factor) / factor;
}
static double round_to_decimal_places(double value, int decimal_places);
uint32_t value() const { return static_cast<uint32_t>(qty); }
double full_value() const { return qty; }
float full_value_float() const { return static_cast<float>(full_value()); }
uint32_t int_part() { return static_cast<uint32_t>(qty); }
double fractional_part() { return full_value() - int_part(); }
uint32_t int_part() const { return static_cast<uint32_t>(qty); }
double fractional_part() const { return full_value() - int_part(); }
static bool equal_d(double d1, double d2) {
return std::abs(d1 - d2) <= err;
}
@@ -102,7 +99,7 @@ struct CQuantity {
}
bool is_zero() const { return *this == create_new(0.0); }
void make_zero_if_negative() { qty = std::max(0.0, qty); }
void round() { qty = round_to_decimal_places(qty, 6); }
void round() { qty = round_to_decimal_places(qty, precision_digits); }
};
inline std::ostream &operator<<(std::ostream &out, CQuantity const &qty) {
@@ -115,5 +112,5 @@ using Quantity = CQuantity;
using UserLogin = utils::ValueWrapper<struct UserLoginTag, std::string>;
} // namespace bts
FMT_IMPL(bts::Price, p, "{}", p.value());
FMT_IMPL(bts::Quantity, q, "{}", q.full_value());
FMT_IMPL(bts::Price, p, "{:.6f}", p.value());
FMT_IMPL(bts::Quantity, q, "{:.6f}", q.full_value());
+2 -1
View File
@@ -290,7 +290,8 @@ struct GeneralMarketDataListener::PImpl {
// if this is set to true, then the security key passed on in the
// on_securities function is used. If this is not set to true, then a new key
// is generated
const bool use_given_sec_key = false;
const bool use_given_sec_key =
true; // with true, replay works, with orders goign in ((did not push it in, since it was always false - need to test))
const std::string cc_name_prefix = "cc_security";
size_t n_bk_updates = 0;
bts::persistence::IDGenerator &id_gen =
+4 -2
View File
@@ -159,11 +159,13 @@ TimeStamp copy_synthetic_depth(
// Bid
domain::BookSide &cbids = depth_cache.at(x);
DomainSecurityUtils::convert(eff_bid, COCONUT_MAX_DEPTH, cbids);
synth_bids = synth_bids.add_leg({rel_qty, cbids.data(), cbids.size()});
synth_bids = synth_bids.add_leg(
{rel_qty, cbids.data(), cbids.size(), leg.price_multiplier()});
// Ask
domain::BookSide &casks = depth_cache.at(x + synth.legs.size());
DomainSecurityUtils::convert(eff_ask, COCONUT_MAX_DEPTH, casks);
synth_asks = synth_asks.add_leg({rel_qty, casks.data(), casks.size()});
synth_asks = synth_asks.add_leg(
{rel_qty, casks.data(), casks.size(), leg.price_multiplier()});
}
DomainSecurityUtils::convert(
synth_bids.data(), synth_bids.size(), bid, count);
+4
View File
@@ -5,6 +5,7 @@
#include "lib/config/config.h"
#include "lib/config/protos/cme_reference_data.pb.h"
#include "lib/config/protos/market_data.pb.h"
#include "lib/domain_model/Security.pb.h"
#include "lib/utils/parse_enum.h"
#ifndef _WIN32
@@ -61,6 +62,9 @@ struct CppOverride : public ISecurityMapper {
}
void apply(FixedSecurity &f) const override {
if (f.exchange != api::VENUE_GLBX) {
return;
}
auto iter = _map.find(f.symbol());
if (iter != _map.end()) {
f.cash_per_point = iter->second;
+5 -5
View File
@@ -29,12 +29,12 @@ OrderRouter::RouteQueryResult OrderRouter::get_routing_record(
bts::admin::AdminServiceUtils::get_routing_record(
me->data, account_id.value() == 0 ? 1 : account_id.value(),
user_id.value() == 0 ? 1 : user_id.value(), sec, result);
LOG(OrderMan, Info, "OrderRouter::get_routing_record rr: {} result: {}, {}",
utils::pb::to_string(rr), result.is_success(), result.error_text());
if (result.is_success())
if (result.is_success()) {
return RouteQueryResult{true, std::string(""), rr};
else
return RouteQueryResult{false, result.error_text(), rr};
}
LOG(OrderMan, Error, "OrderRouter::get_routing_record rr: {} result: {}, {}",
utils::pb::to_string(rr), result.is_success(), result.error_text());
return RouteQueryResult{false, result.error_text(), rr};
}
OrderRouter::ReverseRoutingQueryResult OrderRouter::get_reverse_routing_record(
+10 -15
View File
@@ -31,6 +31,7 @@
#include "lib/orderman/events/ExchangePositionEvent.h"
#include "lib/orderman/events/OrderOpResultEvent.h"
#include "lib/orderman/gateways/i_execution_gateway_provider.h"
#include "lib/orderman/order_desc.h"
#include "lib/orderman/order_factory/DynamicOrderRegistry.h"
#include "lib/orderman/orderman_utils/ExchangeOpenOrders.h"
#include "lib/orderman/orders/Order.h"
@@ -404,7 +405,8 @@ OrderServiceServer::~OrderServiceServer() {
void OrderServiceServer::on_orders(
std::vector<api::orderman::OrderRecord> const &orders) {
LOG(OrderMan, Info, "Received order update for publishing");
LOG(OrderMan, Info, "OrderServiceServer::on_orders. count: {}",
orders.size());
std::vector<api::orderman::OrderRecord> created;
created.reserve(orders.size());
std::vector<api::orderman::OrderRecord> updated;
@@ -420,18 +422,12 @@ void OrderServiceServer::on_orders(
} else
updated.push_back(rec);
}
LOG(OrderMan, Info,
"OrderServiceServer::on_orders publishing orders to the service API. "
"created {}, updated {}",
created.size(), updated.size());
me->publish_orders(&created, &updated, nullptr);
}
void OrderServiceServer::on_fills(
std::vector<api::orderman::Fill> const &fills) {
LOG(OrderMan, Info, "Received fill update for publishing");
LOG(OrderMan, Info, "OrderServiceServer::on_fills. count: {}", fills.size());
std::vector<api::orderman::Fill> created;
created.reserve(fills.size());
std::vector<api::orderman::Fill> updated;
@@ -445,19 +441,16 @@ void OrderServiceServer::on_fills(
} else
updated.push_back(f);
}
LOG(OrderMan, Info, "OrderServiceServer::on_fills created: {} updated: {}",
created.size(), updated.size());
me->publish_fills(&created, &updated, nullptr);
}
void OrderServiceServer::create_order(
const api::orderman::Create &c,
const api::Response<api::orderman::Response> &r) {
LOG(OrderMan, Info,
"OrderServiceServer::create_order create: {} pr: {} qty: {} frc_qty: "
"{}",
utils::pb::to_string(c), c.price(), c.quantity(),
c.has_fractional_quantity() ? c.fractional_quantity().fraction() : 0.0);
LOG(OrderMan, Info, "OrderServiceServer::create_order. {}",
OrderDesc::from_create(c));
LOG(OrderMan, Diag, "OrderServiceServer::create_order.\n{}",
utils::pb::to_string(c));
if (!me->service_ready) {
api::orderman::Response response;
response.set_error_code(api::orderman::ORDER_ERROR_SERVER_NOT_READY);
@@ -486,6 +479,8 @@ void OrderServiceServer::update_order(
const api::orderman::Update &update,
const api::Response<api::orderman::Response> &r) {
LOG(OrderMan, Info, "OrderServiceServer::update_order update: {}",
OrderDesc::from_update(update));
LOG(OrderMan, Diag, "OrderServiceServer::update_order update:\n{}",
utils::pb::to_string(update));
if (!me->service_ready) {
api::orderman::Response response;
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff. Show more