Compare commits

...
28 Commits
Author SHA1 Message Date
John Colvin eadd6a0706 fix: Update title in new ladder ticket when sec received 2026-08-14 14:50:31 -04:00
John Colvin 503a5e2715 fix: Revert ladder to use fixed price list instead of virtual rows 2026-08-14 14:42:12 -04:00
John Colvin 786736b38f Add Orders column placeholder to new ladder grid 2026-08-14 14:42:12 -04:00
John Colvin 1ef2ed9d57 Make new ladder grid use 1 row per tick with fixed row count 2026-08-14 14:42:12 -04:00
John Colvin b39ea9528c Use fixed price rows (per tick) in new ladder 2026-08-14 14:42:12 -04:00
John Colvin c59ce773a7 Set security in OnSecurityMd callback in new ladder 2026-08-07 17:20:14 -04:00
arham 400981738f fix: bug fix now sell works 2026-08-05 15:47:21 -04:00
arham 3fd749dcd9 add: change colors and add ladder link 2026-08-05 14:04:32 -04:00
John Colvin a250967286 feat: Launch modify ticket w/ ladder 2026-07-30 14:13:35 -04:00
John Colvin a81126d888 feat: Launch ticket w/ ladder popup from mview 2026-07-30 14:13:35 -04:00
John Colvin c38ff5926e feat: Add new order ticket with ladder price levels 2026-07-30 14:13:35 -04:00
John Colvin 49df45574f Rename get_ladder_profile PascalCase 2026-07-30 11:51:37 -04:00
John Colvin 7b2e19f7c8 Remove unused ext fn subscribe_ladder_profile 2026-07-30 11:51:37 -04:00
John Colvin c9e3cf8c67 Rename AuxiliaryDataResources -> OrderAuxiliaryPopupResources
The old name was unnecessarily vague
2026-07-30 11:48:00 -04:00
John Colvin 6b31f7383c Remove unused code ConstrainedTextBlock 2026-07-30 11:48:00 -04:00
arham f2c95d6c28 sec filter on the basis of expiration in mdp3 2026-07-27 09:32:10 +00:00
Taha 1d3bbcc75f TC2 ATM highlighting wokrs now without AW2 setup 2026-07-22 16:52:49 -04:00
arham 94bfdacf8b add: trade latency logs 2026-07-02 16:21:08 +00:00
arham 2a8d407f08 conf: add config to run crypto on aws, bybi and gate not working 2026-06-24 01:03:49 +00:00
Taha 55ed962c63 Fixing empty TC2 issue, caused by exceptions in _override_theos 2026-06-17 23:10:57 -04:00
arham 68f766e00b fix: param syntax change for args 2026-06-17 10:33:29 -04:00
arham c0252e06c5 fix: fill time nanos stored 2026-06-16 09:58:03 +00:00
John Colvin 4258bd4c82 fix: Use unique ProtoMember ids in orderbook FilterStates
The Tag property had the same id as accounts (5)
2026-06-15 15:29:07 -04:00
John Colvin 4981d70bf2 fix: Return NaN instead of throwing if priceType invalid
It's possible this fixes the ladder freezing bug reported by Ophir
2026-06-15 15:22:28 -04:00
John Colvin b85c249a7d improve: Map nullable Color to Color type in BTSColorExtension
So we don't need "Type={x:Type Color}" in ColorAnimation xaml
2026-06-15 15:21:31 -04:00
John Colvin 6721e07cb6 fix: Use BTSColor markup ext instead of StaticResource 2026-06-15 15:08:28 -04:00
arham 8aa5529218 conf: increased shm mem size 2026-06-02 03:04:55 +00:00
arham ba5fff0168 add: ping service to sdk 2026-06-01 10:28:06 +00:00
48 changed files with 2349 additions and 542 deletions

No files matched your search

+1 -1
View File
@@ -285,7 +285,7 @@
"windows": {
"program": "Spark.exe",
"args": [
"--sparkd",
"--Spark:ConfigServer",
"${input:sparkConfigServer}"
],
"cwd": "build/Debug/bin/net8.0-windows",
@@ -0,0 +1,5 @@
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/all_crypto
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/gateio_dev/requirements.txt
SPARKD__DATA_DIR=/home/ubuntu/8000/sparkdata/data
SPARKD__IMAGE=sparkd_symbols
SPARKD__RUNTIME_CONF_DIR=/home/ubuntu/8000/sparkdata/conf
@@ -0,0 +1,11 @@
{
"currencies": [
"USDT"
],
"futures": [
"BTC",
"ETH",
"SOL",
"XRP"
]
}
@@ -0,0 +1,16 @@
{
"currencies": [
"USDT"
],
"futures": [
"BTC",
"ETH",
"SOL",
"XRP"
],
"use_testnet": false,
"logging": {
"console_level": "WARNING",
"file_level": "INFO"
}
}
@@ -0,0 +1,5 @@
numpy==2.2.6
pybit==5.11.0
requests==2.31.0
websocket-client==1.8.0
websockets==15.0.1
@@ -0,0 +1,71 @@
PublisherOptions {
value {
network_interface: "0.0.0.0"
heartbeat_ms: 5000
heartbeat_boot_seconds: 60
skip_options: true
hot_initial: 1024
hot_growth: 1024
cold_initial: 8192
cold_growth: 2048
channel_prefix: ""
disable_heartbeat: true
}
}
Logging {
value {
initial_queue_size: 120001
max_queue_growth: 4
file {
default_level: "Info"
subsystem {
name: "md"
level: "Diag"
}
}
}
}
MarketDataConfig {
value {
# python_md_providers {
# venue: VENUE_GATEIO
# module_path: "python_md_providers"
# provider_type: "gateio_md_provider.GateIoMdProvider"
# }
python_md_providers {
venue: VENUE_BYBIT
module_path: "python_interop_plugins"
provider_type: "bybit_md_provider.BybitMdProvider"
}
binance_usds_futures {
num_depth_levels: 5
url_rest_base: "https://fapi.binance.com"
url_ws_base: "wss://fstream.binance.com"
security_criteria {
clauses {
security_types: SECURITY_TYPE_FUTURE
symbols: @SymbolLists.list[Gateio].symbol
}
}
}
okx {
num_depth_levels: 5
url_rest_base: "https://www.okx.com"
url_ws_base: "wss://ws.okx.com:8443"
security_criteria {
clauses {
security_types: SECURITY_TYPE_FUTURE
symbols: @SymbolLists.list[Gateio].symbol
}
}
is_application_ping_pong_enabled: true
application_ping_interval_ms: 20000
application_pong_timeout_ms: 10000
is_protocol_ping_pong_enabled: true
protocol_ping_interval_ms: 60000
protocol_pong_timeout_ms: 30000
}
}
}
@@ -0,0 +1,32 @@
SymbolLists {
value {
list {
key: "Gateio"
symbol: "SOL"
symbol: "BTC"
symbol: "ETH"
symbol: "XRP"
}
}
}
Symbols {
value {
venues {
name: "VENUE_GATEIO"
spot: @SymbolLists.list[Gateio].symbol
}
venues {
name: "VENUE_BYBIT"
future: @SymbolLists.list[Gateio].symbol
}
venues {
name: "VENUE_BINANCE_USDS_FUTURES"
future: @SymbolLists.list[Gateio].symbol
}
venues {
name: "VENUE_OKX"
future: @SymbolLists.list[Gateio].symbol
}
}
}
@@ -3,3 +3,5 @@ SPARKD__DATA_DIR=/home/wedbushprod/8000/sparkdata/data
SPARKD__IMAGE=sparkd_symbols
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/ophir_prod_live_wb/requirements.txt
SPARKD__RUNTIME_CONF_DIR=/home/wedbushprod/8000/sparkdata/conf
SPARKD__SHM_SIZE=8g
SPARKD__MEMORY=16g
@@ -1,4 +1,5 @@
#include "MDP3Listener.h"
#include "spark/md/log_messages/trade_latency_log.h"
#include "spark/config/protos/market_data.pb.h"
#include "spark/md/protos/co2.pb.h"
#include "spark/utils/datetime.h"
@@ -189,6 +190,9 @@ void on_trade(
PriceSize ps = pricesize_of_price_size(
price, data->handle.security().display_factor, size);
auto date = ll_of_timestamp(onixs_date);
log_trade_latency(
TradeLatencyStage::MDP3_RECEIVED, data->handle.security(), date, ps,
map_aggressor(agside), data->electronic_volume + size, false);
data->handle.add_trade(
ts, ps, date, map_aggressor(agside), data->electronic_volume + size);
}
@@ -287,6 +291,19 @@ void MDP3Listener::heartbeat() {
void MDP3Listener::add_security(
const FixedSecurity &security, co2::TradingStatus status) {
bts_assert(security.numeric_id != 0);
if (security.type == api::SECURITYTYPE_OPTION) {
const auto &option = static_cast<const FixedOptionSecurity &>(security);
const datetime::Date expiration(
option.expiration_year, option.expiration_month,
option.expiration_day);
if (!option_expiration_is_allowed(expiration)) {
LOG(md, Info,
"Skipping MDP3 option {} because it expires beyond the configured "
"cutoff.",
std::string(security.symbol()));
return;
}
}
// Set up the shared memory for this security
auto handle = _producer.add_security(security);
// Add it to the index so it can receive messages
@@ -1,6 +1,8 @@
#pragma once
#include "CMESecurityIndex.h"
#include "spark/config/config.h"
#include "spark/config/protos/toggles.pb.h"
#include "spark/io/JsonDumper.h"
#include "spark/io/json_formatter.h"
#include "spark/md/MBOProcessor.h"
@@ -21,6 +23,28 @@
namespace bts {
namespace md {
inline bool option_expiration_is_allowed(const datetime::Date &expiration) {
const auto max_days =
config::get<config::toggles::Mdp3OptionExpirationCutoff>()
.max_days_until_expiration();
if (expiration.year == 0) {
return true;
}
if (max_days == 0) {
return true;
}
const auto cutoff = core::TimeStamp::now().date() +
core::TimeSpan::from_days(max_days);
const auto expiration_timestamp =
core::TimeStamp(expiration.year, expiration.month, expiration.day);
const bool allowed = expiration_timestamp <= cutoff;
uint32_t cutoff_year;
uint32_t cutoff_month;
uint32_t cutoff_day;
cutoff.to_date(cutoff_year, cutoff_month, cutoff_day);
return allowed;
}
inline co2::AggressorSide map_aggressor(onixs::AggressorSide::Enum agside) {
switch (agside) {
default:
@@ -781,6 +805,11 @@ void MDP3Listener::onOption(
exchange_id, sec_name, Mdp3SecuritySkipReason::ALREADY_ADDED);
return;
}
if (!option_expiration_is_allowed(date_of_maturity(sec))) {
LOG_MDP3_ON_OPTION_SKIP(
exchange_id, sec_name, Mdp3SecuritySkipReason::EXPIRED);
return;
}
LOG_MDP3_ON_OPTION_BEGIN(exchange_id, std::string(sec.symbol().items()));
FixedOptionSecurity fsec;
+1 -1
View File
@@ -160,7 +160,7 @@ namespace mbts.spark {
private bool _compute_midmarket {
get {
return true;
return false;
}
}
+35 -13
View File
@@ -26,6 +26,8 @@ using LadderSettings = mbts.spark.LadderSettings;
namespace bts.shell {
[Export(typeof(IShellDaemon))]
class OrderTicketDaemon : IShellDaemon {
static readonly bool useTicketLadder = true;
readonly IBus _bus;
readonly OrdersModel _model;
readonly UserSettingsHub _settings_hub;
@@ -71,13 +73,23 @@ namespace bts.shell {
void _show_order_ticket(SecurityMD smd) {
var settings_src = _settings_hub.subscribe_default_settings()
.Select(s => s.get_ladder_profile(smd.security)?.value ?? LadderSettings.Default);
.Select(s => s.GetLadderProfile(smd.security)?.value ?? LadderSettings.Default);
var accounts_src = _bus.subscribe(SparkTags.AccountsTag);
_show_order_window(new OrderTicket {
DataContext = new NewOrderTicketVM(_model, smd, accounts_src, settings_src) {
close_on_send = _close_on_send
}
});
_show_order_window(useTicketLadder ? OrderTicketLadderWindow() : OrderTicketWindow());
Window OrderTicketWindow() =>
new OrderTicket {
DataContext = new NewOrderTicketVM(_model, smd, accounts_src, settings_src) {
close_on_send = _close_on_send
}
};
Window OrderTicketLadderWindow() =>
new OrderTicketLadder() {
DataContext = new NewOrderTicketLadderVM(
_model, smd, accounts_src, settings_src,
ladderSecurity => _bus.post(SparkTags.ShowLadderTag, ladderSecurity))
};
}
void _show_order_ticket(OrderTicketParams ot_params) {
@@ -114,13 +126,23 @@ namespace bts.shell {
return;
}
var md = _md_map.TryGetValue(ori.order.security_key());
_show_order_window(new OrderModificationDialog {
DataContext = new ExistingOrderTicketVM(_model,
Observable.Return((md, ori.order)),
_bus.subscribe(SparkTags.AccountsTag),
Observable.Return(LadderSettings.Default.calculator_buttons),
md?.quantity_display)
});
var accountsSrc = _bus.subscribe(SparkTags.AccountsTag);
var orderSrc = Observable.Return((md, ori.order));
var calcBnsSrc = Observable.Return(LadderSettings.Default.calculator_buttons);
_show_order_window(useTicketLadder ? OrderTicketLadderWindow() : OrderTicketWindow());
Window OrderTicketWindow() =>
new OrderModificationDialog {
DataContext = new ExistingOrderTicketVM(_model, orderSrc, accountsSrc, calcBnsSrc, md?.quantity_display)
};
Window OrderTicketLadderWindow() =>
new OrderModificationDialog {
DataContext = new ExistingOrderTicketLadderVM(
_model, orderSrc, accountsSrc, calcBnsSrc,
ladderSecurity => _bus.post(SparkTags.ShowLadderTag, ladderSecurity),
md?.quantity_display)
};
}
}
}
@@ -14,16 +14,11 @@ namespace mbts.spark {
/// <summary>
/// Gets the ladder settings profile for a particular security
/// </summary>
public static LadderSettingsProfile? get_ladder_profile(
public static LadderSettingsProfile? GetLadderProfile(
this UserSettings settings, SecurityImmutable security) =>
settings.ladder_profiles.FirstOrDefault(lsp =>
lsp?.security_criteria?.includes(security) ?? false);
public static IObservable<LadderSettingsProfile?> subscribe_ladder_profile(
this UserSettingsHub settings_hub, SecurityImmutable security) =>
settings_hub.subscribe_default_settings().Select(s =>
s.get_ladder_profile(security));
/// <summary>
/// Gets the default account for a particular security (from the
/// <see cref="LadderSettingsProfile"/> associated with that security)
@@ -31,7 +26,7 @@ namespace mbts.spark {
public static IObservable<AccountTuple?> subscribe_default_account(
this UserSettingsHub settings_hub, IBus bus, SecurityImmutable security) {
var acc_id_src = settings_hub.subscribe_default_settings().Select(s =>
s.get_ladder_profile(security)?.value?.default_account);
s.GetLadderProfile(security)?.value?.default_account);
return acc_id_src.CombineLatest(
bus.subscribe(SparkTags.AccountsTag),
(acc_id, accs) => acc_id.HasValue && accs.TryGetValue(acc_id.Value, out var acc_tup) ?
@@ -1,10 +1,10 @@
using bts;
using bts.api;
using mbts.spark;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.extensions;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
@@ -23,6 +23,7 @@ namespace mbts.ui.spark {
/// </summary>
partial class MarketViewView : UserControl {
public static readonly RoutedCommand CellClickCommand = new RoutedCommand();
static readonly bool useTicketLadder = true;
MarketViewVM _vm;
DIYGridPopupManager _popup_man_backing;
@@ -92,7 +93,7 @@ namespace mbts.ui.spark {
void _show_mini_orderbook(IReadOnlyList<SecurityImmutable> securities, Columns col) {
var (vm, context, title, fg) = _vm.orderbook_params(securities, col);
_show_popup(vm, new AuxiliaryDataHeader {
_show_popup(vm, new OrderAuxiliaryPopupHeader {
context = context,
title = title,
title_foreground = fg
@@ -104,7 +105,7 @@ namespace mbts.ui.spark {
void _show_mini_fills(IEnumerable<SecurityImmutable> securities, Columns col) {
var (vm, context, title, fg) = _vm.fills_params(securities, col);
_show_popup(vm, new AuxiliaryDataHeader {
_show_popup(vm, new OrderAuxiliaryPopupHeader {
context = context,
title = title,
title_foreground = fg
@@ -114,29 +115,40 @@ namespace mbts.ui.spark {
async void _show_order_ticket(
SecurityMD security_md, double price = double.NaN, double size = 0) {
var resources = new OrderTicketResources();
resources.MergedDictionaries.Add(new AuxiliaryDataResources());
resources.MergedDictionaries.Add(new OrderAuxiliaryPopupResources());
var account = await _vm.get_default_account(security_md.security);
var def = _model.factory.order(security_md.security, account);
var calc_buttons = LadderSettings.Default.calculator_buttons;
var ticket_vm = new NewOrderTicketVM(
_model,
security_md,
_bus.subscribe(SparkTags.AccountsTag),
new(def),
price,
size,
Observable.Return(calc_buttons));
var accounts = _bus.subscribe(SparkTags.AccountsTag);
var (vm, header) = OrderTicketFactory();
_show_popup(
ticket_vm,
new OrderTicketHeader { bbo = ticket_vm.bbo },
vm,
header,
resources,
view => Behavior.SetAutoFocusTargetProvider(view, AutoFocusTargetProvider.TypeProvider(typeof(TextBox), view)));
(ViewModelBase, object) OrderTicketFactory() {
if (useTicketLadder) {
var vm = new NewOrderTicketLadderVM(
_vm.orders_model, security_md, accounts, new(def), price, size,
Observable.Return(calc_buttons),
ladderSecurity => _bus.post(SparkTags.ShowLadderTag, ladderSecurity));
var header = new OrderTicketLadderHeader() {
Title = security_md.security.ToString()
};
return (vm, header);
} else {
var vm = new NewOrderTicketVM(_model, security_md, accounts, new(def), price, size, Observable.Return(calc_buttons));
var header = new OrderTicketHeader { bbo = vm.bbo };
return (vm, header);
}
}
}
void _show_popup(object content, object header) {
var resources = new AuxiliaryDataResources();
var resources = new OrderAuxiliaryPopupResources();
resources.MergedDictionaries.Add(new AutoHideScrollbarResources());
_show_popup(content, header, resources);
}
@@ -148,9 +160,9 @@ namespace mbts.ui.spark {
Header = header,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Resources = resources ?? new AuxiliaryDataResources()
Resources = resources ?? new OrderAuxiliaryPopupResources()
};
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
view.Style = view.Resources[OrderAuxiliaryPopupResources.PopupChildStyleKey] as Style;
init_view?.Invoke(view);
_popup_man.show(view);
}
@@ -27,13 +27,13 @@ namespace Spark.Trading {
_sec_greeks_in = sec_greeks_in;
}
public IObservable<double> get_price_source(FairValueKey key) {
public IObservable<double> get_price_source(FairValueKey key, double? errorPrice = double.NaN) {
return key.type switch {
FairValueLine.PriceType.Bid => bid_source(),
FairValueLine.PriceType.Ask => ask_source(),
FairValueLine.PriceType.Mid => mid_source(),
FairValueLine.PriceType.Theo => theo_source(),
_ => throw new NotImplementedException()
_ => errorPrice.HasValue ? Observable.Return(errorPrice.Value) : throw new NotImplementedException()
};
IObservable<double> bid_source() =>
@@ -1,4 +1,4 @@
using bts;
using bts;
using bts.api;
using bts.api.md.auxiliary;
using bts.collections;
@@ -15,7 +15,6 @@ using mbts.utils;
using Spark.Core.Collections;
using Spark.Core.Disposables;
using Spark.Core.Math;
using System;
using System.Collections.Generic;
@@ -36,8 +35,9 @@ namespace mbts.spark {
using RegionalTickFactors = user.settings.RegionalTickFactors;
partial class LadderGridHost : Control {
static readonly int _initial_row_count;
static readonly bool _show_scrollbar;
const int _min_row_count = 501;
const int _max_row_count = 2001;
static readonly bool _show_scrollbar = true;
readonly VisualHost _visual_host = new();
readonly LadderOrdersHost _ordershost;
@@ -80,13 +80,11 @@ namespace mbts.spark {
LadderVM _vm;
LadderTactileAdorner _tactile_adorner;
ulong _prices_updated;
ulong lastTradeTime;
NativePriceSize[] _bids = new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH], _asks = new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH];
SecurityMDExtensions.Book _depth = SecurityMDExtensions.Book.Empty;
bool _price_dirty = false, _piq_dirty = false;
LadderPriceList _price_list = new() {
length = _initial_row_count,
};
LadderPriceList _pending_price_list = null;
IList<double> _price_list;
IComparer<double> _price_comparer;
double _last_origin_price;
readonly MarketBreakLines _mkt_break_lines;
@@ -107,7 +105,6 @@ namespace mbts.spark {
static LadderGridHost() {
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
_show_scrollbar = knobs.ShowScrollbar;
_initial_row_count = (int)knobs.ScrollableTickWindowSize;
}
public LadderGridHost() {
@@ -132,33 +129,7 @@ namespace mbts.spark {
_render_grid_background();
_init_tactile_adorner();
}
IDisposable scroll_viewer_hook = null;
loaded_hook = Disposable.Create(
() => {
_scroll_viewer_source.value_changed -= on_scroll_viewer;
Disposable.dispose(ref scroll_viewer_hook);
});
on_scroll_viewer(_scroll_viewer);
void on_scroll_viewer(ScrollViewer new_value) {
Disposable.dispose(ref scroll_viewer_hook);
if (new_value is null) {
return;
}
scroll_viewer_hook = _connect_scroll_viewer(new_value);
delayed_init();
// This is a bit of a hack: the scroll viewer drifts away from the intended VerticalOffset
// unless it is done after a delay.
async void delayed_init() {
await Task.Delay(100);
await Dispatcher.InvokeAsync(() => {
_pending_scroll_to_origin = true;
_refresh_prices();
_refresh_all_visuals();
scroll_to_pending_target();
});
}
}
_scroll_to_center();
};
recenter_command = new DelegateCommand(_scroll_to_center);
DataContextChanged += (s, e) => _connect_vm();
@@ -282,26 +253,6 @@ namespace mbts.spark {
set => SetValue(PIQColumnWidthProperty, value);
}
#region TopTick
public static readonly DependencyProperty TopTickProperty =
WPFHelper.create_dp<int, LadderGridHost>(
nameof(TopTick),
FrameworkPropertyMetadataOptions.AffectsRender,
(o, v) => {
if (o._price_list is null) {
return;
}
o._set_scrollbar_range(high_tick: v);
o._refresh_prices();
o._refresh_all_visuals();
o._ordershost.refresh_orders();
});
public int? TopTick {
get => GetValue(TopTickProperty) as int?;
set => SetValue(TopTickProperty, value);
}
#endregion
#region CanUserSendOrders
public static readonly DependencyProperty CanUserSendOrdersProperty =
@@ -392,7 +343,7 @@ namespace mbts.spark {
}
}
int _row_count => _price_list?.Count ?? 0;
int _row_count = _min_row_count;
bts.api.domain.TickDisplaySettingsImmutable _qty_display =>
_vm is null ?
@@ -425,64 +376,17 @@ namespace mbts.spark {
scroll_to_price(_mkt_break_lines.origin);
public bool scroll_to_price(double price) {
if (price.is_nan()) {
if (_scroll_viewer == null || _price_list.is_null_or_empty())
return false;
}
var tick = _spec.tickdiff(price, 0);
if (tick.HasValue) {
return scroll_to_tick(tick.Value);
}
return false;
}
double _get_target_top_tick(int mid_viewport_tick) {
var curr_rel_off = _scroll_viewer.VerticalOffset + (_scroll_viewer.ViewportHeight / 2.0);
var tgt_rel_off = (_price_list.high_tick - mid_viewport_tick) * _settings.row_height;
var tick_diff = (curr_rel_off - tgt_rel_off) / _settings.row_height;
return _price_list.high_tick + tick_diff;
}
public bool scroll_to_tick(long tick) {
if (tick > int.MaxValue) {
_scroll_to_tick_log_throttle.Execute(() =>
Log.Ui.error($"Unable to scroll to tick {tick} because it exceeds int.MaxValue"));
return false;
}
return scroll_to_tick((int)tick);
}
public bool scroll_to_tick(int tick) {
if (_vm is null || _settings is null || _scroll_viewer is null) {
return false;
}
// var center_offset_ticks = (int)(_scrollViewer.ScrollableHeight / (2.0 * _settings.row_height));
// The scrollbar's vertical offset won't change. Find the 'top tick' that will put the
// target `tick` in the center of the viewport.
// var center_offset_diff = _scrollViewer.VerticalOffset - (_scrollViewer.ScrollableHeight / 2.0);
// var tick_offset = center_offset_ticks
// var tick_offset = (int)(_scrollViewer.ViewportHeight / (2.0 * _settings.row_height));
// The scrollbar's vertical offset won't change. Find the 'top tick' that will put the
// target `tick` in the center of the viewport.
checked {
var top_tick = (int)_get_target_top_tick(tick);
if (_settings.animate_recenter) {
var anim = new Int32Animation {
From = (int)_price_list.high_tick,
To = top_tick,
Duration = TimeSpan.FromSeconds(0.15)
};
var sb = new Storyboard();
sb.Children.Add(anim);
Storyboard.SetTarget(anim, this);
Storyboard.SetTargetProperty(anim, new PropertyPath(TopTickProperty));
sb.Begin();
} else {
_set_scrollbar_range(high_tick: top_tick);
_refresh_prices();
if (_spec.tickdiff(_price_list[0], price) is long target_row) {
int visible_row_count = (int)(_scroll_viewer.ActualHeight / _settings.row_height);
int curr_row = visible_row_count + (int)(_scroll_viewer.VerticalOffset / _settings.row_height);
if ((target_row > curr_row && target_row < _row_count) || (target_row < (curr_row - visible_row_count) && target_row >= 0)) {
_scroll_to_row((int)target_row);
return true;
}
}
return true;
return false;
}
public IObservable<ColumnType> subscribe_hover_column() =>
@@ -564,67 +468,6 @@ namespace mbts.spark {
}
}
IDisposable _connect_scroll_viewer(ScrollViewer scroll_viewer) {
if (scroll_viewer is null) {
return null;
}
const int scroll_tick_padding = 10;
// const double scroll_epsilon = 1E-8;
var dispatcher = Dispatcher.CurrentDispatcher;
bool busy = false;
return scroll_viewer.SubscribeScrollChanged(on_scroll_changed);
async void on_scroll_changed(object sender, ScrollChangedEventArgs e) {
if (e.ExtentHeight == e.ViewportHeight || busy) {
return;
}
busy = true;
await dispatcher.InvokeAsync(ensure_tick_padding, DispatcherPriority.Background);
busy = false;
void ensure_tick_padding() {
if (_price_list is null || _settings is null) {
return;
}
var pct = _scroll_viewer.VerticalOffset / _scroll_viewer.ScrollableHeight;
var new_pl = new LadderPriceList(_price_list);
var stp = scroll_tick_padding * _settings.row_height;
if (pct == 0) {
if (_scroll_viewer.ExtentHeight - _scroll_viewer.ViewportHeight < stp) {
new_pl.extend_up(scroll_tick_padding);
} else {
new_pl.shift_up(scroll_tick_padding);
}
_price_dirty = true;
}
if (pct == 1) {
if (_scroll_viewer.ExtentHeight - _scroll_viewer.ViewportHeight < stp) {
new_pl.extend_down(scroll_tick_padding);
} else {
new_pl.shift_down(scroll_tick_padding);
}
_price_dirty = true;
}
if (new_pl != _price_list) {
_pending_price_list = new_pl;
}
_ordershost.refresh_orders();
}
}
}
void _set_scrollbar_range(int? high_tick = null, int? length = null) {
if (high_tick.HasValue) {
_price_list.high_tick = high_tick.Value;
}
if (length.HasValue) {
_price_list.length = length.Value;
}
_mkt_break_lines.invalidate_indices();
_mkt_break_lines.refresh();
_refresh_all_visuals();
}
void _send_parameterized_order(Point point, Side side) {
Disposable.dispose(ref _parameterized_order_popup);
void on_cannot_send(string reason) {
@@ -762,10 +605,7 @@ namespace mbts.spark {
Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)) {
_vm?.recenter_all_ladders();
} else {
var tgt = _spec.tickdiff(_mkt_break_lines.origin, 0.0);
if (tgt.HasValue) {
scroll_to_tick(tgt.Value);
}
_scroll_to_center();
}
}
}
@@ -842,9 +682,14 @@ namespace mbts.spark {
}
internal int _row_for_price(double price) => LadderRender.index_of(_price_list, price, _price_comparer);
internal int _row_for_price(double price) {
if ((_price_list?.Count ?? 0) == 0 || !price.is_real()) {
return -1;
}
return LadderRender.index_of(_price_list, price, _price_comparer);
}
public double y_for_price(double price) => _row_for_price(price) * (_settings?.row_height ?? double.NaN);
// public double y_for_price(double price) => _row_for_price(price) * (_settings?.row_height ?? double.NaN);
#endregion
#region Column Utility Methods
@@ -889,11 +734,30 @@ namespace mbts.spark {
render_market_col(_depth.Bids);
break;
case ColumnType.Price:
_refresh_prices(column);
break;
case ColumnType.Ask:
render_market_col(_depth.Asks);
if (_settings.net_change != LadderSettings.NetChangeTerms.None) {
LadderRender.render_prices(
column.visual_tup.visual,
column.visual_tup.glyph_context,
_row_count,
_settings.row_height,
_price_list,
_vm.md.settlement_price,
_settings.net_change == LadderSettings.NetChangeTerms.Ticks,
column.settings,
_spec);
} else {
LadderRender.render_prices(
column.visual_tup.visual,
column.visual_tup.glyph_context,
_row_count,
_settings.row_height,
_price_list,
column.settings,
_spec,
_settings.price_display_format);
}
break;
case ColumnType.Ask: render_market_col(_depth.Asks); break;
case ColumnType.Volume:
_volume_container.mark_dirty();
_refresh_volume_at_price();
@@ -1086,9 +950,8 @@ namespace mbts.spark {
Storyboard.SetTargetProperty(anim, new PropertyPath(Behavior.AnimatedScrollViewerVerticalOffsetProperty));
sb.Begin();
} else {
_scroll_to_row(_price_list.Count / 2);
_scroll_to_row(_row_count / 2);
}
_refresh_frozen_mkt_lines();
}
}
@@ -1102,76 +965,23 @@ namespace mbts.spark {
return Disposable.Create(() => _can_user_send_orders_changed -= o.OnNext);
});
LadderPriceList _create_price_list() {
if (_settings is not null) {
switch (_settings.net_change) {
case LadderSettings.NetChangeTerms.BasisPoints:
case LadderSettings.NetChangeTerms.NetBasisPoints:
return new() {
length = _initial_row_count,
regional_tick = new RegionalTickFactors(new user.settings.TickFactors(10000, 1)),
};
}
}
return new() {
length = _initial_row_count,
regional_tick = _spec
};
}
#region Timer.Tick, Refresh Methods
void _tick() {
if (_settings != null) {
ulong unused = 0;
bool last_trade_changed = _vm.md.get_last_trade(ref unused, out _last_trade, out DateTime _, out ulong __);
bool last_trade_changed = _vm.md.get_last_trade(ref lastTradeTime, out _last_trade, out DateTime _, out ulong __);
bool depth_changed = _vm.md.get_depth(ref _prices_updated, ref _bids, ref _asks, NativePriceHelpers.COCONUT_MAX_DEPTH);
_depth = _vm.security_md.Depth(_bids, _asks, _vm.securities);
_mkt_break_lines.refresh();
if (_price_list is null) {
Log.assert(_price_dirty);
_price_list = _create_price_list();
var mid_tick = security.tickdiff(_mkt_break_lines.origin, 0) ?? 0;
if (mid_tick > int.MaxValue) {
Log.Ui.error(
$"Unable to set scrollbar range about mid_tick {mid_tick}: " +
$"tick count exceeds int.MaxValue");
} else {
_set_scrollbar_range(high_tick: (int)mid_tick + (_price_list.Count << 1));
}
_price_dirty = true;
bool prices_changed =
_price_dirty ||
(double.IsNaN(_last_origin_price) && !double.IsNaN(_mkt_break_lines.origin)) ||
_mkt_break_lines.origin >= _price_list[0] || _mkt_break_lines.origin <= _price_list[_price_list.Count - 1];
if (!prices_changed && _prices_out_of_range(out double low, out double high)) {
prices_changed = _refresh_price_range(low, high);
}
if (_price_dirty) {
if (_pending_price_list != null && !_pending_price_list.Equals(_price_list)) {
var prev_price_list = _price_list;
_price_list = _pending_price_list;
_pending_price_list = null;
if (prev_price_list.Count != _price_list.Count) {
_reset_height();
}
// maintain previous visible price range if possible
double percent_scroll_offset = _scroll_viewer.VerticalOffset / _scroll_viewer.ScrollableHeight;
var n_visible_rowsd = _scroll_viewer.ViewportHeight / _settings.row_height;
var n_visible_rows = (int)n_visible_rowsd;
var offset = n_visible_rowsd - n_visible_rows;
var n_rows_to_scroll = _price_list.Count - n_visible_rows - 1;
int prev_target_index = (int)(percent_scroll_offset * n_rows_to_scroll);
// int prev_target_index = (int)(percent_scroll_offset * prev_price_list.Count - 1);
if (prev_price_list.TryGetAt(prev_target_index, out double prev_target_price)) {
int new_target_index = LadderRender.index_of(_price_list, prev_target_price, _price_comparer);
if (_price_list.within_range(new_target_index)) {
// force _scrollViewer to re-calculate ScrollableHeight
_scroll_viewer.UpdateLayout();
double new_vertical_offset = new_target_index * _settings.row_height;
// double new_vertical_offset = ((double)new_target_index / _price_list.Count) * _scrollViewer.ScrollableHeight;
_scroll_viewer.ScrollToVerticalOffset(new_vertical_offset);
}
}
}
_refresh_prices();
scroll_to_pending_target();
if (prices_changed) {
_refresh_price_list();
_refresh_all_visuals();
} else {
scroll_to_pending_target();
if (_mkt_break_lines.bid_dirty)
_refresh_bid_mkt_break();
if (_mkt_break_lines.ask_dirty)
@@ -1243,6 +1053,75 @@ namespace mbts.spark {
_settings.price_display_format);
}
static readonly TimeSpan _price_out_of_range_render_interval = TimeSpan.FromSeconds(5);
DateTime _last_price_range_calc_time = DateTime.MinValue;
void _refresh_price_list() {
if (_settings is null || _spec is null || !_mkt_break_lines.origin.is_real()) {
return;
}
var prev_price_list = _price_list;
_price_list = (!(_settings.net_change == LadderSettings.NetChangeTerms.None)) ?
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _vm.md.settlement_price, _settings.net_change == LadderSettings.NetChangeTerms.Ticks, _row_count, _settings.row_height, _settings.price_column, _spec) :
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _row_count, _settings.row_height, _settings.price_column, _spec, _settings.price_display_format);
_ordershost.update_settings(_settings.row_height, _settings.orders_column);
if (prev_price_list != null) {
// maintain previous visible price range if possible
double percent_scroll_offset = _scroll_viewer.VerticalOffset / _scroll_viewer.ScrollableHeight;
int prev_target_index = (int)(percent_scroll_offset * prev_price_list.Count);
if (prev_price_list.TryGetAt(prev_target_index, out double prev_target_price)) {
int new_target_index = LadderRender.index_of(_price_list, prev_target_price, _price_comparer);
if (_price_list.within_range(new_target_index)) {
// force _scroll_viewer to re-calculate ScrollableHeight
_scroll_viewer.UpdateLayout();
double new_vertical_offset = ((double)new_target_index / _price_list.Count) * _scroll_viewer.ScrollableHeight;
_scroll_viewer.ScrollToVerticalOffset(new_vertical_offset);
}
}
}
}
bool _refresh_price_range(double low, double high) {
bool prices_changed = false;
// The market can be wider than the 1001 rows (ticks) we [initially] allot.
// Rendering prices is expensive, and will lock up the ui if done continuously.
// Therefore, ensure a minimum time interval has elapsed before rendering prices again.
var now = DateTime.Now;
if ((now - _last_price_range_calc_time) > _price_out_of_range_render_interval) {
SparkUtils.lazy_warn(!double.IsNaN(_mkt_break_lines.origin) && _mkt_break_lines.origin >= low && _mkt_break_lines.origin <= high,
() => fmt_log_msg($"market break ({_mkt_break_lines.origin}) is unacceptable for the market range ({low} - {high})"));
int prev_row_count = _row_count;
var n_ticks_to_low = (int)(_spec.tickdiff(_mkt_break_lines.origin, low) ?? 0).within_range(int.MinValue, int.MaxValue);
var n_ticks_to_high = (int)(_spec.tickdiff(high, _mkt_break_lines.origin) ?? 0).within_range(int.MinValue, int.MaxValue);
int desired_row_count = Math.Max(n_ticks_to_low, n_ticks_to_high) * 2 + 1;
desired_row_count = Math.Max(desired_row_count, _min_row_count);
_row_count = Math.Min(_max_row_count, desired_row_count);
if (_row_count != prev_row_count) {
if (desired_row_count > _max_row_count)
Log.Ui.warn(fmt_log_msg($"ticks between market range ({low} - {high}) exceeds the maximum number of rows allowed ({_max_row_count})"));
else
Log.Ui.info(fmt_log_msg($"increase row count to {_row_count} to accomodate market range ({low} - {high})"));
prices_changed = true;
_reset_height();
} else {
prices_changed = _mkt_break_lines.origin != _last_origin_price;
}
_last_price_range_calc_time = now;
//
string fmt_log_msg(string msg) =>
$"Ladder {security} (id:{security.Id}, low/high tick:{security.ContractSpec.LowTick}/{security.ContractSpec.HighTick}): {msg}.";
}
return prices_changed;
}
bool _prices_out_of_range(out double low, out double high) {
low = _bids == null ? double.NaN : TickUtils.low_tick(_bids, _asks);
high = _asks == null ? double.NaN : TickUtils.high_tick(_bids, _asks);
return
(!double.IsNaN(low) && _price_list[_price_list.Count - 1] > low) ||
(!double.IsNaN(high) && _price_list[0] < high);
}
void _refresh_all_visuals() {
if (_scroll_viewer is null || _settings is null) {
return;
@@ -1296,50 +1175,36 @@ namespace mbts.spark {
}
void _refresh_session_low_line() {
if (_settings.session_low_line.show) {
_render_session_line(_session_low_vis, _mkt_break_lines.low_render_index ?? -1, _settings.session_low_line.color);
}
if (_settings.session_low_line.show && _mkt_break_lines.low_render_index.HasValue)
_render_session_line(_session_low_vis, _mkt_break_lines.low_render_index.Value, _settings.session_low_line.color);
}
void _refresh_session_high_line() {
if (_settings.session_high_line.show) {
_render_session_line(_session_high_vis, _mkt_break_lines.high_render_index ?? -1, _settings.session_high_line.color);
}
if (_settings.session_high_line.show && _mkt_break_lines.high_render_index.HasValue)
_render_session_line(_session_high_vis, _mkt_break_lines.high_render_index.Value, _settings.session_high_line.color);
}
void _refresh_last_trade() {
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
if (last_index > 0)
LadderRender.render_last(
_last_vis_tup.visual,
_last_vis_tup.glyph_context,
_last_trade,
last_index,
_settings.row_height,
_settings.last_column,
_fmt_mkt_depth_item);
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
}
void _refresh_piq() {
if (!_settings.piq_column.show)
return;
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
LadderRender.render_piqs(
_piq_vis_tup.visual,
_last_vis_tup.glyph_context,
_price_list,
_vm.price_vs_piq,
_settings.row_height,
_spec,
_settings.piq_column,
_fmt_mkt_quantity);
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, _price_list, _vm.price_vs_piq, _settings.row_height, _spec, _settings.piq_column, _fmt_mkt_quantity);
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, _price_list, _vm.price_vs_piq, _settings.row_height, _spec, _settings.piq_column, _fmt_mkt_quantity);
}
void _refresh_sizes() {
Log.assert_d(_price_list != null);
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _depth.Bids, _settings.row_height, _spec, _settings.bid_column, _fmt_price_level);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _depth.Asks, _settings.row_height, _spec, _settings.ask_column, _fmt_price_level);
var bidItems = _bids.Select(nps => (nps.price, (double)nps.size, nps.price * nps.size));
var askItems = _asks.Select(nps => (nps.price, (double)nps.size, nps.price * nps.size));
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, bidItems, _settings.row_height, _spec, _settings.bid_column, _fmt_price_level);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, askItems, _settings.row_height, _spec, _settings.ask_column, _fmt_price_level);
}
/// <summary>
@@ -1408,12 +1273,9 @@ namespace mbts.spark {
void _clear_visuals() {
if (_settings == null)
return;
if (_price_list is null) {
_price_list = new LadderPriceList {
regional_tick = _spec
};
}
_price_list.reset(null, null, _initial_row_count);
_price_list = new double[_row_count];
for (int i = 0; i < _price_list.Count; i++)
_price_list[i] = double.NaN;
Array.Clear(_bids, 0, _bids.Length);
Array.Clear(_asks, 0, _asks.Length);
_price_dirty = true;
@@ -1421,7 +1283,6 @@ namespace mbts.spark {
_refresh_bid_mkt_break();
_refresh_ask_mkt_break();
_refresh_mid_mkt_break();
_refresh_fair_value_lines(true);
_refresh_session_low_line();
_refresh_session_high_line();
_refresh_session_range();
@@ -1431,58 +1292,38 @@ namespace mbts.spark {
#endregion
#region Market Break, Session Low/High Utils
static double _get_top_price(NativePriceSize[] prices) {
if (prices == null) {
return double.NaN;
}
if (prices.Length == 0) {
return double.NaN;
}
if (prices[0].size == 0) {
return double.NaN;
}
return prices[0].price;
}
static double _get_top_price(NativePriceSize[] prices) =>
prices != null && prices.Length > 0 ? prices[0].price : double.NaN;
double _get_bid_market_break() {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
var v = best_bid
.if_nan(_spec.tickdown(best_ask))
.if_nan(_vm?.settlement ?? double.NaN);
return _spec.round_to_tick(v, RoundingMode.AbsDown);
return double.IsNaN(best_bid) ?
double.IsNaN(best_ask) ? _vm.settlement : _spec.tickdown(best_ask) :
best_bid;
}
double _normalize_price(double price) =>
TickUtils.normalize_price(_spec, price);
double _get_ask_market_break() {
if (_vm is null) {
return double.NaN;
}
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
var v = _spec.tickdown(best_ask)
.if_nan(best_bid)
.if_nan(_vm?.settlement ?? double.NaN);
return _spec.round_to_tick(v, RoundingMode.AbsDown);
return double.IsNaN(best_ask) ?
double.IsNaN(best_bid) ? _vm.settlement : best_bid :
_spec.tickdown(best_ask);
}
double _get_mid_market_break() {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
var v = TickUtils.mid_tick(_spec, best_bid, best_ask)
.if_nan(best_bid)
.if_nan(_spec?.tickdown(best_ask) ?? double.NaN)
.if_nan(_vm?.settlement ?? double.NaN);
return _spec.round_to_tick(v, RoundingMode.AbsDown);
return double.IsNaN(best_bid) ?
double.IsNaN(best_ask) ? _vm.settlement : _spec.tickdown(best_ask) :
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(_spec, best_bid, best_ask);
}
double _get_session_low() => _normalize_price(_get_session_low_raw());
double _get_session_high() => _normalize_price(_get_session_high_raw());
double _get_session_low_raw() => _vm?.md?.session_low ?? double.NaN;
double _get_session_high_raw() => _vm?.md?.session_high ?? double.NaN;
double _get_session_low() => _vm.md.session_low;
double _get_session_high() => _vm.md.session_high;
void _render_mkt_break(DrawingVisual vis, double price, uint color) =>
LadderRender.render_mkt_break(vis, _row_for_price(price), _settings.row_height, color);
@@ -1506,15 +1347,6 @@ namespace mbts.spark {
_settings = _vm?.get_save_state() ?? s;
_pending_scroll_to_origin |= _settings == null;
_invalidate_spec();
if (_price_list != null) {
_pending_price_list = null;
if (_price_list.Count > 0) {
var high_price = _price_list.First();
var new_tickdiff = _spec.count_ticks(high_price);
_price_list.high_tick = (long)new_tickdiff;
}
_price_list.regional_tick = _spec;
}
_bid_vis_tup.glyph_context = glyph_context(s.bid_column);
_ask_vis_tup.glyph_context = glyph_context(s.ask_column);
_price_vis_tup.glyph_context = glyph_context(s.price_column);
@@ -1598,9 +1430,22 @@ namespace mbts.spark {
// if (_scroll_viewer != null) {
// _scroll_viewer.Background = s.last_column.background.rgb_brush();
// }
scroll_to_pending_target();
visible_columns_changed?.Invoke();
_refresh_all_visuals();
if (_scroll_viewer is not null && _settings is not null) {
_vm.md.get_last_trade(ref lastTradeTime, out _last_trade, out DateTime _, out ulong __);
_vm.md.get_depth(ref _prices_updated, ref _bids, ref _asks, NativePriceHelpers.COCONUT_MAX_DEPTH);
_mkt_break_lines.refresh();
double low = _bids == null ? double.NaN : TickUtils.low_tick(_bids, _asks);
double high = _asks == null ? double.NaN : TickUtils.high_tick(_bids, _asks);
_refresh_price_range(low, high);
_refresh_price_list();
_refresh_all_visuals();
_refresh_piq();
_piq_dirty = false;
_refresh_volume_at_price();
_last_origin_price = _mkt_break_lines.origin;
_price_dirty = false;
}
}
// This is designed to be a redundant null check for _fmt_mkt_depth_item_impl.
@@ -1631,6 +1476,9 @@ namespace mbts.spark {
return impl is null ? value.ToString() : impl(value, price);
}
string _fmt_mkt_quantity(NativePriceSize nps) =>
_fmt_mkt_quantity(nps.size, nps.price);
void _init_tactile_adorner() {
Disposable.dispose(ref _tactile_adorner);
if (_scroll_viewer != null)
@@ -1745,7 +1593,7 @@ namespace mbts.spark {
return null;
}
if (!double.IsNaN(_session_low)) {
_low_index = _owner._price_list.IndexOf(_session_low);
_low_index = LadderRender.index_of(_owner._price_list, _session_low, _owner._price_comparer);
if (_session_low < _owner._price_list.Last()) {
_low_index = _owner._price_list.Count - 1;
}
@@ -1,6 +1,7 @@
using bts.api;
using bts.utils;
using Spark.Core;
using Spark.Core.Diagnostics;
using System;
@@ -30,6 +31,16 @@ namespace mbts.spark {
regional_tick = other.regional_tick;
}
public LadderPriceList(RegionalTickFactors regionalTick, IList<double> prices) {
regional_tick = regionalTick;
_length = prices.Count;
checked {
_high_tick = prices.Count > 0
? (int)regionalTick.tickdiff(prices[0], 0).assert_value()
: 0;
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
RegionalTickFactors _regional_tick;
@@ -1,4 +1,4 @@
using bts;
using bts;
using bts.api;
using bts.api.md.auxiliary;
using bts.collections;
@@ -101,6 +101,24 @@ namespace mbts.spark {
settings.font_size, settings.width, row_height, HorizontalAlignment.Right,
-4, VerticalAlignment.Center, row * row_height, true);
public static double[] gen_prices(double mid, int row_count, RegionalTickFactors spec) {
// require odd number
Log.assert(row_count % 2 == 1);
var result = new double[row_count];
int range = row_count / 2;
int middle = row_count / 2;
result[middle] = mid;
double up = mid, down = mid;
// Starting from the middle iterate out to the edges
for (int x = 1; x <= range; ++x) {
up = spec.tickup(up);
down = spec.tickdown(down);
result[middle - x] = up;
result[middle + x] = down;
}
return result;
}
public static GlyphRun[] format_prices(
IList<double> prices,
double reference_price,
@@ -196,6 +214,45 @@ namespace mbts.spark {
}
}
public static double[] render_prices(DrawingVisual price, GlyphContext glyph_context, double mid, double wrt_price, bool format_in_tick_terms, int row_count, double row_height,
LadderColumnSettings settings, RegionalTickFactors spec) {
var dprices = gen_prices(mid, row_count, spec);
render_prices(price, glyph_context, row_count, row_height, dprices, wrt_price, format_in_tick_terms, settings, spec);
return dprices;
}
public static double[] render_prices(DrawingVisual price, GlyphContext glyph_context, double mid, int row_count, double row_height,
LadderColumnSettings settings, RegionalTickFactors spec, LadderSettings.PriceDisplayFormat price_display_format) {
var dprices = gen_prices(mid, row_count, spec);
render_prices(price, glyph_context, row_count, row_height, dprices, settings, spec, price_display_format);
return dprices;
}
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, IList<double> dprices, double wrt_price, bool format_in_tick_terms, LadderColumnSettings settings, RegionalTickFactors spec) {
var foreground = settings.foreground.rgb_brush();
//var background = new SolidColorBrush(settings.background.rgb_color());
var terms = format_in_tick_terms ? LadderSettings.NetChangeTerms.Ticks : LadderSettings.NetChangeTerms.Dollars;
var fprices = format_prices(dprices, wrt_price, terms, row_height, settings, glyph_context, spec);
using (var dc = price.RenderOpen())
using (_draw_with_clip(dc, settings, row_height, row_count)) {
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, row_count * row_height));
for (int x = 0; x < row_count; ++x)
dc.DrawGlyphRun(foreground, fprices[x]);
}
}
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, IList<double> dprices, LadderColumnSettings settings, RegionalTickFactors spec, LadderSettings.PriceDisplayFormat price_display_format) {
var foreground = settings.foreground.rgb_brush();
//var background = new SolidColorBrush(settings.background.rgb_color());
var fprices = format_prices(dprices, double.NaN, LadderSettings.NetChangeTerms.Dollars, row_height, settings, glyph_context, spec, price_display_format);
using (var dc = price.RenderOpen())
using (_draw_with_clip(dc, settings, row_height, row_count)) {
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, row_count * row_height));
for (int x = 0; x < row_count; ++x)
dc.DrawGlyphRun(foreground, fprices[x]);
}
}
public static void render_grid(DrawingVisual dv, IEnumerable<LadderGridHost.IColumn> columns, int rowcount, double row_height, UInt32 line_color) {
var height = rowcount * row_height;
var brush = line_color.rgb_brush();
@@ -26,7 +26,7 @@ namespace bts.shell.orderbook {
[ProtoMember(5)]
public string[] accounts;
[ProtoMember(5)]
[ProtoMember(28)]
public string Tag { get; set; }
[ProtoMember(6)]
@@ -407,16 +407,16 @@ namespace bts.shell.pnl {
void _show_auxiliary_popup<T>(T content, string context_desc, string title_desc, Color fg) {
var view = new HeaderedContentControl {
Content = content,
Header = new AuxiliaryDataHeader {
Header = new OrderAuxiliaryPopupHeader {
context = context_desc,
title = title_desc,
title_foreground = fg
},
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Resources = new AuxiliaryDataResources()
Resources = new OrderAuxiliaryPopupResources()
};
view.Style = view.Resources[AuxiliaryDataResources.PopupChildStyleKey] as Style;
view.Style = view.Resources[OrderAuxiliaryPopupResources.PopupChildStyleKey] as Style;
_popup_man.show(view);
}
@@ -250,6 +250,19 @@ namespace BCG
return Double.NaN;
}
private double _get_rep_driver() {
var map =
_threaded_bb.read_state(SparkTags.OptionDriverBBOTag((_nmsheet_symbol.option_name, _nmsheet_symbol.option_exchange)));
if (map == null)
return Double.NaN;
var price_map = map.get_price_map();
if (price_map == null || price_map.Count == 0)
return Double.NaN;
var md = price_map.ElementAt(0).Value;
double pr = md.get_driver();
return pr;
}
private IDictionary<DateTime, ATMData> _get_atm_data(ExpStrikeKeyedData<mbts.spark.TheoPoint> theo_data)
{
NMSheetSettings sheet_settings = _threaded_bb.read_state(_in_bb_sheet_settings_tag) ?? new NMSheetSettings();
@@ -261,7 +274,7 @@ namespace BCG
exp.security_type == SecurityType.Option)
?.ToDictionaryCrushed(kv => kv.Key.expiration, kv => kv.Value)
?? new();
IDictionary<DateTime, ATMData> atm_data = OptionATMDataSource.compute_atm_data(_get_onefive(), theo_data, option_exp_roll_data);
IDictionary<DateTime, ATMData> atm_data = OptionATMDataSource.compute_atm_data(_get_onefive(), theo_data, option_exp_roll_data, _get_rep_driver());
if (sheet_settings.setting_atm.select_atm_nearest_strike_only)
foreach (ATMData exp_atm_data in atm_data.Values)
exp_atm_data.reduce_to_nearest_expiration();
@@ -5,6 +5,7 @@ using mbts.ui;
using mbts;
using BCG.Utilities;
using mbts.spark;
using mbts.ui.converters;
namespace BCG {
/// <summary>
@@ -34,8 +35,10 @@ namespace BCG {
}
internal static IDictionary<DateTime, ATMData> compute_atm_data(
double one_five, ExpStrikeKeyedData<TheoPoint> theo_data, IDictionary<DateTime, double> opt_exp_rolls) {
double one_five, ExpStrikeKeyedData<TheoPoint> theo_data, IDictionary<DateTime, double> opt_exp_rolls,
double rep_driver) {
IDictionary<DateTime, double> exp_vs_drivers = _get_computed_drivers(theo_data);
exp_vs_drivers = exp_vs_drivers.ToDictionary(kv => kv.Key, kv => rep_driver);
IDictionary<DateTime, ATMData> rv = new Dictionary<DateTime, ATMData>();
if (one_five != 0)
foreach (DateTime exp in new List<DateTime>(exp_vs_drivers.Keys))
@@ -0,0 +1,236 @@
#nullable enable
namespace mbts.spark;
using bts;
using bts.api;
using bts.api.domain;
using bts.api.orderman;
using mbts.co2;
using mbts.ui;
using mbts.ui.shell;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using System.Windows.Threading;
class ExistingOrderTicketLadderVM : DisposableViewModelBase, IRequestViewClose {
const ushort numPriceLevels = 5;
readonly Property<SecurityImmutable> security;
readonly Property<TickDisplaySettingsImmutable> priceDisplay;
readonly Property<TickDisplaySettingsImmutable> quantityDisplay;
readonly Property<user.settings.RegionalTickFactors> priceTick;
readonly ObservableCollection<OrderTicketLadderPriceLevelVM> bids = [];
readonly ObservableCollection<OrderTicketLadderPriceLevelVM> asks = [];
readonly Property<double> price, currentPrice;
readonly Property<uint> currentQuantity;
readonly Property<AccountTuple> currentAccount;
readonly Property<TimeInForce> currentTimeInForce;
readonly Property<CustomOrderDefinition.CustomOneofCase> currentOrderType;
readonly Property<bool> priceDirty, quantityDirty, accountDirty, timeInForceDirty, orderTypeDirty;
Side side = Side.Buy;
public ExistingOrderTicketLadderVM(
OrdersModel model,
IObservable<(SecurityMD? SecurityMd, OrderRecordImmutable Order)> orderSource,
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
IObservable<double[]?> calculatorButtons,
Action<SecurityMD> showLadder,
TickDisplaySettingsImmutable? quantityDisplay = null
) {
SecurityMD? currentSecurityMd = null;
uint? key = null;
DispatcherTimer? mdPollTimer = null;
var nativeBids = new NativePriceSize[numPriceLevels];
var nativeAsks = new NativePriceSize[numPriceLevels];
for (int i = 0; i < numPriceLevels; i++) {
nativeBids[i] = NativePriceSize.NO_MARKET;
nativeAsks[i] = NativePriceSize.NO_MARKET;
Bids.Add(new(Side.Buy));
Asks.Add(new(Side.Sell));
}
var secMdSrc = orderSource.Where(ot => ot.SecurityMd.HasValue).Select(ot => ot.SecurityMd!.Value);
security = backing(nameof(Security), secMdSrc.Select(smd => smd.security), bts.api.Security.Defaults.SecurityImmutable);
priceTick = backing(nameof(PriceTick), user.settings.RegionalTickFactors.Default);
PropertiesVm = new OrderPropertiesEditorVM(
model,
secMdSrc,
accountsSource,
orderSource.Select(ot => new InlineOrderProperties(model, ot.Order.Def)));
this.quantityDisplay = backing(nameof(QuantityDisplay), quantityDisplay ?? bts.api.Security.Defaults.quantity_display_settingsi);
priceDisplay = backing(nameof(PriceDisplay), bts.api.Security.Defaults.price_display_settingsi);
SizeCalculator = new(0, calculatorButtons);
SetPriceCmd = new DelegateCommand<double>(SetPrice);
ShowLadderCmd = new DelegateCommand(() => {
if (currentSecurityMd.HasValue)
showLadder(currentSecurityMd.Value);
});
price = backing(nameof(Price), double.NaN);
currentPrice = backing(nameof(CurrentPrice), orderSource.Select(ort => ort.Order.State.Price));
currentQuantity = backing(nameof(CurrentQuantity), orderSource.Select(ort => ort.Order.State.Quantity));
currentAccount = backing(nameof(CurrentAccount), orderSource.Select(ort => new AccountTuple(ort.Order.Def.AccountId, ort.Order.Def.Account)));
currentTimeInForce = backing(nameof(CurrentTimeInForce), orderSource.Select(ort => ort.Order.Def.TimeInForce));
currentOrderType = backing(nameof(CurrentOrderType), orderSource.Select(ort => ort.Order.Def.CustomDef.CustomCase));
//
priceDirty = backing(
nameof(PriceDirty),
price.observe_current(true).CombineLatest(
currentPrice.observe_current(true),
(val, curr) => !double.IsNaN(val) && val != curr));
quantityDirty = backing(
nameof(QuantityDirty),
SizeCalculator.observe_actual_value()
.CombineLatest(currentQuantity.observe_current(true),
(val, curr) => val.HasValue && val.Value != curr));
accountDirty = backing(
nameof(AccountDirty),
PropertiesVm.observe_account(true)
.CombineLatest(currentAccount.observe_current(true),
(val, curr) => val != curr));
timeInForceDirty = backing(
nameof(TimeInForceDirty),
PropertiesVm.observe_time_in_force(true)
.CombineLatest(currentTimeInForce.observe_current(true),
(val, curr) => val != curr));
orderTypeDirty = backing(
nameof(OrderTypeDirty),
PropertiesVm.observe_order_type(true)
.CombineLatest(currentOrderType.observe_current(true),
(val, curr) => val != null && val.order_type != curr));
//
var priceDirtySrc = priceDirty.Select(vals => vals.newval).StartWith(false);
var quantityDirtySrc = quantityDirty.Select(vals => vals.newval).StartWith(false);
var sendOrderCmd = new ObservantCommand(SendOrder);
SendOrderCmd = sendOrderCmd;
var canSendOrderSrc = priceDirty.observe_current(true)
.CombineLatest(quantityDirty.observe_current(true), (pd, sd) => pd || sd)
.DistinctUntilChanged();
var isDirtySrc = Observable.Merge(priceDirty, quantityDirty, accountDirty, timeInForceDirty, orderTypeDirty);
_dispose = Disposable.Create(
SizeCalculator,
PropertiesVm,
secMdSrc.OnDispatcher().Subscribe(OnSecurityMd),
orderSource.OnDispatcher().Subscribe(ort => {
key = ort.Order.Key;
if (DoubleHelper.IsNaNOrInfinity(Price))
Price = ort.Order.State.Price;
if (!SizeCalculator.value.HasValue || !SizeCalculator.value_setter_called)
SizeCalculator.bypass_value_setter(ort.Order.State.Quantity, true);
}),
isDirtySrc.OnDispatcher().Subscribe(_ => RaisePropertyChanged(nameof(DirtyItemsText))),
canSendOrderSrc.OnDispatcher().Subscribe(sendOrderCmd));
void SetPrice(double price) {
Price = price;
RaisePropertyChanged(nameof(Price));
}
void SendOrder() {
if (!key.HasValue) {
return;
}
if (priceDirty.Value) {
if (quantityDirty.Value) {
model.update_order(key.Value, Price, SizeCalculator.value!.Value).ignore();
} else {
model.update_order_price(key.Value, Price).ignore();
}
} else if (quantityDirty.Value) {
model.update_order_size(key.Value, SizeCalculator.value!.Value).ignore();
} else {
SizeCalculator.flash_error();
flash_error(nameof(Price)).ignore();
return;
}
RequestClose?.Invoke(this, new EventArgs());
}
void OnSecurityMd(SecurityMD smd) {
currentSecurityMd = smd;
priceTick.set(smd.security.regional_ticks());
priceDisplay.set(smd.price_display);
this.quantityDisplay.set(smd.quantity_display);
SizeCalculator.display_settings = smd.quantity_display;
ulong lastMdPollTime = 0;
mdPollTimer?.Stop();
mdPollTimer = new DispatcherTimer(TimeSpan.FromSeconds(0.1), DispatcherPriority.DataBind, PollMd, Dispatcher.CurrentDispatcher);
void PollMd(object? sender, EventArgs e) {
if (smd.md.get_depth(ref lastMdPollTime, ref nativeBids, ref nativeAsks, numPriceLevels)) {
for (int i = 0; i < numPriceLevels; i++) {
asks[numPriceLevels - i - 1].Price = nativeAsks[i].price;
asks[numPriceLevels - i - 1].Quantity = nativeAsks[i].size;
}
for (int i = 0; i < numPriceLevels; i++) {
bids[i].Price = nativeBids[i].price;
bids[i].Quantity = nativeBids[i].size;
}
}
}
}
}
public event EventHandler? RequestClose;
public SecurityImmutable Security => security.Value;
public string Title => Security.ToString() ?? string.Empty;
public double Price { get; set; }
public user.settings.RegionalTickFactors PriceTick => priceTick.Value;
public TickDisplaySettingsImmutable PriceDisplay => priceDisplay.Value;
public TickDisplaySettingsImmutable QuantityDisplay => quantityDisplay.Value;
public CalculatorLogic<double> SizeCalculator { get; }
public Side Side {
get => side;
set => Set(ref side, value);
}
public OrderPropertiesEditorVM PropertiesVm { get; }
public ObservableCollection<OrderTicketLadderPriceLevelVM> Bids => bids;
public ObservableCollection<OrderTicketLadderPriceLevelVM> Asks => asks;
public double CurrentPrice => currentPrice.Value;
public uint CurrentQuantity => currentQuantity.Value;
public AccountTuple CurrentAccount => currentAccount.Value;
public TimeInForce CurrentTimeInForce => currentTimeInForce.Value;
public CustomOrderDefinition.CustomOneofCase CurrentOrderType => currentOrderType.Value;
public bool PriceDirty => priceDirty.Value;
public bool QuantityDirty => quantityDirty.Value;
public bool AccountDirty => accountDirty.Value;
public bool TimeInForceDirty => timeInForceDirty.Value;
public bool OrderTypeDirty => orderTypeDirty.Value;
public bool CanChangeAccount { get; } = false;
public bool CanChangeTimeInForce { get; } = false;
public bool CanChangeOrderType { get; } = false;
public string DirtyItemsText {
get {
var dirty_items = dirtyItems().ToList();
if (dirty_items.Count == 0) {
return "(no changes)";
}
return "change " + string.Join(", ", dirty_items);
}
}
public bool CloseOnSend { get; set; }
public ICommand SendOrderCmd { get; }
public ICommand SetPriceCmd { get; }
public ICommand ShowLadderCmd { get; }
IEnumerable<string> dirtyItems() {
if (priceDirty.Value) yield return "price";
if (quantityDirty.Value) yield return "quantity";
if (accountDirty.Value) yield return "account";
if (timeInForceDirty.Value) yield return "tif";
if (orderTypeDirty.Value) yield return "type";
}
}
@@ -0,0 +1,298 @@
#nullable enable
namespace mbts.spark;
using bts;
using bts.api;
using bts.api.domain;
using bts.api.orderman;
using bts.collections;
using bts.utils;
using mbts.co2;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
class NewOrderTicketLadderVM : DisposableViewModelBase, IOrderModificationLadderVM {
const ushort numPriceLevels = 5;
const uint numPriceRowsPerSide = 500;
readonly Property<SecurityImmutable> security;
readonly Property<string> title;
readonly Property<TickDisplaySettingsImmutable> priceDisplay;
readonly Property<TickDisplaySettingsImmutable> quantityDisplay;
readonly Property<user.settings.RegionalTickFactors> priceTick;
readonly ObservableCollection<OrderTicketLadderPriceLevelVM> priceLevels = [];
Side side = Side.Buy;
public NewOrderTicketLadderVM(
OrdersModel model,
SecurityMD securityMd,
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
OrderDefinitionImmutable def,
double price,
double size,
IObservable<double[]> calculatorButtons,
Action<SecurityMD> showLadder)
: this(
model,
Observable.Return(securityMd),
accountsSource,
Observable.Return(new InlineOrderTicketProperties(model, def, price, size)),
calculatorButtons,
showLadder) { }
public NewOrderTicketLadderVM(
OrdersModel model,
SecurityMD securityMd,
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
IObservable<LadderSettings> settings,
Action<SecurityMD> showLadder)
: this(
model,
Observable.Return(securityMd),
accountsSource,
settings.Select(s => new InlineOrderTicketProperties(model, s)),
settings.Select(s => s.calculator_buttons),
showLadder) { }
public NewOrderTicketLadderVM(
OrdersModel model,
IObservable<SecurityMD> securityMdSource,
IObservable<IReadOnlyDictionary<uint, AccountTuple>> accountsSource,
IObservable<InlineOrderTicketProperties> propsSource,
IObservable<double[]?> calculatorButtons,
Action<SecurityMD> showLadder,
TickDisplaySettingsImmutable? quantityDisplay = null
) {
SecurityMD? currentSecurityMd = null;
DispatcherTimer? mdPollTimer = null;
var nativeBids = new NativePriceSize[numPriceLevels];
var nativeAsks = new NativePriceSize[numPriceLevels];
for (int i = 0; i < numPriceLevels; i++) {
nativeBids[i] = NativePriceSize.NO_MARKET;
nativeAsks[i] = NativePriceSize.NO_MARKET;
}
for (int i = 0; i < numPriceRowsPerSide; i++) {
priceLevels.Add(new(Side.Sell));
}
for (int i = 0; i < numPriceRowsPerSide; i++) {
priceLevels.Add(new(Side.Buy));
}
security = backing(nameof(Security), bts.api.Security.Defaults.SecurityImmutable);
title = backing(nameof(Title), security.observe_current(true), s => s.ToString() ?? string.Empty, string.Empty);
priceTick = backing(nameof(PriceTick), user.settings.RegionalTickFactors.Default);
PropertiesVm = new(model, securityMdSource, accountsSource, propsSource.Select(p => p.order_props));
this.quantityDisplay = backing(nameof(QuantityDisplay), quantityDisplay ?? bts.api.Security.Defaults.quantity_display_settingsi);
priceDisplay = backing(nameof(PriceDisplay), bts.api.Security.Defaults.price_display_settingsi);
SizeCalculator = new(0, calculatorButtons);
SendOrderCmd = new DelegateCommand(SendOrder);
SetPriceCmd = new DelegateCommand<double>(SetPrice);
ShowLadderCmd = new DelegateCommand(() => {
if (currentSecurityMd.HasValue)
showLadder(currentSecurityMd.Value);
});
RecenterCmd = new DelegateCommand(Recenter);
_dispose = Disposable.Create(
securityMdSource.OnDispatcher().Subscribe(OnSecurityMd),
SizeCalculator,
propsSource.OnDispatcher().Subscribe(p => OnProps(p.price, p.size)));
void OnProps(
InlineUserProperty<double> priceProp, InlineUserProperty<double> sizeProp) {
if (priceProp.has_value && (priceProp.force_override || DoubleHelper.IsNaNOrInfinity(Price)))
SetPrice(priceProp.value);
if (sizeProp.has_value && (sizeProp.force_override || SizeCalculator.value == 0))
SizeCalculator.value = sizeProp.value;
}
void SetPrice(double price) {
Price = price;
RaisePropertyChanged(nameof(Price));
}
void SendOrder() {
bool priceValid = validatePrice();
bool quantityValid = SizeCalculator.validate();
if (priceValid && quantityValid && !HasErrors && !PropertiesVm.HasErrors) {
model.send_order(Security, PropertiesVm.create(side), Price, SizeCalculator.value!.Value).ignore();
}
}
void Recenter() =>
RequestRecenter?.Invoke();
void OnSecurityMd(SecurityMD smd) {
currentSecurityMd = smd;
var spec = TickUtils.multiply_tick_size(smd.security.regional_ticks(), 1);
security.set(smd.security);
priceTick.set(smd.security.regional_ticks());
priceDisplay.set(smd.price_display);
this.quantityDisplay.set(smd.quantity_display);
SizeCalculator.display_settings = smd.quantity_display;
ulong lastMdPollTime = 0;
mdPollTimer?.Stop();
mdPollTimer = new DispatcherTimer(TimeSpan.FromSeconds(0.1), DispatcherPriority.DataBind, PollMd, Dispatcher.CurrentDispatcher);
void PollMd(object? sender, EventArgs e) {
if (smd.md.get_depth(ref lastMdPollTime, ref nativeBids, ref nativeAsks, numPriceLevels)) {
if (double.IsNaN(priceLevels[0].Price)) {
// Initialize prices and recenter
if (!TryGetOrigin(out var origin)) {
return;
}
double price = spec.tickdown(origin, numPriceRowsPerSide - 1);
for (int i = priceLevels.Count - 1; i >= 0; i--) {
priceLevels[i].Price = price;
price = spec.tickup(price);
}
Recenter();
}
var hi = priceLevels[0].Price;
var lo = priceLevels[^1].Price;
Log.assert(hi.is_real() && lo.is_real() && hi > lo);
var wrappedPrices = priceLevels.WrapSelector(p => p.Price, null);
var rPriceComp = spec.price_comparer().reverse();
// Clear old prices
for (int i = 0; i < numPriceLevels; i++) {
// asks
var px = nativeAsks[i].price;
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
if (idx < 0)
idx = ~idx;
priceLevels[idx].Quantity = 0;
}
// bids
px = nativeBids[i].price;
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
if (idx < 0)
idx = ~idx;
priceLevels[idx].Quantity = 0;
}
}
// Set new prices
for (int i = 0; i < numPriceLevels; i++) {
// asks
var px = nativeAsks[i].price;
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
if (idx < 0)
idx = ~idx;
priceLevels[idx].Quantity = nativeAsks[i].size;
priceLevels[idx].Side = Side.Sell;
}
// bids
px = nativeBids[i].price;
if (!double.IsNaN(px) && px.is_within_range(lo, hi)) {
int idx = CollectionHelper.binary_search(wrappedPrices, px, rPriceComp);
if (idx < 0)
idx = ~idx;
priceLevels[idx].Quantity = nativeBids[i].size;
priceLevels[idx].Side = Side.Buy;
}
}
}
}
bool TryGetOrigin(out double origin) {
if (!nativeBids[0].is_nan) {
origin = nativeBids[0].price;
} else if (!nativeAsks[0].is_nan) {
origin = nativeAsks[0].price;
} else {
origin = double.NaN;
return false;
}
return true;
}
}
}
public event Action? RequestRecenter;
public SecurityImmutable Security => security.Value;
public string Title => title.Value;
public double Price { get; set; }
public user.settings.RegionalTickFactors PriceTick => priceTick.Value;
public TickDisplaySettingsImmutable PriceDisplay => priceDisplay.Value;
public TickDisplaySettingsImmutable QuantityDisplay => quantityDisplay.Value;
public CalculatorLogic<double> SizeCalculator { get; }
public Side Side {
get => side;
set => Set(ref side, value);
}
public OrderPropertiesEditorVM PropertiesVm { get; }
public ObservableCollection<OrderTicketLadderPriceLevelVM> PriceLevels => priceLevels;
public ICommand SendOrderCmd { get; }
public ICommand SetPriceCmd { get; }
public ICommand ShowLadderCmd { get; }
public ICommand RecenterCmd { get; }
bool validatePrice() {
if (!Price.is_real()) {
flash_error(nameof(Price)).ignore();
return false;
}
return true;
}
}
class OrderTicketLadderPriceLevelVM : ViewModelBase {
Color background = Colors.Transparent;
Color priceForeground = BTSColors.Control.normal_foreground;
Side side;
double price = double.NaN;
double quantity = double.NaN;
public OrderTicketLadderPriceLevelVM(Side side) {
this.side = side;
background = BTSColors.Ladder.price_column_background;
priceForeground = BTSColors.Ladder.price_column_foreground;
}
public Side Side {
get => side;
set => Set(ref side, value, dependent_props: nameof(QuantityForeground));
}
public double Price {
get => price;
set => Set(ref price, value);
}
public double Quantity {
get => quantity;
set => Set(ref quantity, value);
}
public Color Background {
get => background;
set => Set(ref background, value);
}
public Color PriceForeground {
get => priceForeground;
set => Set(ref priceForeground, value);
}
public Color QuantityForeground =>
side switch {
Side.Buy => BTSColors.Ladder.bid_column_foreground,
Side.Sell => BTSColors.Ladder.ask_column_foreground,
_ => BTSColors.UI.default_foreground,
};
}
@@ -0,0 +1,415 @@
<UserControl
x:Class="mbts.ui.spark.OrderModificationLadderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:api="clr-namespace:bts.api;assembly=Bts.Spark.Api"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:uag="clr-namespace:mbts.ui.spark.algo"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
u:AttachedCommands.MiddleClick="{Binding RecenterCmd}" >
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Colors.xaml"/>
<ResourceDictionary Source="/view/Resources/Converters.xaml"/>
<ResourceDictionary Source="/view/Controls/Order/OrderDetailsPopupControl.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Consts -->
<sys:Double x:Key="HorizontalSpacing">8.0</sys:Double>
<sys:Double x:Key="VerticalSpacing">8.0</sys:Double>
<sys:Double x:Key="ButtonHeight">22.0</sys:Double>
<!-- Converters -->
<cv:NullableDoubleConverter x:Key="TickDefaultPriceConverter" DefaultValue="0"/>
<cv:ValueMap x:Key="SideToBrushConverter" Default="{x:Static Brushes.Transparent}">
<cv:KeyValue Key="{u:Enum Buy, Type=api:Side}" Value="{u:BTSBrush MarketData.bid, Opacity=0.33}" />
<cv:KeyValue Key="{u:Enum Sell, Type=api:Side}" Value="{u:BTSBrush MarketData.ask, Opacity=0.33}" />
</cv:ValueMap>
<cv:ValueMap x:Key="SideToIsBuyConverter" Default="{u:False}">
<cv:KeyValue Key="{u:Enum Buy, Type=api:Side}" Value="{u:True}" />
</cv:ValueMap>
<cv:ValueMap x:Key="SideToIsSellConverter" Default="{u:False}">
<cv:KeyValue Key="{u:Enum Sell, Type=api:Side}" Value="{u:True}" />
</cv:ValueMap>
<cv:ValueMap x:Key="SideToBidVisibilityConverter" Default="{u:Visibility Collapsed}">
<cv:KeyValue Key="{u:Enum Buy, Type=api:Side}" Value="{u:Visibility Visible}" />
</cv:ValueMap>
<cv:ValueMap x:Key="SideToAskVisibilityConverter" Default="{u:Visibility Collapsed}">
<cv:KeyValue Key="{u:Enum Sell, Type=api:Side}" Value="{u:Visibility Visible}" />
</cv:ValueMap>
<!-- Styles -->
<Style TargetType="Button" BasedOn="{StaticResource DefaultButton}">
<Setter Property="Height" Value="{StaticResource ButtonHeight}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
<Style TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="Height" Value="{StaticResource ButtonHeight}"/>
<Style.Triggers>
<Trigger Property="usp:OrderModificationLadderControl.IsDirty" Value="True">
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="usp:PriceEntryControl" BasedOn="{StaticResource {x:Type usp:PriceEntryControl}}">
<Style.Triggers>
<Trigger Property="usp:OrderModificationLadderControl.IsDirty" Value="True">
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="c:TickUpDownNullableUInt32" BasedOn="{StaticResource {x:Type c:TickUpDownNullableUInt32}}">
<Style.Triggers>
<Trigger Property="usp:OrderModificationLadderControl.IsDirty" Value="True">
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="PriceLevelItemTemplate">
<Button
Background="{Binding Background, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
MinWidth="96"
Padding="4,2"
Command="{Binding DataContext.SetPriceCmd, RelativeSource={RelativeSource AncestorType=usp:OrderModificationLadderControl}}"
CommandParameter="{Binding Price}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="40" SharedSizeGroup="Orders" />
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="BidSize" />
<ColumnDefinition Width="Auto" MinWidth="56" SharedSizeGroup="Price" />
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="AskSize" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Right"
Foreground="{Binding QuantityForeground, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
FontSize="{x:Static u:FontSizes.Small}"
Visibility="{Binding Side, Converter={StaticResource SideToBidVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{x:Static cv:TickDisplayConverter.Instance}">
<Binding Path="Quantity" Mode="OneWay"/>
<Binding Path="DataContext.QuantityDisplay" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationLadderControl}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock
Grid.Column="2"
HorizontalAlignment="Center"
Foreground="{Binding PriceForeground, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
FontSize="{x:Static u:FontSizes.Small}">
<TextBlock.Text>
<MultiBinding Converter="{x:Static cv:TickDisplayConverter.Instance}">
<Binding Path="Price" Mode="OneWay"/>
<Binding Path="DataContext.PriceDisplay" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationLadderControl}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock
Grid.Column="3"
HorizontalAlignment="Left"
Foreground="{Binding QuantityForeground, Converter={x:Static cv:ColorToBrushConverter.Instance}}"
FontSize="{x:Static u:FontSizes.Small}"
Visibility="{Binding Side, Converter={StaticResource SideToAskVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{x:Static cv:TickDisplayConverter.Instance}">
<Binding Path="Quantity" Mode="OneWay"/>
<Binding Path="DataContext.QuantityDisplay" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationLadderControl}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Button>
</DataTemplate>
<ControlTemplate x:Key="BuyRadioButtonTemplate" TargetType="RadioButton">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="hoverOverlay" Storyboard.TargetProperty="Opacity" To="0.3" BeginTime="0" Duration="0" />
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed" />
<VisualState x:Name="Disabled">
<Storyboard>
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="#555" Duration="0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Checked">
<Storyboard>
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="{u:BTSColor MarketData.bid}" BeginTime="0" Duration="0"/>
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="BorderBrush.Color" To="{u:BTSColor MarketData.bid}" BeginTime="0" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Unchecked" />
<VisualState x:Name="Indeterminate" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border
x:Name="background"
BorderThickness="{TemplateBinding BorderThickness}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
Padding="{TemplateBinding Padding}"
CornerRadius="2">
<Border.Background>
<SolidColorBrush Color="Transparent" />
</Border.Background>
<Border.BorderBrush>
<SolidColorBrush Color="Black"/>
</Border.BorderBrush>
</Border>
<ContentPresenter
Name="presenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding HorizontalContentAlignment}" />
<Border Name="hoverOverlay" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black" Opacity="0" IsHitTestVisible="False" />
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="SellRadioButtonTemplate" TargetType="RadioButton">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="hoverOverlay" Storyboard.TargetProperty="Opacity" To="0.3" BeginTime="0" Duration="0" />
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed" />
<VisualState x:Name="Disabled">
<Storyboard>
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="#555" Duration="0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Checked">
<Storyboard>
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="Background.Color" To="{u:BTSColor MarketData.ask}" BeginTime="0" Duration="0"/>
<ColorAnimation Storyboard.TargetName="background" Storyboard.TargetProperty="BorderBrush.Color" To="{u:BTSColor MarketData.ask}" BeginTime="0" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Unchecked" />
<VisualState x:Name="Indeterminate" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border
x:Name="background"
BorderThickness="{TemplateBinding BorderThickness}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
Padding="{TemplateBinding Padding}"
CornerRadius="2">
<Border.Background>
<SolidColorBrush Color="Transparent" />
</Border.Background>
<Border.BorderBrush>
<SolidColorBrush Color="Black"/>
</Border.BorderBrush>
</Border>
<ContentPresenter
Name="presenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<Border Name="hoverOverlay" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black" Opacity="0" IsHitTestVisible="False" />
</Grid>
</ControlTemplate>
<Style x:Key="BuyRadioButtonStyle" TargetType="RadioButton">
<Setter Property="Padding" Value="2" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{u:BTSBrush UI.default_text}" />
<Setter Property="Template" Value="{StaticResource BuyRadioButtonTemplate}" />
</Style>
<Style x:Key="SellRadioButtonStyle" TargetType="RadioButton">
<Setter Property="Padding" Value="2" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{u:BTSBrush UI.default_text}" />
<Setter Property="Template" Value="{StaticResource SellRadioButtonTemplate}" />
</Style>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSpacing}}" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- Ladder (market depth) -->
<Grid Grid.Column="2" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
Background="{u:BTSBrush header}"
TextElement.Foreground="{u:BTSBrush header_text}"
TextElement.FontSize="{x:Static u:FontSizes.Small}"
TextElement.FontWeight="SemiBold">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="40" SharedSizeGroup="Orders" />
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="BidSize" />
<ColumnDefinition Width="Auto" MinWidth="56" SharedSizeGroup="Price" />
<ColumnDefinition Width="*" MinWidth="40" SharedSizeGroup="AskSize" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Center" Text="Orders" />
<TextBlock Grid.Column="1" HorizontalAlignment="Center" Text="Bid" />
<TextBlock Grid.Column="2" HorizontalAlignment="Center" Text="Price" />
<TextBlock Grid.Column="3" HorizontalAlignment="Center" Text="Ask" />
</Grid>
<ScrollViewer Grid.Row="1" x:Name="priceLevelsScrollViewer" MaxHeight="240">
<ItemsControl ItemsSource="{Binding PriceLevels}" ItemTemplate="{StaticResource PriceLevelItemTemplate}" />
</ScrollViewer>
</Grid>
<!-- Order Inputs -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
<RowDefinition Height="*" />
<RowDefinition Height="{Binding Source={StaticResource VerticalSpacing}}" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Buy/Sell toggle -->
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" MinWidth="72" />
<ColumnDefinition Width="1*" MinWidth="72" />
</Grid.ColumnDefinitions>
<RadioButton
Grid.Column="0"
GroupName="Side"
IsChecked="{Binding Side, Converter={StaticResource SideToIsBuyConverter}}"
Style="{StaticResource BuyRadioButtonStyle}">
<TextBlock FontWeight="Bold" Foreground="{u:BTSBrush UI.default_text}">
Buy
</TextBlock>
</RadioButton>
<RadioButton
Grid.Column="1"
GroupName="Side"
IsChecked="{Binding Side, Converter={StaticResource SideToIsSellConverter}}"
Style="{StaticResource SellRadioButtonStyle}">
<TextBlock FontWeight="Bold" Foreground="{u:BTSBrush UI.default_text}">
Sell
</TextBlock>
</RadioButton>
</Grid>
<usp:PriceEntryControl
Grid.Row="2"
usp:OrderModificationControl.IsDirty="{Binding PriceDirty, FallbackValue={x:Null}}"
RegionalTick="{Binding PriceTick}"
Focusable="False"
TickDefaultValue="{Binding CurrentPrice, Converter={StaticResource TickDefaultPriceConverter}, FallbackValue={u:Double 0}}"
ToolTip="Price"
Value="{Binding Price}" />
<c:TickUpDownNullableDouble
Grid.Row="4"
usp:OrderModificationControl.IsDirty="{Binding QuantityDirty, FallbackValue={x:Null}}"
CommitOnEnter="True"
Focusable="False"
Increment="{Binding SizeCalculator.unit_quantity}"
Minimum="0"
TickDisplaySettings="{Binding SizeCalculator.display_settings}"
ToolTip="Quantity"
Value="{Binding SizeCalculator.display_value}" />
<uag:OrderDetailsPopupControl
Grid.Row="6"
DataContext="{Binding PropertiesVm}"
usp:OrderModificationControl.IsDirty="{Binding DataContext.order_type_dirty, RelativeSource={RelativeSource AncestorType=usp:OrderModificationControl}, FallbackValue={u:False}}"
HasEditableProperties="{Binding can_expand, FallbackValue={u:False}}"
IsAlgoTemplateToolBarReadOnly="True"
IsEnabled="{Binding DataContext.can_change_order_type, RelativeSource={RelativeSource AncestorType=usp:OrderModificationControl}, FallbackValue={u:True}}"
IsOpen="{Binding is_expanded, FallbackValue={u:False}}"
ItemsSource="{Binding algo_definitions_view}"
SelectedItem="{Binding selected_algo_definition}"
ToolTip="Order/Algo Type">
<uag:OrderDetailsPopupControl.Style>
<Style TargetType="uag:OrderDetailsPopupControl" BasedOn="{StaticResource {x:Type uag:OrderDetailsPopupControl}}">
<Style.Triggers>
<Trigger Property="usp:OrderModificationControl.IsDirty" Value="True">
<Setter Property="BorderBrush" Value="{u:BTSColor UI.input_dirty_border}"/>
</Trigger>
</Style.Triggers>
</Style>
</uag:OrderDetailsPopupControl.Style>
</uag:OrderDetailsPopupControl>
<ComboBox
Grid.Row="8"
usp:OrderModificationControl.IsDirty="{Binding AccountDirty, FallbackValue={u:False}}"
IsEnabled="{Binding CanChangeAccount, FallbackValue={u:True}}"
ItemsSource="{Binding PropertiesVm.accounts}"
SelectedItem="{Binding PropertiesVm.selected_account}"
ToolTip="Account" />
<ComboBox
Grid.Row="10"
usp:OrderModificationControl.IsDirty="{Binding TimeInForceDirty, FallbackValue={u:False}}"
IsEnabled="{Binding CanChangeTimeInForce, FallbackValue={u:True}}"
ItemsSource="{Binding PropertiesVm.time_in_forces}"
SelectedItem="{Binding PropertiesVm.selected_time_in_force}"
ToolTip="Time In Force" />
<Button
Grid.Row="12"
Height="NaN"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="Green"
Foreground="{u:BTSBrush UI.default_text}"
FontSize="{x:Static u:FontSizes.Largest}"
FontWeight="Bold"
Command="{Binding SendOrderCmd}"
ToolTip="{Binding Side, StringFormat='Send {0} Order'}">
Send
</Button>
<TextBlock
Grid.Row="14"
HorizontalAlignment="Center"
FontSize="{x:Static u:FontSizes.Small}">
<Hyperlink
Command="{Binding ShowLadderCmd}"
ToolTip="Open the original ladder for this security">Open full ladder</Hyperlink>
</TextBlock>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,51 @@
#nullable enable
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace mbts.ui.spark {
interface IOrderModificationLadderVM {
event Action? RequestRecenter;
}
/// <summary>
/// Interaction logic for OrderModificationLadderControl.xaml
/// </summary>
public partial class OrderModificationLadderControl : UserControl {
public static readonly DependencyProperty IsDirtyProperty = DependencyProperty.RegisterAttached(
"IsDirty", typeof(bool?), typeof(OrderModificationLadderControl), new PropertyMetadata(null));
public static bool? GetIsDirty(DependencyObject d) => d.GetValue(IsDirtyProperty) as bool?;
public static void SetIsDirty(DependencyObject d, bool? value) => d.SetValue(IsDirtyProperty, value);
public OrderModificationLadderControl() {
InitializeComponent();
DataContextChanged += onDataContextChanged;
}
void onDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) {
if (e.OldValue is IOrderModificationLadderVM oldVm) {
oldVm.RequestRecenter -= recenter;
}
if (e.NewValue is IOrderModificationLadderVM newVm) {
newVm.RequestRecenter += recenter;
}
}
void recenter() {
var targetOffset = priceLevelsScrollViewer.ScrollableHeight / 2;
DoubleAnimation anim = new() {
From = priceLevelsScrollViewer.VerticalOffset,
To = targetOffset,
Duration = TimeSpan.FromSeconds(0.15)
};
Storyboard sb = new();
sb.Children.Add(anim);
Storyboard.SetTarget(anim, priceLevelsScrollViewer);
Storyboard.SetTargetProperty(anim, new PropertyPath(Behavior.AnimatedScrollViewerVerticalOffsetProperty));
sb.Begin();
}
}
}
@@ -0,0 +1,120 @@
<Window
x:Class="mbts.ui.spark.OrderTicketLadder"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:msys="clr-namespace:bts.api;assembly=Bts.Spark.Api"
xmlns:u="clr-namespace:mbts.ui"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
u:WindowStyle.SizeToContent="WidthAndHeight"
u:WindowStyle.TitleContent="{Binding Title, Mode=OneWay}"
BorderBrush="#00C000"
BorderThickness="1"
Style="{StaticResource BTSWindowTitleTemplate}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Resources/Converters.xaml"/>
</ResourceDictionary.MergedDictionaries>
<sys:Double x:Key="HorizontalSpacing">8.0</sys:Double>
<sys:Double x:Key="VerticalSpacing">4.0</sys:Double>
<sys:Double x:Key="ButtonHeight">22.0</sys:Double>
<Style x:Key="SellButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="{u:BTSColor MarketData.ask_dark}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="MinHeight" Value="40" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Text="Sell" />
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Border"
CornerRadius="0"
BorderThickness="1"
BorderBrush="{u:BTSColor UI.Button.normal_border}"
Background="{u:BTSColor MarketData.ask}"
Padding="2"
Height="Auto"
SnapsToDevicePixels="True">
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
Name="button_content"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#99c00000" />
<Setter TargetName="Border" Property="BorderBrush" Value="{u:BTSColor UI.Button.mouseover_border}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="BuyButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="{u:BTSColor MarketData.bid_dark}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="MinHeight" Value="40" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Text="Buy" />
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Border"
CornerRadius="0"
BorderThickness="1"
BorderBrush="{u:BTSColor UI.Button.normal_border}"
Background="{u:BTSColor MarketData.bid}"
Padding="2"
Height="Auto"
SnapsToDevicePixels="True">
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
Name="button_content"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#99167ed3" />
<Setter TargetName="Border" Property="BorderBrush" Value="{u:BTSColor UI.Button.mouseover_border}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Window.Resources>
<Window.InputBindings>
<KeyBinding Key="Esc" Command="{x:Static SystemCommands.CloseWindowCommand}"/>
</Window.InputBindings>
<usp:OrderModificationLadderControl Margin="10,4" />
<u:WindowStyle.TitleContentTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Center" FontWeight="Bold" Text="{Binding}" Style="{StaticResource TextBlock.Dark}" />
</DataTemplate>
</u:WindowStyle.TitleContentTemplate>
</Window>
@@ -0,0 +1,15 @@
using mbts.ui.utility;
using System.Windows;
namespace mbts.ui.spark {
/// <summary>
/// Interaction logic for OrderTicketLadder.xaml
/// </summary>
public partial class OrderTicketLadder : Window {
public OrderTicketLadder() {
InitializeComponent();
WindowUtilities.listen_for_close_requests(this);
}
}
}
@@ -1,117 +1,119 @@
#nullable enable
namespace mbts.ui {
using bts.utils;
using Spark.Core.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Media;
[MarkupExtensionReturnType]
class BTSColorExtension : MarkupExtension {
static readonly Dictionary<string, Func<BTSColor>>
_color_map = _normalized_color_props(typeof(BTSColors), null).ToDictionary();
public BTSColorExtension() { }
public BTSColorExtension(string? name) =>
Name = name;
public BTSColorExtension(string? name, string? mixture) {
Name = name;
Mixture = mixture;
}
[ConstructorArgument("name")]
public string? Name { get; set; }
[ConstructorArgument("mixture")]
public string? Mixture { get; set; }
public ColorMixtureMode MixtureMode { get; set; } = ColorMixtureMode.Default;
public Type? Type { get; set; }
public double Opacity { get; set; } = double.NaN;
public static BTSColor get_color(string name) =>
_color_map.TryGetValue(name, out var color) ?
color() : throw new ColorNotFoundException(name);
public override object? ProvideValue(IServiceProvider service_provider) {
if (service_provider is null) {
return null;
}
if (Name is null) {
throw new InvalidOperationException("Name is null");
}
if (!Opacity.is_nan() && !Opacity.is_within_range(0, 1)) {
throw new InvalidOperationException("Opacity (pct) must between 0 and 1 (if not NaN)");
}
if (Name.Contains("Launcher.Button")) {
}
var pvt = service_provider.GetValueTargetProvider() ??
throw new InvalidOperationException("IProvideValueTarget is null");
var tgt_type = Type ?? _deduce_target_type(pvt);
var names = Name.Split(',');
var mixtures = Mixture?.Split(',');
var color = _get_color_blend(names, mixtures, MixtureMode);
if (!Opacity.is_nan()) {
color = color.with((byte)(Opacity * byte.MaxValue));
}
return tgt_type == typeof(Color) ? (Color)color : (Brush)color;
}
static Type _deduce_target_type(IProvideValueTarget pvt) {
if (pvt.TargetProperty is null) {
return typeof(Color);
}
if (pvt.TargetProperty is DependencyProperty dp) {
return dp.PropertyType ?? throw new InvalidOperationException();
}
if (pvt.TargetObject is Setter setter) {
return setter.Property?.PropertyType ?? typeof(Brush);
}
return typeof(Brush);
}
/// <summary>
/// Creates a (potentially weighted) blend of colors
/// </summary>
static BTSColor _get_color_blend(string[] names, string[]? mixture, ColorMixtureMode mode) {
if (mixture is null || mixture.Length == 0) {
// create a uniform blend
return names.Select(get_color).blend(mode);
}
if (names.Length != mixture.Length) {
throw new InvalidOperationException(
"Names and Mixture must provide same number of elements");
}
return names.Select(get_color).Zip(mixture.Select(double.Parse)).blend();
}
static IEnumerable<(string, Func<BTSColor>)> _normalized_color_props(
Type type, string? prefix) {
var bts_colors = ReflectionHelper.static_property_getters<BTSColor>(type)
.Select(tup => (full_name(tup.name), tup.get));
var media_colors = ReflectionHelper.static_property_getters<Color>(type)
.Select(tup => (full_name(tup.name), new Func<BTSColor>(() => tup.get())));
var nested_types = type.GetNestedTypes(BindingFlags.Public | BindingFlags.Static);
var nested_colors = nested_types.SelectMany(
t => _normalized_color_props(t, full_name(t.Name))).ToList();
return bts_colors
.Concat(media_colors)
.Concat(nested_colors);
string full_name(string prop) =>
prefix is null ? prop : $"{prefix}.{prop}";
}
}
}
#nullable enable
namespace mbts.ui {
using bts.utils;
using Spark.Core.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Media;
[MarkupExtensionReturnType]
class BTSColorExtension : MarkupExtension {
static readonly Dictionary<string, Func<BTSColor>>
_color_map = _normalized_color_props(typeof(BTSColors), null).ToDictionary();
public BTSColorExtension() { }
public BTSColorExtension(string? name) =>
Name = name;
public BTSColorExtension(string? name, string? mixture) {
Name = name;
Mixture = mixture;
}
[ConstructorArgument("name")]
public string? Name { get; set; }
[ConstructorArgument("mixture")]
public string? Mixture { get; set; }
public ColorMixtureMode MixtureMode { get; set; } = ColorMixtureMode.Default;
public Type? Type { get; set; }
public double Opacity { get; set; } = double.NaN;
public static BTSColor get_color(string name) =>
_color_map.TryGetValue(name, out var color) ?
color() : throw new ColorNotFoundException(name);
public override object? ProvideValue(IServiceProvider service_provider) {
if (service_provider is null) {
return null;
}
if (Name is null) {
throw new InvalidOperationException("Name is null");
}
if (!Opacity.is_nan() && !Opacity.is_within_range(0, 1)) {
throw new InvalidOperationException("Opacity (pct) must between 0 and 1 (if not NaN)");
}
if (Name.Contains("Launcher.Button")) {
}
var pvt = service_provider.GetValueTargetProvider() ??
throw new InvalidOperationException("IProvideValueTarget is null");
var tgt_type = Type ?? _deduce_target_type(pvt);
var names = Name.Split(',');
var mixtures = Mixture?.Split(',');
var color = _get_color_blend(names, mixtures, MixtureMode);
if (!Opacity.is_nan()) {
color = color.with((byte)(Opacity * byte.MaxValue));
}
return tgt_type == typeof(Color) || tgt_type == typeof(Color?)
? (Color)color
: (Brush)color;
}
static Type _deduce_target_type(IProvideValueTarget pvt) {
if (pvt.TargetProperty is null) {
return typeof(Color);
}
if (pvt.TargetProperty is DependencyProperty dp) {
return dp.PropertyType ?? throw new InvalidOperationException();
}
if (pvt.TargetObject is Setter setter) {
return setter.Property?.PropertyType ?? typeof(Brush);
}
return typeof(Brush);
}
/// <summary>
/// Creates a (potentially weighted) blend of colors
/// </summary>
static BTSColor _get_color_blend(string[] names, string[]? mixture, ColorMixtureMode mode) {
if (mixture is null || mixture.Length == 0) {
// create a uniform blend
return names.Select(get_color).blend(mode);
}
if (names.Length != mixture.Length) {
throw new InvalidOperationException(
"Names and Mixture must provide same number of elements");
}
return names.Select(get_color).Zip(mixture.Select(double.Parse)).blend();
}
static IEnumerable<(string, Func<BTSColor>)> _normalized_color_props(
Type type, string? prefix) {
var bts_colors = ReflectionHelper.static_property_getters<BTSColor>(type)
.Select(tup => (full_name(tup.name), tup.get));
var media_colors = ReflectionHelper.static_property_getters<Color>(type)
.Select(tup => (full_name(tup.name), new Func<BTSColor>(() => tup.get())));
var nested_types = type.GetNestedTypes(BindingFlags.Public | BindingFlags.Static);
var nested_colors = nested_types.SelectMany(
t => _normalized_color_props(t, full_name(t.Name))).ToList();
return bts_colors
.Concat(media_colors)
.Concat(nested_colors);
string full_name(string prop) =>
prefix is null ? prop : $"{prefix}.{prop}";
}
}
}
@@ -380,7 +380,7 @@
<VisualState Name="Normal"/>
<VisualState Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="{StaticResource u:BTSColor UI.ScrollBar.thumb_mouse_over}"/>
<ColorAnimation Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="{u:BTSColor UI.ScrollBar.thumb_mouse_over}"/>
</Storyboard>
</VisualState>
<VisualState Name="Pressed">
@@ -1,5 +1,5 @@
<ResourceDictionary
x:Class="mbts.ui.spark.AuxiliaryDataResources"
x:Class="mbts.ui.spark.OrderAuxiliaryPopupResources"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -17,7 +17,7 @@
<sp:MiniOrderBookView/>
</DataTemplate>
<!--Header (Title Bar)-->
<DataTemplate DataType="{x:Type usp:AuxiliaryDataHeader}">
<DataTemplate DataType="{x:Type usp:OrderAuxiliaryPopupHeader}">
<Grid MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ContentPresenter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -101,6 +101,20 @@
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type usp:OrderTicketLadderHeader}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock FontWeight="Bold" Text="{Binding Title}" Style="{StaticResource TextBlock.Dark}" />
<CheckBox Grid.Column="2" VerticalAlignment="Center" Margin="4,0,0,0" Focusable="False" IsChecked="{Binding StaysOpen, RelativeSource={RelativeSource AncestorType=Popup}}">Keep Open</CheckBox>
<Button Grid.Column="3" HorizontalAlignment="Right" Margin="4" Command="{x:Static ApplicationCommands.Close}" CommandTarget="{Binding RelativeSource={RelativeSource AncestorType=HeaderedContentControl}}" Style="{StaticResource RoundCloseButton}"/>
</Grid>
</DataTemplate>
<ControlTemplate x:Key="PopupChildTemplate" TargetType="HeaderedContentControl">
<Border
Background="{TemplateBinding Background}"
@@ -0,0 +1,49 @@
#nullable enable
using mbts.spark;
using mbts.ui.shell;
using System.Windows;
using System.Windows.Media;
namespace mbts.ui.spark {
partial class OrderAuxiliaryPopupResources : ResourceDictionary {
public OrderAuxiliaryPopupResources() =>
InitializeComponent();
public static string PopupChildStyleKey => nameof(PopupChildStyleKey);
}
class OrderAuxiliaryPopupHeader : ViewModelBase {
string?
_title,
_context;
Color _title_foreground;
bool _keep_open;
public Color title_foreground { get => _title_foreground; set => Set(ref _title_foreground, value); }
public string? title { get => _title; set => Set(ref _title, value); }
public string? context { get => _context; set => Set(ref _context, value); }
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
}
class OrderTicketHeader : ViewModelBase {
bool _keep_open;
public BBOVM? bbo { get; set; }
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
}
class OrderTicketLadderHeader : ViewModelBase {
bool keepOpen;
string? title;
public string? Title {
get => title;
set => Set(ref title, value);
}
public bool KeepOpen {
get => keepOpen;
set => Set(ref keepOpen, value);
}
}
}
@@ -102,6 +102,10 @@
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type sp:NewOrderTicketLadderVM}">
<usp:OrderModificationLadderControl />
</DataTemplate>
<DataTemplate DataType="{x:Type sp:ExistingOrderTicketVM}">
<Grid Margin="10">
<Grid.RowDefinitions>
@@ -1,36 +0,0 @@
using mbts.spark;
using mbts.ui.shell;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace mbts.ui.spark {
partial class AuxiliaryDataResources : ResourceDictionary {
public AuxiliaryDataResources() =>
InitializeComponent();
public static string PopupChildStyleKey => nameof(PopupChildStyleKey);
}
class AuxiliaryDataHeader : ViewModelBase {
string
_title,
_context;
Color _title_foreground;
bool _keep_open;
public Color title_foreground { get => _title_foreground; set => Set(ref _title_foreground, value); }
public string title { get => _title; set => Set(ref _title, value); }
public string context { get => _context; set => Set(ref _context, value); }
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
}
class OrderTicketHeader : ViewModelBase {
bool _keep_open;
public BBOVM bbo { get; set; }
public bool keep_open { get => _keep_open; set => Set(ref _keep_open, value); }
}
class ConstrainedTextBlock : TextBlock {
}
}
@@ -26,6 +26,39 @@ namespace mbts.ui {
public static void SetDoubleClickParameter(DependencyObject target, object value) { target.SetValue(DoubleClickParameterProperty, value); }
#endregion
#region MiddleClick
public static readonly DependencyProperty MiddleClickProperty =
DependencyProperty.RegisterAttached("MiddleClick", typeof(ICommand), typeof(AttachedCommands), new PropertyMetadata(onMiddleClickChanged));
static void onMiddleClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var el = (UIElement)d;
el.ClearMouseBindings(MouseAction.MiddleClick);
if (e.NewValue is ICommand cmd) {
var mb = new MouseBinding(cmd, new MouseGesture(MouseAction.MiddleClick));
var bd = new Binding { Path = new PropertyPath(MiddleClickParameterProperty), Source = d };
BindingOperations.SetBinding(mb, InputBinding.CommandParameterProperty, bd);
el.InputBindings.Add(mb);
}
}
public static ICommand GetMiddleClick(DependencyObject target) =>
target.GetValue(MiddleClickProperty) as ICommand;
public static void SetMiddleClick(DependencyObject target, ICommand value) =>
target.SetValue(MiddleClickProperty, value);
#endregion
#region MiddleClickParameter
public static readonly DependencyProperty MiddleClickParameterProperty =
DependencyProperty.RegisterAttached("MiddleClickParameter", typeof(object), typeof(AttachedCommands));
public static object GetMiddleClickParameter(DependencyObject target) =>
target.GetValue(MiddleClickParameterProperty);
public static void SetMiddleClickParameter(DependencyObject target, object value) =>
target.SetValue(MiddleClickParameterProperty, value);
#endregion
#region Enter
public static readonly DependencyProperty EnterProperty = DependencyProperty.RegisterAttached("Enter", typeof(ICommand), typeof(AttachedCommands), new PropertyMetadata(OnEnterChanged));
static void OnEnterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
@@ -8,6 +8,7 @@ using bts.api.core;
using bts.api.orderman;
using bts.api.position;
using bts.co2;
using bts.metrics;
using bts.md;
using Microsoft.Extensions.Configuration;
@@ -86,6 +87,7 @@ public static class SparkApiServices {
services.AddSparkdService<OrderService>();
services.AddSingleton<IOrdersClient, OrdersClient>();
}
services.AddSparkdService<ShellLatencyService>();
if (configurationSection.GetValue("EnablePosition", true)) {
services.TryAddSingleton<ISecurityResolver>(ActivatorUtilities.GetServiceOrCreateInstance<PassiveSecurityResolver>);
services.AddSparkdService<PositionService>()
@@ -176,6 +178,7 @@ public static class SparkApiServices {
services.AddSparkdService<OrderService>();
services.AddSingleton<IOrdersClient, OrdersClient>();
}
services.AddSparkdService<ShellLatencyService>();
if (isPositionsEnabled) {
services.AddSparkdService<PositionService>();
}
@@ -3,6 +3,7 @@ using bts.api;
using bts.data.ticks;
using bts.utils;
using mbts.co2;
using mbts.user.settings;
using Spark.Core;
@@ -113,6 +114,24 @@ namespace mbts.spark {
return rv;
}
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double low = bids[bids.Length - 1].price;
if (double.IsNaN(low))
low = bids.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(low))
low = asks[0].price;
return low;
}
public static double high_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double high = asks[asks.Length - 1].price;
if (double.IsNaN(high))
high = asks.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(high))
high = bids[0].price;
return high;
}
public static long? tickdiff(this ContractSpecificationImmutable spec, double x, double y) =>
get_regional_ticks(spec).tickdiff(x, y);
@@ -3,6 +3,7 @@ namespace Bts.Spark.Sdk;
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.metrics;
using global::Spark.Api.Admin;
using global::Spark.Api.MarketData;
@@ -28,6 +29,7 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
readonly IOrdersClient ordersClient;
readonly IAdminClient adminClient;
readonly ISecurityResolver securityResolver;
readonly ShellLatencyService shellLatencyService;
readonly ISparkLoginService loginService;
readonly SimpleSparkClientConfig config;
readonly ILogger<AuthenticatedSimpleSparkClient> logger;
@@ -39,6 +41,7 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
IOrdersClient ordersClient,
IAdminClient adminClient,
ISecurityResolver securityResolver,
ShellLatencyService shellLatencyService,
ISparkLoginService loginService,
IOptions<SimpleSparkClientConfig> config,
ILogger<AuthenticatedSimpleSparkClient> logger) {
@@ -46,6 +49,7 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
this.ordersClient = ordersClient;
this.adminClient = adminClient;
this.securityResolver = securityResolver;
this.shellLatencyService = shellLatencyService;
this.loginService = loginService;
this.config = config.Value;
this.logger = logger;
@@ -61,7 +65,7 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
}
cancellationToken.ThrowIfCancellationRequested();
var user = loginRes.user.Value;
simpleClient = new(user.name, user.id, mdClient, ordersClient, adminClient, securityResolver, config.OrderPoolCapacity, config.OrderPoolReplenishInterval);
simpleClient = new(user.name, user.id, mdClient, ordersClient, shellLatencyService, adminClient, securityResolver, config.OrderPoolCapacity, config.OrderPoolReplenishInterval);
simpleClientSubject.OnNext(simpleClient);
logger.LogInformation("SimpleSparkClient started");
}
@@ -97,6 +101,9 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
public IObservable<TableUpdate<PriceTicker, uint>> PriceTicker(SecurityCriteria? securityCriteria) =>
simpleClient?.PriceTicker(securityCriteria) ?? simpleClientSubject.Take(1).Select(c => c.PriceTicker(securityCriteria)).FlattenSeries();
public Task<bool> PingAsync() =>
simpleClient?.PingAsync() ?? pingBeforeStartAsync();
public Task<Response> CreateOrder(OrderDefinition orderDef, double quantity, double price) =>
orderOp(cli => cli.CreateOrder(orderDef, quantity, price));
@@ -134,4 +141,15 @@ public class AuthenticatedSimpleSparkClient : ISimpleSparkClient, IHostedService
async Task<T> LateImpl() =>
await op(await simpleClientSubject.FirstAsync());
}
async Task<bool> pingBeforeStartAsync() {
var request = new BufferMessage() {
Sequence = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Data = "spark-sdk-ping"
};
var result = await shellLatencyService.ping_async(request);
return result.success &&
result.value.Sequence == request.Sequence &&
result.value.Data == request.Data;
}
}
@@ -48,6 +48,9 @@ public class ExecutionFacadeSimpleSparkClient : ISimpleSparkClient {
public IObservable<TableUpdate<PriceTicker, uint>> PriceTicker(SecurityCriteria? securityCriteria) =>
sparkClient.PriceTicker(securityCriteria);
public Task<bool> PingAsync() =>
sparkClient.PingAsync();
public Task<Response> CreateOrder(OrderDefinition orderDef, double quantity, double price) {
logger.LogInformation("CreateOrder {account} {tag} {securityId} {side} {quantity} @ {price}", orderDef.Account, orderDef.Tag, orderDef.SecurityKey, orderDef.Side, quantity, price);
return Task.FromResult(facadeResponse());
@@ -12,6 +12,8 @@ using mbts.spark;
using System;
public interface ISimpleSparkClient {
Task<bool> PingAsync();
// Market Data
IObservable<TableUpdate<SecurityImmutable, uint>> Securities(SecurityCriteria? securityCriteria);
IObservable<TableUpdate<BookRecord, uint>> Books(SecurityCriteria? securityCriteria);
+15
View File
@@ -16,12 +16,14 @@ using global::Spark.Core.Disposables;
using global::Spark.Core.Reactive;
using mbts.spark;
using bts.metrics;
using System.Reactive.Linq;
public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
readonly IMarketDataClient mdClient;
readonly IOrdersClient ordersClient;
readonly ShellLatencyService shellLatencyService;
readonly OrderPool limitOrderPool;
readonly OrderPool marketOrderPool;
readonly TableUpdateDemuxer<OrderRecordTupleWithRoot, uint, uint> orderSecurityDmx;
@@ -46,6 +48,7 @@ public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
uint userId,
IMarketDataClient mdClient,
IOrdersClient ordersClient,
ShellLatencyService shellLatencyService,
IAdminClient adminClient,
ISecurityResolver securityResolver,
int orderPoolCapacity,
@@ -53,6 +56,7 @@ public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
) {
this.mdClient = mdClient;
this.ordersClient = ordersClient;
this.shellLatencyService = shellLatencyService;
limitOrderPool = new(orderPoolCapacity, () => new() { User = userName, UserId = userId, TimeInForce = TimeInForce.Gtc, CustomDef = new() { Limit = new() } });
marketOrderPool = new(orderPoolCapacity, () => new() { User = userName, UserId = userId, TimeInForce = TimeInForce.Gtc, CustomDef = new() { Market = new() } });
orderSecurityDmx = new TableUpdateDemuxer<OrderRecordTupleWithRoot, uint, uint>(o => o.order.order.Key, o => o.security_id);
@@ -140,6 +144,17 @@ public class SimpleSparkClient : ISimpleSparkClient, IDisposable {
}
}
public async Task<bool> PingAsync() {
var request = new BufferMessage() {
Sequence = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Data = "spark-sdk-ping"
};
var result = await shellLatencyService.ping_async(request);
return result.success &&
result.value.Sequence == request.Sequence &&
result.value.Data == request.Data;
}
public IObservable<TableUpdate<BookRecord, uint>> Books(SecurityCriteria? securityCriteria) {
var booksSource = this.booksSource;
var secFilter = securityCriteria?.ToPredicate();
@@ -5,9 +5,15 @@ FillsSoftCleanup {
}
OrdersSoftCleanup {
value {
enabled: true
}
value {
enabled: true
}
}
Mdp3OptionExpirationCutoff {
value {
max_days_until_expiration: 14
}
}
PlacementInQueue {
value{
@@ -32,11 +32,17 @@ message SoundNotifications {
bool enabled = 1;
}
message FilterExpiredFills {
bool enabled = 1;
}
message VolumeAtPrice {
message FilterExpiredFills {
bool enabled = 1;
}
// MDP3 will not publish CME options whose maturity is beyond this many days.
// A value of zero disables this cutoff.
message Mdp3OptionExpirationCutoff {
uint32 max_days_until_expiration = 1;
}
message VolumeAtPrice {
bool enabled = 1;
}
@@ -124,3 +130,8 @@ message VolSurfaceUsageUnit{
message VolSurfaceUsage{
repeated VolSurfaceUsageUnit items = 1;
}
message LogTrades{
string sec_name = 1;
bool log = 2;
}
@@ -1,4 +1,5 @@
#include "spark/logging/logging.h"
#include "spark/md/log_messages/trade_latency_log.h"
#ifndef NO_CPP0X
#include "MDProducerHandle.h"
#include "common.h"
@@ -133,6 +134,10 @@ void MDProducerHandle::add_trade(
trade.set_key(key());
_channel.add_trade(trade);
}
log_trade_latency(
TradeLatencyStage::SHM_WRITTEN, security(), tradetime, price, agside,
volume, from_spread);
}
void MDProducerHandle::set_last_trade(
TimeStamp time,
@@ -0,0 +1,124 @@
#pragma once
#include "spark/config/config.h"
#include "spark/config/protos/toggles.pb.h"
#include "spark/core/FMT_IMPL.h"
#include "spark/core/time_stamp.h"
#include "spark/logging/logging.h"
#include "spark/md/PriceSize.h"
#include "spark/md/Shm/FixedSecurity.h"
#include "spark/md/protos/aggressor_side.h"
#include "spark/utils/datetime.h"
#include <string>
#include <cstdint>
#include <utility>
namespace bts::md {
enum class TradeLatencyStage {
MDP3_RECEIVED,
SHM_WRITTEN,
};
inline const char *TradeLatencyStage_Name(TradeLatencyStage stage) {
switch (stage) {
case TradeLatencyStage::MDP3_RECEIVED:
return "mdp3_received";
case TradeLatencyStage::SHM_WRITTEN:
return "shm_written";
}
return "unknown";
}
inline std::string format_ll_datetime(datetime::LLDateTime ts) {
return datetime::iso_date_time_string(datetime::ll_to_ptime(ts));
}
inline bool should_log_trade_latency(const FixedSecurity &security) {
const auto &cfg = config::get<config::toggles::LogTrades>();
if (!cfg.log() || cfg.sec_name().empty()) {
return false;
}
return cfg.sec_name() == security.security_desc() ||
cfg.sec_name() == security.symbol();
}
struct TradeLatencyLog {
TradeLatencyStage stage;
std::string sec_name;
uint32_t security_key;
int64_t trade_ts_unix_ns;
std::string trade_ts_iso;
int64_t log_ts_unix_ns;
std::string log_ts_iso;
uint64_t monotonic_log_ts_ns;
int64_t exchange_to_log_us;
double price;
double quantity;
std::string aggressor_side;
uint32_t total_volume;
bool from_spread;
TradeLatencyLog(
TradeLatencyStage stage,
const FixedSecurity &security,
datetime::LLDateTime trade_ts,
core::TimeStamp log_ts,
utils::TimeStamp monotonic_log_ts,
const PriceSize &price_size,
co2::AggressorSide aggressor_side,
uint32_t total_volume,
bool from_spread)
: stage(stage), sec_name(security.security_desc()),
security_key(security.key), trade_ts_unix_ns(trade_ts),
trade_ts_iso(format_ll_datetime(trade_ts)),
log_ts_unix_ns(log_ts.nanos()), log_ts_iso(log_ts.to_iso_string()),
monotonic_log_ts_ns(monotonic_log_ts.to_uint64()),
exchange_to_log_us(
(log_ts - core::TimeStamp::from_ptime(datetime::ll_to_ptime(trade_ts)))
.total_micros()),
price(price_size.price()), quantity(price_size.size()),
aggressor_side(co2::AggressorSide_Name(aggressor_side)),
total_volume(total_volume), from_spread(from_spread) {}
};
inline void log_trade_latency(
TradeLatencyStage stage,
const FixedSecurity &security,
datetime::LLDateTime trade_ts,
const PriceSize &price_size,
co2::AggressorSide aggressor_side,
uint32_t total_volume,
bool from_spread) {
if (!should_log_trade_latency(security)) {
return;
}
const auto log_ts = core::TimeStamp::now();
const auto monotonic_log_ts = utils::TimeStamp::now();
LOG(md, Info, "[trade_latency.json]{}",
TradeLatencyLog(
stage, security, trade_ts, log_ts, monotonic_log_ts, price_size,
aggressor_side, total_volume, from_spread));
}
} // namespace bts::md
FMT_IMPL(
bts::md::TradeLatencyLog,
l,
R"({{ "stage": "{}", "sec": "{}", "key": {}, "tradeTsUnixNs": {}, "tradeTsIso": "{}", "logTsUnixNs": {}, "logTsIso": "{}", "logMonoNs": {}, "exchangeToLogUs": {}, "price": {}, "qty": {}, "agg": "{}", "totalVolume": {}, "fromSpread": {} }})",
bts::md::TradeLatencyStage_Name(l.stage),
l.sec_name,
l.security_key,
l.trade_ts_unix_ns,
l.trade_ts_iso,
l.log_ts_unix_ns,
l.log_ts_iso,
l.monotonic_log_ts_ns,
l.exchange_to_log_us,
l.price,
l.quantity,
l.aggressor_side,
l.total_volume,
l.from_spread);
@@ -23,6 +23,7 @@
#include "spark/utils/overloaded.h"
#include <boost/date_time/posix_time/posix_time.hpp>
#include <cctype>
#include <google/protobuf/timestamp.pb.h>
#include <string>
#include <sys/time.h>
@@ -308,15 +309,60 @@ void FillStore::on_execution_report(
auto exchange_ts = std::visit(
overloaded{
[](const orderman::ExchangeTimeStampStr &ts) {
return utils::pb::to_timestamp(
[&](const orderman::ExchangeTimeStampStr &ts) {
auto gts = utils::pb::to_timestamp(
datetime::parse_FIX_timestamp(to_str(ts)));
const std::string ts_str = to_str(ts);
try {
const auto dot_pos = ts_str.find('.');
if (dot_pos != std::string::npos) {
std::string frac_digits;
for (std::size_t i = dot_pos + 1;
i < ts_str.size() &&
std::isdigit(static_cast<unsigned char>(
ts_str[i]));
++i) {
frac_digits.push_back(ts_str[i]);
}
if (!frac_digits.empty()) {
if (frac_digits.size() > 9) {
frac_digits.resize(9);
}
while (frac_digits.size() < 9) {
frac_digits.push_back('0');
}
gts.set_nanos(
static_cast<int32_t>(
std::stoul(frac_digits)));
}
}
} catch (const std::exception &e) {
WARN_FN(
"on_execution_report",
"fill {} transact_time(str) raw='{}' "
"failed to inspect fractional nanos: {}",
fk, ts_str, std::string(e.what()));
}
INFO_FN(
"on_execution_report",
"fill {} transact_time(str) raw='{}' "
"converted_seconds={} converted_nanos={}",
fk, ts_str, gts.seconds(), gts.nanos());
return gts;
},
[](ExchangeTimeStamp64 ts) {
[&](ExchangeTimeStamp64 ts) {
google::protobuf::Timestamp gts;
auto seconds = ts.value() / 1E9;
static constexpr std::uint64_t nanos_per_second =
1'000'000'000ULL;
auto seconds = ts.value() / nanos_per_second;
gts.set_seconds(seconds);
gts.set_nanos(ts.value() - (seconds * 1E9));
gts.set_nanos(ts.value() % nanos_per_second);
INFO_FN(
"on_execution_report",
"fill {} transact_time(u64) raw_ns={} "
"converted_seconds={} converted_nanos={}",
fk, ts.value(), gts.seconds(), gts.nanos());
return gts;
}},
er.transact_time);
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import argparse
import csv
import math
from collections import defaultdict
from pathlib import Path
PAIR_KEY_FIELDS = (
"sec",
"key",
"tradeTsUnixNs",
"price",
"qty",
"agg",
"totalVolume",
"fromSpread",
)
PERCENTILES = (50, 90, 95, 99)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Print simple stats for trade latency CSV output."
)
parser.add_argument("csv_file", help="CSV file created by trade_latency_to_csv.py")
return parser.parse_args()
def to_int(row: dict, key: str) -> int:
value = row.get(key, "")
return int(value) if value not in ("", None) else 0
def percentile(values: list[float], pct: int) -> float:
if not values:
return float("nan")
if len(values) == 1:
return values[0]
rank = (len(values) - 1) * (pct / 100.0)
lower = math.floor(rank)
upper = math.ceil(rank)
if lower == upper:
return values[lower]
weight = rank - lower
return values[lower] * (1 - weight) + values[upper] * weight
def print_stats(title: str, values: list[float], unit: str) -> None:
values = sorted(values)
if not values:
print(f"{title}: no data")
return
print(f"{title}:")
print(f" count={len(values)}")
print(f" avg={sum(values) / len(values):.3f} {unit}")
print(f" min={values[0]:.3f} {unit}")
print(f" max={values[-1]:.3f} {unit}")
for pct in PERCENTILES:
print(f" p{pct}={percentile(values, pct):.3f} {unit}")
def build_pair_key(row: dict) -> tuple[str, ...]:
return tuple(str(row.get(field, "")) for field in PAIR_KEY_FIELDS)
def main() -> int:
args = parse_args()
csv_path = Path(args.csv_file)
rows = []
with csv_path.open("r", encoding="utf-8", newline="") as infile:
reader = csv.DictReader(infile)
for row in reader:
rows.append(row)
by_stage = defaultdict(list)
grouped = defaultdict(lambda: defaultdict(list))
for row in rows:
exchange_to_log_us = float(row["exchangeToLogUs"])
by_stage[row["stage"]].append(exchange_to_log_us)
grouped[build_pair_key(row)][row["stage"]].append(row)
print_stats("mdp3_received exchange_to_log_us", by_stage["mdp3_received"], "us")
print()
print_stats("shm_written exchange_to_log_us", by_stage["shm_written"], "us")
pipeline_us = []
for stages in grouped.values():
received = sorted(stages.get("mdp3_received", []), key=lambda r: to_int(r, "logMonoNs"))
written = sorted(stages.get("shm_written", []), key=lambda r: to_int(r, "logMonoNs"))
for recv_row, write_row in zip(received, written):
delta_us = (to_int(write_row, "logMonoNs") - to_int(recv_row, "logMonoNs")) / 1000.0
pipeline_us.append(delta_us)
print()
print_stats("pipeline mdp3_received_to_shm_written", pipeline_us, "us")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
import argparse
import csv
import json
from pathlib import Path
CSV_FIELDS = [
"line_ts",
"line_mono_ns",
"stage",
"sec",
"key",
"tradeTsUnixNs",
"tradeTsIso",
"logTsUnixNs",
"logTsIso",
"logMonoNs",
"exchangeToLogUs",
"price",
"qty",
"agg",
"totalVolume",
"fromSpread",
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Extract [trade_latency.json] log lines into CSV."
)
parser.add_argument("log_file", help="Path to the spark log file")
parser.add_argument(
"-o",
"--output",
help="Output CSV path. Defaults to <log_file>.trade_latency.csv",
)
return parser.parse_args()
def extract_json_payload(line: str) -> dict | None:
marker = "[trade_latency.json]"
idx = line.find(marker)
if idx < 0:
return None
payload = line[idx + len(marker) :].strip()
try:
return json.loads(payload)
except json.JSONDecodeError:
return None
def extract_line_columns(line: str) -> tuple[str, str]:
parts = line.rstrip("\n").split("|")
line_ts = parts[0].strip() if len(parts) > 0 else ""
line_mono_ns = parts[1].strip() if len(parts) > 1 else ""
return line_ts, line_mono_ns
def build_row(line: str, payload: dict) -> dict:
line_ts, line_mono_ns = extract_line_columns(line)
row = {
"line_ts": line_ts,
"line_mono_ns": line_mono_ns,
}
for field in CSV_FIELDS[2:]:
row[field] = payload.get(field, "")
return row
def main() -> int:
args = parse_args()
log_path = Path(args.log_file)
if args.output:
out_path = Path(args.output)
else:
suffix = log_path.suffix if log_path.suffix else ".log"
out_path = log_path.with_suffix(suffix + ".trade_latency.csv")
rows = []
with log_path.open("r", encoding="utf-8", errors="replace") as infile:
for line in infile:
payload = extract_json_payload(line)
if payload is None:
continue
rows.append(build_row(line, payload))
with out_path.open("w", newline="", encoding="utf-8") as outfile:
writer = csv.DictWriter(outfile, fieldnames=CSV_FIELDS)
writer.writeheader()
writer.writerows(rows)
print(f"wrote {len(rows)} rows to {out_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())