Compare commits

..
104 changed files with 2015 additions and 2750 deletions

No files matched your search

+1
View File
@@ -18,6 +18,7 @@ src/client/spark/Properties/launchSettings.json
compile_commands.json
.cquery_cached_index/
src/client/sparkcmd/Properties/launchSettings.json
src/sdk/examples/client_sdk/bts.api.csharp/src/Properties/launchSettings.json
node_modules
paket-files
project.lock.json
+1 -1
View File
@@ -55,7 +55,7 @@ else ()
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/web ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/data_files ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${CMAKE_BINARY_DIR}/src/lib/libspark.so ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.16.0.0 ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libOnixS.IceImpactMulticastPriceFeedHandlerCpp.so.3.13.0.0 ${CMAKE_BINARY_DIR}/server
COMMAND ln ARGS -sf ${EXTERNALS_ROOT}/lib/Release/libACE-5.7.5.so ${CMAKE_BINARY_DIR}/server
COMMAND mkdir ARGS -p ${CMAKE_BINARY_DIR}/server/plugins
COMMENT "Creating server runtime layout"
@@ -17,7 +17,7 @@ namespace bts.shell {
class MarketShellModule : IShellModule {
[ImportingConstructor]
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model) {
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub, orders_model.security_auxiliary_model);
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub);
_mvf = new MarketViewWindowFactory(this, bus, settings_hub, orders_model);
}
@@ -1,12 +1,10 @@
using bts.api;
using bts.api.orderman;
using bts.collections;
using bts.utils;
using mbts.co2;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
@@ -26,6 +24,7 @@ namespace mbts.spark {
readonly AnimationManager _animation = new AnimationManager();
readonly ObservableCollection<OrderRecordImmutable> _orders = new ObservableCollection<OrderRecordImmutable>();
readonly ObservableFilterCollection<OrderRecordImmutable> _working_orders, _working_bids, _working_asks;
readonly string _security_text, _security_key_text;
readonly Property<double> _bid_price, _ask_price, _last_price, _day_change, _settlement, _session_high, _session_low;
readonly Property<bool> _last_price_from_spread;
readonly CellProperty<int> _position;
@@ -34,51 +33,48 @@ namespace mbts.spark {
readonly CellProperty<bool> _inside_bids, _inside_asks;
readonly CellProperty<int> _working_bid_volume, _working_ask_volume;
readonly CellProperty<double> _working_best_bid, _working_best_ask;
readonly CellProperty<string> _working_bids_text, _working_asks_text;
readonly MyOrderSummarizer _order_summarizer = new MyOrderSummarizer();
readonly MDDisplayHelper.SizeFormatter _size_formatter = MDDisplayHelper.get_formatter(MarketSizeDisplay.get_default());
PropertyAutoWrapper<uint, uint>
_bid_direct_size,
_bid_implied_size,
_bid_merged_size,
_ask_direct_size,
_ask_implied_size,
_ask_merged_size;
Property<uint> _bid_direct_size, _bid_merged_size, _ask_direct_size, _ask_merged_size;
double _raw_day_position;
string _bid_merged_size_text, _ask_merged_size_text;
ulong _bbo_ref, _last_ref;
uint? _display_qty_divisor_backing;
public Row(IBus bus, SecurityMD smd, OrdersModel orders_model) : base(smd) {
_bus = bus;
_orders_model = orders_model;
_security_text = smd.security.ToString();
_security_key_text = security_key.ToString();
string format_price(double price) => security.ContractSpec.format(price);
_position = new CellProperty<int>(this, nameof(position), fmt_display_qty_int);
_position = new CellProperty<int>(this, nameof(position), 0);
_bid_price = backing(nameof(bid_price), double.NaN);
_ask_price = backing(nameof(ask_price), double.NaN);
_last_price = backing(nameof(last_price), double.NaN);
_last_size = new CellProperty<uint>(this, nameof(last_size), fmt_display_qty_uint);
_last_size = new CellProperty<uint>(this, nameof(last_size), 0);
_day_change = backing(nameof(day_change), double.NaN);
_session_low = backing(nameof(session_low), double.NaN);
_session_high = backing(nameof(session_high), double.NaN);
_last_price_from_spread = backing(nameof(last_price_from_spread), false);
_settlement = backing(nameof(settlement), double.NaN);
_volume = new CellProperty<ulong>(this, nameof(volume), fmt_display_qty_ulong, 0);
_volume = new CellProperty<ulong>(this, nameof(volume), 0);
_inside_bids = new CellProperty<bool>(this, nameof(inside_bids));
_inside_asks = new CellProperty<bool>(this, nameof(inside_asks));
_working_bid_volume = new CellProperty<int>(this, nameof(working_bid_volume));
_working_ask_volume = new CellProperty<int>(this, nameof(working_ask_volume));
//
_bid_direct_size = backing(nameof(bid_direct_size), 0u, convert_display_qty_uint);
_bid_implied_size = backing(nameof(bid_implied_size), 0u, convert_display_qty_uint);
_bid_merged_size = backing(nameof(bid_merged_size), 0u, convert_display_qty_uint);
_ask_direct_size = backing(nameof(ask_direct_size), 0u, convert_display_qty_uint);
_ask_implied_size = backing(nameof(ask_implied_size), 0u, convert_display_qty_uint);
_ask_merged_size = backing(nameof(ask_merged_size), 0u, convert_display_qty_uint);
_bid_direct_size = backing(nameof(bid_direct_size), default(uint));
_bid_merged_size = backing(nameof(bid_merged_size), default(uint));
_ask_direct_size = backing(nameof(ask_direct_size), default(uint));
_ask_merged_size = backing(nameof(ask_merged_size), default(uint));
//
_working_orders = new ObservableFilterCollection<OrderRecordImmutable>(_orders, o => o.is_active());
_working_bids = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Buy);
_working_asks = new ObservableFilterCollection<OrderRecordImmutable>(_working_orders, o => o.Def.Side == Side.Sell);
_working_best_bid = new CellProperty<double>(this, nameof(working_best_bid), format_price, double.NaN);
_working_best_ask = new CellProperty<double>(this, nameof(working_best_ask), format_price, double.NaN);
_working_bids_text = new CellProperty<string>(this, nameof(working_bids_text), string.Empty);
_working_asks_text = new CellProperty<string>(this, nameof(working_asks_text), string.Empty);
//
default_command = new DelegateCommand(_show_ladder);
bid_price_click = new DelegateCommand(() => _show_order_ticket(MarketViewView.Columns.Bid));
@@ -94,45 +90,17 @@ namespace mbts.spark {
_animation.AnimationsChanged += on_animations_changed;
void on_animations_changed() => RaisePropertyChanged(nameof(animations));
_dispose = Disposable.Create(
attach_prop_changed_cb(_bid_direct_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_direct_size_text))),
attach_prop_changed_cb(_bid_implied_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_implied_size_text))),
attach_prop_changed_cb(_bid_merged_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(bid_merged_size_text))),
attach_prop_changed_cb(_ask_direct_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_direct_size_text))),
attach_prop_changed_cb(_ask_implied_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_implied_size_text))),
attach_prop_changed_cb(_ask_merged_size, mk_echo_prop_changed_cb<(uint,uint)>(nameof(ask_merged_size_text))),
_orders_model.on_security_root_orders(security.Id).OnDispatcher().Subscribe(_on_orders),
_position.OnDispatcher().Subscribe(v => _animation.animate_position(v.newval > v.oldval)),
_orders_model.on_security_positions(security.Id).OnDispatcher().Subscribe(t => {
if (Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position))) {
RaisePropertyChanged(nameof(raw_day_position_text));
if (Set(ref _raw_day_position, t.state.Values.Sum(p => p.Item2.raw_day_pos()), nameof(raw_day_position)))
raise_invalidated();
}
}),
_inside_bids.OnDispatcher().Subscribe(v => _animation.animate_working_bids_inside(v.newval)),
_inside_asks.OnDispatcher().Subscribe(v => _animation.animate_working_asks_inside(v.newval)),
_day_change.OnDispatcher().Subscribe(v => _animation.animate_day_change(v.newval > v.oldval)),
Disposable.Create(() => _animation.AnimationsChanged -= on_animations_changed)
);
uint convert_display_qty_uint(uint value) =>
value.safe_divide(_display_qty_divisor);
string fmt_display_qty_int(int value) =>
value.safe_divide(_display_qty_divisor).ToString();
string fmt_display_qty_uint(uint value) =>
value.safe_divide(_display_qty_divisor).ToString();
string fmt_display_qty_ulong(ulong value) =>
value.safe_divide(_display_qty_divisor).ToString();
PropertyChange<T> mk_echo_prop_changed_cb<T>(params string[] props) =>
(oldval, newval) => RaisePropertyChanged(props);
IDisposable attach_prop_changed_cb<TView, TModel>(PropertyAutoWrapper<TModel, TView> prop, PropertyChange<(TModel, TView)> callback) {
prop.changed += callback;
return Disposable.Create(() => prop.changed -= callback);
}
}
public event Action invalidated;
@@ -141,63 +109,32 @@ namespace mbts.spark {
public bool inside_bids => _inside_bids.value;
public bool inside_asks => _inside_asks.value;
public int position => _position.value;
public string position_text => _position.text;
public double raw_day_position => _raw_day_position;
public string raw_day_position_text => SparkUtils.fmt_position(_raw_day_position.safe_divide(_display_qty_divisor));
public double last_price => _last_price.Value;
public bool last_price_from_spread => _last_price_from_spread.Value;
public uint last_size => _last_size.value;
public string last_size_text => _last_size.text;
// bid price/size
public double bid_price => _bid_price.Value;
public uint bid_direct_size => _bid_direct_size.model;
public uint bid_implied_size => _bid_merged_size.model - _bid_direct_size.model;
public uint bid_merged_size => _bid_merged_size.model;
// bid size text
public string bid_direct_size_text => _bid_direct_size.view.ToString();
public string bid_implied_size_text => _bid_implied_size.view.ToString();
public string bid_merged_size_text => _bid_merged_size.view.ToString();
// ask price/size
public uint bid_direct_size => _bid_direct_size.Value;
public uint bid_implied_size => _bid_merged_size.Value - _bid_direct_size.Value;
public uint bid_merged_size => _bid_merged_size.Value;
public string bid_merged_size_text => _bid_merged_size_text ?? (_bid_merged_size_text = _size_formatter(_bid_merged_size.Value, bid_implied_size));
public double ask_price => _ask_price.Value;
public uint ask_direct_size => _ask_direct_size.model;
public uint ask_implied_size => _ask_implied_size.model;
public uint ask_merged_size => _ask_merged_size.model;
// ask size text
public string ask_direct_size_text => _ask_direct_size.view.ToString();
public string ask_implied_size_text => _ask_implied_size.view.ToString();
public string ask_merged_size_text => _ask_merged_size.view.ToString();
//
public uint ask_direct_size => _ask_direct_size.Value;
public uint ask_implied_size => _ask_merged_size.Value - _ask_direct_size.Value;
public uint ask_merged_size => _ask_merged_size.Value;
public string ask_merged_size_text => _ask_merged_size_text ?? (_ask_merged_size_text = _size_formatter(_ask_merged_size.Value, ask_implied_size));
public ulong volume => _volume.value;
public string volume_text => _volume.text;
public double settlement => _settlement.Value;
public double day_change => _day_change.Value;
public double session_low => _session_low.Value;
public double session_high => _session_high.Value;
public int working_bid_volume => _working_bid_volume.value;
public int working_ask_volume => _working_ask_volume.value;
public string working_bids_text => OrderUtils.order_collection_summary(
_order_summarizer.best_working_bid,
_order_summarizer.best_working_bid_qty.safe_divide(_display_qty_divisor),
_order_summarizer.working_bid_qty.safe_divide(_display_qty_divisor),
security.ContractSpec);
public string working_asks_text => OrderUtils.order_collection_summary(
_order_summarizer.best_working_ask,
_order_summarizer.best_working_ask_qty.safe_divide(_display_qty_divisor),
_order_summarizer.working_ask_qty.safe_divide(_display_qty_divisor),
security.ContractSpec);
public string working_bids_text => OrderUtils.order_collection_summary(_order_summarizer.best_working_bid, _order_summarizer.best_working_bid_qty, _order_summarizer.working_bid_qty, security.ContractSpec);
public string working_asks_text => OrderUtils.order_collection_summary(_order_summarizer.best_working_ask, _order_summarizer.best_working_ask_qty, _order_summarizer.working_ask_qty, security.ContractSpec);
public double working_best_bid => _working_best_bid.value;
public double working_best_ask => _working_best_ask.value;
// ice props
public string market_description => security.market_description();
public string cleared_alias => security.cleared_alias();
public string product_description => security.product_name();
public string hub_alias => security.hub_alias();
public string strip_name => security.strip_name();
public string mic_code => security.mic_code();
// animations
public IList<ui.spark.CellAnimation> animations => _animation.get_animations().ToList();
public ObservableCollection<RowAnimation> row_animations { get; } = new ObservableCollection<RowAnimation>();
// commands
public ICommand bid_price_click { get; }
public ICommand bid_merged_size_click { get; }
public ICommand bid_direct_size_click { get; }
@@ -209,30 +146,27 @@ namespace mbts.spark {
public ICommand ask_implied_size_click { get; }
public ICommand working_asks_click { get; }
public ICommand default_command { get; }
public string market_description {get;}
public string cleared_alias { get; }
public string product_description { get; }
public string hub_alias { get; }
public string strip_name{ get; }
public string mic_code { get; }
public ObservableCollection<RowAnimation> row_animations { get; } = new ObservableCollection<RowAnimation>();
public IReadOnlyList<DIYGrid.IGridCellStore> cell_stores => new DIYGrid.IGridCellStore[0];
#endregion
uint _display_qty_divisor => _display_qty_divisor_backing ??
(_display_qty_divisor_backing = _orders_model.security_auxiliary_model.get_display_quantity_divisor(security)).Value;
public void raise_invalidated() => invalidated?.Invoke();
public void refresh() {
bool dirty = false;
if (md.md.get_bbo(ref _bbo_ref, out NativePriceSize b, out NativePriceSize a)) {
_bid_price.set(b.price);
// don't use the '||' operator - every 'set_new' call must be made
// (the operation mustn't terminate the first time a set_new call returns true).
dirty |= (
_bid_direct_size.set_new(b.direct_size) |
_bid_implied_size.set_new(b.implied_size) |
_bid_merged_size.set_new(b.size) |
_ask_direct_size.set_new(a.direct_size) |
_ask_implied_size.set_new(a.implied_size) |
_ask_merged_size.set_new(a.size));
refresh_size(ref b, _bid_direct_size, _bid_merged_size, ref _bid_merged_size_text, nameof(bid_merged_size_text));
_ask_price.set(a.price);
refresh_size(ref a, _ask_direct_size, _ask_merged_size, ref _ask_merged_size_text, nameof(ask_merged_size_text));
_inside_bids.on_next_distinct(_bid_price.Value >= _working_best_bid.value, ref dirty);
_inside_asks.on_next_distinct(_ask_price.Value <= _working_best_ask.value, ref dirty);
}
@@ -249,6 +183,16 @@ namespace mbts.spark {
_volume.on_next_distinct(md.md.total_volume, ref dirty);
if (dirty)
invalidated?.Invoke();
void refresh_size(ref NativePriceSize nps, Property<uint> direct, Property<uint> merged, ref string text, string text_prop) {
if (nps.size != merged.Value || nps.direct_size != direct.Value) {
merged.set(nps.size);
direct.set(nps.direct_size);
text = null;
dirty = true;
RaisePropertyChanged(text_prop);
}
}
}
public bool Equals(Row other) =>
@@ -9,7 +9,6 @@ using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Data;
using System.Windows.Media;
@@ -19,9 +18,6 @@ using static mbts.ui.spark.MarketViewView;
namespace mbts.spark {
partial class MarketViewVM : SecurityMDGrid<MarketViewVM.Row, SecurityMD> {
static IReadOnlyList<Func<MarketViewVM, IDataGridColumnFilter<Row>>> _filter_getters =
ObjectSnapshot.property_delegates_of_type<MarketViewVM, IDataGridColumnFilter<Row>>();
readonly IBus _bus;
readonly OrdersModel _orders_model;
IWindowSettingsManager<GridSettings> _settings_manager;
@@ -41,21 +37,21 @@ namespace mbts.spark {
const double working_orders_col_width = 105;
IMultiValueConverter price_converter = new ui.spark.converters.TickDisplayConverter();
add_col(Columns.Security, "Security", r => r.security.ToString(), nameof(Row.security), DefaultColumnWidths.Security, security_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Volume, "Volume", r => r.volume_text, nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
add_col(Columns.Volume, "Volume", r => r.volume.ToString(), nameof(Row.volume), DefaultColumnWidths.Volume, volume_filter, DataGridFilterKnownTemplates.ULongMinMax);
add_col(Columns.LastSpd, "Spd", r => r.last_price_from_spread ? "S" : "", nameof(Row.last_price_from_spread), DefaultColumnWidths.Side, last_price_from_spread_filter, DataGridFilterKnownTemplates.Regex);
add_col(Columns.Last, "Last", r => r.format_price(r.last_price), nameof(Row.last_price), DefaultColumnWidths.Price, last_price_filter, DataGridFilterKnownTemplates.DoubleMinMax);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size_text, nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.Position, "Pos", r => r.position_text, nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.RawDayPosition, "Raw Day Pos", r => r.raw_day_position_text, nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.LastSize, merged_size_col_header, r => r.last_size.ToString(), nameof(Row.last_size), DefaultColumnWidths.Size, last_size_filter, DataGridFilterKnownTemplates.UIntMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.Position, "Pos", r => r.position.ToString(), nameof(Row.position), DefaultColumnWidths.Size, position_filter, DataGridFilterKnownTemplates.IntMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.RawDayPosition, "Raw Day Pos", r => SparkUtils.fmt_position(r.raw_day_position), nameof(Row.raw_day_position), DefaultColumnWidths.Size, raw_day_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingBids, working_col_header, nameof(Row.working_bids_text), nameof(Row.working_best_bid), nameof(Row.working_bids_click), working_bid_volume_filter, BTSColors.MarketData.bid, System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size_text, nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size_text, nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidImpliedSize, implied_size_col_header, r => r.bid_implied_size.ToString(), nameof(Row.bid_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.BidDirectSize, direct_size_col_header, r => r.bid_direct_size.ToString(), nameof(Row.bid_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, bid_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.BidMergedSize, merged_size_col_header, nameof(Row.bid_merged_size), nameof(Row.bid_merged_size_text), nameof(Row.bid_merged_size_click), bid_merged_size_filter, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Bid, "Bid", nameof(Row.bid_price), nameof(Row.bid_price_click), bid_price_filter, BTSColors.header_text, BTSColors.MarketData.bid, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_price_col(Columns.Ask, "Ask", nameof(Row.ask_price), nameof(Row.ask_price_click), ask_price_filter, BTSColors.header_text, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_merged_size_col(Columns.AskMergedSize, merged_size_col_header, nameof(Row.ask_merged_size), nameof(Row.ask_merged_size_text), nameof(Row.ask_merged_size_click), ask_merged_size_filter, BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size_text, nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size_text, nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskDirectSize, direct_size_col_header, r => r.ask_direct_size.ToString(), nameof(Row.ask_direct_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_direct_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.AskImpliedSize, implied_size_col_header, r => r.ask_implied_size.ToString(), nameof(Row.ask_implied_size), DefaultColumnWidths.ImpliedDirectMergedSize, ask_implied_size_filter, DataGridFilterKnownTemplates.UIntMinMax, font_color: BTSColors.MarketData.ask, h_alignment: System.Windows.HorizontalAlignment.Right);
add_hl_working_orders_col(Columns.WorkingAsks, working_col_header, nameof(Row.working_asks_text), nameof(Row.working_best_ask), nameof(Row.working_asks_click), working_ask_volume_filter, BTSColors.MarketData.ask, System.Windows.HorizontalAlignment.Right);
add_col(Columns.Settlement, "Settlement", r => r.format_price(r.settlement), nameof(Row.settlement), DefaultColumnWidths.Settlement, settlement_filter, DataGridFilterKnownTemplates.DoubleMinMax, h_alignment: System.Windows.HorizontalAlignment.Right);
add_col(Columns.DayChange, "Change", r => r.format_price(r.day_change), nameof(Row.day_change), DefaultColumnWidths.Price, day_change_filter, DataGridFilterKnownTemplates.DoubleMinMax, true, h_alignment: System.Windows.HorizontalAlignment.Right);
@@ -155,16 +151,14 @@ namespace mbts.spark {
working_ask_volume_filter.load_state(filters.working_ask_volume);
volume_filter.load_state(filters.volume);
security_key_filter.load_state(filters.security_key);
market_description_filter.load_state(filters.market_description);
cleared_alias_filter.load_state(filters.cleared_alias);
product_description_filter.load_state(filters.product_description);
hub_alias_filter.load_state(filters.hub_alias);
strip_name_filter.load_state(filters.strip_name);
mic_code_filter.load_state(filters.mic_code);
}
rows_view.set_sort_descriptions(s.sorting_criterion, columns);
}
}), DataGridFilterHelper.hook_filters(rows_view, _filters),
}), DataGridFilterHelper.hook_filters(rows_view, security_filter,
bid_price_filter, bid_merged_size_filter, bid_direct_size_filter, bid_implied_size_filter,
ask_price_filter, ask_merged_size_filter, ask_direct_size_filter, ask_implied_size_filter,
last_price_filter, last_price_from_spread_filter, last_size_filter, day_change_filter, settlement_filter,
session_low_filter, session_high_filter, position_filter, raw_day_position_filter, working_bid_volume_filter, working_ask_volume_filter, volume_filter, security_key_filter),
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager),
WorkLoop.Start("MarketView", _refresh)
);
@@ -239,15 +233,13 @@ namespace mbts.spark {
public IDataGridColumnPersistentFilter<Row, ULongMinMaxState> volume_filter { get; } = new ULongMinMax<Row>(r => r.volume, nameof(Row.volume));
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.market_description, nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.cleared_alias, nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.product_description, nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.hub_alias, nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.strip_name, nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.mic_code, nameof(Row.mic_code));
IEnumerable<IDataGridColumnFilter<Row>> _filters =>
_filter_getters.Select(fn => fn(this));
public IDataGridColumnPersistentFilter<Row, string> market_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MarketDescription : "", nameof(Row.market_description));
public IDataGridColumnPersistentFilter<Row, string> cleared_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ClearedAlias : "", nameof(Row.cleared_alias));
public IDataGridColumnPersistentFilter<Row, string> product_description_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.ProductName : "", nameof(Row.product_description));
public IDataGridColumnPersistentFilter<Row, string> hub_alias_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.HubAlias : "", nameof(Row.hub_alias));
public IDataGridColumnPersistentFilter<Row, string> strip_name_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.StripName : "", nameof(Row.strip_name));
public IDataGridColumnPersistentFilter<Row, string> mic_code_filter { get; } = new RegexFilterVM<Row>(r => r.security.HasIce ? r.security.Ice.MicCode : "", nameof(Row.mic_code));
public FilterStates get_filter_states() {
return new FilterStates {
@@ -22,15 +22,12 @@ namespace bts.shell.time_and_sales {
.to_safe();
readonly TradeMessageImmutable _trade;
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
uint? _display_size;
public TimeAndSalesRowVM(int row_key, SecurityImmutable security, TradeMessageImmutable trade, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
public TimeAndSalesRowVM(int row_key, SecurityImmutable security, TradeMessageImmutable trade) {
this.row_key = row_key;
this.security = security;
_trade = trade;
trade_key = trade.Key;
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
}
public event Action invalidated;
@@ -42,8 +39,6 @@ namespace bts.shell.time_and_sales {
public DateTime time => ((ulong)_trade.Time).lldatetime().ToLocalTime();
public double price => _trade.price();
public uint size => _trade.Size;
public uint display_size => _display_size ??
(_display_size = size.safe_divide(_get_display_qty_divisor(security))).Value;
public AggressorSide aggressor_side => _trade.AggressorSide;
public void raise_invalidated() =>
@@ -40,7 +40,6 @@ namespace bts.shell.time_and_sales {
_refresh_filter,
_refresh_sort,
_refresh_selected_rows;
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
IComparer<TimeAndSalesRowVM> _comparer_backing;
Func<TimeAndSalesRowVM, bool> _filter_backing;
ImmutableHashSet<TimeAndSalesRowVM> _selected_rows = ImmutableHashSet<TimeAndSalesRowVM>.Empty;
@@ -59,9 +58,8 @@ namespace bts.shell.time_and_sales {
_filter_change_count,
_sort_change_count;
public TimeAndSalesVM(IWindowSettingsManager<GridSettings> settings_manager, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
public TimeAndSalesVM(IWindowSettingsManager<GridSettings> settings_manager) {
_sort_manager = new ColumnSortManager(columns.WrapSelector(c => c as ISortableDIYGridColumn, null), TimeAndSalesView.Columns.RowKey.ToString());
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
_comparer_backing = _sort_manager.BuildComparer(_get_column_comparer);
_log = new LoggerContext(Log.Ui, $"Time And Sales VM ({Interlocked.Increment(ref _id_counter)})", null);
_settings_manager = settings_manager;
@@ -97,7 +95,7 @@ namespace bts.shell.time_and_sales {
System.Windows.HorizontalAlignment.Right);
add_column(
TimeAndSalesView.Columns.Size,
r => r.display_size.ToString(),
r => r.size.ToString(),
DefaultColumnHeaders.Size,
DefaultColumnWidths.Size,
size_filter,
@@ -306,7 +304,7 @@ namespace bts.shell.time_and_sales {
}
var all_rows_bd = _all_rows.ToBuilder();
foreach (var t in trades) {
var r = _row_factory.create(t.security, t.trade, _get_display_qty_divisor);
var r = _row_factory.create(t.security, t.trade);
all_rows_bd.Add(r);
if (_filter_backing == null || _filter_backing(r))
rows.InsertSorted(r, _comparer);
@@ -402,11 +400,11 @@ namespace bts.shell.time_and_sales {
// the row key must be unique to each row in the collection
int _row_id_counter;
public TimeAndSalesRowVM create(TradeInfo trade_info, Func<SecurityImmutable?, uint> get_display_qty_divisor) =>
new TimeAndSalesRowVM(_row_id_counter++, trade_info.security, trade_info.trade, get_display_qty_divisor);
public TimeAndSalesRowVM create(TradeInfo trade_info) =>
new TimeAndSalesRowVM(_row_id_counter++, trade_info.security, trade_info.trade);
public TimeAndSalesRowVM create(SecurityImmutable security, TradeMessageImmutable trade, Func<SecurityImmutable?, uint> get_display_qty_divisor) =>
new TimeAndSalesRowVM(_row_id_counter++, security, trade, get_display_qty_divisor);
public TimeAndSalesRowVM create(SecurityImmutable security, TradeMessageImmutable trade) =>
new TimeAndSalesRowVM(_row_id_counter++, security, trade);
}
}
}
@@ -26,7 +26,6 @@ namespace bts.shell.time_and_sales {
readonly TimeAndSalesWindowFactory _factory;
readonly IWindowSettingsManager<GridSettings> _settings_manager;
readonly bool _can_pause_updates;
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
TimeAndSalesVM _vm;
TimeAndSalesModel _model;
SecurityCriteria _security_criteria_backing;
@@ -36,21 +35,12 @@ namespace bts.shell.time_and_sales {
SparkMenuDisplayVM _menu_display_vm;
IDisposable _subscriptions;
public TimeAndSalesWindow(
TimeAndSalesWindowFactory factory,
IBus bus,
MDConsumer consumer,
TimeAndSalesSettings state,
ImageSource icon,
IWindowSettingsManager<GridSettings> settings_manager,
bool can_pause_updates,
Func<SecurityImmutable?, uint> get_display_qty_divisor)
public TimeAndSalesWindow(TimeAndSalesWindowFactory factory, IBus bus, MDConsumer consumer, TimeAndSalesSettings state, ImageSource icon, IWindowSettingsManager<GridSettings> settings_manager, bool can_pause_updates)
: base("Time And Sales", bus, factory, SparkCommands.ShowTimeAndSales.icon) {
Log.assert(state != null);
_consumer = consumer;
_factory = factory;
_state = state ?? TimeAndSalesSettings.Default(new SecurityCriteria());
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
if (_state.security_criteria == null)
_state = _state.with(new SecurityCriteria());
_security_criteria_backing = _state.security_criteria;
@@ -96,7 +86,7 @@ namespace bts.shell.time_and_sales {
_settings_manager.instance_settings = _state.grid;
_menu_display_vm = new SparkMenuDisplayVM();
_menu_display_vm.load_from(_state.menu_display);
_vm = new TimeAndSalesVM(_settings_manager, _get_display_qty_divisor);
_vm = new TimeAndSalesVM(_settings_manager);
var selected_rows_src = Observable.Create<ImmutableHashSet<TimeAndSalesRowVM>>(o => {
_vm.selected_rows_changed += o.OnNext;
return Disposable.Create(() => _vm.selected_rows_changed -= o.OnNext);
@@ -20,15 +20,12 @@ namespace bts.shell {
readonly MDConsumer _consumer;
readonly UserSettingsHub _settings_hub;
readonly TimeAndSalesConfig _time_and_sales_conf;
readonly ISecurityAuxiliaryDataModel _security_auxiliary_model;
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>();
public TimeAndSalesWindowFactory(IShellModule module, IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, ISecurityAuxiliaryDataModel security_auxiliary_model)
: base(module, "TimeAndSales") {
public TimeAndSalesWindowFactory(IShellModule module, IBus bus, MDConsumer consumer, UserSettingsHub settings_hub) : base(module, "TimeAndSales") {
_bus = bus;
_consumer = consumer;
_settings_hub = settings_hub;
_security_auxiliary_model = security_auxiliary_model;
_time_and_sales_conf = new TimeAndSalesConfig(Configuration.get<bts.config.TimeAndSalesOptions>());
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
@@ -41,8 +38,7 @@ namespace bts.shell {
public override Task<IShellWindow> load(TimeAndSalesSettings state) =>
Task.FromResult<IShellWindow>(new TimeAndSalesWindow(this, _bus, _consumer, state, SparkCommands.PickTimeAndSales.icon,
new WindowSettingsManager<GridSettings>(_settings_hub, state.grid, s => s.time_and_sales_grid, (s, g) => s.with(time_and_sales:g)),
_time_and_sales_conf.can_pause_updates,
_security_auxiliary_model.get_display_quantity_divisor));
_time_and_sales_conf.can_pause_updates));
public async void show(SecurityCriteria criteria) {
var w = await load(new TimeAndSalesSettings(criteria));
@@ -236,20 +236,12 @@ namespace mbts.spark {
get => algo_root;
set => algo_root = value;
}
IAlgoVM IAlgoParamVM<IAlgoVM>.display_value {
get => algo_root;
set => algo_root = value;
}
bool IAlgoParamVM.has_value => _algo_root != null;
bool IAlgoParamVM.is_enabled { get; set; } = true;
object IAlgoParamVM.value {
get => algo_root;
set => algo_root = (IAlgoVM)value;
}
object IAlgoParamVM.display_value {
get => algo_root;
set => algo_root = (IAlgoVM)value;
}
AlgoParamPermissions _permissions = AlgoParamPermissions.ReadWrite;
AlgoParamPermissions IAlgoParamVM.permissions { get => _permissions; set => _permissions = value; }
@@ -199,30 +199,18 @@
Grid.Column="1"
HorizontalAlignment="Center"
Foreground="{u:BTSColor MarketData.bid}"
Text="{Binding DataContext.bbo.bid_size, RelativeSource={RelativeSource AncestorType=usp:AlgoEditorGridView}}"
ToolTip="Best Bid Size"
>
<TextBlock.Text>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="DataContext.bbo.bid_size" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
/>
<TextBlock
x:Name="ask_text"
Grid.Column="2"
HorizontalAlignment="Center"
Margin="4,0"
Foreground="{u:BTSColor MarketData.ask}"
Text="{Binding DataContext.bbo.ask_size, RelativeSource={RelativeSource AncestorType=usp:AlgoEditorGridView}}"
ToolTip="Best Ask Size"
>
<TextBlock.Text>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="DataContext.bbo.ask_size" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:AlgoEditorGridView}" Mode="OneWay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
/>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding path_disambiguation}" Value="{x:Static sys:String.Empty}">
@@ -284,14 +272,6 @@
</Grid>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<!--TODO: replace all the param.value bindings with param.display_value.
The reason this has not already been done is that the display_value property is new,
and only necessary for the size param as of 6/8/2020 (slightly more uniform code doesn't warrant rocking the boat).-->
<u:TemplateSelectorKeyValue Key="{x:Static usp:AlgoParamTemplateKeys.Size}">
<DataTemplate>
<TextBlock Text="{Binding param.display_value}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</usp:AlgoParamTemplateSelector.KeyedTemplates>
<!--Bool-->
<usp:AlgoParamTemplateSelector.BoolTemplate>
@@ -380,22 +360,6 @@
/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<!--TODO: replace all the param.value bindings with param.display_value.
The reason this has not already been done is that the display_value property is new,
and only necessary for the size param as of 6/8/2020 (slightly more uniform code doesn't warrant rocking the boat).-->
<u:TemplateSelectorKeyValue Key="{x:Static usp:AlgoParamTemplateKeys.Size}">
<DataTemplate>
<c:TickUpDownNullableInt Focusable="False" HorizontalAlignment="Stretch" Value="{Binding param.display_value, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl">
<c:TickUpDownNullableInt.CommandBindings>
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
</c:TickUpDownNullableInt.CommandBindings>
<c:TickUpDownNullableInt.InputBindings>
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
</c:TickUpDownNullableInt.InputBindings>
</c:TickUpDownNullableInt>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</usp:AlgoParamTemplateSelector.KeyedTemplates>
<!--Bool-->
<usp:AlgoParamTemplateSelector.BoolTemplate>
@@ -122,7 +122,7 @@ namespace mbts.spark {
add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, dv_filter_rx(_model.factory.subscribe_accounts().Select(d => d.Values.Ordered()), r => r.account), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account);
add_col(Columns.Exchange, DefaultColumnHeaders.Exchange, r => r.exchange.ToString(), dv_filter(EnumHelper.get_values<Venue>().ToArray(), r => r.security.exchange_or_default()), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Exchange);
add_col(Columns.TimeInForce, DefaultColumnHeaders.TimeInForce, r => r.time_in_force.ToString(), dv_filter(EnumHelper.get_values<TimeInForce>().ToArray(), r => r.order?.time_in_force() ?? TimeInForce.Undefined), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.TimeInForce);
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.display_size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
add_col(Columns.Price, DefaultColumnHeaders.Price, r => r.format_price(r.price), new DoubleMinMax<FillTuple>(r => r.fill.LastPrice), DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.Price);
add_col(Columns.FillKind, DefaultColumnHeaders.FillKind, r => r.fill_kind.ToString(), dv_filter(EnumHelper.get_values<FillKind>().ToArray(), r => r.fill.FillKind), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
add_col(Columns.FillStatus, DefaultColumnHeaders.FillStatus, r => r.fill_status.ToString(), dv_filter(EnumHelper.get_values<FillStatus>().ToArray(), r => r.fill.FillStatus), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind);
@@ -286,7 +286,6 @@ namespace mbts.spark {
Color? _root_algo_badge_color;
OrderMetadata _order_metadata;
bool _is_selected;
double? _display_size;
public Row(FillTuple fill_tuple, OrdersModel model) :
base(fill_tuple.security ?? SecurityDataHelper.Defaults.SecurityImmutable) {
@@ -307,8 +306,6 @@ namespace mbts.spark {
public Side side => _fill.Side;
public double price => _fill.LastPrice;
public double size => _fill.LastShares;
public double display_size => _display_size ??
(_display_size = size.safe_divide(_model.security_auxiliary_model.get_display_quantity_divisor(security))).Value;
public FillKind fill_kind => _fill.FillKind;
public FillStatus fill_status => _fill.FillStatus;
public string fill_key => _fill.FillKey;
@@ -87,15 +87,6 @@ namespace mbts.spark {
public static bool operator !=(LadderColumnSettings x, LadderColumnSettings y) => !(x == y);
}
[ProtoContract]
class LadderOrderLimits : IEquatable<LadderOrderLimits> {
[ProtoMember(1)]
public ToggleInt32Proto max_settlement_cross_ticks { get; set; }
public bool Equals(LadderOrderLimits other) =>
other != null && max_settlement_cross_ticks == other.max_settlement_cross_ticks;
}
[ProtoContract]
class HistogramBarAnimation : IEquatable<HistogramBarAnimation> {
// this class is 100% mutable (there are literally 0 readonly members),
@@ -323,8 +314,6 @@ namespace mbts.spark {
// Settings are liable to change in the near future, so an internal IEqualityComparer simply won't do as it risks becoming out of date.
[ProtoContract]
class LadderSettings : IEquatable<LadderSettings> {
// RESERVED ProtoMember indices
// 900-999: dj settings
const double _default_order_entry_panel_width = 110.0;
const double _default_zoom_factor = 1.0;
const bool _default_show_warn_on_cross = true;
@@ -603,9 +592,6 @@ namespace mbts.spark {
[ProtoMember(61), DefaultValue(true)]
public bool status_message_visible { get; set; }
[ProtoMember(900)]
public LadderOrderLimits ladder_order_limits { get; set; }
public void sanitize() =>
sanitize(this);
@@ -711,8 +697,7 @@ namespace mbts.spark {
a.enforce_max_clip == b.enforce_max_clip &&
a.max_clip == b.max_clip &&
a.price_display_format == b.price_display_format &&
a.status_message_visible == b.status_message_visible &&
a.ladder_order_limits.safe_equals(b.ladder_order_limits);
a.status_message_visible == b.status_message_visible;
public bool Equals(LadderSettings other) =>
profile_equals(this, other) &&
@@ -4,7 +4,6 @@ using bts.shell.orderbook;
using mbts.persist;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -170,8 +169,7 @@ namespace mbts.spark {
}
private void _set_badge_text() {
var fmt = MDDisplayHelper.get_value_formatter(_owner.display_quantity_divisor);
text_lines = BadgeRender.format(side, quantity, filled, _current_badge_style, fmt);
text_lines = BadgeRender.format(side, quantity, filled, _current_badge_style);
RaisePropertyChanged(nameof(text_lines));
}
@@ -3,7 +3,6 @@ using bts.api;
using bts.api.orderman;
using bts.collections;
using bts.persist.algo;
using bts.proto;
using bts.shell.orderbook;
using bts.utils;
using mbts.collections;
@@ -45,8 +44,7 @@ namespace mbts.spark {
LadderSettings.NetChangeTerms _price_format;
LadderSettings.PriceDisplayFormat _price_display_format;
double _order_entry_panel_width, _zoom_factor;
bool _auto_clear, _show_warn_on_cross, _enforce_max_clip, _enforce_max_settlement_cross_ticks;
int _max_settlement_cross_ticks;
bool _auto_clear, _show_warn_on_cross, _enforce_max_clip;
LadderSettings _current;
LadderColumnSettingsVM
_orders = new OrdersColumnSettingsVM(),
@@ -192,8 +190,6 @@ namespace mbts.spark {
public bool show_warn_on_cross { get => _show_warn_on_cross; set => Set(ref _show_warn_on_cross, value); }
public bool enforce_max_clip { get => _enforce_max_clip; set => Set(ref _enforce_max_clip, value); }
public uint max_clip { get => _max_clip.Value; set => _max_clip.Value = value; }
public bool enforce_max_settlement_cross_ticks { get => _enforce_max_settlement_cross_ticks; set => Set(ref _enforce_max_settlement_cross_ticks, value); }
public int max_settlement_cross_ticks { get => _max_settlement_cross_ticks; set => Set(ref _max_settlement_cross_ticks, value); }
// favorites
public FavoriteAlgosVM favorite_algos_vm { get; }
@@ -374,10 +370,6 @@ namespace mbts.spark {
show_warn_on_cross = settings.show_warn_on_cross;
enforce_max_clip = settings.enforce_max_clip;
max_clip = settings.max_clip;
if (settings.ladder_order_limits != null) {
enforce_max_settlement_cross_ticks = settings.ladder_order_limits.max_settlement_cross_ticks.is_enabled;
max_settlement_cross_ticks = settings.ladder_order_limits.max_settlement_cross_ticks.value;
}
}
}
@@ -456,10 +448,6 @@ namespace mbts.spark {
clone.show_warn_on_cross = show_warn_on_cross;
clone.enforce_max_clip = enforce_max_clip;
clone.max_clip = max_clip;
if (clone.ladder_order_limits == null)
clone.ladder_order_limits = new LadderOrderLimits();
clone.ladder_order_limits.max_settlement_cross_ticks =
new ToggleInt32Proto(max_settlement_cross_ticks, enforce_max_settlement_cross_ticks);
return clone;
}
@@ -1,6 +1,6 @@
using bts.api;
using bts.api.orderman;
using bts.proto;
using bts.collections;
using bts.shell;
using bts.spark;
using bts.utils;
@@ -8,7 +8,6 @@ using mbts.co2;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.utils;
using System;
using System.Collections.Generic;
@@ -67,8 +66,6 @@ namespace mbts.spark {
current_settings = init;
this.settings = settings;
_equality = TickUtils.tick_equality_comparer(_model.security.ContractSpec);
display_quantity_divisor = model.order_gateway.security_auxiliary_model.get_display_quantity_divisor(model.security);
size_calculator.display_divisor = display_quantity_divisor;
_slot_price_vs_orders = new Dictionary<double, LadderOrderVM>(_equality);
this.order_properties_vm = order_properties_vm;
_high = backing(nameof(high), double.NaN);
@@ -295,7 +292,6 @@ namespace mbts.spark {
remove => _on_disposed_impl -= value;
}
public SecurityImmutable security => _model.security;
public uint display_quantity_divisor { get; }
public LadderSettings current_settings { get; private set; }
public double high => _high.Value;
public double low => _low.Value;
@@ -373,14 +369,14 @@ namespace mbts.spark {
get {
string rv = null;
if (show_day_position)
rv = SparkUtils.fmt_position(day_position / display_quantity_divisor);
rv = SparkUtils.fmt_position(day_position);
if (show_raw_day_position) {
var raw_day_text = SparkUtils.fmt_position(raw_day_position / display_quantity_divisor);
var raw_day_text = SparkUtils.fmt_position(raw_day_position);
rv = rv == null ? raw_day_text : $"{rv} / {raw_day_text}";
}
if (show_account_position) {
var acc_text = SparkUtils.fmt_position(position / display_quantity_divisor);
rv = rv == null ? acc_text : $"{rv} ({acc_text})";
var acc_text = SparkUtils.fmt_position(position);
rv = rv == null ? SparkUtils.fmt_position(position) : $"{rv} ({acc_text})";
}
return rv ?? "";
}
@@ -490,10 +486,6 @@ namespace mbts.spark {
}
public async Task send_order_async(Side side, uint size, double price) {
if (!_validate_price(side, price, out var invalid_price_msg)) {
status_update(new Activity { security = security, message = invalid_price_msg, category = ActivityLogCategory.order });
return;
}
var order = order_properties_vm.create(side);
if (order.CustomDef != null) {
if (order.CustomDef.Stop != null) {
@@ -525,23 +517,6 @@ namespace mbts.spark {
}
}
bool _validate_price(Side side, double price, out string msg) {
msg = null;
if (!price.is_real()) {
msg = "not a real number";
} else if (settlement.is_real()) {
var ticks_from_settle = security.ContractSpec.tickdiff(price, settlement);
if (ticks_from_settle.HasValue) {
var max_cross = (current_settings?.ladder_order_limits?.max_settlement_cross_ticks)?.as_nullable();
if (max_cross.HasValue) {
if (side == Side.Sell ? ticks_from_settle < (-1 * max_cross.Value) : ticks_from_settle > max_cross.Value)
msg = $"exceeds settle cross ticks limit ({ticks_from_settle.Value})";
}
}
}
return msg == null;
}
void _update_header() {
bool last_updated = md.get_last_trade(ref _last_trade_time, out NativePriceSize last, out DateTime time, out ulong volume);
if (_misc_time != _model.md.misc_timestamp || last_updated) {
@@ -46,12 +46,12 @@ namespace mbts.spark {
void _reset_ladder_vm() {
if (_value == null && !_security_md.HasValue)
return;
_value?.Dispose();
_value = _security_md.HasValue ?
LadderVM.create_readonly(_bus,
new LadderModel(security_md.Value, _model, _model.mbo_service,
new Func<OrderSecurity, bool>(osec => _selected_order_tree.Contains(osec.order.Key))),
Observable.Return(LadderSettings.sanitize(LadderSettings.Default))) : null;
Disposable.replace(ref _value,
_security_md.HasValue ?
LadderVM.create_readonly(_bus,
new LadderModel(security_md.Value, _model, _model.mbo_service,
new Func<OrderSecurity, bool>(osec => _selected_order_tree.Contains(osec.order.Key))),
Observable.Return(LadderSettings.sanitize(LadderSettings.Default))) : null);
RaisePropertyChanged(nameof(value));
}
}
@@ -66,8 +66,7 @@ namespace mbts.spark {
double _last_origin_price;
MarketBreakLines _mkt_break_lines;
NativePriceSize _last_trade;
Func<NativePriceSize, string> _fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default());
Func<uint, string> _fmt_mkt_quantity_impl = v => v.ToString();
Func<NativePriceSize, string> _fmt_size = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default());
int _old_tooltip_row = int.MaxValue;
NativePriceSize? _old_last_trade;
string _direction_arrow = string.Empty;
@@ -469,7 +468,7 @@ namespace mbts.spark {
security_id: _vm.model.security.Id,
price_level: price_for_point(point)
);
var model = new MBOPopupModel(_vm.model.mbo_service, _vm.model.security, query, fmt_mbo_qty);
var model = new MBOPopupModel(_vm.model.mbo_service, _vm.model.security, query);
// Load the first set of orders so we know if there are any before showing the popup
var result = await model.load_orders();
if (result.success && result.value.orders.Count > 0) {
@@ -490,8 +489,6 @@ namespace mbts.spark {
};
view.RequestClose += (o, _) => popup.IsOpen = false;
}
string fmt_mbo_qty(int value) =>
value < 0 ? $"-{_fmt_mkt_quantity((uint)value)}" : _fmt_mkt_quantity((uint)value);
}
protected override void OnMouseDoubleClick(MouseButtonEventArgs e) {
@@ -659,7 +656,7 @@ namespace mbts.spark {
}
void render_market_col(NativePriceSize[] depth) {
var mid = LadderRender.index_of(_price_list, _last_origin_price, _price_comparer);
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings, _fmt_mkt_depth_item);
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings, _fmt_size);
}
}
_apply_column_visual_offset();
@@ -737,7 +734,7 @@ namespace mbts.spark {
_hide_tool_tip();
}
tooltip_view.Content = _fmt_mkt_quantity(volume);
tooltip_view.Content = $"{volume}";
tooltip_view.IsOpen = true;
this.ToolTip = tooltip_view;
} else {
@@ -853,7 +850,7 @@ namespace mbts.spark {
if (last_trade_changed) {
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
if (last_index > 0)
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column);
}
if (depth_changed)
_refresh_sizes();
@@ -895,21 +892,21 @@ namespace mbts.spark {
void _refresh_last_trade() {
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
if (last_index > 0)
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column, _fmt_mkt_quantity);
LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column);
}
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, origin_index, _price_list, _vm.price_vs_piq, _settings.row_height, security.ContractSpec, _settings.piq_column, _fmt_mkt_quantity);
LadderRender.render_piqs(_piq_vis_tup.visual, _last_vis_tup.glyph_context, origin_index, _price_list, _vm.price_vs_piq, _settings.row_height, security.ContractSpec, _settings.piq_column);
}
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, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column, _fmt_mkt_depth_item);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column, _fmt_mkt_depth_item);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column, _fmt_size);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column, _fmt_size);
}
/// <summary>
@@ -931,17 +928,7 @@ namespace mbts.spark {
if (vapls != null) {
Log.assert_d(_price_list != null);
var last_index = LadderRender.index_of(_price_list, _last_trade.price, _price_comparer);
LadderRender.render_volumes(
_volume_vis_tup.visual,
_volume_vis_tup.glyph_context,
_price_list,
vapls,
_settings.row_height,
security.ContractSpec,
_settings.volume_column,
last_index,
_get_last_arrow_string(),
_fmt_mkt_quantity);
LadderRender.render_volumes(_volume_vis_tup.visual, _volume_vis_tup.glyph_context, _price_list, vapls, _settings.row_height, security.ContractSpec, _settings.volume_column, last_index, _get_last_arrow_string());
_old_last_trade = _last_trade;
}
@@ -964,7 +951,7 @@ namespace mbts.spark {
}
// regardless of if we've arrived here via a new Last (and new direction arrow)
// or we're reusing the old one, this format bit is the same
return $"{_direction_arrow} {_fmt_mkt_quantity(_last_trade.size)} ";
return $"{_direction_arrow} {_last_trade.size} ";
}
}
@@ -1069,7 +1056,7 @@ namespace mbts.spark {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
return double.IsNaN(best_bid) ?
double.IsNaN(best_ask) ? _vm.settlement : security.ContractSpec.tickdown(best_ask) :
double.IsNaN(best_ask) ? _vm.low : security.ContractSpec.tickdown(best_ask) :
best_bid;
}
@@ -1077,7 +1064,7 @@ namespace mbts.spark {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
return double.IsNaN(best_ask) ?
double.IsNaN(best_bid) ? _vm.settlement : best_bid :
double.IsNaN(best_bid) ? security.ContractSpec.tickdown(_vm.high) : best_bid :
security.ContractSpec.tickdown(best_ask);
}
@@ -1085,7 +1072,7 @@ namespace mbts.spark {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
return double.IsNaN(best_bid) ?
double.IsNaN(best_ask) ? _vm.settlement : security.ContractSpec.tickdown(best_ask) :
double.IsNaN(best_ask) ? 0.5 * (_vm.low + _vm.high) : security.ContractSpec.tickdown(best_ask) :
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(security.ContractSpec, best_bid, best_ask);
}
@@ -1122,9 +1109,7 @@ namespace mbts.spark {
GlyphContext glyph_context(LadderColumnSettings cs) =>
GlyphContext.create(cs.font_family, cs.font_weight);
var mkt_qty_display_divisor = _vm?.display_quantity_divisor ?? 1;
_fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(_settings.size_display, mkt_qty_display_divisor);
_fmt_mkt_quantity_impl = MDDisplayHelper.get_value_formatter(mkt_qty_display_divisor);
_fmt_size = MDDisplayHelper.get_formatter_nps(_settings.size_display);
_set_columns();
_apply_column_visual_offset();
if (prev_row_height != _settings.row_height)
@@ -1173,22 +1158,6 @@ namespace mbts.spark {
}
visible_columns_changed?.Invoke();
}
// This is designed to be a redundant null check for _fmt_mkt_depth_item_impl.
string _fmt_mkt_depth_item(NativePriceSize value) {
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
// This shouldn't happen, but better to avoid the potential null ref just in case it does. Expect the unexpected.
var impl = _fmt_mkt_depth_item_impl;
return impl is null ? MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default())?.ToString() ?? "Error" : impl(value);
}
// This is designed to be a redundant null check for _fmt_mkt_quantity_impl.
string _fmt_mkt_quantity(uint value) {
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
// This shouldn't happen, but better to avoid the potential null ref just in case it does. Expect the unexpected.
var impl = _fmt_mkt_quantity_impl;
return impl is null ? value.ToString() : impl(value);
}
void _init_tactile_adorner() {
Disposable.dispose(ref _tactile_adorner);
@@ -12,24 +12,20 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using bts.utils;
namespace mbts.spark {
static class BadgeRender {
// IMPORTANT: the return values for this method MUST BE IN SYNC with the "format" method, else "measure_height" and "LadderRender.measure_row_height" will be broken.
static int _line_count_for_style(LadderSettings.BadgeStyle badge_style) => badge_style == LadderSettings.BadgeStyle.Alternate ? 2 : 1;
// IMPORTANT: at the risk of being redundant, see above comment.
public static IReadOnlyList<string> format(Side side, uint quantity, uint filled, LadderSettings.BadgeStyle style, Func<uint, string> fmt_value = null) {
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
public static IReadOnlyList<string> format(Side side, uint quantity, uint filled, LadderSettings.BadgeStyle style) {
switch (style) {
case LadderSettings.BadgeStyle.Alternate:
var working_qty = quantity > filled ? quantity - filled : 0;
return new[] {
$"{side.abbreviate()}:{fmt_value(filled)}",
$"W:{fmt_value(working_qty)}"
$"{side.abbreviate()}:{filled}",
$"W:{quantity - filled}"
};
default: return new[] { $"{fmt_value(filled)}/{fmt_value(quantity)}" };
default: return new[] { $"{filled}/{quantity}" };
}
}
@@ -197,17 +193,16 @@ namespace mbts.spark {
}
public static void render_volumes(
DrawingVisual dv,
GlyphContext glyph_context,
double[] prices,
IEnumerable<VolumeAtPriceLevelImmutable> data,
double row_height,
ContractSpecificationImmutable spec,
VolumeColumnSettings settings,
int last_index,
string last_size_and_direction,
Func<uint, string> fmt_value) {
//
DrawingVisual dv,
GlyphContext glyph_context,
double[] prices,
IEnumerable<VolumeAtPriceLevelImmutable> data,
double row_height,
ContractSpecificationImmutable spec,
VolumeColumnSettings settings,
int last_index,
string last_size_and_direction
) {
if (!data.Any()) {
_clear_visual(dv);
return;
@@ -229,8 +224,6 @@ namespace mbts.spark {
_clear_visual(dv);
return;
}
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
// inflate the max so that the graph never fills the available space
double max = data.Max(v => v.Volume) * 1.1;
_render_cells(dv, row_height, data.Select(d => (d.Price, d)), settings.general, prices, spec, render_cell);
@@ -262,11 +255,11 @@ namespace mbts.spark {
}
x_bar += width;
if (show_numbers)
numbers_text.Add(fmt_value(v));
numbers_text.Add(v.ToString());
}
if (settings.total.show_numbers) {
numbers_text.Add(fmt_value(vap.Volume));
numbers_text.Add(vap.Volume.ToString());
}
if (settings.show_ltq && last_trade_here) {
numbers_text.Clear();
@@ -316,33 +309,19 @@ namespace mbts.spark {
}
}
internal static void render_last(DrawingVisual dv, GlyphContext glyph_context, NativePriceSize last, int index, double row_height, LadderColumnSettings settings, Func<uint, string> fmt_value) {
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
internal static void render_last(DrawingVisual dv, GlyphContext glyph_context, NativePriceSize last, int index, double row_height, LadderColumnSettings settings) {
using (var dc = dv.RenderOpen()) {
var gr = format_text(fmt_value(last.size), index, row_height, settings, glyph_context);
var gr = format_text(last.size.ToString(), index, row_height, settings, glyph_context);
dc.DrawGlyphRun(settings.foreground.rgb_brush(), gr);
}
}
// this is basically a copy of redner_sizes up above
internal static void render_piqs(
DrawingVisual dv,
GlyphContext glyph_context,
int origin_index,
double[] prices,
IEnumerable<(double price, uint piq)> pvp,
double row_height,
ContractSpecificationImmutable spec,
LadderColumnSettings settings,
Func<uint, string> fmt_value) {
//
internal static void render_piqs(DrawingVisual dv, GlyphContext glyph_context, int origin_index, double[] prices, IEnumerable<(double price, uint piq)> pvp, double row_height, ContractSpecificationImmutable spec, LadderColumnSettings settings) {
using (var dc = dv.RenderOpen()) {
using (_draw_with_clip(dc, settings, row_height, prices.Length)) {
if (!prices.within_range(origin_index) || pvp.is_null_or_empty())
return;
if (fmt_value is null)
fmt_value = FormattingDelegates.default_string<uint>();
var foreground = settings.foreground.rgb_brush();
int curr_index = origin_index;
double curr_price = prices[curr_index];
@@ -353,7 +332,7 @@ namespace mbts.spark {
curr_index += tick_diff;
curr_price = price;
if (curr_index >= 0 && curr_index < prices.Length) {
var gr = format_text(fmt_value(entry.piq), curr_index, row_height, settings, glyph_context);
var gr = format_text(entry.piq.ToString(), curr_index, row_height, settings, glyph_context);
dc.DrawGlyphRun(foreground, gr);
} else break;
}
@@ -392,4 +371,4 @@ namespace mbts.spark {
using (dv.RenderOpen()) { }
}
}
}
}
@@ -1019,47 +1019,45 @@
<Grid HorizontalAlignment="Left" Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- market cross -->
<CheckBox Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding show_warn_on_cross}">
<TextBlock
Style="{StaticResource EnableDisableFeatureText}"
IsEnabled="{Binding show_warn_on_cross}"
Text="Warn on Market Cross"
/>
<CheckBox Name="warn" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding show_warn_on_cross}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight"/>
</Style>
</CheckBox.Resources>
</CheckBox>
<TextBlock Grid.Column="1"
Style="{StaticResource EnableDisableFeatureText}"
IsEnabled="{Binding ElementName=warn, Path=IsChecked}"
Text="Warn on Market Cross" />
<!-- max order qty -->
<CheckBox Grid.Row="2" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding enforce_max_clip}">
<TextBlock
Style="{StaticResource EnableDisableFeatureText}"
IsEnabled="{Binding enforce_max_clip}"
Text="Max Order Quantity"
/>
<CheckBox Name="enforce_max_clip" Grid.Row="1" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding enforce_max_clip}">
<CheckBox.Resources>
<Style TargetType="{x:Type Path}">
<Setter Property="FlowDirection" Value="LeftToRight"/>
</Style>
</CheckBox.Resources>
</CheckBox>
<c:TickUpDownUInt
Grid.Row="2"
Grid.Column="2"
IsEnabled="{Binding enforce_max_clip}"
Value="{Binding max_clip}"
Width="60"
/>
<CheckBox Grid.Row="4" VerticalContentAlignment="Center" IsChecked="{Binding enforce_max_settlement_cross_ticks}">
<TextBlock
Style="{StaticResource EnableDisableFeatureText}"
IsEnabled="{Binding enforce_max_settlement_cross_ticks}"
Text="Max Ticks from Settlement"
/>
</CheckBox>
<c:TickUpDownInt Grid.Row="4" Grid.Column="2" IsEnabled="{Binding enforce_max_settlement_cross_ticks}" Value="{Binding max_settlement_cross_ticks}"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Style="{StaticResource EnableDisableFeatureText}"
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
MinWidth="{Binding ElementName=warn, Path=ActualWidth}"
Text="Max Order Quantity"/>
<c:TickUpDownUInt Grid.Row="1" Grid.Column="2"
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
Value="{Binding max_clip}" />
</Grid>
</TabItem>
<TabItem Header="Mini Order Book">
@@ -426,26 +426,9 @@
Text="{Binding left_panel_width, Converter={StaticResource VolumeTextConverter}}"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock
Grid.Column="2"
Grid.Row="5"
FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"
>
<TextBlock.Text>
<MultiBinding>
<MultiBinding.Converter>
<converters:MultiValueConverterPipe>
<x:Static MemberType="{x:Type converters:DynamicDoubleDivisorConverter}" Member="Instance"/>
<x:Static MemberType="{x:Type converters:ValueConverters}" Member="SimpleString"/>
</converters:MultiValueConverterPipe>
</MultiBinding.Converter>
<Binding Path="volume" Mode="OneWay"/>
<Binding Path="display_quantity_divisor" Mode="OneTime"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Grid.Column="2" Grid.Row="5" Text="{Binding volume, Mode=OneWay}" FontWeight="Bold"
Margin="0,0,0,4"
Visibility="{Binding show_volume, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Grid>
<!-- Account combobox -->
<ComboBox DataContext="{Binding order_properties_vm}" Margin="2,10,2,2"
@@ -524,7 +507,7 @@
mbts:TextBoxBehavior.IncrementDecrementOnMouseClick="True"
HorizontalContentAlignment="Right"
Margin="2"
Text="{Binding size_calculator.display_value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding size_calculator.value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ContextMenu="{x:Null}"
/>
<!-- Qty quick buttons -->
@@ -539,14 +522,7 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Content="{Binding}" Focusable="False">
<Button.CommandParameter>
<MultiBinding Converter="{x:Static converters:DynamicDoubleMultiplierConverter.Instance}">
<Binding/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=spark:LadderView}"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
<Button Margin="2" Command="{Binding Path=DataContext.size_calculator.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding}" Content="{Binding}" Focusable="False"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -3,7 +3,6 @@ using bts.api.position;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
@@ -26,27 +25,24 @@ namespace bts.shell.manual_fills {
readonly Property<bool> _is_dirty;
readonly Property<EditableItemState> _editable_item_state;
readonly DelegateProperty<bool> _is_valid;
readonly DisplayQuantityMultiplier _size = new DisplayQuantityMultiplier();
readonly Func<SecurityImmutable?, uint> _get_display_qty_divisor;
SecurityImmutable? _security;
AccountTuple? _account;
UserTuple? _user;
Side _side;
uint _size;
double _price;
bool _is_selected;
ManualFillData? _data;
IDisposable _committed_snapshot;
public ManualFillRow(ManualFillData data, Func<SecurityImmutable?, uint> get_display_qty_divisor) {
public ManualFillRow(ManualFillData data) {
_data = data;
_get_display_qty_divisor = get_display_qty_divisor ?? new Func<SecurityImmutable?, uint>(sec => 1);
key = data.key;
_security = data.value.security;
_account = data.value.account;
_user = data.value.user;
_side = data.value.manual_fill.Side;
_size.actual_value = data.value.manual_fill.Quantity;
_size.conversion_data = new QuantityConversionData(_get_display_qty_divisor(data.value.security), QuantityConversionData.OpType.DivideActual);
_size = data.value.manual_fill.Quantity;
_price = data.value.manual_fill.Price;
if (placeholder_key.HasValue) {
_is_dirty = backing(nameof(is_dirty), false);
@@ -57,7 +53,6 @@ namespace bts.shell.manual_fills {
_editable_item_state = backing(nameof(editable_item_state), _is_dirty, ch => ch.newval ? EditableItemState.Modified : EditableItemState.None);
}
_is_valid = backing(nameof(is_valid), () => _validate(this));
_dispose = _size.connect_owner(this, nameof(size), nameof(display_size));
}
public event Action<bool> is_selected_changed;
@@ -70,10 +65,8 @@ namespace bts.shell.manual_fills {
public SecurityImmutable? security {
get => _security;
set {
if (_set(ref _security, value)) {
_size.conversion_data = new QuantityConversionData(_get_display_qty_divisor(value), QuantityConversionData.OpType.DivideActual);
if (_set(ref _security, value))
RaisePropertyChanged(nameof(spec));
}
}
}
public ContractSpecificationImmutable? spec => security.spec_or_default();
@@ -82,8 +75,7 @@ namespace bts.shell.manual_fills {
public UserTuple? user { get => _user; set => _set(ref _user, value); }
// side/size/price
public Side side { get => _side; set => _set(ref _side, value); }
public uint size { get => _size.actual_value; set => _size.actual_value = value; }
public uint display_size { get => _size.display_value; set => _size.display_value = value; }
public uint size { get => _size; set => _set(ref _size, value); }
public double price { get => _price; set => _set(ref _price, value); }
// is selected
public bool is_selected {
@@ -71,7 +71,7 @@
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Accounts}}" SelectedItem="{Binding account}"/>
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Users}}" SelectedItem="{Binding user}"/>
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Sides}}" SelectedItem="{Binding side}" Width="48"/>
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding display_size}"/>
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding size}"/>
<usp:PriceEntryControl Grid.Row="2" Grid.Column="10" MinWidth="55" Value="{Binding price}" ContractSpec="{Binding spec}"/>
</Grid>
<c:DialogSkirt Grid.Row="1" Margin="0,6,0,0">
@@ -1,5 +1,4 @@
using bts.api;
using bts.api.core;
using bts.api.core;
using bts.api.position;
using bts.collections;
using mbts.spark;
@@ -104,8 +103,5 @@ namespace bts.shell.manual_fills {
public async Task<bool> update_fills(IEnumerable<ManualFill> fills) =>
await _model.update_manual_fills(fills);
public uint get_display_qty_divisor(SecurityImmutable? security) =>
_model.security_auxiliary_model.get_display_quantity_divisor(security);
}
}
@@ -68,7 +68,7 @@ namespace bts.shell.manual_fills {
RXCollection.connect(_persisted_rows,
_model.manual_fills.OnDispatcher(),
Observable.Return(Comparer<ManualFillData>.Create((a, b) => a.key.CompareTo(b.key))),
mfd => new ManualFillRow(mfd, model.get_display_qty_divisor),
mfd => new ManualFillRow(mfd),
mfr => mfr.data),
_bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(m => {
security_map = new FilteredDictionary<uint, SecurityMD>(m, smd => smd.security.ContractCase == Security.ContractOneofCase.Future);
@@ -126,8 +126,7 @@ namespace bts.shell.manual_fills {
new ManualFillKey(_placeholder_id_counter++),
new ManualFillTuple(new ManualFillImmutable(new ManualFill {
Side = Side.Buy
}), null, accounts.FirstOrDefault(), users.FirstOrDefault())),
_model.get_display_qty_divisor);
}), null, accounts.FirstOrDefault(), users.FirstOrDefault())));
void _add_fill(ManualFillRow new_row) =>
_placeholder_rows.InsertSorted((new_row ?? (new_row = row_factory())).placeholder_key.HasValue ?
@@ -211,20 +210,11 @@ namespace bts.shell.manual_fills {
} else {
add_imported_fills();
}
void add_imported_fills() {
_placeholder_rows.InsertSorted(parsed_fills.Select(mk_row), _row_key_comparer);
ManualFillRow mk_row(ParsedManualFill pmf) {
var data = new ManualFillData(
new ManualFillKey(_placeholder_id_counter++),
new ManualFillTuple(
new ManualFillImmutable(pmf.result),
pmf.result.SecurityKey != 0 && security_map.TryGetValue(pmf.result.SecurityKey, out var smd) ? smd.security : (SecurityImmutable?)null,
accounts.FirstOrDefault(a => a?.id == pmf.result.AccountId),
users.FirstOrDefault(a => a?.id == pmf.result.UserId)));
return new ManualFillRow(data, _model.get_display_qty_divisor);
}
}
void add_imported_fills() =>
_placeholder_rows.InsertSorted(parsed_fills.Select(pmf => new ManualFillRow(new ManualFillData(new ManualFillKey(_placeholder_id_counter++), new ManualFillTuple(new ManualFillImmutable(pmf.result),
pmf.result.SecurityKey != 0 && security_map.TryGetValue(pmf.result.SecurityKey, out var smd) ? smd.security : (SecurityImmutable?)null,
accounts.FirstOrDefault(a => a?.id == pmf.result.AccountId),
users.FirstOrDefault(a => a?.id == pmf.result.UserId))))), _row_key_comparer);
} catch (Exception ex) {
BTSMessageBox.Error($"Error importing fills: {ex}.", ManualFillsWindowFactory.window_name);
}
@@ -212,12 +212,12 @@
<DataGridTemplateColumn c:DataGridEx.CellKey="{x:Static Size, MemberType={u:Nested usp:ManualFillsView.Columns}}" Header="Qty" SortMemberPath="side">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding display_size}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding size}" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<c:TickUpDownUInt MinWidth="40" Value="{Binding display_size, UpdateSourceTrigger=PropertyChanged}"/>
<c:TickUpDownUInt MinWidth="40" Value="{Binding size, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
@@ -597,19 +597,19 @@ namespace mbts.spark {
public class SizeColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
public SizeColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
: base(Columns.Size, "Qty", r => r.display_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.quantity)) { }
: base(Columns.Size, "Qty", r => r.quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.quantity)) { }
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.display_quantity.ToString(), position, row_height,
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.quantity.ToString(), position, row_height,
RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding);
}
public class FilledColumn : OrderbookColumnBase<UIntMinMaxState>, IRenderColumn<OrderBookRowVM> {
public FilledColumn(IDataGridColumnPersistentFilter<OrderSecurityRootPair, UIntMinMaxState> filter)
: base(Columns.Filled, "Filled Qty", r => r.display_filled_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.filled_quantity)) { }
: base(Columns.Filled, "Filled Qty", r => r.filled_quantity.ToString(), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax, _wrap_comparer(r => r.filled_quantity)) { }
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.display_filled_quantity.ToString(), position, row_height,
CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.filled_quantity.ToString(), position, row_height,
RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding);
}
@@ -1163,7 +1163,7 @@ namespace mbts.spark {
public override FrameworkElement view_factory() {
var tick_up_down = new TickUpDownNullableUIntCommit();
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.SourceValueProperty, nameof(OrderBookRowVM.prospective_display_quantity));
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.SourceValueProperty, nameof(OrderBookRowVM.prospective_quantity));
tick_up_down.SetBinding(TickUpDownNullableUIntCommit.OnCommitProperty, nameof(OrderBookRowVM.modify_order_quantity_andor_price));
tick_up_down.InputBindings.Add(_create_keyinput_binding(Key.Enter, nameof(OrderBookRowVM.modify_order_quantity_andor_price)));
tick_up_down.InputBindings.Add(_create_keyinput_binding(Key.Escape, nameof(OrderBookRowVM.revert_prospective_order_quantity_andor_price)));
@@ -29,9 +29,7 @@ namespace mbts.spark {
int _depth_level;
SecurityImmutable? _security => order_tuple.order.security;
// for mini-ob
DisplayQuantityMultiplier _prospective_qty_translator_backing;
uint _display_qty_divisor_backing;
bool _display_qty_divisor_dirty = true;
uint _prospective_quantity;
double _prospective_price;
Action<bool> _on_selection_changed;
@@ -39,6 +37,7 @@ namespace mbts.spark {
_owner = owner;
this.order_tuple = order_tuple;
_is_expanded = is_expanded;
prospective_quantity = this.quantity;
prospective_price = this.price;
_on_selection_changed = on_selection_changed;
}
@@ -95,18 +94,14 @@ namespace mbts.spark {
public string last_price_text => _fmt_price(last_price);
public string avg_price_text => _fmt_price(avg_price);
public uint quantity => order.quantity();
public uint display_quantity => quantity.safe_divide(_display_qty_divisor);
public uint filled_quantity => order.filled();
public uint display_filled_quantity => filled_quantity.safe_divide(_display_qty_divisor);
public string last_msg => order.last_msg();
// piq
public uint piq => order_tuple.order.piq ?? uint.MaxValue;
public string piq_text => piq == uint.MaxValue ? string.Empty : piq.safe_divide(_display_qty_divisor).ToString();
public string piq_text => piq == uint.MaxValue ? string.Empty : piq.ToString();
public bool is_non_terminal => order.is_non_terminal();
public bool is_attachable { get => _is_attachable; set => SetVisual(ref _is_attachable, value); }
public bool is_detachable { get => _is_detachable; set => SetVisual(ref _is_detachable, value); }
public string filled_status_desc => order.filled_status_description(security.spec_or_default(), _display_qty_divisor);
public string filled_status_desc => order.filled_status_description(security.spec_or_default());
// Root Algo
public CustomOrderDefinition.CustomOneofCase root_algo_order_type => order_tuple.root.algo.value.CustomCase;
@@ -134,12 +129,12 @@ namespace mbts.spark {
}
}
public uint prospective_quantity {
get => _prospective_qty_translator.actual_value;
set => _prospective_qty_translator.actual_value = value;
}
public uint prospective_display_quantity {
get => _prospective_qty_translator.display_value;
set => _prospective_qty_translator.display_value = value;
get => _prospective_quantity;
set {
if (Set(ref _prospective_quantity, value)) {
RaisePropertyChanged(nameof(is_qty_dirty));
}
}
}
public double prospective_price {
get => _prospective_price;
@@ -229,29 +224,6 @@ namespace mbts.spark {
}
}
uint _display_qty_divisor {
get {
_sync_display_qty_if_dirty();
return _display_qty_divisor_backing;
}
}
IDisplayTranslator<uint> _prospective_qty_translator {
get {
if (_prospective_qty_translator_backing == null) {
_prospective_qty_translator_backing = new DisplayQuantityMultiplier {
conversion_data = new QuantityConversionData(_display_qty_divisor, QuantityConversionData.OpType.DivideActual),
actual_value = quantity
};
_prospective_qty_translator.value_changed += () =>
RaisePropertyChanged(nameof(is_qty_dirty));
} else {
_sync_display_qty_if_dirty();
}
return _prospective_qty_translator_backing;
}
}
#region IHierarchicalRow
bool IHierarchicalRow.is_parent => is_parent;
bool IHierarchicalRow.is_expanded => is_expanded;
@@ -295,35 +267,22 @@ namespace mbts.spark {
if ((Columns.OrderType & dirty) != 0)
RaisePropertyChanged(nameof(order_type), nameof(order_metadata));
// quantity
if ((Columns.Size & dirty) != 0) {
RaisePropertyChanged(nameof(quantity), nameof(is_qty_dirty));
// we need to update the prospective_quantity here b/c an update could have come in while the row was in edit mode
// via the Mini OB update column mechanism (vs the EditableQty column)
// prospecive price does not need to be updated in the same manner b/c if the price is changed, the whole
// row will no longer match the filter
prospective_quantity = quantity;
}
// price
if ((Columns.Price & dirty) != 0) {
RaisePropertyChanged(nameof(price));
RaisePropertyChanged(nameof(is_price_dirty));
}
// security
if ((Columns.Security & dirty) != 0) {
_display_qty_divisor_dirty = true;
RaisePropertyChanged(
// security props
nameof(security),
nameof(security_text),
// qty props
nameof(display_quantity),
nameof(prospective_quantity),
nameof(prospective_display_quantity),
nameof(is_qty_dirty),
//
nameof(piq_text),
nameof(display_filled_quantity));
}
if ((Columns.Size & dirty) != 0) {
RaisePropertyChanged(nameof(quantity), nameof(display_quantity));
if (_prospective_qty_translator_backing != null)
_prospective_qty_translator_backing.actual_value = quantity;
}
if ((Columns.Filled & dirty) != 0) {
RaisePropertyChanged(nameof(filled_quantity), nameof(display_filled_quantity));
}
if ((Columns.Security & dirty) != 0)
RaisePropertyChanged(nameof(security), nameof(security_text));
// badge fill
if (old_order_tuple.order.meta?.badge_color != order_tuple.order.meta?.badge_color) {
// invalidate by setting to null (will be re-instantiated in the public getter)
@@ -400,15 +359,6 @@ namespace mbts.spark {
security.Value.ContractSpec.format(v) :
Math.Round(v, _unknown_security_price_num_fractional_digits, MidpointRounding.AwayFromZero).ToString(_unknown_security_price_format_string);
void _sync_display_qty_if_dirty() {
if (_display_qty_divisor_dirty) {
_display_qty_divisor_dirty = false;
_display_qty_divisor_backing = _owner.get_display_qty_divisor(security);
if (_prospective_qty_translator_backing != null)
_prospective_qty_translator_backing.conversion_data = new QuantityConversionData(_display_qty_divisor_backing, QuantityConversionData.OpType.DivideActual);
}
}
void RaisePropertyChanged(params string[] props) {
var pc = PropertyChanged;
if (pc == null)
@@ -447,7 +397,6 @@ namespace mbts.spark {
Columns.LastMessage |
Columns.Piq,
filled_status_desc_mask =
Columns.Security |
Columns.Filled |
Columns.AvgFilled |
Columns.Status;
@@ -639,9 +639,6 @@ namespace mbts.spark {
public OrderMetadata get_metadata(OrderBookRowVM row) => _model.factory.get_metadata(row.order.actual_order_type());
internal uint get_display_qty_divisor(SecurityImmutable? security) =>
_model.security_auxiliary_model.get_display_quantity_divisor(security);
internal void show_order_modify(OrderBookRowVM row) =>
_bus.post(SparkTags.ShowModifyOrderTag, row.order_key);
@@ -801,7 +798,7 @@ namespace mbts.spark {
readonly Property<bool> _is_dirty;
IRaiseCanExecuteChangedCommand _payup_bid_cmd, _payup_ask_cmd;
IAlgoParamVM<double> _price_param;
AlgoParamVM<uint?> _size_param;
IAlgoParamVM<uint?> _size_param;
AlgoParamPermissions
_price_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write,
_size_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write;
@@ -838,9 +835,7 @@ namespace mbts.spark {
_price_param = null;
_size_param = null;
} else {
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size, EqualityComparer<uint?>.Default,
to_display_value: v => v.HasValue ? v.Value / display_quantity_divisor : v,
from_display_value: v => v.HasValue ? v.Value * display_quantity_divisor : v) {
_size_param = new AlgoParamVM<uint?>("size", "Size", owner._selected_params_mon.size) {
permissions = _size_permissions & _param_permissions
};
var price_param = new AlgoParamVM<double>("price", "Price", owner._selected_params_mon.price, spec?.tick_equality_comparer() ?? MathUtils.epsilon_eq_comparer) {
@@ -883,11 +878,10 @@ namespace mbts.spark {
get => _security_md;
set {
if (Set(ref _security_md, value)) {
RaisePropertyChanged(nameof(security), nameof(spec), nameof(display_quantity_divisor));
RaisePropertyChanged(nameof(security), nameof(spec));
bbo.security_md = value;
_payup_bid_cmd.RaiseCanExecuteChanged();
_payup_ask_cmd.RaiseCanExecuteChanged();
_size_param?.refresh_display_value();
}
}
}
@@ -909,7 +903,6 @@ namespace mbts.spark {
public AlgoDefinitionVM algo_vm { get; }
public IReadOnlyObservableCollection<AlgoParamRowVM> param_rows { get; }
public BBOVM bbo { get; }
public uint display_quantity_divisor => _owner.get_display_qty_divisor(security);
public ICommand send_order { get; }
public ICommand payup_bid_cmd => _payup_bid_cmd;
public ICommand payup_ask_cmd => _payup_ask_cmd;
@@ -62,7 +62,7 @@
</Setter>
</Style>
<Style x:Key="{x:Static usp:OrderParamsPanel.BestBidSizeButtonStyleKey}" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_actual_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="CommandParameter" Value="{Binding BestBidSize, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="ToolTip">
<Setter.Value>
@@ -82,7 +82,7 @@
</Setter>
</Style>
<Style x:Key="{x:Static usp:OrderParamsPanel.BestAskSizeButtonStyleKey}" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_actual_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="Command" Value="{Binding DataContext.size_calculator.set_value_command, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="CommandParameter" Value="{Binding BestAskSize, RelativeSource={RelativeSource AncestorType=usp:OrderParamsPanel}}"/>
<Setter Property="ToolTip">
<Setter.Value>
@@ -101,7 +101,7 @@
<TextBlock.Inlines>
<Run Text="{Binding security, Mode=OneWay}" FontWeight="Bold"/>
<Run Text="{Binding side, Mode=OneWay}" Foreground="{Binding side, Converter={StaticResource SideToBrushConverter}}"/>
<Run Text="{Binding display_quantity, Mode=OneWay}"/>
<Run Text="{Binding quantity, Mode=OneWay}"/>
<Run>@</Run>
<Run>
<Run.Text>
@@ -5,7 +5,6 @@ using bts.utils;
using mbts;
using mbts.spark;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.controls;
using mbts.utils;
using System;
@@ -141,18 +140,15 @@ namespace bts.shell.pnl {
public PositionPriceSize summary =>
new PositionPriceSize(long_data.working_total, long_data.avg_price, short_data.working_total, short_data.avg_price);
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders, QuantityConversionData qty_conversion) =>
Create(orders.Select(o => (o, qty_conversion)));
public static WorkingOrderData Create(IEnumerable<OrderRecordImmutable> orders) =>
orders.Aggregate(Empty, (seed, order) => seed.Add(order));
public static WorkingOrderData Create(IEnumerable<(OrderRecordImmutable order, QuantityConversionData qty_conversion)> orders) =>
orders.Aggregate(Empty, (seed, tup) => seed.Add(tup.order, tup.qty_conversion));
public WorkingOrderData Add(OrderRecordImmutable order, QuantityConversionData qty_conversion) {
public WorkingOrderData Add(OrderRecordImmutable order) {
if (order.working_quantity() <= 0)
return new WorkingOrderData(long_data, short_data);
switch (order.side()) {
case Side.Buy: return new WorkingOrderData(long_data.Add(order, qty_conversion), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order, qty_conversion));
case Side.Buy: return new WorkingOrderData(long_data.Add(order), short_data);
case Side.Sell: return new WorkingOrderData(long_data, short_data.Add(order));
default: throw ExceptionBuilder.Argument.unexpected_case(order.side(), "order.side");
}
}
@@ -169,21 +165,18 @@ namespace bts.shell.pnl {
}
readonly struct WorkingOrderSideData {
public readonly double avg_price, display_avg_price;
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable order, QuantityConversionData qty_conversion)>> data;
public readonly double avg_price;
public readonly ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data;
public readonly int working_total;
public readonly int display_working_total;
WorkingOrderSideData(double avg_price, int working_total, double display_avg_price, int display_working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>> data) {
WorkingOrderSideData(double avg_price, int working_total, ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderRecordImmutable>> data) {
this.avg_price = avg_price;
this.working_total = working_total;
this.display_avg_price = display_avg_price;
this.data = data;
this.display_working_total = display_working_total;
}
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(OrderDataHelper.PriceComparer));
public static WorkingOrderSideData EmptyLong => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(new ReverseComparer<double>(OrderDataHelper.PriceComparer)));
public static WorkingOrderSideData EmptyShort => new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(OrderDataHelper.PriceComparer));
public double top_price =>
data.Count > 0 ?
@@ -192,35 +185,23 @@ namespace bts.shell.pnl {
public int top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.order.working_quantity()) :
data.Values.First().Values.Sum(o => o.working_quantity()) :
0;
public int display_top_volume =>
data.Count > 0 ?
data.Values.First().Values.Sum(o => o.qty_conversion.to_display(o.order.working_quantity())) :
0;
public WorkingOrderSideData Add(OrderRecordImmutable order_record) =>
Add(order_record, new QuantityConversionData());
public WorkingOrderSideData Add(OrderRecordImmutable order_record, QuantityConversionData qty_conversion) {
public WorkingOrderSideData Add(OrderRecordImmutable order_record) {
uint order = order_record.Key;
int working_qty = order_record.working_quantity();
double price = order_record.price();
if (working_qty <= 0)
throw new ArgumentException(nameof(working_qty));
uint order = order_record.Key;
int dworking_qty = qty_conversion.to_display(working_qty);
double price = order_record.price();
if (working_total == 0) {
Log.assert(display_working_total == 0);
return new WorkingOrderSideData(price, working_qty, display_avg_price, dworking_qty, data.Add(price, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>.Empty.Add(order, (order_record, qty_conversion))));
return new WorkingOrderSideData(price, working_qty, data.Add(price, ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
} else return new WorkingOrderSideData(
avg_price + ((price - avg_price) * ((double)working_qty / (working_total + working_qty))),
working_total + working_qty,
display_avg_price + ((price - display_avg_price) * ((double)dworking_qty / (display_working_total + dworking_qty))),
display_working_total + dworking_qty,
data.SetItem(price, data.TryGetValue(price, out var curr) ?
curr.Add(order, (order_record, qty_conversion)) :
ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>.Empty.Add(order, (order_record, qty_conversion))));
curr.Add(order, order_record) :
ImmutableDictionary<uint, OrderRecordImmutable>.Empty.Add(order, order_record)));
}
public WorkingOrderSideData Remove(uint order, double price) {
@@ -229,30 +210,22 @@ namespace bts.shell.pnl {
throw new KeyNotFoundException(nameof(price));
if (!wdata.TryGetValue(order, out var worder))
throw new KeyNotFoundException(nameof(order));
var wqty = worder.order.working_quantity();
var dwqty = worder.qty_conversion.to_display(wqty);
var wqty = worder.working_quantity();
wdata = wdata.Remove(order);
if (wdata.Count == 0)
tmp_data = tmp_data.Remove(price);
if (tmp_data.Count == 0)
return new WorkingOrderSideData(double.NaN, 0, double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, (OrderRecordImmutable, QuantityConversionData)>>(data.KeyComparer));
return new WorkingOrderSideData(double.NaN, 0, ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderRecordImmutable>>(data.KeyComparer));
return new WorkingOrderSideData(
avg_price - ((price - avg_price) * (wqty / (working_total - wqty))),
working_total - wqty,
display_avg_price - ((price - display_avg_price) * (dwqty / (display_working_total - dwqty))),
display_working_total - dwqty,
tmp_data);
}
public override string ToString() {
if (data.Count > 0) {
var best_order = data.Values.First().Values.First();
return OrderUtils.order_collection_summary(
best_order.order.price(),
display_avg_price,
best_order.qty_conversion.to_display(best_order.order.working_quantity()),
display_working_total,
SecurityDataHelper.Defaults.ContractSpecificationImmutable);
return OrderUtils.order_collection_summary(best_order.price(), avg_price, best_order.working_quantity(), working_total, SecurityDataHelper.Defaults.ContractSpecificationImmutable);
}
return string.Empty;
}
@@ -275,7 +248,6 @@ namespace bts.shell.pnl {
public readonly PNLKey key;
public readonly SecurityImmutable? security;
public readonly QuantityConversionData qty_conversion;
public readonly string account_name;
public readonly PNLCalculationType calc_type;
public readonly PositionPriceSize day, raw_day, sod;
@@ -301,8 +273,7 @@ namespace bts.shell.pnl {
WorkingOrderData? working = null,
SecurityImmutable? security = null,
string account_name = null,
PNLCalculationType calc_type = PNLCalculationType.Mid,
QuantityConversionData? qty_conversion = null) {
PNLCalculationType calc_type = PNLCalculationType.Mid) {
//
this.key = key;
this.cpp = cpp;
@@ -317,7 +288,6 @@ namespace bts.shell.pnl {
this.security = security;
this.account_name = account_name;
this.calc_type = calc_type;
this.qty_conversion = qty_conversion ?? new QuantityConversionData();
}
[Flags]
@@ -398,10 +368,6 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
sod_long_avg_price_delta_raw, sod.long_pos,
sod_short_avg_price_delta_raw, sod.short_pos);
// display
public double display_sod_net_pos => qty_conversion.to_display(sod_net_pos);
public double display_sod_long_pos => qty_conversion.to_display(sod_long_pos);
public double display_sod_short_pos => qty_conversion.to_display(sod_short_pos);
/// Day
public double day_pnl => cpp *
@@ -429,19 +395,11 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
day_long_avg_price_delta_raw, day.long_pos,
day_short_avg_price_delta_raw, day.short_pos);
// display
public double display_day_net_pos => qty_conversion.to_display(day_net_pos);
public double display_day_long_pos => qty_conversion.to_display(day_long_pos);
public double display_day_short_pos => qty_conversion.to_display(day_short_pos);
/// Raw Day
public double raw_day_long_pos => raw_day.long_pos;
public double raw_day_short_pos => raw_day.short_pos;
public double raw_day_net_pos => raw_day.net_pos;
// display
public double display_raw_day_net_pos => qty_conversion.to_display(raw_day_net_pos);
public double display_raw_day_long_pos => qty_conversion.to_display(raw_day_long_pos);
public double display_raw_day_short_pos => qty_conversion.to_display(raw_day_short_pos);
/// Total
public double total_pnl => DoubleUtil.nan_to_zero_sum(sod_pnl, day_pnl);
@@ -470,10 +428,6 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
total_long_avg_price_delta_raw, total_long_pos,
total_short_avg_price_delta_raw, total_short_pos);
// display
public double display_total_net_pos => qty_conversion.to_display(total_net_pos);
public double display_total_long_pos => qty_conversion.to_display(total_long_pos);
public double display_total_short_pos => qty_conversion.to_display(total_short_pos);
#region Working
// long
@@ -499,10 +453,6 @@ namespace bts.shell.pnl {
MathUtils.weighted_average(
working_long_avg_price_delta_raw, working_long_pos,
working_short_avg_price_delta_raw, working_short_pos);
// display
public double display_working_net_pos => qty_conversion.to_display(working_net_pos);
public double display_working_long_pos => qty_conversion.to_display(working_long_pos);
public double display_working_short_pos => qty_conversion.to_display(working_short_pos);
#endregion
WaterfallPriceFlags _waterfall_flags() {
@@ -558,8 +508,7 @@ namespace bts.shell.pnl {
settlement_price.Equals(other.settlement_price) &&
calc_type.Equals(other.calc_type) &&
raw_day == other.raw_day &&
working.summary == other.working.summary &&
qty_conversion == other.qty_conversion;
working.summary == other.working.summary;
public override bool Equals(object obj) =>
obj is PNLRowData other && Equals(other);
@@ -574,7 +523,7 @@ namespace bts.shell.pnl {
}
static class PNLRowDataFactory {
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value, QuantityConversionData qty_conversion) t) =>
public static PNLRowData create((SecurityImmutable? sec, PNLUnitImmutable value) t) =>
new PNLRowData(new PNLKey(t.value.Pos.AccountId, t.value.Pos.SecurityId),
t.value.CashValue,
t.value.BidPrice,
@@ -586,8 +535,7 @@ namespace bts.shell.pnl {
t.value.Pos.raw_day_pps(),
WorkingOrderData.Empty,
t.sec,
t.value.Pos.Account,
qty_conversion: t.qty_conversion);
t.value.Pos.Account);
}
static class PNLDataExtensions {
@@ -606,7 +554,6 @@ namespace bts.shell.pnl {
working ?? pnl_data.working,
security ?? pnl_data.security,
account_name ?? pnl_data.account_name,
calc_type ?? pnl_data.calc_type,
qty_conversion: pnl_data.qty_conversion);
calc_type ?? pnl_data.calc_type);
}
}
}
@@ -16,15 +16,7 @@ namespace bts.shell.pnl {
decimal sod_long_pos,
decimal sod_short_pos,
decimal working_long_pos = 0,
decimal working_short_pos = 0,
decimal? display_day_long_pos = null,
decimal? display_day_short_pos = null,
decimal? display_raw_day_long_pos = null,
decimal? display_raw_day_short_pos = null,
decimal? display_sod_long_pos = null,
decimal? display_sod_short_pos = null,
decimal? display_working_long_pos = null,
decimal? display_working_short_pos = null) {
decimal working_short_pos = 0) {
//
this.day_pnl = day_pnl;
this.day_long_pos = day_long_pos;
@@ -36,15 +28,6 @@ namespace bts.shell.pnl {
this.sod_short_pos = sod_short_pos;
this.working_long_pos = working_long_pos;
this.working_short_pos = working_short_pos;
// display
this.display_day_long_pos = display_day_long_pos ?? day_long_pos;
this.display_day_short_pos = display_day_short_pos ?? day_short_pos;
this.display_raw_day_long_pos = display_raw_day_long_pos ?? raw_day_long_pos;
this.display_raw_day_short_pos = display_raw_day_short_pos ?? raw_day_short_pos;
this.display_sod_long_pos = display_sod_long_pos ?? sod_long_pos;
this.display_sod_short_pos = display_sod_short_pos ?? sod_short_pos;
this.display_working_long_pos = display_working_long_pos ?? working_long_pos;
this.display_working_short_pos = display_working_short_pos ?? working_short_pos;
}
// sod
@@ -52,48 +35,24 @@ namespace bts.shell.pnl {
public decimal sod_long_pos { get; }
public decimal sod_short_pos { get; }
public decimal sod_net_pos => sod_long_pos - sod_short_pos;
// sod display
public decimal display_sod_long_pos { get; }
public decimal display_sod_short_pos { get; }
public decimal display_sod_net_pos => display_sod_long_pos - display_sod_short_pos;
// day
public decimal day_pnl { get; }
public decimal day_long_pos { get; }
public decimal day_short_pos { get; }
public decimal day_net_pos => day_long_pos - day_short_pos;
// display
public decimal display_day_long_pos { get; }
public decimal display_day_short_pos { get; }
public decimal display_day_net_pos => display_day_long_pos - display_day_short_pos;
// raw day
public decimal raw_day_long_pos { get; }
public decimal raw_day_short_pos { get; }
public decimal raw_day_net_pos => raw_day_long_pos - raw_day_short_pos;
// display
public decimal display_raw_day_long_pos { get; }
public decimal display_raw_day_short_pos { get; }
public decimal display_raw_day_net_pos => display_raw_day_long_pos - display_raw_day_short_pos;
// total
public decimal total_pnl => sod_pnl + day_pnl;
public decimal total_net_pos => total_long_pos - total_short_pos;
public decimal total_long_pos => sod_long_pos + day_long_pos;
public decimal total_short_pos => sod_short_pos + day_short_pos;
// display
public decimal display_total_long_pos => display_sod_long_pos + display_day_long_pos;
public decimal display_total_short_pos => display_sod_short_pos + display_day_short_pos;
public decimal display_total_net_pos => display_total_long_pos - display_total_short_pos;
// working
public decimal working_long_pos { get; }
public decimal working_short_pos { get; }
public decimal working_net_pos => working_long_pos - working_short_pos;
// display
public decimal display_working_long_pos { get; }
public decimal display_working_short_pos { get; }
public decimal display_working_net_pos => display_working_long_pos - display_working_short_pos;
public decimal working_short_pos { get; }
public bool Equals(PNLNormalizedData other) =>
day_pnl == other.day_pnl &&
@@ -105,17 +64,6 @@ namespace bts.shell.pnl {
sod_long_pos == other.sod_long_pos &&
sod_short_pos == other.sod_short_pos &&
working_long_pos == other.working_long_pos &&
working_short_pos == other.working_short_pos &&
// display
sod_long_pos == other.sod_long_pos &&
sod_short_pos == other.sod_short_pos &&
day_long_pos == other.day_long_pos &&
day_short_pos == other.day_short_pos &&
raw_day_long_pos == other.raw_day_long_pos &&
raw_day_short_pos == other.raw_day_short_pos &&
total_long_pos == other.total_long_pos &&
total_short_pos == other.total_short_pos &&
working_long_pos == other.working_long_pos &&
working_short_pos == other.working_short_pos;
public override bool Equals(object obj) =>
@@ -154,16 +102,7 @@ namespace bts.shell.pnl {
a.sod_long_pos + b.sod_long_pos,
a.sod_short_pos + b.sod_short_pos,
a.working_long_pos + b.working_long_pos,
a.working_short_pos + b.working_short_pos,
// display
a.display_day_long_pos + b.display_day_long_pos,
a.display_day_short_pos + b.display_day_short_pos,
a.display_raw_day_long_pos + b.display_raw_day_long_pos,
a.display_raw_day_short_pos + b.display_raw_day_short_pos,
a.display_sod_long_pos + b.display_sod_long_pos,
a.display_sod_short_pos + b.display_sod_short_pos,
a.display_working_long_pos + b.display_working_long_pos,
a.display_working_short_pos + b.display_working_short_pos);
a.working_short_pos + b.working_short_pos);
public static PNLNormalizedData operator -(PNLNormalizedData a, PNLNormalizedData b) =>
new PNLNormalizedData(
@@ -176,15 +115,6 @@ namespace bts.shell.pnl {
a.sod_long_pos - b.sod_long_pos,
a.sod_short_pos - b.sod_short_pos,
a.working_long_pos - b.working_long_pos,
a.working_short_pos - b.working_short_pos,
// display
a.display_day_long_pos - b.display_day_long_pos,
a.display_day_short_pos - b.display_day_short_pos,
a.display_raw_day_long_pos - b.display_raw_day_long_pos,
a.display_raw_day_short_pos - b.display_raw_day_short_pos,
a.display_sod_long_pos - b.display_sod_long_pos,
a.display_sod_short_pos - b.display_sod_short_pos,
a.display_working_long_pos - b.display_working_long_pos,
a.display_working_short_pos - b.display_working_short_pos);
a.working_short_pos - b.working_short_pos);
}
}
@@ -14,7 +14,7 @@ namespace bts.shell.pnl {
public readonly DIYGridAnimationModel<PNLView.Columns, TRow> animation_model = new DIYGridAnimationModel<PNLView.Columns, TRow>(TimeSpan.FromSeconds(0.3));
static readonly Func<WorkingOrderSideData, string> _fmt_working_order_data =
d => OrderUtils.order_collection_summary(d.top_price, d.display_top_volume, d.display_working_total);
d => OrderUtils.order_collection_summary(d.top_price, d.top_volume, d.working_total);
static readonly IComparer<WorkingOrderSideData> _working_total_comparer =
Comparer<int>.Default.wrap<int, WorkingOrderSideData>(d => d.working_total);
@@ -31,40 +31,40 @@ namespace bts.shell.pnl {
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.security?.Symbol ?? string.Empty);
security_cell = _cell_map.add(ColumnKeys.Security, d => (d.security), null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
security_cell = _cell_map.add(ColumnKeys.Security, d => d.security, null, EqualityComparer<SecurityImmutable?>.Default, Comparer<SecurityImmutable>.Default.to_nullable());
security_key = _cell_map.add(ColumnKeys.SecurityKey, d => d.key.security_id);
cpp = _cell_map.add(ColumnKeys.CPP, d => d.cpp);
// sod
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.display_sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.display_sod_long_pos);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price_raw);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.display_sod_short_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price_raw);
// day
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.display_day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.display_day_long_pos);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price_raw);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.display_day_short_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price_raw);
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
// raw day
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.display_raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.display_raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.display_raw_day_short_pos);
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
// working
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.display_working_net_pos);
working_net_pos = _cell_map.add_net_pos(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
working_long_pos = _cell_map.add_working_long(ColumnKeys.WorkingLongPos, d => d.working.long_data, d => d.bid_price);
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price_raw);
working_short_pos = _cell_map.add_working_short(ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price);
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price_raw);
// total
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.display_total_net_pos);
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.display_total_long_pos);
total_net_pos = _cell_map.add_net_pos(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_long_pos = _cell_map.add_long_pos(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price_raw);
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.display_total_short_pos);
total_short_pos = _cell_map.add_short_pos(ColumnKeys.TotalShortPos, d => d.total_short_pos);
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price_raw);
// market
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
@@ -112,16 +112,7 @@ namespace bts.shell.pnl {
sod_long,
sod_short,
DoubleUtil.round_to_decimal(data.working_long_pos),
DoubleUtil.round_to_decimal(data.working_short_pos),
// display
DoubleUtil.round_to_decimal(data.display_day_long_pos),
DoubleUtil.round_to_decimal(data.display_day_short_pos),
DoubleUtil.round_to_decimal(data.display_raw_day_long_pos),
DoubleUtil.round_to_decimal(data.display_raw_day_short_pos),
DoubleUtil.round_to_decimal(data.display_sod_long_pos),
DoubleUtil.round_to_decimal(data.display_sod_short_pos),
DoubleUtil.round_to_decimal(data.display_working_long_pos),
DoubleUtil.round_to_decimal(data.display_working_short_pos));
DoubleUtil.round_to_decimal(data.working_short_pos));
if (_normalized_data != nd) {
_normalized_data = nd;
updated?.Invoke(nd);
@@ -215,4 +206,4 @@ namespace bts.shell.pnl {
public override string ToString() =>
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
}
}
}
@@ -112,27 +112,27 @@ namespace bts.shell.pnl {
});
// sod
sod_pnl = _cell_map.add_pnl(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.display_sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.display_sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.display_sod_short_pos);
sod_net_pos = _cell_map.add_net_pos(ColumnKeys.SODNetPos, d => d.sod_net_pos);
sod_long_pos = _cell_map.add_long_pos(ColumnKeys.SODLongPos, d => d.sod_long_pos);
sod_short_pos = _cell_map.add_short_pos(ColumnKeys.SODShortPos, d => d.sod_short_pos);
// day
day_pnl = _cell_map.add_pnl(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.display_day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.display_day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.display_day_short_pos);
day_net_pos = _cell_map.add_net_pos(ColumnKeys.DayNetPos, d => d.day_net_pos);
day_long_pos = _cell_map.add_long_pos(ColumnKeys.DayLongPos, d => d.day_long_pos);
day_short_pos = _cell_map.add_short_pos(ColumnKeys.DayShortPos, d => d.day_short_pos);
// raw day
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.display_raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.display_raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.display_raw_day_short_pos);
raw_day_net_pos = _cell_map.add_net_pos(ColumnKeys.RawDayNetPos, d => d.raw_day_net_pos);
raw_day_long_pos = _cell_map.add_long_pos(ColumnKeys.RawDayLongPos, d => d.raw_day_long_pos);
raw_day_short_pos = _cell_map.add_short_pos(ColumnKeys.RawDayShortPos, d => d.raw_day_short_pos);
// total
total_pnl = _cell_map.add(ColumnKeys.TotalPNL, d => d.total_pnl, FormattingDelegates.Price<decimal>.Value);
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.display_total_net_pos);
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.display_total_long_pos);
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.display_total_short_pos);
total_net_pos = _cell_map.add(ColumnKeys.TotalNetPos, d => d.total_net_pos);
total_long_pos = _cell_map.add(ColumnKeys.TotalLongPos, d => d.total_long_pos);
total_short_pos = _cell_map.add(ColumnKeys.TotalShortPos, d => d.total_short_pos);
// working
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.display_working_net_pos);
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.display_working_long_pos);
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.display_working_short_pos);
working_net_pos = _cell_map.add(ColumnKeys.WorkingNetPos, d => d.working_net_pos);
working_long_pos = _cell_map.add(ColumnKeys.WorkingLongPos, d => d.working_long_pos);
working_short_pos = _cell_map.add(ColumnKeys.WorkingShortPos, d => d.working_short_pos);
_cell_map.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
+1 -13
View File
@@ -10,7 +10,6 @@ namespace mbts.spark {
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged, ITemplateSelectorKey {
readonly IEqualityComparer<T> _comparer;
readonly string _template_key;
readonly Func<T, T> _to_display_value, _from_display_value;
AlgoParamPermissions _permissions;
bool _is_enabled, _has_value;
T _committed_value, _value;
@@ -21,15 +20,13 @@ namespace mbts.spark {
public AlgoParamVM(string id, string display_name, T value, string template_key = null)
: this(id, display_name, value, EqualityComparer<T>.Default, template_key) { }
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null, Func<T, T> to_display_value = null, Func<T, T> from_display_value = null) {
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null) {
this.id = id;
this.display_name = display_name;
_value = value;
_committed_value = value;
_comparer = comparer;
_template_key = template_key;
_to_display_value = to_display_value;
_from_display_value = from_display_value;
}
public event PropertyChangedEventHandler PropertyChanged;
@@ -46,10 +43,6 @@ namespace mbts.spark {
_toggle_is_dirty();
}
}
public T display_value {
get => _to_display_value == null ? value : _to_display_value(value);
set => this.value = _from_display_value == null ? value : _from_display_value(value);
}
public AlgoParamPermissions permissions {
get => _permissions;
set {
@@ -61,7 +54,6 @@ namespace mbts.spark {
public bool has_value { get => _has_value; set => _set(ref _has_value, value); }
public bool is_dirty { get; private set; }
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
object IAlgoParamVM.display_value { get => display_value; set => display_value = (T)value; }
string ITemplateSelectorKey.key => _template_key;
@@ -72,9 +64,6 @@ namespace mbts.spark {
}
}
public void refresh_display_value() =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(display_value)));
public IObservable<T> subscribe_value() {
return Observable.Create<T>(o => {
o.OnNext(_value);
@@ -124,7 +113,6 @@ namespace mbts.spark {
value_changed(_value, _value = new_value);
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(value)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(display_value)));
}
void _toggle_is_dirty() {
@@ -18,7 +18,6 @@ namespace mbts.spark {
readonly Property<TimeInForce> _current_time_in_force;
readonly Property<CustomOrderDefinition.CustomOneofCase> _current_order_type;
readonly Property<bool> _price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty;
uint _display_quantity_divisor = 1;
public ExistingOrderTicketVM(OrdersModel model, IObservable<(SecurityMD? security_md, OrderRecordImmutable order)> order_source, IObservable<int[]> calculator_buttons) {
uint? key = null;
@@ -41,7 +40,7 @@ namespace mbts.spark {
(val, curr) => !double.IsNaN(val) && val != curr));
_size_dirty = backing(
nameof(size_dirty),
size_calculator.observe_actual_value()
size_calculator.observe_current(true)
.CombineLatest(_current_size.observe_current(true),
(val, curr) => val.HasValue && val.Value != curr));
_account_dirty = backing(
@@ -84,17 +83,14 @@ namespace mbts.spark {
_dispose = Disposable.Create(
size_calculator,
properties_vm,
security_md_source.OnDispatcher().Subscribe(smd => {
bbo.security_md = smd;
display_quantity_divisor = model.security_auxiliary_model.get_display_quantity_divisor(smd.security);
}),
security_md_source.OnDispatcher().Subscribe(smd => bbo.security_md = smd),
bbo,
order_source.OnDispatcher().Subscribe(ort => {
key = ort.order.Key;
if (DoubleHelper.IsNaNOrInfinity(price))
price = ort.order.State.Price;
if (!size_calculator.value.HasValue || !size_calculator.value_setter_called)
size_calculator.bypass_value_setter(ort.order.State.Quantity, true);
size_calculator.bypass_value_setter(ort.order.State.Quantity);
}),
Observable.Merge(_price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty).OnDispatcher().Subscribe(_ => RaisePropertyChanged(nameof(dirty_items_text))),
_price_dirty.observe_current(true).CombineLatest(_size_dirty.observe_current(true), (pd, sd) => pd || sd).DistinctUntilChanged().OnDispatcher().Subscribe(modify_order));
@@ -108,13 +104,6 @@ namespace mbts.spark {
public OrderPropertiesEditorVM properties_vm { get; }
public double price { get => _price.Value; set => _price.Value = value; }
public CalculatorLogic size_calculator { get; }
public uint display_quantity_divisor {
get => _display_quantity_divisor;
private set {
Set(ref _display_quantity_divisor, value);
size_calculator.display_divisor = value;
}
}
public double current_price => _current_price.Value;
public uint current_size => _current_size.Value;
public AccountTuple current_account => _current_account.Value;
@@ -148,4 +137,4 @@ namespace mbts.spark {
if (_order_type_dirty.Value) yield return "type";
}
}
}
}
+1 -12
View File
@@ -31,7 +31,6 @@ namespace mbts.spark {
class NewOrderTicketVM : DisposableViewModelBase, IRequestViewClose {
readonly Property<SecurityImmutable> _security;
uint _display_quantity_divisor = 1;
public NewOrderTicketVM(OrdersModel model, SecurityMD security_md, OrderDefinitionImmutable def, double price, uint size, IObservable<int[]> calculator_buttons)
: this(model, Observable.Return(security_md), Observable.Return(new InlineOrderTicketProperties(model, def, price, size)), calculator_buttons) { }
@@ -62,10 +61,7 @@ namespace mbts.spark {
set_price_command = new DelegateCommand<double>(set_price);
_dispose = Disposable.Create(
bbo,
security_md_source.OnDispatcher().Subscribe(smd => {
bbo.security_md = smd;
display_quantity_divisor = model.security_auxiliary_model.get_display_quantity_divisor(smd.security);
}),
security_md_source.OnDispatcher().Subscribe(smd => bbo.security_md = smd),
size_calculator = new CalculatorLogic(0, calculator_buttons),
props_source.OnDispatcher().Subscribe(p => on_props(p.price, p.size))
);
@@ -87,13 +83,6 @@ namespace mbts.spark {
public SecurityImmutable security => _security.Value;
public double price { get; set; }
public CalculatorLogic size_calculator { get; }
public uint display_quantity_divisor {
get => _display_quantity_divisor;
private set {
Set(ref _display_quantity_divisor, value);
size_calculator.display_divisor = value;
}
}
public OrderPropertiesEditorVM properties_vm { get; }
public BBOVM bbo { get; }
public bool close_on_send { get; set; }
@@ -85,7 +85,7 @@
usp:OrderModificationControl.IsDirty="{Binding size_dirty, FallbackValue={x:Null}}"
CommitOnEnter="True"
Focusable="False"
Value="{Binding size_calculator.display_value}"
Value="{Binding size_calculator.value}"
/>
</Grid>
<Grid Grid.Column="2">
@@ -114,26 +114,15 @@
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button
Command="{Binding size_calculator.set_actual_value_command}"
CommandParameter="{Binding bbo.bid_size}"
Foreground="{u:BTSColor MarketData.bid}"
>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="bbo.bid_size"/>
<Binding Path="display_quantity_divisor"/>
</MultiBinding>
</Button>
Content="{Binding bbo.bid_size}" Command="{Binding size_calculator.set_value_command}"
CommandParameter="{Binding bbo.bid_size}" Foreground="{u:BTSColor MarketData.bid}"
/>
<Button
Grid.Column="2"
Command="{Binding size_calculator.set_actual_value_command}"
Content="{Binding bbo.ask_size}" Command="{Binding size_calculator.set_value_command}"
CommandParameter="{Binding bbo.ask_size}"
Foreground="{u:BTSColor MarketData.ask}"
>
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}">
<Binding Path="bbo.ask_size"/>
<Binding Path="display_quantity_divisor"/>
</MultiBinding>
</Button>
/>
</Grid>
<Grid Grid.Row="4" Grid.RowSpan="3">
<Grid.RowDefinitions>
@@ -150,18 +139,9 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Margin="2,4,2,0"
Content="{Binding}"
Focusable="False"
Margin="2,4,2,0" Content="{Binding}" Focusable="False" CommandParameter="{Binding}"
Command="{Binding Path=DataContext.click, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
>
<Button.CommandParameter>
<MultiBinding Converter="{x:Static cv:DynamicDoubleMultiplierConverter.Instance}">
<Binding/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderModificationControl}"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
+12 -25
View File
@@ -1,16 +1,13 @@
<Window
x:Class="mbts.ui.spark.OrderTicket"
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=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 bbo, Mode=OneWay}"
Style="{StaticResource BTSWindowTitleTemplate}"
>
<Window x:Class="mbts.ui.spark.OrderTicket"
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=spark_api"
xmlns:u="clr-namespace:mbts.ui"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
u:WindowStyle.SizeToContent="WidthAndHeight"
u:WindowStyle.TitleContent="{Binding bbo, Mode=OneWay}"
Style="{StaticResource BTSWindowTitleTemplate}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -126,12 +123,7 @@
<TextBlock>
<TextBlock.Inlines>
<Run Text="{Binding security, Mode=OneWay}" FontWeight="Bold"/>
<Run Foreground="{u:BTSColor MarketData.bid}" FontSize="10">
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}" Mode="OneWay" StringFormat="({0})">
<Binding Path="bid_size" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderTicket}"/>
</MultiBinding>
</Run>
<Run Foreground="{u:BTSColor MarketData.bid}" FontSize="10" Text="{Binding bid_size, Mode=OneWay, StringFormat='{}({0})'}"/>
<Run Foreground="{u:BTSColor MarketData.bid}" FontWeight="Bold">
<Run.Text>
<MultiBinding Converter="{StaticResource TickDisplayConverter}" Mode="OneWay">
@@ -148,12 +140,7 @@
</MultiBinding>
</Run.Text>
</Run>
<Run Foreground="{u:BTSColor MarketData.ask}" FontSize="10">
<MultiBinding Converter="{x:Static cv:DynamicDoubleDivisorConverter.Instance}" Mode="OneWay" StringFormat="({0})">
<Binding Path="ask_size" Mode="OneWay"/>
<Binding Path="DataContext.display_quantity_divisor" RelativeSource="{RelativeSource AncestorType=usp:OrderTicket}"/>
</MultiBinding>
</Run>
<Run Foreground="{u:BTSColor MarketData.ask}" FontSize="10" Text="{Binding ask_size, Mode=OneWay, StringFormat='{}({0})'}"/>
</TextBlock.Inlines>
</TextBlock>
</DataTemplate>
@@ -70,7 +70,6 @@ namespace mbts.spark {
string id { get; }
string display_name { get; }
object value { get; set; }
object display_value { get; set; }
bool is_enabled { get; set; }
bool is_dirty { get; }
bool has_value { get; }
@@ -82,7 +81,6 @@ namespace mbts.spark {
interface IAlgoParamVM<T> : IAlgoParamVM {
event PropertyChange<T> value_changed;
new T value { get; set; }
new T display_value { get; set; }
}
interface IAlgoSelector : IAlgoParamVM<IAlgoVM>, IObserver<(IReadOnlyDictionary<AlgoKey, AlgoData> algo_data, AlgoKey? default_algo)> {
@@ -985,10 +985,8 @@ namespace mbts.spark {
#region IAlgoParamVM
IAlgoVM IAlgoParamVM<IAlgoVM>.value { get => selected_algo; set => selected_algo = value; }
IAlgoVM IAlgoParamVM<IAlgoVM>.display_value { get => selected_algo; set => selected_algo = value; }
event PropertyChange<IAlgoVM> IAlgoParamVM<IAlgoVM>.value_changed { add => selected_algo_changed += value; remove => selected_algo_changed -= value; }
object IAlgoParamVM.value { get => selected_algo; set => selected_algo = (IAlgoVM)value; }
object IAlgoParamVM.display_value { get => selected_algo; set => selected_algo = (IAlgoVM)value; }
bool IAlgoParamVM.has_value => _algo != null;
// changing child algos is not allowed, so an algo selector is never dirty
bool IAlgoParamVM.is_dirty => false;
+2 -2
View File
@@ -240,8 +240,8 @@ namespace mbts.spark {
public static string price_quantity_description(double price, uint quantity, ContractSpecificationImmutable spec) =>
new StringBuilderEx(quantity.ToString(), " @ ", spec.format(price) ?? price.ToString("#0.00##")).ToString();
public static string filled_status_description(this OrderRecordImmutable order, ContractSpecificationImmutable spec, uint display_qty_divisor = 1) {
var sb = new StringBuilderEx("filled ", order.filled().safe_divide(display_qty_divisor).ToString());
public static string filled_status_description(this OrderRecordImmutable order, ContractSpecificationImmutable spec) {
var sb = new StringBuilderEx("filled ", order.filled().ToString());
if (order.filled() > 0)
sb.Add(" @ ", spec.format(order.avg_price()));
sb.Add(", ", camel_to_title_case(order.status().ToString(), TextCapitalizationFormat.NoCaps));
+8 -28
View File
@@ -10,7 +10,6 @@ using bts.spark;
using bts.utils;
using mbts.config;
using mbts.spark.utilities;
using mbts.ui;
using mbts.utils;
using System;
using System.Collections.Generic;
@@ -27,9 +26,8 @@ using OrderUpdatePiq = bts.api.core.TableUpdate<bts.api.orderman.OrderSecurity,
namespace mbts.spark {
[Export]
class OrdersModel : IDisposable {
public static readonly bool piq_enabled = Configuration.get<bts.config.toggles.PlacementInQueue>().enabled;
public readonly MarketDataAuxiliaryModel md_auxiliary_model;
public readonly ISecurityAuxiliaryDataModel security_auxiliary_model;
public static readonly bool piq_enabled = Configuration.get<bts.config.toggles.PlacementInQueue>().enabled;
readonly IBus _bus;
readonly OrderFactory _factory;
@@ -46,20 +44,8 @@ namespace mbts.spark {
IDisposable _dispose;
[ImportingConstructor]
OrdersModel(IBus bus,
OrderFactory factory,
OrderService service,
PositionService position_service,
MBOService mbo_service,
SpreaderModel spreader,
DynamicOrderHelper dynamic_helper,
AdminModel admin_model,
IAlgoStudioModel algo_model,
ISecurityAuxiliaryDataModel security_auxiliary_model,
MarketDataAuxiliaryModel md_auxiliary_model,
SecurityStore sstore,
ErrorReporter error) {
//
OrdersModel(IBus bus, OrderFactory factory, OrderService service, PositionService position_service, MBOService mbo_service,
SpreaderModel spreader, DynamicOrderHelper dynamic_helper, AdminModel admin_model, IAlgoStudioModel algo_model, MarketDataAuxiliaryModel md_auxiliary_model, SecurityStore sstore, ErrorReporter error) {
_bus = bus;
_factory = factory;
_service = service;
@@ -71,7 +57,6 @@ namespace mbts.spark {
this.admin_model = admin_model;
this.algo_model = algo_model;
this.md_auxiliary_model = md_auxiliary_model;
this.security_auxiliary_model = security_auxiliary_model;
var scheduler = ThreadPoolScheduler.Instance;
var interval = TimeSpan.FromMilliseconds(100);
on_orders = admin_model
@@ -95,10 +80,7 @@ namespace mbts.spark {
_position_service.subscribei_pnl_data().Naggle(interval, scheduler),
p => p.Pos.AccountId)
.join(sstore, p => p.Pos.SecurityId)
.Select(
tup => (tup.sec, tup.val, new QuantityConversionData(security_auxiliary_model.get_display_quantity_divisor(tup.sec), QuantityConversionData.OpType.DivideActual)),
tup => tup.val.RecordId)
.TableCheckedAutoCorrect(EqualityComparer<(SecurityImmutable?, PNLUnitImmutable, QuantityConversionData)>.Default)
.TableCheckedAutoCorrect(EqualityComparer<(SecurityImmutable? sec, PNLUnitImmutable data)>.Default)
.Where(t => t.has_changes())
.Cache();
var working_orders = on_orders.Filter(tup => tup.order.is_active());
@@ -185,8 +167,8 @@ namespace mbts.spark {
working_account_order_groups,
PNLRowData.key_fn,
acc_orders => acc_orders.group_key,
(row, acc_orders, pnl_key) => row.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order), row.qty_conversion)),
(row, pnl_id) => row,
(pnl_row_data, acc_orders, pnl_key) => pnl_row_data.with(working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order))),
(pnl_row_data, pnl_id) => pnl_row_data,
(acc_orders, pnl_id) => {
string account;
SecurityImmutable? security;
@@ -200,9 +182,7 @@ namespace mbts.spark {
return new PNLRowData(pnl_id,
security: security,
account_name: account,
working: WorkingOrderData.Create(
acc_orders.table.state.Values.Select(t => t.order),
new QuantityConversionData(security_auxiliary_model.get_display_quantity_divisor(security), QuantityConversionData.OpType.DivideActual)));
working: WorkingOrderData.Create(acc_orders.table.state.Values.Select(t => t.order)));
},
PNLRowData.key_fn)
.Where(t => t.has_changes())
@@ -302,7 +282,7 @@ namespace mbts.spark {
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples { get; }
public IObservable<TableUpdate<FillTuple, string>> on_fill_tuples_with_root { get; }
public IObservable<IReadOnlyDictionary<uint, OrderSecurity>> working_orders { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable, QuantityConversionData), int>> on_pnl { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable), int>> on_pnl { get; }
public IObservable<TableUpdate<PNLRowData, PNLKey>> security_account_pnl { get; }
public IObservable<TableUpdate<(SecurityImmutable?, PositionUnitImmutable), int>> on_positions { get; }
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
@@ -1,45 +0,0 @@
using bts.api;
using bts.utils;
using mbts.config;
using System.Collections.Generic;
using System.ComponentModel.Composition;
namespace mbts.spark {
interface ISecurityAuxiliaryDataModel {
SecurityAuxiliaryData? get_auxiliary_data(SecurityImmutable security);
}
readonly struct SecurityAuxiliaryData {
public SecurityAuxiliaryData(uint quantity_divisor) =>
this.quantity_divisor = quantity_divisor;
public static SecurityAuxiliaryData Default => new SecurityAuxiliaryData(1);
public uint quantity_divisor { get; }
}
[Export(typeof(ISecurityAuxiliaryDataModel))]
class SecurityAuxiliaryDataModel : ISecurityAuxiliaryDataModel {
readonly Dictionary<string, SecurityAuxiliaryData> _auxiliary_data = new Dictionary<string, SecurityAuxiliaryData>();
public SecurityAuxiliaryDataModel() {
var sym_config = Configuration.get<bts.config.SymbolUIConfiguration>();
var sym_config_items = Configuration.get<bts.config.SymbolUIConfiguration>().items;
foreach (var item in sym_config_items)
_auxiliary_data[item.symbol] = new SecurityAuxiliaryData(item.display_quantity_divisor);
}
public SecurityAuxiliaryData? get_auxiliary_data(SecurityImmutable security) =>
security.ToString().SafeContains("ICE Lots") ?
new SecurityAuxiliaryData(1) :
_auxiliary_data.TryGetValue(security.Symbol, out var d) ? d : (SecurityAuxiliaryData?)null;
}
static class SecurityAuxiliaryDataModelExtensions {
public static uint get_display_quantity(this ISecurityAuxiliaryDataModel model, SecurityImmutable? security, uint actual_qty) =>
actual_qty.safe_divide(model.get_display_quantity_divisor(security));
public static uint get_display_quantity_divisor(this ISecurityAuxiliaryDataModel model, SecurityImmutable? security) =>
security.HasValue ? model.get_auxiliary_data(security.Value)?.quantity_divisor ?? 1 : 1;
}
}
@@ -38,6 +38,9 @@ namespace mbts {
public static bool IsNonZero(this double value) =>
value != 0 && !double.IsNaN(value) && !double.IsInfinity(value);
public static bool IsRealNonZero(this double value) =>
value > 0 && value != double.PositiveInfinity;
public static double CoerceNaN(double value) => CoerceNaN(value, 0.0);
public static double CoerceNaN(double value, double if_nan) => double.IsNaN(value) ? if_nan : value;
public static double CoerceNaNOrInfinity(double value) => CoerceNaNOrInfinity(value, 0.0);
@@ -1,34 +0,0 @@
using ProtoBuf;
using System;
namespace bts.proto {
[ProtoContract]
readonly struct ToggleInt32Proto : IEquatable<ToggleInt32Proto> {
public ToggleInt32Proto(int value, bool is_enabled) {
this.value = value;
this.is_enabled = is_enabled;
}
[ProtoMember(1)]
public int value { get; }
[ProtoMember(2)]
public bool is_enabled { get; }
public bool Equals(ToggleInt32Proto other) =>
value == other.value && is_enabled == other.is_enabled;
public override bool Equals(object obj) =>
obj is ToggleInt32Proto other && Equals(other);
public override int GetHashCode() =>
(value % (1 << 31)) | (is_enabled ? 1 << 31 : 0);
public static bool operator ==(ToggleInt32Proto a, ToggleInt32Proto b) => a.Equals(b);
public static bool operator !=(ToggleInt32Proto a, ToggleInt32Proto b) => !a.Equals(b);
}
static class ToggleInt32ProtoExtensions {
public static int? as_nullable(this ToggleInt32Proto toggle) =>
toggle.is_enabled ? toggle.value : (int?)null;
}
}
@@ -1,43 +1,39 @@
using System;
using System.Text.RegularExpressions;
namespace mbts.spark.filters {
static class RegexFilterUtils {
public static Regex build_regex(string str) {
if (str == null)
return null;
str = str.Trim();
if (str == string.Empty)
return null;
return new Regex(str, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
}
}
class RegexFilterVM<TRow> : FilterVMBase<TRow, string, string> {
string _text = string.Empty;
Regex _regex;
public RegexFilterVM(Func<TRow, string> get_row_val, string prop_name = null) : base(get_row_val, prop_name) { }
public string text {
get => _text;
set => _regex = RegexFilterUtils.build_regex(_text = (value ?? string.Empty));
}
public override string get_state() => _text ?? string.Empty;
protected override bool _get_active() => _regex != null;
protected override bool _filter(string value) =>
_regex != null && (
_regex.IsMatch(value) ||
(!string.IsNullOrEmpty(value) && _regex.IsMatch(value.Replace(" ", ""))));
using System;
using System.Text.RegularExpressions;
protected override void _load_state_internal(string state) => text = state;
protected override void _clear() {
_close();
_text = string.Empty;
_regex = null;
RaisePropertyChanged(nameof(text));
_raise_filter_changed();
}
}
namespace mbts.spark.filters {
static class RegexFilterUtils {
public static Regex build_regex(string str) {
if (str == null)
return null;
str = str.Trim();
if (str == string.Empty)
return null;
return new Regex(str, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
}
}
class RegexFilterVM<TRow> : FilterVMBase<TRow, string, string> {
string _text = string.Empty;
Regex _regex;
public RegexFilterVM(Func<TRow, string> get_row_val, string prop_name = null) : base(get_row_val, prop_name) { }
public string text {
get => _text;
set => _regex = RegexFilterUtils.build_regex(_text = (value ?? string.Empty));
}
public override string get_state() => _text ?? string.Empty;
protected override bool _get_active() => _regex != null;
protected override bool _filter(string value) => _regex != null && _regex.IsMatch(value);
protected override void _load_state_internal(string state) => text = state;
protected override void _clear() {
_close();
_text = string.Empty;
_regex = null;
RaisePropertyChanged(nameof(text));
_raise_filter_changed();
}
}
}
@@ -1,38 +0,0 @@
using bts.utils;
using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
namespace mbts.ui.converters {
[ValueConversion(typeof(double), typeof(string))]
class DoubleDivisorConverter : IValueConverter {
public double Divisor { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value is double d ? d.safe_divide(Divisor) :
value is int i ? i.safe_divide(Divisor) :
value is uint u ? u.safe_divide(Divisor) :
double.NaN;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
value is double d ? d * Divisor :
value is int i ? (double)i * Divisor :
value is uint u ? (double)u * Divisor :
double.NaN;
}
class DynamicDoubleDivisorConverter : IMultiValueConverter {
public static DynamicDoubleDivisorConverter Instance { get; } = new DynamicDoubleDivisorConverter();
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (ConvertUtils.try_convert_args(values, out double value, out double div)) {
return value.safe_divide(div);
}
return new ValidationResult(false, "unable to convert values to doubles.");
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
}
@@ -1,6 +1,5 @@
using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
namespace mbts.ui.converters {
@@ -20,18 +19,4 @@ namespace mbts.ui.converters {
value is uint u ? (double)u / Multiplier :
double.NaN;
}
class DynamicDoubleMultiplierConverter : IMultiValueConverter {
public static DynamicDoubleMultiplierConverter Instance { get; } = new DynamicDoubleMultiplierConverter();
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (ConvertUtils.try_convert_args(values, out double value, out double mult)) {
return value * mult;
}
return new ValidationResult(false, "unable to convert values to doubles.");
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
}
}
@@ -183,7 +183,7 @@ namespace mbts.ui.dpi {
}
public static void DrawHorizontalLine(this DrawingContext drawing_context, System.Windows.Media.Pen pen, Point start, double length, bool snap_to_device_pixels) {
if (pen == null || !length.is_real_positive()) {
if (pen == null || !length.IsRealNonZero()) {
// no need to flip out and throw an exception
return;
}
@@ -1,5 +1,4 @@
using bts.utils;
using System;
using System;
using System.ComponentModel;
using System.Windows.Markup;
@@ -20,7 +19,7 @@ namespace mbts.ui {
public double RoundingUnit { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider) =>
RoundingUnit.is_real_non_zero() ? Math.Round(Value * RoundingUnit) / RoundingUnit : Value;
RoundingUnit.IsRealNonZero() ? Math.Round(Value * RoundingUnit) / RoundingUnit : Value;
}
internal class DoubleStringTypeConverter : TypeConverter
@@ -45,16 +45,15 @@ namespace mbts.spark {
readonly SecurityImmutable _security;
readonly MBOSecurityQueryImmutable _query;
readonly Dictionary<(string,bool), FormattedText> _cache;
readonly Func<int, string> _fmt_mbo_qty;
int _max_order_size = 1;
double _height = 0;
double _number_width = 0;
public MBOPopupModel(MBOService mbo, SecurityImmutable security, MBOSecurityQueryImmutable query, Func<int, string> fmt_mbo_qty) {
public MBOPopupModel(MBOService mbo, SecurityImmutable security, MBOSecurityQueryImmutable query) {
_mbo = mbo;
_security = security;
_query = query;
_fmt_mbo_qty = fmt_mbo_qty ?? new Func<int, string>(v => v.ToString());
// prime the cache with numbers on the left hand side
_cache =
@@ -111,7 +110,7 @@ namespace mbts.spark {
pdp.row_numbers = p.orders.Select((o, i) => format_text((i+1).ToString(), ismine(o))).ToList();
pdp.numbers_width = _number_width = Math.Max(_number_width, (int)pdp.row_numbers.Max(r => r.Width) + text_margin);
pdp.row_labels = p.orders.Select(o => format_text(_fmt_mbo_qty(o.Size) + (ismine(o) ? "*" : ""), ismine(o))).ToList();
pdp.row_labels = p.orders.Select(o => format_text(o.Size.ToString() + (ismine(o) ? "*" : ""), ismine(o))).ToList();
var max_label_width = pdp.row_labels.Max(l => l.Width) + text_margin;
var available_width = pdp.width - pdp.numbers_width - max_label_width;
@@ -7,53 +7,26 @@ using System.Windows.Input;
namespace mbts.spark {
public class CalculatorLogic : DisposableViewModelBase {
readonly UserProperty<uint?> _value;
readonly OptionalDisplayQuantityMultiplier _display_translator = new OptionalDisplayQuantityMultiplier();
IDisposable _prop_changes_echo;
int[] _buttons;
public CalculatorLogic(uint? initial_size) {
public CalculatorLogic(uint? initial_size) =>
_value = new UserProperty<uint?>(backing(nameof(value), initial_size));
_echo_props_changed();
}
public CalculatorLogic(uint? initial_size, IObservable<int[]> buttons_source) : this(initial_size) =>
_dispose = buttons_source.Subscribe(newval => buttons = newval);
public uint display_divisor {
get => _display_translator.conversion_data.value;
set => _display_translator.conversion_data = new QuantityConversionData(value, QuantityConversionData.OpType.DivideActual);
}
public uint? value {
get => _display_translator.actual_value;
set => _display_translator.actual_value = value;
}
public uint? display_value {
get => _display_translator.display_value;
set => _display_translator.display_value = value;
}
public uint? value { get => _value.Value; set => _value.Value = value; }
public bool value_setter_called => _value.setter_called;
public IObservable<uint?> value_observable => _value.Select(vals => vals.newval);
public int[] buttons { get => _buttons; set => Set(ref _buttons, value); }
public ICommand click => new DelegateCommand<uint>(handle_calculator_click);
public ICommand set_display_value_command => new DelegateCommand<uint>(v => _display_translator.display_value = v);
public ICommand set_actual_value_command => new DelegateCommand<uint>(v => _display_translator.actual_value = v);
public ICommand clear => new DelegateCommand(() => _display_translator.actual_value = null);
public ICommand set_value_command => new DelegateCommand<uint>(v => _value.Value = v);
public ICommand clear => new DelegateCommand(() => _value.Value = null);
public IObservable<uint?> observe_actual_value() =>
_display_translator.subscribe_actual_value();
public IObservable<uint?> observe_current(bool start_immediately = false) =>
_value.observe_current(start_immediately);
// Don't use a default value for 'is_actual_value.'
// Quietly multiplying (or not multiplying) the size input is dangerous -
// force the caller to be specific.
public void bypass_value_setter(uint value, bool is_actual_value) {
Disposable.dispose(ref _prop_changes_echo);
if (is_actual_value) {
this.value = value;
} else {
display_value = value;
}
if (_prop_changes_echo != null)
_echo_props_changed();
}
public void bypass_value_setter(uint value) => _value.bypass_setter(value);
public bool validate() {
if (!value.HasValue || value.Value == 0) {
@@ -65,34 +38,25 @@ namespace mbts.spark {
public void flash_error() =>
flash_error(nameof(value)).ignore();
protected override void Dispose(bool disposing) {
Disposable.dispose(ref _prop_changes_echo);
base.Dispose(disposing);
}
void _echo_props_changed() {
// dispose the echo to avoid raising prop changed events more than once
_prop_changes_echo?.Dispose();
_prop_changes_echo = _display_translator.connect_owner(this, nameof(display_value), nameof(value));
}
void handle_calculator_click(uint value) {
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) || value == 0) {
_display_translator.actual_value = value;
_value.Value = value;
} else if (KeyboardExtensions.IsCtrlDown()) {
if (!_display_translator.actual_value.HasValue) {
_display_translator.actual_value = 0;
} else if (value > _display_translator.actual_value) {
_display_translator.actual_value = 0;
} else {
_display_translator.actual_value -= value;
if (!_value.Value.HasValue)
_value.Value = 0;
else {
if (value > _value.Value)
_value.Value = 0;
else
_value.Value -= value;
}
} else {
if (!_display_translator.actual_value.HasValue)
_display_translator.actual_value = value;
}
else {
if (!_value.Value.HasValue)
_value.Value = value;
else
_display_translator.actual_value += value;
_value.Value += value;
}
}
}
}
}
@@ -1,172 +0,0 @@
using bts.utils;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
namespace mbts.ui {
interface IDisplayTranslator<T> {
T display_value { get; set; }
T actual_value { get; set; }
event Action value_changed;
}
readonly struct QuantityConversionData : IEquatable<QuantityConversionData> {
public QuantityConversionData(uint value, OpType op) {
this.value = value;
this.op = op;
}
public enum OpType {
None,
MultiplyActual,
DivideActual
}
public uint value { get; }
public OpType op { get; }
public bool Equals(QuantityConversionData other) =>
value == other.value &&
op == other.op;
public override bool Equals(object obj) =>
obj is QuantityConversionData other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.auto_phase(value.GetHashCode(), op.GetHashCode());
public static bool operator ==(QuantityConversionData x, QuantityConversionData y) => x.Equals(y);
public static bool operator !=(QuantityConversionData x, QuantityConversionData y) => !x.Equals(y);
}
class DisplayQuantityMultiplier : IDisplayTranslator<uint> {
uint _actual_value;
QuantityConversionData _conversion_data;
public event Action value_changed;
public uint actual_value {
get => _actual_value;
set {
if (_actual_value != value) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public uint display_value {
get => _conversion_data.to_display(_actual_value);
set => actual_value = _conversion_data.from_display(value);
}
public QuantityConversionData conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
class OptionalDisplayQuantityMultiplier : IDisplayTranslator<uint?> {
uint? _actual_value;
QuantityConversionData _conversion_data;
public event Action value_changed;
public uint? actual_value {
get => _actual_value;
set {
if (_actual_value != value) {
_actual_value = value;
value_changed?.Invoke();
}
}
}
public uint? display_value {
get => _actual_value.HasValue ? _conversion_data.to_display(_actual_value.Value) : _actual_value;
set => actual_value = value.HasValue ? _conversion_data.from_display(value.Value) : value;
}
public QuantityConversionData conversion_data {
get => _conversion_data;
set {
if (_conversion_data != value) {
_conversion_data = value;
value_changed?.Invoke();
}
}
}
}
static class DisplayTranslatorExtensions {
public static IDisposable connect_owner<T>(this IDisplayTranslator<T> display_translator, IRaiseNotifyPropertyChanged owner, params string[] properties) {
if (display_translator is null)
throw new ArgumentNullException(nameof(display_translator));
if (owner is null)
throw new ArgumentNullException(nameof(owner));
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
for (int i = 0; i < properties.Length; i++)
owner.raise_notify_property_changed(properties[i]);
}
}
public static IObservable<T> subscribe_actual_value<T>(this IDisplayTranslator<T> display_translator) =>
subscribe_actual_value(display_translator, EqualityComparer<T>.Default);
public static IObservable<T> subscribe_actual_value<T>(this IDisplayTranslator<T> display_translator, IEqualityComparer<T> comparer) {
if (comparer is null)
throw new ArgumentNullException(nameof(comparer));
return Observable.Create<T>(o => {
var curr = display_translator.actual_value;
o.OnNext(curr);
display_translator.value_changed += on_value_changed;
return Disposable.Create(() => display_translator.value_changed -= on_value_changed);
void on_value_changed() {
var next = display_translator.actual_value;
if (!comparer.Equals(curr, next))
o.OnNext(curr = next);
}
});
}
public static uint to_display(this QuantityConversionData quantity_conversion_data, uint actual_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return actual_value.safe_divide(quantity_conversion_data.value);
case QuantityConversionData.OpType.MultiplyActual: return actual_value * quantity_conversion_data.value;
default: return actual_value;
}
}
public static int to_display(this QuantityConversionData quantity_conversion_data, int actual_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return (int)(actual_value.safe_divide(quantity_conversion_data.value));
case QuantityConversionData.OpType.MultiplyActual: return (int)(actual_value * quantity_conversion_data.value);
default: return actual_value;
}
}
public static double to_display(this QuantityConversionData quantity_conversion_data, double actual_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return actual_value.safe_divide(quantity_conversion_data.value);
case QuantityConversionData.OpType.MultiplyActual: return actual_value * quantity_conversion_data.value;
default: return actual_value;
}
}
public static uint from_display(this QuantityConversionData quantity_conversion_data, uint display_value) {
switch (quantity_conversion_data.op) {
case QuantityConversionData.OpType.DivideActual: return display_value * quantity_conversion_data.value;
case QuantityConversionData.OpType.MultiplyActual: return display_value.safe_divide(quantity_conversion_data.value);
default: return display_value;
}
}
}
}
@@ -109,9 +109,6 @@ namespace mbts.ui.spark {
/// These are private members, so any code-readability cost is confined to a limited scope.
delegate void _SizeFormatter(ref uint merged, ref uint implied, ref StringBuilder sb);
delegate void _SizeFormatterNPS(ref NativePriceSize nps, ref StringBuilder sb);
public static Func<uint, string> get_value_formatter(uint divisor) =>
value => (value / divisor).ToString();
public static Func<NativePriceSize, string> get_formatter_nps(MarketSizeDisplay size_display) {
_SizeFormatterNPS implied_absent = _get_formatter_nps(size_display.normal);
@@ -123,11 +120,6 @@ namespace mbts.ui.spark {
};
}
public static Func<NativePriceSize, string> get_formatter_nps(MarketSizeDisplay size_display, uint display_divisor) {
var raw_fmt_size = get_formatter(size_display);
return nps => raw_fmt_size((nps.implied_size + nps.direct_size) / display_divisor, nps.implied_size / display_divisor);
}
public static SizeFormatter get_formatter(MarketSizeDisplay size_display) {
_SizeFormatter implied_absent = _get_formatter(size_display.normal);
_SizeFormatter implied_present = _get_formatter(size_display.implied_present ?? size_display.normal);
@@ -289,4 +281,4 @@ namespace mbts.ui.spark {
public void refresh() => _display.refresh();
}
}
}
}
@@ -39,18 +39,13 @@ namespace bts.api {
static string _format(SecurityImmutable? security) {
switch (security?.ContractCase) {
case ContractOneofCase.Future:
if(security?.HasIce == true)
return _format_ICE(security);
return format_future(security?.Symbol, security?.security_desc(), security.Value.Future);
case ContractOneofCase.Future: return format_future(security?.Symbol, security?.security_desc(), security.Value.Future);
case ContractOneofCase.Spread:
if (security?.HasGlbx == true) {
// In case we ever bring it back, pretty printed spread names can be found here:
// GlbxSecurityFormatter.FormatSpread(security, out string result)
return security?.Glbx.SecurityDesc;
}
else if(security?.HasIce == true)
return _format_ICE(security);
return string.Join("", _format_future_legs(SecurityHelper.get_flat_future_legs(security?.Spread.Legs)));
case ContractOneofCase.Synthetic: return security?.Synthetic.Name;
default: return "Unknown-security";
@@ -84,28 +79,5 @@ namespace bts.api {
default: return $"{fl.quantity:+0;-0}{sym_exp_name}";
}
}
static string _format_ICE (SecurityImmutable? security){
string symbol = security?.Symbol,
strip = security?.Ice.StripName,
md_desc = security?.Ice.MarketDescription,
type_append = "";
if(md_desc.Contains("25k ICE Lots")){
if(md_desc.Contains("TAS"))
type_append = "TAS";
}
else if(md_desc.Contains("ICE Lots")){
if(md_desc.Contains("TAS"))
type_append = "ICE Lots TAS";
else
type_append = "ICE Lots";
}
else if(md_desc.Contains("TAS"))
type_append = "TAS";
type_append = string.IsNullOrEmpty(type_append) ?
" " : $" {type_append} ";
return $"{symbol}{type_append}{strip}";
}
}
}
-43
View File
@@ -110,28 +110,6 @@ namespace bts.utils {
return epsilon_equals(with_int_value, d);
}
public static bool is_real(this double value) {
switch (value) {
case double.PositiveInfinity:
case double.NegativeInfinity:
case double.NaN:
return false;
default:
return true;
}
}
public static bool is_real_positive(this double value) =>
value > 0 && value != double.PositiveInfinity;
public static bool is_real_negative(this double value) =>
value < 0 && value != double.NegativeInfinity;
public static bool is_real_non_zero(this double value) =>
value > 0 ?
!double.IsPositiveInfinity(value) :
value < 0 && !double.IsNegativeInfinity(value);
public static double factor_out(double value, params double[] factors) {
foreach (double f in factors)
value = factor_out(value, f, out int _);
@@ -151,27 +129,6 @@ namespace bts.utils {
return value;
}
public static int safe_divide(this int value, int divisor) =>
divisor == 0 ? value : value / divisor;
public static int safe_divide(this int value, uint divisor) =>
divisor == 0 ? value : (int)(value / divisor);
public static double safe_divide(this int value, double divisor) =>
divisor.is_real_non_zero() ? value / divisor : value;
public static uint safe_divide(this uint value, uint divisor) =>
divisor == 0 ? value : value / divisor;
public static double safe_divide(this uint value, double divisor) =>
divisor.is_real_non_zero() ? value / divisor : value;
public static ulong safe_divide(this ulong value, uint divisor) =>
divisor == 0 ? value : value / divisor;
public static double safe_divide(this double value, double divisor) =>
divisor.is_real_non_zero() && value.is_real_non_zero() ? value / divisor : value;
/// <summary>
/// Adds <see cref="mod"/> to the modulos result if negative.
/// </summary>
-3
View File
@@ -14,9 +14,6 @@ namespace bts.utils {
}
public static class FormattingDelegates {
public static Func<T, string> default_string<T>() =>
v => v.ToString();
public static class Price {
public static readonly Func<double, string> Value =
p => double.IsNaN(p) ? "" : p.ToString("#,0.00");
@@ -13,8 +13,5 @@ namespace bts.utils {
[Pure]
public static bool IsNullOrEmpty(this string str) =>
string.IsNullOrEmpty(str);
public static bool SafeContains(this string str, string value) =>
str != null && str.Contains(value);
}
}
@@ -15,7 +15,7 @@
<server name="default">
<ip>63.247.113.163</ip>
<port>3000</port>
<userName>katas2f-ps</userName>
<userName>bluetrd-ps</userName>
<password>Starts123</password>
<getStripInfoMessages>N</getStripInfoMessages>
<strategyPreference>New</strategyPreference>
@@ -24,8 +24,8 @@
<server name="endex">
<ip>63.247.113.77</ip>
<port>3000</port>
<userName>katas2f-ps</userName>
<password>Starts123</password>
<userName>Value from ICE Support</userName>
<password>Value from ICE Support</password>
<getStripInfoMessages>N</getStripInfoMessages>
<strategyPreference>New</strategyPreference>
<localNetworkInterface></localNetworkInterface>
+2 -23
View File
@@ -105,35 +105,14 @@ message BasePositionBatch {
google.protobuf.Timestamp current_as_of = 10; // UTC
}
// security overrides /////////////////////////////////////
message CppOverride{
double value = 1;
}
message SecurityParameters{
uint32 security_key = 1;
CppOverride cpp_override = 2;
// other overrides
}
message SecurityParametersBatchOp{
repeated SecurityParameters op_params = 1;
}
message SecurityParametersBatchOpResponse {
bool ok = 1;
string msg = 2;
}
service PositionService {
option (bts.api.service_description) = "Provides Position and PNL, per security, per account";
rpc create_manual_fill (ManualFillBatch) returns (PositionServiceResponse);
rpc update_manual_fill (ManualFillBatch) returns (PositionServiceResponse);
rpc remove_manual_fill (ManualFillBatch) returns (PositionServiceResponse);
rpc set_base_position (BasePositionBatch) returns (SetBasePositionResponse);
rpc update_security_parameters (SecurityParametersBatchOp) returns (SecurityParametersBatchOpResponse);
rpc set_base_position (BasePositionBatch) returns (SetBasePositionResponse);
// subscription
rpc manual_fills(bts.api.VoidMessage) returns (stream ManualFill);
rpc position_data(bts.api.VoidMessage) returns (stream PositionUnit);
+136 -130
View File
@@ -1,130 +1,136 @@
#
# Common Market data options
#
MarketDataConfig {
rule: "DeploymentType.type == 'PROD'"
value {
use_sim: false
}
}
PublisherOptions {
rule: "DeploymentType.type == 'dev'"
value {
disable_heartbeat: true
}
}
PlatformMap {
value {
glbx_platform: "PLATFORM_MDP3ONIXS"
ice_platform: "PLATFORM_ICEONIXS"
}
}
PublisherNamespace {
value {
shared_memory_namespace: @System.os_user
}
}
CMEOptions {
value {
feed_engine {
name: "mdp3feed"
# udp_socket_buffer_size: 425984
# channels_per_thread: 1
spin_time: 0
}
channel {
id: "*"
feed_engine: "mdp3feed"
use_tcp_replay: false
}
}
}
#to make this work in dev clone the operations repo
#go here: c:\\code\\bts\\operations\\ice\\ICE_iMpactMulticastFeed_GettingStarted-1_1_36\\bin\\easyTunnel>
#modify the easyTunnelProxy.properties file to replace
#tunnel.multicast.interface=10.96.18.37 with your local ip
#run easyTunnelProxy.bat BRENT-FI for Brent Full implied
#you should see ice data
ICEOptions {
value {
channel {
name: "oil" #these names are arbitrary and are for debugging
market_type: 3
uds_market_type: 3
market_type: 4 #ICE IPE Gas Oil Futures
uds_market_type: 4
market_type: 5 #ICE Futures Brent, IPE Brent Futures
uds_market_type: 5
market_type: 9 #ICE Futures WTI, ICE WTI Crude Futures
uds_market_type: 9
market_type: 13 #ICE Heating Oil Futures
uds_market_type: 13
market_type: 15 #ICE NYH (RBOB) Gasoline Futures
uds_market_type: 15
##market_type: 21 #ICE Futures US Agricultures, Coffee C
##uds_market_type: 21
##market_type: 23 #ICE Futures US Agricultures, FCOJ A
##uds_market_type: 23
##market_type: 26 #ICE Futures US Financials DX
##uds_market_type: 26
uds_market_type: 11 #ICE WTI-Brent spreads
uds_market_type: 14 #ICE Heating Oil/WTI Futures Crack
uds_market_type: 16 #ICE NYH (RBOB) Gasoline/WTI Futures Crack
uds_market_type: 58 #ICE Futures UI Heating Oil / Brent
uds_market_type: 59 #ICE NYH (RBOB) Gasoline/Brent Futures Crack
uds_market_type: 66 #ICE NYH (RBOB) Gasoline/Heating Oil Spread
uds_market_type: 70 #ICE Heating Oil/LS Gasoil Futures Spread
uds_market_type: 71 #ICE LS Gasoil/Brent Futures Crack
uds_market_type: 72 #ICE NYH (RBOB) Gasoline/LS Gasoil Futures Spread
#onixs_log_mode: Debug
}
# channel {
# name: "juice"
# market_type: 26 #ICE Futures US Agricultures, FCOJ A
# }
ice_config_file: "ConnectivityConfiguration.APITest.xml"
#ice_config_file: "ConnectivityConfiguration.PerfTest.xml"
}
}
MDBootDelayWindow {
rule: "DeploymentType.type == 'dev'"
value {
delay_seconds: 0
}
}
MDBootDelayWindow {
value {
delay_seconds: 120
standard {
start: "16:30"
end: "16:40"
}
sunday {
start: "14:00"
end: "14:10"
}
}
}
DisplayFactorOverride {
value {
override {
symbol: "SOM"
exchange: "GLBX"
type: "SPREAD"
price_multiplier: 1
}
}
}
#
# Common Market data options
#
MarketDataConfig {
rule: "DeploymentType.type == 'PROD'"
value {
use_sim: false
}
}
PublisherOptions {
rule: "DeploymentType.type == 'dev'"
value {
disable_heartbeat: true
}
}
PlatformMap {
value {
glbx_platform: "PLATFORM_MDP3ONIXS"
ice_platform: "PLATFORM_ICEONIXS"
}
}
PublisherNamespace {
value {
shared_memory_namespace: @System.os_user
}
}
CMEOptions {
value {
feed_engine {
name: "mdp3feed"
# udp_socket_buffer_size: 425984
# channels_per_thread: 1
spin_time: 0
}
channel {
id: "*"
feed_engine: "mdp3feed"
use_tcp_replay: false
}
}
}
ICEPublisherOptions {
value {
network_interface: "eth1"
}
}
#to make this work in dev clone the operations repo
#go here: c:\\code\\bts\\operations\\ice\\ICE_iMpactMulticastFeed_GettingStarted-1_1_36\\bin\\easyTunnel>
#modify the easyTunnelProxy.properties file to replace
#tunnel.multicast.interface=10.96.18.37 with your local ip
#run easyTunnelProxy.bat BRENT-FI for Brent Full implied
#you should see ice data
ICEOptions {
value {
channel {
name: "oil" #these names are arbitrary and are for debugging
market_type: 3
uds_market_type: 3
market_type: 4 #ICE IPE Gas Oil Futures
uds_market_type: 4
market_type: 5 #ICE Futures Brent, IPE Brent Futures
uds_market_type: 5
market_type: 9 #ICE Futures WTI, ICE WTI Crude Futures
uds_market_type: 9
market_type: 13 #ICE Heating Oil Futures
uds_market_type: 13
market_type: 15 #ICE NYH (RBOB) Gasoline Futures
uds_market_type: 15
##market_type: 21 #ICE Futures US Agricultures, Coffee C
##uds_market_type: 21
##market_type: 23 #ICE Futures US Agricultures, FCOJ A
##uds_market_type: 23
##market_type: 26 #ICE Futures US Financials DX
##uds_market_type: 26
uds_market_type: 11 #ICE WTI-Brent spreads
uds_market_type: 14 #ICE Heating Oil/WTI Futures Crack
uds_market_type: 16 #ICE NYH (RBOB) Gasoline/WTI Futures Crack
uds_market_type: 58 #ICE Futures UI Heating Oil / Brent
uds_market_type: 59 #ICE NYH (RBOB) Gasoline/Brent Futures Crack
uds_market_type: 66 #ICE NYH (RBOB) Gasoline/Heating Oil Spread
uds_market_type: 70 #ICE Heating Oil/LS Gasoil Futures Spread
uds_market_type: 71 #ICE LS Gasoil/Brent Futures Crack
uds_market_type: 72 #ICE NYH (RBOB) Gasoline/LS Gasoil Futures Spread
#onixs_log_mode: Debug
}
# channel {
# name: "juice"
# market_type: 26 #ICE Futures US Agricultures, FCOJ A
# }
ice_config_file: "ConnectivityConfiguration.APITest.xml"
#ice_config_file: "ConnectivityConfiguration.PerfTest.xml"
}
}
MDBootDelayWindow {
rule: "DeploymentType.type == 'dev'"
value {
delay_seconds: 0
}
}
MDBootDelayWindow {
value {
delay_seconds: 120
standard {
start: "16:30"
end: "16:40"
}
sunday {
start: "14:00"
end: "14:10"
}
}
}
DisplayFactorOverride {
value {
override {
symbol: "SOM"
exchange: "GLBX"
type: "SPREAD"
price_multiplier: 1
}
}
}
-9
View File
@@ -168,13 +168,4 @@ message OffsetAttachUIOptions {
message ManualFillsOptions {
optional bool modal_transactions = 1 [default = true];
}
message SymbolUIConfigurationItem {
optional string symbol = 1 [default = ""];
optional uint32 display_quantity_divisor = 2 [default = 1];
}
message SymbolUIConfiguration {
repeated SymbolUIConfigurationItem items = 1;
}
-4
View File
@@ -59,10 +59,6 @@ message EnsureOnScreenWithWorkAreaRect {
optional bool enabled = 1 [default = false];
}
message CustomFormatIceSecurities{
optional bool enabled = 1 [default = true];
}
message IgnoreNansInDayPnL {
optional bool enabled = 1 [default = true];
}
+1 -3
View File
@@ -170,7 +170,7 @@ void NativeOrder::on_init_recovered(OrderRecord const &rec,
me->sec = serv.sec_db.resolve(SecurityKey(def.security_key()));
me->previous_working_position = Quantity(0);
auto &native_state = get_native_state(me->state);
auto const &native_state = get_native_state(me->state);
switch (me->state.status()) {
case ORDER_STATUS_WORKING:
@@ -202,8 +202,6 @@ void NativeOrder::on_init_recovered(OrderRecord const &rec,
}
// if we made it this far we are good.
me->routing_record = rq.routing_record;
*native_state.mutable_routing_record() = rq.routing_record;
if (me->state.status() != ORDER_STATUS_PENDING_WORKING)
me->exchange_order_id = native_state.exchange_order_id();
init_fix();
@@ -215,6 +215,13 @@ api::orderman::OrderDefinition
ICENativeOrder::on_init_from_exe_report(const ExecutionReport& er, const PreActivationOrderServices& os)
{
auto k = er.order_key;
auto nextid = me->services->gen_id();
if(k.value() < nextid)
{
k = OrderKey(nextid);
LOG(OrderMan, Error, "Updating recovered order id with newly generated id {}", k.value());
}
OrderRecord orecord;
orecord.set_key(k.value());
orecord.set_parent_key(null_order_key().value());
@@ -1,15 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1</TargetFrameworks>
<OutputType>Exe</OutputType>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="spark_sdk" Version="2.15.10" />
<PackageReference Include="System.Reactive" Version="4.4.1" />
<PackageReference Include="System.Reactive.Core" Version="4.4.1" />
<PackageReference Include="System.Reactive.Interfaces" Version="4.4.1" />
<PackageReference Include="System.Reactive.Linq" Version="4.4.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,68 @@
using bts.api.orderman;
using CommandLine;
namespace bts.api.csharp {
[Verb("watch", HelpText = "List items in the system.")]
class WatchOptions {
[Option("accounts", Required = false, SetName = "Accounts", HelpText = "print accounts in the system.")]
public bool accounts { get; set; }
[Option("securities", Required = false, SetName = "Securities", HelpText = "print securities in the system (may be combined with --filter-symbols, or --filter-securities).")]
public bool securities { get; set; }
[Option("symbols", Required = false, SetName = "Symbols", HelpText = "print all symbols in the system (may not be combined with other options).")]
public bool symbols { get; set; }
[Option("bbo", Required = false, SetName = "Books", HelpText = "print bbo for an optionally filtered set of securities/symbols.")]
public bool bbo { get; set; }
[Option("orders", Required = false, SetName = "Orders", HelpText = "print orders in the system.")]
public bool orders { get; set; }
[Option("positions", Required = false, SetName = "Positions", HelpText = "print positions in the system (may be combined with --filter-symbols, --filter-securities, and --filter-accounts).")]
public bool positions { get; set; }
// auxiliary filters
[Option("filter-securities", Required = false, HelpText = "filter securities with a regular expression (may be combined with --securities, and --bbo).")]
public string filter_securities { get; set; }
[Option("filter-symbols", Required = false, HelpText = "filter securities for a particular set of symbols (may be combined with --securities, and --bbo). Do not confuse with the --symbols option, which prints all symbols in the system.")]
public string filter_symbols { get; set; }
[Option("filter-accounts", Required = false, HelpText = "filter for a particular set of accounts.")]
public string filter_accounts { get; set; }
}
[Verb("send-order", HelpText = "Send orders.")]
class SendOrdersOptions {
// this option only applies to Iceberg orders
[Option("show-quantity", Required = false, SetName = "Iceberg")]
public double show_quantity { get; set; } = 5;
[Option("price", Required = false, HelpText = "specify the order price.")]
public double price { get; set; } = 100;
[Option("quantity", Required = false, HelpText = "specify the order quantity.")]
public uint quantity { get; set; } = 1;
[Option("account", Required = false, HelpText = "specify the account with which the order should be sent.")]
public string account { get; set; }
[Option("order-type", Required = false, HelpText = "specify the order type (limit, market, or iceberg).")]
public CustomOrderDefinition.CustomOneofCase order_type { get; private set; } = CustomOrderDefinition.CustomOneofCase.Limit;
/// Run the program with the argument <see cref="WatchOptions.securities"/> to display securities/security keys:
/// - display all securities:
/// bts.api.csharp.exe watch securities
/// - display all 'ES' securities:
/// bts.api.csharp.exe watch securities --symbols "ES"
/// - display all 'ES' and 'GC' securities:
/// bts.api.csharp.exe watch securities --symbols "ES,GC"
/// If no security key is specified, a random security is used.
[Option("security", Required = false, HelpText = "specify the order security by name, or id.")]
public string security { get; set; }
[Option("side", Required = false, HelpText = "specify the order side (buy/sell).")]
public Side side { get; set; } = Side.Buy;
}
}
@@ -1,74 +1,88 @@
using bts.api.core;
using bts.api.csharp.examples;
using bts.api.admin;
using bts.api.core;
using bts.api.csharp.data;
using bts.api.csharp.utils;
using bts.api.csharp.watch;
using bts.api.orderman;
using bts.api.position;
using CommandLine;
using System;
using System.Collections.Generic;
using System.Threading;
namespace bts.api.csharp {
class Program {
static void Main(string[] args) =>
Parser.Default.ParseArguments<Options>(args).WithParsed(_run);
static void _run(Options options) {
var exit_event = new ManualResetEvent(false);
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
var service_context = new ServiceContext();
var login = service_context.login("bts", "");
login.Wait();
var examples = new List<IDisposable>();
if (options.subscribe_securities ||! options.any_enabled) {
examples.Add(new SecurityExample(service_context.locate<MarketDataService>()));
}
if (options.subscribe_positions || !options.any_enabled) {
examples.Add(new PositionExample(service_context.locate<PositionService>()));
}
if (options.subscribe_orders || !options.any_enabled) {
examples.Add(new OrderExample(service_context.locate<OrderService>()));
}
if (options.subscribe_book || !options.any_enabled) {
examples.Add(new BookExample(service_context.locate<MarketDataService>()));
}
if (options.send_orders || !options.any_enabled) {
examples.Add(SendOrderLoop.init_loop(service_context.locate<OrderService>(), service_context.locate<MarketDataService>()));
}
Console.CancelKeyPress += on_cancel_key_press;
exit_event.WaitOne();
foreach (var ex in examples)
ex.Dispose();
//
void on_cancel_key_press(object sender, ConsoleCancelEventArgs e) {
exit_event.Set();
e.Cancel = true;
}
static void Main(string[] args) {
new Parser(cfg => cfg.CaseInsensitiveEnumValues = true)
.ParseArguments<WatchOptions, SendOrdersOptions>(args)
.MapResult(
_execution_shell<WatchOptions>(_watch),
_execution_shell<SendOrdersOptions>(_send_orders, true),
errors => 1);
}
class Options {
[Option("subscribe-securities", Required = false, HelpText = "run security-subscription example.")]
public bool subscribe_securities { get; set; }
static Func<T, int> _execution_shell<T>(Func<ServiceContext, T, IDisposable> execute, bool auto_quit = false) {
return opts => {
var exit_event = new ManualResetEvent(auto_quit);
Console.CancelKeyPress += on_cancel_key_press;
var service_context = new ServiceContext();
service_context.login("bts", "").Wait();
execute(service_context, opts);
exit_event.WaitOne();
return 0;
//
void on_cancel_key_press(object sender, ConsoleCancelEventArgs e) {
exit_event.Set();
e.Cancel = true;
}
};
}
[Option("subscribe-orders", Required = false, HelpText = "run order-subscription example.")]
public bool subscribe_orders { get; set; }
static IDisposable _watch(ServiceContext service_context, WatchOptions options) {
if (options.accounts) {
// watch accounts
return WatchAccounts.run(service_context.locate<AdminService>());
} else if (options.securities) {
// watch securities
var security_predicate = CLIUtils.parse_security_predicate(options.filter_symbols, options.filter_securities);
return WatchSecurities.run(service_context.locate<MarketDataService>(), security_predicate);
} else if (options.bbo) {
// watch bbo
return WatchBooks.run(new bts.md.MarketDataClient(),
CLIUtils.parse_security_predicate(options.filter_symbols, options.filter_securities));
} else if (options.symbols) {
// watch symbols
return WatchSymbols.run(service_context.locate<MarketDataService>());
} else if (options.orders) {
// watch orders (optionally filtered by account)
var account_predicate = CLIUtils.parse_delimited_predicate<OrderRecordImmutable>(
options.filter_accounts, order => order.Def.Account);
return WatchOrders.run(service_context.locate<OrderService>(), account_predicate);
} else if (options.positions) {
// watch positions (optionally filtered by account)
var account_predicate = CLIUtils.parse_delimited_predicate<PositionUnitImmutable>(
options.filter_accounts, pos => pos.Account);
return WatchPositions.run(service_context.locate<PositionService>(), account_predicate);
}
return null;
}
[Option("subscribe-positions", Required = false, HelpText = "run position-subscription example.")]
public bool subscribe_positions { get; set; }
[Option("subscribe-books", Required = false, HelpText = "run book-subscription example.")]
public bool subscribe_book { get; set; }
[Option("send-orders", Required = false, HelpText = "run send-orders example.")]
public bool send_orders { get; set; }
// if none are enabled, all options will be flipped on,
// so the user need not specify a single argument to see the examples run.
public bool any_enabled =>
subscribe_securities ||
subscribe_orders ||
subscribe_positions ||
subscribe_book ||
send_orders;
static IDisposable _send_orders(ServiceContext service_context, SendOrdersOptions options) {
var custom_order_def = OrderUtils.custom_order_definitioni(options.order_type);
if (!custom_order_def.HasValue) {
Console.WriteLine($"Unable to create order of type '{options.order_type}.' The following types are available:\n\t{string.Join("\n\t", OrderUtils.known_order_types)}");
return null;
}
var user = new NameIDPair(service_context.user, service_context.user_id);
var admin_service = service_context.locate<AdminService>();
var account = AdminUtils.get_account_async(user, admin_service, options.account).WaitResult();
var md_service = service_context.locate<MarketDataService>();
var security = SecurityUtils.get_security_async(md_service.subscribei_securities(), options.security).WaitResult();
var model = new OrderModel(service_context.locate<OrderService>());
/// Note that the call below (<see cref="OrderModel.send_order(NameIDPair, NameIDPair, uint, Side, uint, double, CustomOrderDefinition)"/>)
/// blocks the current thread until an ack is received from the server. If you wish to avoid blocking the thread as such,
/// use the following method instead: <see cref="OrderModel.send_order_async(NameIDPair, NameIDPair, uint, Side, uint, double, CustomOrderDefinition)"/>.
model.send_order(user, account, security.Id, options.side, options.quantity, options.price, custom_order_def.Value.CloneToMutable());
return null;
}
}
}
@@ -1,6 +1,6 @@
using System;
namespace bts.api.csharp {
namespace bts.api.csharp.data {
readonly struct NameIDPair {
public NameIDPair(string name, uint id) {
this.name = name ?? throw new ArgumentNullException(nameof(name));
@@ -0,0 +1,71 @@
using bts.api.csharp.utils;
using bts.api.orderman;
using System;
using System.Threading.Tasks;
namespace bts.api.csharp.data {
/// <summary>
/// Exemplifies how the <see cref="OrderService"/> api may be used to send orders.
/// </summary>
class OrderModel {
readonly OrderService _service;
public OrderModel(OrderService service) =>
_service = service;
public static OrderDefinition create_order_definition(NameIDPair user, NameIDPair account, uint security_key, Side side, CustomOrderDefinition custom_def) {
return new OrderDefinition {
Account = account.name,
AccountId = account.id,
User = user.name,
UserId = user.id,
SecurityKey = security_key,
Side = side,
CustomDef = custom_def
};
}
/// <summary>
/// Sends an order of the type specified by <paramref name="custom_order_definition"/> (asynchronous).
/// </summary>
/// <returns>The order id if the submission was successful, otherwise null.</returns>
public async Task<uint?> send_order_async(NameIDPair user, NameIDPair account, uint security_key, Side side, uint quantity, double price, CustomOrderDefinition custom_order_definition) {
Console.WriteLine($"Creating order {OrderUtils.fmt_order(side, quantity, price, user.name, account.name, custom_order_definition)}");
var response = await _service.create_order_async(new Create {
Quantity = quantity,
Price = price,
NewOrder = create_order_definition(user, account, security_key, side, custom_order_definition)
});
if (response.success) {
Console.WriteLine($"Order submission succeeded. Order id is {response.value.OrderKey}.");
return response.value.OrderKey;
} else {
Console.WriteLine($"Order submission failed: [{response.value.ErrorCode}] {response.value.Msg}.");
return null;
}
}
/// <summary>
/// Sends an order with specified parameters (the thread will be blocked until an ack is received from the server).
/// </summary>
public uint? send_order(NameIDPair user, NameIDPair account, uint security_key, Side side, uint quantity, double price, CustomOrderDefinition custom_order_definition) {
/// Note, the thread will be blocked until a response is received from the server.
/// For this reason, <see cref="OrderService.create_order_async(Create)"/> is usually preferred.
/// An example of how to use that method is provided in <see cref="send_order_async(NameIDPair, NameIDPair, CustomOrderDefinition, uint, double)"/>.
Console.WriteLine($"Creating order {OrderUtils.fmt_order(side, quantity, price, user.name, account.name, custom_order_definition)}");
var response = _service.create_order(new Create {
Quantity = quantity,
Price = price,
NewOrder = create_order_definition(user, account, security_key, side, custom_order_definition)
});
switch (response.ErrorCode) {
case OrderError.Ok:
Console.WriteLine($"Order submission succeeded. Order id is {response.OrderKey}.");
return response.OrderKey;
default:
Console.WriteLine($"Order submission failed: [{response.ErrorCode}] {response.Msg}.");
return null;
}
}
}
}
@@ -1,60 +0,0 @@
using bts.api.core;
using bts.api.csharp.utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using SecurityKey = System.UInt32;
namespace bts.api.csharp.examples {
/// <summary>
/// Uses the <see cref="MarketDataService"/> api to observe book updates
/// for security in the system, and organize them into a Dictionary.
/// </summary>
class BookExample : IDisposable {
readonly Dictionary<SecurityKey, BookEntryImmutable> _books = new Dictionary<SecurityKey, BookEntryImmutable>();
readonly IDisposable _subscription;
public BookExample(MarketDataService market_data_service) {
_subscription = market_data_service.subscribei_book()
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_on_book_update);
}
public void Dispose() {
_subscription.Dispose();
}
void _on_book_update(Update<BookEntryImmutable, uint> update) {
foreach (var new_book in update.added) {
// of course, we shouldn't have entries for any books in update.added yet
Debug.Assert(!_books.ContainsKey(new_book.Key));
// display bbo in the console window
_print_book_info(new_book.Key, $"book received for the first time, bbo is: {BookUtils.fmt_bbo(new_book)}");
// add the new book to our map
_books.Add(new_book.Key, new_book);
}
foreach (var change in update.changes) {
// of course, the security key shouldn't change
Debug.Assert(change.Old.Key == change.New.Key);
// our book entry for this security should be identitical to change.Old
Debug.Assert(change.Old.Bid.SequenceEqual(_books[change.Old.Key].Bid));
Debug.Assert(change.Old.Ask.SequenceEqual(_books[change.Old.Key].Ask));
// display the current book in the console window
_print_book_info(change.New.Key, $"book changed, now bbo is: {BookUtils.fmt_bbo(change.New)}");
// update our map with the changed book
_books[change.New.Key] = change.New;
}
foreach (var old_book in update.removed) {
// of course, we should have entries for all books in update.removed
Debug.Assert(_books.Remove(old_book.Key));
}
}
static void _print_book_info(SecurityKey key, string message) {
Console.WriteLine($"Security {key}: {message}.");
}
}
}
@@ -1,139 +0,0 @@
using bts.api.core;
using bts.api.orderman;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using AccountKey = System.UInt32;
using OrderKey = System.UInt32;
namespace bts.api.csharp.examples {
/// <summary>
/// Uses the <see cref="OrderService"/> to observe order updates,
/// and group them by account.
/// </summary>
class OrderExample : IDisposable {
readonly Dictionary<AccountKey, Dictionary<OrderKey, OrderRecordImmutable>> _data = new Dictionary<AccountKey, Dictionary<AccountKey, OrderRecordImmutable>>();
IDisposable _subscription;
public OrderExample(OrderService order_service) {
/// 'OrderService' has 2 methods which can be used to subscribe to order updates.
/// -------------------------------------------------------------------------------------
/// 1) <see cref="OrderService.subscribe_orders(string)"/>.
/// 2) <see cref="OrderService.subscribei_orders(string)"/>.
/// This method (no. 2) is typically preferrable because the data it provides is immutable.
/// --------------------------------------------------------------------------------------
_subscription = order_service.subscribei_orders()
// the 'ObserveOn' call below ensures that the
// callback will be invoked this thread.
// Be sure to do this if you don't want to worry
// about multi-threading issues (locking, etc).
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_on_update);
}
public event Action<AccountKey> account_orders_changed;
public void Dispose() {
_subscription.Dispose();
}
public IEnumerable<OrderRecordImmutable> get_account_orders(AccountKey account) {
if (_data.TryGetValue(account, out var account_orders)) {
return account_orders.Values;
}
return Enumerable.Empty<OrderRecordImmutable>();
}
void _on_order_added(OrderRecordImmutable new_order) {
if (!_data.TryGetValue(new_order.Def.AccountId, out var account_orders)) {
// this is the first order for this account.
// initialize the account orders dictionary, and add it to our data.
account_orders = new Dictionary<AccountKey, OrderRecordImmutable>();
_data.Add(new_order.Def.AccountId, account_orders);
}
// now add the order to account_orders
account_orders.Add(new_order.Key, new_order);
}
void _on_order_changed(Change<OrderRecordImmutable> change) {
// ch.New and ch.Old have the same key.
Debug.Assert(change.New.Key == change.Old.Key);
var key = change.New.Key;
// react to a changed order
if (change.New.State.FilledQuantity != change.Old.State.FilledQuantity) {
// Display new fills in the console window.
var num_new_fills = change.New.State.FilledQuantity - change.Old.State.FilledQuantity;
_print_order_info(key, $"received {num_new_fills} new fills.");
// Be mindful that orders can be overfilled.
if (change.New.State.FilledQuantity > change.New.State.Quantity) {
// Display overfills in the console window.
var num_over_fills = change.New.State.FilledQuantity - change.New.State.Quantity;
_print_order_info(key, $"overfilled by {num_over_fills}");
} else {
// Display working qty in the console window.
var working_qty = change.New.State.Quantity - change.New.State.FilledQuantity;
_print_order_info(key, $"still working {working_qty}.");
}
}
// Display order status changes in the console window.
if (change.New.State.Status != change.Old.State.Status) {
_print_order_info(key, $"status changed from {change.Old.State.Status} to {change.New.State.Status}.");
}
// Account will not change, so our dictionary will definitely have a key for this account.
Debug.Assert(_data.ContainsKey(change.New.Def.AccountId));
var account_orders = _data[change.New.Def.AccountId];
// The value for this key in account_orders should equal ch.Old.
Debug.Assert(account_orders[key].Equals(change.Old));
// update account orders with the changed order.
account_orders[key] = change.New;
}
void _on_order_removed(OrderRecordImmutable old_val) {
// An entry for this order should be present in our data,
// as it is in the 'removed' items.
// Assert this is the case to illustrate the point.
Debug.Assert(_data.ContainsKey(old_val.Def.AccountId));
var account_orders = _data[old_val.Def.AccountId];
// the removed value should match the one currently in our data.
// Assert this is the case, then remove the order.
Debug.Assert(account_orders[old_val.Key].Equals(old_val));
account_orders.Remove(old_val.Key);
_print_order_info(old_val.Key, "has been removed.");
}
void _on_update(Update<OrderRecordImmutable, OrderKey> update) {
// record which accounts experienced an order update, then
// at the end of this method, fire the account_orders_changed event for each
var accounts_with_order_updates = new HashSet<AccountKey>();
foreach (OrderRecordImmutable new_order in update.added) {
_on_order_added(new_order);
accounts_with_order_updates.Add(new_order.Def.AccountId);
}
foreach (Change<OrderRecordImmutable> change in update.changes) {
_on_order_changed(change);
accounts_with_order_updates.Add(change.New.Def.AccountId);
}
foreach (OrderRecordImmutable old_order in update.removed) {
_on_order_removed(old_order);
accounts_with_order_updates.Add(old_order.Def.AccountId);
}
// display all accounts with order updates in the Console window.
Console.WriteLine("The following accounts experienced order changes: ");
foreach (var account in accounts_with_order_updates) {
Console.WriteLine("\n\t" + account);
}
// lastly, fire the account_orders_changed event for each account experienced an order update.
foreach (var account in accounts_with_order_updates) {
account_orders_changed?.Invoke(account);
}
}
void _print_order_info(OrderKey order_key, string message) {
Console.WriteLine($"Order {order_key}: {message}.");
}
}
}
@@ -1,125 +0,0 @@
using bts.api.core;
using bts.api.position;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Linq;
using System.Threading;
namespace bts.api.csharp.examples {
/// <summary>
/// A pair of security, and account id,
/// which uniquely identifies each PositionUnit.
/// </summary>
readonly struct PositionKey {
public PositionKey(uint security_id, uint account_id) {
this.security_id = security_id;
this.account_id = account_id;
}
public uint security_id { get; }
public uint account_id { get; }
public static PositionKey from_position(PositionUnitImmutable p) =>
new PositionKey(p.SecurityId, p.AccountId);
public override string ToString() =>
$"Sec {security_id}, Acc {account_id}";
}
/// <summary>
/// Uses the <see cref="PositionService"/> api to monitor positions
/// for every account/security pair, and organize them into a Dictionary.
/// </summary>
class PositionExample : IDisposable {
readonly Dictionary<PositionKey, PositionUnitImmutable> _data = new Dictionary<PositionKey, PositionUnitImmutable>();
IDisposable _subscription;
public PositionExample(PositionService position_service) {
/// 'PositionService' has 2 methods which can be used to subscribe to position updates.
/// -------------------------------------------------------------------------------------
/// 1) <see cref="PositionService.subscribe_position_data(string)"/>.
/// 2) <see cref="PositionService.subscribei_position_data(string)"/>.
/// This method (no. 2) is typically preferrable because the data it provides is immutable.
/// --------------------------------------------------------------------------------------
_subscription = position_service.subscribei_position_data()
// the 'ObserveOn' call below ensures that the
// callback will be invoked this thread.
// Be sure to do this if you don't want to worry
// about multi-threading issues (locking, etc).
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_on_update);
}
public event Action data_changed;
public IReadOnlyDictionary<PositionKey, PositionUnitImmutable> data => _data;
public void Dispose() =>
_subscription.Dispose();
void _on_position_added(PositionUnitImmutable new_val) {
var k = PositionKey.from_position(new_val);
// this entry should NOT be present in our _data, as it is in the 'added' items.
// Assert this is the case to illustrate the point.
Debug.Assert(!_data.ContainsKey(k));
_print_position_info(k, $"initial position received. Day: Long {new_val.DayLongPos}, Short {new_val.DayShortPos}, Net {new_val.DayLongPos - new_val.DayShortPos}.");
// add the new position unit to _data
_data.Add(k, new_val);
}
void _on_position_changed(Change<PositionUnitImmutable> change) {
var k = PositionKey.from_position(change.New);
// ch.New and ch.Old have the same key (i.e., the Account/Security pair),
Debug.Assert(change.New.AccountId == change.Old.AccountId);
Debug.Assert(change.New.SecurityId == change.Old.SecurityId);
// of course, the value for this key in our dictionary should equal ch.Old.
Debug.Assert(_data[k].Equals(change.Old));
// Display changes to the day position in the console window.
// 1) check if the day long changed
if (change.New.DayLongPos != change.Old.DayLongPos) {
Console.WriteLine($"[{k}] day long changed from {change.Old.DayLongPos} to {change.New.DayLongPos}.");
}
// 2) check if day short changed
if (change.New.DayShortPos != change.Old.DayShortPos) {
Console.WriteLine($"[{k}] day short changed from {change.Old.DayShortPos} to {change.New.DayShortPos}.");
}
// 3) check if net day position changed
var new_net_day_pos = change.New.DayLongPos - change.New.DayShortPos;
var old_net_day_pos = change.Old.DayLongPos - change.Old.DayShortPos;
if (new_net_day_pos != old_net_day_pos) {
Console.WriteLine($"[{k}] day net changed from {old_net_day_pos} to {new_net_day_pos}.");
}
// update _data with the changed position.
_data[k] = change.New;
}
void _on_position_removed(PositionUnitImmutable old_val) {
var k = PositionKey.from_position(old_val);
// this entry should be present in our _data, as it is in the 'removed' items.
// Assert this is the case to illustrate the point.
Debug.Assert(_data.ContainsKey(k));
// this should match our data. Assert this is the case.
Debug.Assert(_data[k].Equals(old_val));
_data.Remove(k);
Console.WriteLine($"[{k}] position has been removed.");
}
void _on_update(Update<PositionUnitImmutable, int> update) {
foreach (PositionUnitImmutable p in update.added) {
_on_position_added(p);
}
foreach (Change<PositionUnitImmutable> ch in update.changes) {
_on_position_changed(ch);
}
foreach (PositionUnitImmutable p in update.removed) {
_on_position_removed(p);
}
data_changed?.Invoke();
}
void _print_position_info(PositionKey position_key, string message) {
Console.WriteLine($"Position {position_key}: {message}.");
}
}
}
@@ -1,48 +0,0 @@
using bts.api.core;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using SecurityKey = System.UInt32;
namespace bts.api.csharp.examples {
/// <summary>
/// Uses the <see cref="MarketDataService"/> api to observe the
/// state of securities in the system.
/// </summary>
class SecurityExample : IDisposable {
readonly IDisposable _subscription;
readonly Dictionary<SecurityKey, SecurityImmutable> _securities = new Dictionary<SecurityKey, SecurityImmutable>();
public SecurityExample(MarketDataService market_data_service) {
_subscription = market_data_service.subscribei_securities()
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_on_securities_update);
}
public void Dispose() {
_subscription.Dispose();
}
void _on_securities_update(Update<SecurityImmutable, uint> update) {
foreach (var new_security in update.added) {
// of course, we shouldn't have entries for any books in update.added yet
Debug.Assert(!_securities.ContainsKey(new_security.Id));
// display bbo in the console window
Console.WriteLine($"received new security: {new_security}.");
// add the new security to our map
_securities.Add(new_security.Id, new_security);
}
// As you may have noticed, all of the subcribe methods in the api
// return an IObservable<Update<T>>. It is worth noting that for security updates,
// 'changes' will always be empty.
Debug.Assert(!update.changes.Any());
foreach (var old_security in update.removed) {
// of course, we should have entries for all books in update.removed
Debug.Assert(_securities.Remove(old_security.Id));
}
}
}
}
@@ -1,110 +0,0 @@
using bts.api.csharp.utils;
using bts.api.orderman;
using System;
using System.Threading.Tasks;
namespace bts.api.csharp.examples {
static class OrderTemplates {
public static CustomOrderDefinition limit() => new CustomOrderDefinition { Limit = new LimitOrderDefinition() };
public static CustomOrderDefinition market() => new CustomOrderDefinition { Market = new MarketOrderDefinition() };
public static CustomOrderDefinition iceberg(double show_quantity) => iceberg(show_quantity, limit());
public static CustomOrderDefinition iceberg(double show_quantity, CustomOrderDefinition child) =>
new CustomOrderDefinition {
Iceberg = new IcebergOrderDefinition {
ShowQuantity = show_quantity,
Child = child
}
};
}
/// <summary>
/// Exemplifies how the <see cref="OrderService"/> api may be used to
/// send several common order types (limit, market, and iceberg).
/// </summary>
class SendOrderModel {
readonly OrderService _service;
public SendOrderModel(OrderService service) {
_service = service;
}
public OrderDefinition create_order_definition(NameIDPair user, NameIDPair account, uint security_key, CustomOrderDefinition custom_def) {
return new OrderDefinition {
Account = account.name,
AccountId = account.id,
User = user.name,
UserId = user.id,
SecurityKey = security_key,
CustomDef = custom_def
};
}
/// <summary>
/// Sends a limit order (asynchronous).
/// </summary>
/// <returns>The order id if the submission was successful, otherwise null.</returns>
public Task<uint?> send_limit_order_async(NameIDPair user, NameIDPair account, uint security_key, uint quantity, double price) {
return send_order_async(user, account, security_key, OrderTemplates.limit(), quantity, price);
}
/// <summary>
/// Sends a market order (asynchronous). Note no price parameter is required for market orders.
/// </summary>
/// <returns>The order id if the submission was successful, otherwise null.</returns>
public Task<uint?> send_market_order_async(NameIDPair user, NameIDPair account, uint security_key, uint quantity) {
return send_order_async(user, account, security_key, OrderTemplates.market(), quantity, 0.0);
}
/// <summary>
/// Sends an iceberg of limit orders (asynchronous).
/// </summary>
/// <returns>The order id if the submission was successful, otherwise null.</returns>
public Task<uint?> send_iceberg_order_async(NameIDPair user, NameIDPair account, uint security_key, uint quantity, double price, double show_quantity) {
return send_order_async(user, account, security_key, OrderTemplates.iceberg(show_quantity), quantity, price);
}
/// <summary>
/// Sends an order of the type specified by <paramref name="custom_order_definition"/> (asynchronous).
/// </summary>
/// <returns>The order id if the submission was successful, otherwise null.</returns>
public async Task<uint?> send_order_async(NameIDPair user, NameIDPair account, uint security_key, CustomOrderDefinition custom_order_definition, uint quantity, double price) {
Console.WriteLine($"submitting order for user ({user}), account ({account}): {OrderUtils.fmt_order(quantity, price, custom_order_definition)}.");
var reply = await _service.create_order_async(new Create {
Quantity = quantity,
Price = price,
NewOrder = create_order_definition(user, account, security_key, custom_order_definition)
});
if (reply.success) {
Console.WriteLine($"received ack that order has been submitted. Order id is {reply.value.OrderKey}.");
return reply.value.OrderKey;
} else {
Console.WriteLine($"order submission was unsuccessful: [{reply.value.ErrorCode}] {reply.value.Msg}.");
return null;
}
}
public void send_order(NameIDPair user, NameIDPair account, uint security_key, CustomOrderDefinition custom_order_definition, uint quantity, double price) {
/// Note, the thread will be blocked until a response is received from the server.
/// For this reason, <see cref="OrderService.create_order_async(Create)"/> is usually preferred.
/// An example of how to use that method is provided in <see cref="send_order_async(NameIDPair, NameIDPair, CustomOrderDefinition, uint, double)"/>.
Console.WriteLine($"Creating order {OrderUtils.fmt_order(quantity, price, custom_order_definition)}");
var response = _service.create_order(new Create {
Quantity = quantity,
Price = price,
NewOrder = create_order_definition(user, account, security_key, custom_order_definition)
});
switch (response.ErrorCode) {
case OrderError.Ok:
_print_order_info(response.OrderKey, "successfully created.");
break;
default:
Console.WriteLine($"Failed to create order: [{response.ErrorCode}] {response.Msg}.");
break;
}
}
static void _print_order_info(uint order_key, string message) {
Console.WriteLine($"Order {order_key}: {message}.");
}
}
}
@@ -1,100 +0,0 @@
using bts.api.core;
using bts.api.orderman;
using mbts;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace bts.api.csharp.examples {
/// <summary>
/// The code in this example is a bit more advanced than the others,
/// as it is designed to send orders, asynchronously, in a continuous loop.
/// For straightforward examples of how to send orders with the api,
/// check out <see cref="SendOrderModel"/>.
/// </summary>
static class SendOrderLoop {
/// <summary>
/// As a security key is required to send an order, this method
/// uses the <see cref="MarketDataService"/> to observe the securities stream,
/// then on the first update, initializes the send-order-loop <see cref="LoopImpl"/>
/// with one of the newly received security keys.
/// </summary>
public static IDisposable init_loop(OrderService order_service, MarketDataService market_data_service) {
// If you need to familiarize yourself with the IObservable pattern, check out the official docs:
// https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=netcore-3.1
var single_security_stream = market_data_service.subscribei_securities()
.Where(u => u.all.Any())
.Take(1)
.Select(u => u.all.FirstOrDefault());
return init_loop(single_security_stream, create_loop);
IDisposable create_loop(SecurityImmutable sec) {
return new LoopImpl(order_service, sec.Id);
}
}
/// <summary>
/// Observes the security stream and creates a new send-order-loop
/// whenever a new security arrives. The previous send-order-loop
/// is disposed when the next security arrives, so only one send-order-loop
/// shall be active at a time.
/// </summary>
/// <param name="security">The security stream used to initialize send-order-loops.</param>
/// <returns>An IDisposable that unsubsribes from the securities stream, and disposes the active send-order-loop.</returns>
public static IDisposable init_loop(IObservable<SecurityImmutable> security, Func<SecurityImmutable, IDisposable> factory) {
IDisposable active_loop = null;
var security_sub = security.Subscribe(s =>
Interlocked.Exchange(ref active_loop, factory(s))?.Dispose());
return Disposable.Create(dispose);
void dispose() {
security_sub.Dispose();
active_loop?.Dispose();
}
}
class LoopImpl : IDisposable {
bool _is_disposed;
public LoopImpl(OrderService service, uint security_key)
: this(service, Users.Default, Accounts.Default, security_key, 10, TimeSpan.FromSeconds(3)) { }
public LoopImpl(OrderService service, NameIDPair user, NameIDPair account, uint security_key, int max_orders, TimeSpan delay_between_orders) {
var model = new SendOrderModel(service);
send_orders_continuously();
async void send_orders_continuously() {
// Periodically send an order with one of the CustomOrderDefinitions below.
// If you are unfamiliar with async/await, this will also serve as a simple example
// of how those keywords may be used. However, there is no substite for the official docs:
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
var order_templates = new CustomOrderDefinitionImmutable[] {
new CustomOrderDefinitionImmutable(OrderTemplates.limit()),
new CustomOrderDefinitionImmutable(OrderTemplates.market()),
new CustomOrderDefinitionImmutable(OrderTemplates.iceberg(5))
};
for (int num_orders_sent = 0; num_orders_sent < max_orders && !_is_disposed; num_orders_sent++) {
int template_index = num_orders_sent % order_templates.Length;
CustomOrderDefinitionImmutable templatei = order_templates[template_index];
// The order templates we created are immutable, however the api uses the mutable form.
// Not to worry tho, there is a nifty method called 'CloneToMutable.'
// Note that all of the non-primitive immutable parameters in the api methods have a 'CloneToMutable' method,
// it is not unique to CustomOrderDefinitionImmutable.
CustomOrderDefinition template = templatei.CloneToMutable();
await model.send_order_async(user, account, security_key, template, 12, 100);
// pause for a moment before sending the next order
await Task.Delay(delay_between_orders);
}
}
}
public void Dispose() {
// flip _is_disposed to true, so the 'send_orders_continuously' loop
// knows it's time to stop.
_is_disposed = true;
}
}
}
}
@@ -0,0 +1,79 @@
using bts.api.admin;
using bts.api.csharp.data;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace bts.api.csharp.utils {
static class AdminUtils {
public static IReadOnlyDictionary<uint, NameIDPair> map_accounts(AdminDataImmutable admin_datai) {
return admin_datai.Accounts.ToDictionary(acc => acc.Id, acc => new NameIDPair(acc.Name, acc.Id));
}
/// <summary>
/// Links users to the set of accounts on which they have trading permissions.
/// </summary>
public static IEnumerable<(NameIDPair user, IEnumerable<uint> accounts)> user_account_permissions(AdminDataImmutable admin_datai) {
return admin_datai.Users
.Select(user => (new NameIDPair(user.Name, user.Id), _user_tradable_accounts(user)));
}
public static IReadOnlyDictionary<NameIDPair, ImmutableHashSet<NameIDPair>> map_user_account_permissions(AdminDataImmutable admin_datai) {
var account_lookup = admin_datai.Accounts.ToLookup(acc => acc.Id);
return admin_datai.Users.ToDictionary(
user => new NameIDPair(user.Name, user.Id),
user => ImmutableHashSet.CreateRange(_user_tradable_accounts(user)
.Select(acc_id => new NameIDPair(account_lookup[acc_id].Single().Name, acc_id))));
}
static IEnumerable<uint> _user_tradable_accounts(BTSUserImmutable user) {
return user.UserData.AccountPermissions.AccountPermissions
.Where(kv => kv.Value.TradingPermission.HasPermissions)
.Select(kv => kv.Key);
}
public static NameIDPair get_account(NameIDPair user, AdminService admin_service, string account_name = null) {
var task = get_account_async(user, admin_service, account_name);
task.Wait();
return task.Result;
}
/// <summary>
/// Either searches for the account with the specified name,
/// or picks a random account for which the specified user has trading permissions.
/// </summary>
public static async Task<NameIDPair> get_account_async(NameIDPair user, AdminService admin_service, string account_name = null) {
return await (account_name == null ? auto_account(user, admin_service) : find_account(admin_service, account_name).FirstAsync());
}
public static IObservable<NameIDPair> auto_account(NameIDPair user, AdminService admin_service) {
return admin_service.subscribei_admin_data()
.Where(update => update.all.Any())
.Select(update => map_user_account_permissions(update.all.First()))
.Select(account_perms => account_perms.TryGetValue(user, out var accs) ? accs : ImmutableHashSet<NameIDPair>.Empty)
.Where(accs => accs.Count > 0)
.Select(accs => accs.First())
.Take(1);
}
public static IObservable<NameIDPair> find_account(AdminService admin_service, string account) {
return admin_service.subscribei_admin_data()
.Select(update => {
foreach (var admin_data in update.all) {
foreach (var acc in admin_data.Accounts) {
if (acc.Name == account)
return new NameIDPair(acc.Name, acc.Id);
}
}
return (NameIDPair?)null;
})
.Where(acc => acc.HasValue)
.Select(acc => acc.Value)
.Take(1);
}
}
}
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace bts.api.csharp.utils {
static class CLIUtils {
public static IEnumerable<string> parse_delimited(string cli_arg, string file_arg, string delimiter) {
var cli_items = _parse_delimited(cli_arg, delimiter);
if (file_arg != null) {
try {
var items = _parse_delimited(File.ReadAllText(file_arg), delimiter);
return cli_items is null ? items : cli_items.Concat(items);
} catch { }
}
return cli_items;
}
public static Func<SecurityImmutable, bool> parse_security_predicate(string cli_symbols = null, string cli_security_regex = null) {
return _merge_predicates(
parse_delimited_predicate<SecurityImmutable>(cli_symbols, sec => sec.Symbol),
RegexUtils.default_regex(cli_security_regex)?.to_predicate<SecurityImmutable>());
}
public static Func<T, bool> parse_delimited_predicate<T>(string cli_arg, Func<T, string> get) =>
parse_delimited_predicate(cli_arg, null, ",", get);
public static Func<T, bool> parse_delimited_predicate<T>(string cli_arg, string file_arg, string delimiter, Func<T, string> get) {
var items = new HashSet<string>(parse_delimited(cli_arg, file_arg, delimiter));
if (items.Count == 0)
return null;
return sec => items.Contains(get(sec));
}
static IEnumerable<string> _parse_delimited(string delimited_string, string delimiter) {
return delimited_string
?.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries)
?.Select(sym => sym.Trim())
?? Enumerable.Empty<string>();
}
static Func<T, bool> _merge_predicates<T>(params Func<T, bool>[] predicates) {
Func<T, bool> result = null;
for (int i = 0; i < predicates.Length; i++) {
if (predicates[i] != null) {
if (result == null) {
result = predicates[i];
} else {
result += predicates[i];
}
}
}
return result;
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace bts.api.csharp.utils {
static class CollectionUtils {
public static bool insert_sorted<T>(List<T> list, T value) =>
insert_sorted(list, value, Comparer<T>.Default);
public static bool insert_sorted<T>(List<T> list, T value, IComparer<T> comparer) {
if (list is null)
throw new ArgumentNullException(nameof(list));
if (comparer is null)
throw new ArgumentNullException(nameof(comparer));
var sorted_index = list.BinarySearch(value, comparer);
if (sorted_index < 0) {
// this is a new symbol
list.Insert(~sorted_index, value);
return true;
}
return false;
}
}
}
@@ -0,0 +1,8 @@
using System;
namespace bts.api.csharp.utils {
static class DelegateUtils {
public static Func<TOuter, TResult> wrap<TInner, TOuter, TResult>(this Func<TInner, TResult> func, Func<TOuter, TInner> get_inner) =>
outer => func(get_inner(outer));
}
}
@@ -1,9 +1,32 @@
using bts.api.orderman;
using System.Collections.Generic;
using System.Linq;
namespace bts.api.csharp.utils {
static class OrderUtils {
// add new templates as needed
static readonly IReadOnlyDictionary<CustomOrderDefinition.CustomOneofCase, CustomOrderDefinitionImmutable>
_custom_def_templates = new Dictionary<CustomOrderDefinition.CustomOneofCase, CustomOrderDefinitionImmutable> {
{ CustomOrderDefinition.CustomOneofCase.Limit, new CustomOrderDefinitionImmutable(new CustomOrderDefinition{ Limit = new LimitOrderDefinition() }) },
{ CustomOrderDefinition.CustomOneofCase.Market, new CustomOrderDefinitionImmutable(new CustomOrderDefinition{ Market = new MarketOrderDefinition() }) },
{ CustomOrderDefinition.CustomOneofCase.Iceberg, new CustomOrderDefinitionImmutable(new CustomOrderDefinition{ Iceberg = new IcebergOrderDefinition() }) }
};
public static IEnumerable<CustomOrderDefinition.CustomOneofCase> known_order_types => _custom_def_templates.Keys;
public static CustomOrderDefinitionImmutable? custom_order_definitioni(CustomOrderDefinition.CustomOneofCase order_type) {
if (_custom_def_templates.TryGetValue(order_type, out var templatei)) {
return templatei.CloneToMutable();
}
return null;
}
public static string fmt_order(uint quantity, double price, CustomOrderDefinition custom_order_definition) {
return $"{quantity} @ {price}: {custom_order_definition}";
}
public static string fmt_order(Side side, uint quantity, double price, string user, string account, CustomOrderDefinition custom_order_definition) {
return $"[{user}][{account}] {side} {fmt_order(quantity, price, custom_order_definition)}.";
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Text.RegularExpressions;
namespace bts.api.csharp.utils {
static class RegexUtils {
public static Regex default_regex(string pattern) {
// these patterns typically come from the user,
// so the 'should_ignore' delegate below allows them
// to include all results by specifying "all" or "*".
return default_regex(pattern, p => string.IsNullOrEmpty(p) || p == "*" || p.ToLower() == "all");
}
public static Regex default_regex(string pattern, Func<string, bool> should_ignore) {
if (pattern == null)
return null;
pattern = pattern.Trim();
if (pattern == string.Empty)
return null;
return new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
}
public static Func<T, bool> to_predicate<T>(this Regex regex) =>
regex == null ? null : new Func<T, bool>(val => regex.IsMatch(val.ToString()));
}
}
@@ -1,8 +1,52 @@
using System;
using bts.api.core;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using SymbolTableGroup = bts.api.core.TableUpdateGrouping<bts.api.SecurityImmutable, uint, string>;
namespace bts.api.csharp.utils {
static class SecurityUtils {
public static IObservable<TableUpdate<SymbolTableGroup, string>> GroupBySymbols(this IObservable<TableUpdate<SecurityImmutable, uint>> securities_source) =>
securities_source.GroupTable(sec => sec.Id, sec => sec.Symbol);
public static async Task<SecurityImmutable> get_security_async(IObservable<Update<SecurityImmutable, uint>> source, string security_str = null) {
if (!string.IsNullOrEmpty(security_str)) {
Func<SecurityImmutable, bool> is_match;
// the user may specify the security string, or security key
if (uint.TryParse(security_str, out var sec_key)) {
is_match = sec => sec.Id == sec_key;
} else {
// ignore case and white space
security_str = security_str.Replace(" ", "").ToLower();
is_match = sec => sec.ToString().Replace(" ", "").ToLower() == security_str;
}
return await source.Select(update => {
foreach (var sec in update.all) {
if (is_match(sec))
return sec;
}
return (SecurityImmutable?)null;
})
.Where(sec => sec.HasValue)
.Select(sec => sec.Value)
.FirstAsync();
} else {
return await source.Where(update => update.all.Any())
.Select(update => update.all.First())
.FirstAsync();
}
}
public static class Predicates {
public static Func<SecurityImmutable, bool> symbols(IEnumerable<string> symbols) {
if (symbols is null)
return sec => true;
// use hashset for quicker Contains check
var symbols_set = new HashSet<string>(symbols);
return sec => symbols_set.Contains(sec.Symbol);
}
}
}
}
@@ -0,0 +1,40 @@
using bts.api.core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace bts.api.csharp.utils {
static class StringUtils {
public static IEnumerable<string> format_columns(IEnumerable<IEnumerable<string>> columns, int min_width = 8, int padding = 3) =>
format_columns(columns.Select(lines => (Math.Max(min_width, lines.Max(l => l.Length) + padding), lines)));
public static IEnumerable<string> format_columns(IEnumerable<(int width, IEnumerable<string> lines)> columns) {
var column_layout = columns.Select((column, index) => (column.lines.GetEnumerator(), column.width, index)).ToList();
int num_active = column_layout.Count;
if (num_active == 0)
yield break;
while (true) {
var sb = new StringBuilder();
for (int i = 0; i < column_layout.Count; i++) {
var (en, width, pos) = column_layout[i];
if (en != null && en.MoveNext()) {
sb.Append(string.Format($"{{{0},{-width}}}", en.Current));
sb.Append(string.Format($"{{{0,1}}}", "| "));
} else {
sb.Append(string.Format($"{{{0},{-width}}}", ""));
sb.Append(string.Format($"{{{0,1}}}", "| "));
if (en != null) {
// set the enumerable to null to inidicate this column is empty for the remaining rows
en.Dispose();
column_layout[i] = (null, width, pos);
if (--num_active == 0)
yield break;
}
}
}
yield return sb.ToString();
}
}
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace bts.api.csharp.utils {
static class TaskExtensions {
public static T WaitResult<T>(this Task<T> task) {
task.Wait();
return task.Result;
}
}
}
@@ -0,0 +1,114 @@
using bts.api.core;
using mbts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace bts.api.csharp.utils {
static class UpdateUtils {
/// <summary>
/// Uses a value-subscription delegate to create subscriptions (incrementally) for each value in the update sequence.
/// When a value is removed or changed, its corresponding subscription is disposed; if it changed, a new subscription is created with the new value.
/// </summary>
/// <typeparam name="T">The type of values in the updates.</typeparam>
/// <typeparam name="TKey">The type of keys in the updates.</typeparam>
/// <param name="update_stream">The update sequence.</param>
/// <param name="subscribe_value">The delegate to create a subscription for a particular value.</param>
/// <returns>An IDisposable that, upon being disposed, disposes the subscription to the update sequence, and all active value subscriptions.</returns>
public static IDisposable subscribe_incremental_values<T, TKey>(IObservable<Update<T, TKey>> update_stream, Func<T, IDisposable> subscribe_value)
where TKey : IEquatable<TKey> {
var locker = new object();
var value_subs = new Dictionary<TKey, IDisposable>();
var source_sub = update_stream.Subscribe(on_update);
return Disposable.Create(dispose);
// local methods
void on_update(Update<T, TKey> update) {
lock (locker) {
foreach (var value in update.removed) {
var k = update.key(value);
value_subs[k].Dispose();
value_subs.Remove(k);
}
foreach (var ch in update.changes) {
var k = update.key(ch.New);
value_subs[k].Dispose();
value_subs[k] = subscribe_value(ch.New);
}
foreach (var value in update.added) {
value_subs.Add(update.key(value), subscribe_value(value));
}
}
}
void dispose() {
lock (locker) {
source_sub.Dispose();
foreach (var loop in value_subs.Values) {
loop.Dispose();
}
}
}
}
public static IObservable<TableUpdate<T, TKey>> SafeFilter<T, TKey>(this IObservable<TableUpdate<T, TKey>> source, Func<T, bool> filter) where TKey : IEquatable<TKey> =>
filter == null ? source : source.Filter(filter);
public static bool is_empty<T, TKey>(this TableUpdate<T, TKey> table) where TKey : IEquatable<TKey> =>
!table.added.Any() && !table.changes.Any() && !table.removed.Any();
public static void print_incremental_changes<T, TKey>(Update<T, TKey> update, Func<T, TKey> key_fn, Func<T, string> fmt_value, string title) where TKey :IEquatable<TKey> {
print_incremental_changes<T, TKey>(update.added, update.changes, update.removed, v => $"{key_fn(v)}: {fmt_value(v)}", title);
}
public static void print_incremental_changes<T, TKey>(TableUpdate<T, TKey> table, Func<T, TKey> key_fn, Func<T, string> fmt_value, string title) where TKey :IEquatable<TKey> {
if (fmt_value is null)
fmt_value = v => v.ToString();
print_incremental_changes<T, TKey>(table.added, table.changes, table.removed, v => $"{key_fn(v)}: {fmt_value(v)}", title);
}
public static void print_incremental_changes<T, TKey>(IEnumerable<T> added, IEnumerable<Change<T>> changes, IEnumerable<T> removed, Func<T, string> fmt, string title) {
if (fmt is null)
fmt = v => v?.ToString() ?? "(null)";
var console_out = new StringBuilder();
Console.WriteLine($"Received {title} update");
var lines = StringUtils.format_columns(new[] {
added.Select(fmt).Prepend(" Added "),
changes.Select(ch => ch.New).Select(fmt).Prepend(" Changed "),
removed.Select(fmt).Prepend(" Removed ")
});
foreach (var l in lines)
Console.WriteLine("\t" + l);
}
public static void _print_incremental_changes<T, TKey>(IEnumerable<T> added, IEnumerable<Change<T>> changes, IEnumerable<T> removed, Func<T, string> fmt, string title) {
if (fmt is null)
fmt = v => v?.ToString() ?? "(null)";
var console_out = new StringBuilder();
console_out.AppendLine($"Received {title} update");
using (var added_en = added.GetEnumerator()) {
if (added_en.MoveNext()) {
console_out.AppendLine(" Added:");
do console_out.AppendLine(" " + fmt(added_en.Current));
while (added_en.MoveNext());
}
}
using (var changes_en = changes.GetEnumerator()) {
if (changes_en.MoveNext()) {
console_out.AppendLine(" Changes");
do console_out.AppendLine($" {fmt(changes_en.Current.Old)} --> {fmt(changes_en.Current.New)}");
while (changes_en.MoveNext());
}
}
using (var removed_en = removed.GetEnumerator()) {
if (removed_en.MoveNext()) {
console_out.AppendLine(" Removed");
do console_out.AppendLine(" " + fmt(removed_en.Current));
while (removed_en.MoveNext());
}
}
Console.Write(console_out.ToString());
}
}
}
@@ -0,0 +1,46 @@
using bts.api.admin;
using bts.api.core;
using bts.api.csharp.data;
using bts.api.csharp.utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
namespace bts.api.csharp.watch {
static class WatchAccounts {
/// <summary>
/// Uses the <see cref="AdminService"/> to subscribe to administrative data (users, accounts, permissions, etc.),
/// and displays account updates in the console window.
/// </summary>
public static IDisposable run(AdminService admin_service) {
// extract accounts from the admin data into an IObservable<IReadOnlyDictionary<uint, NameIDPair>>.
IObservable<IReadOnlyDictionary<uint, NameIDPair>> accounts_source =
admin_service.subscribei_admin_data()
.Select(update => {
// Note that update.all is an IEnumerable<AdminDataImmutable>,
// however, it should only have one element. Pass the first element
// to the _map_accounts method, and return the resultant IReadOnlyDictionary.
return _map_accounts(update.all.FirstOrDefault());
});
// Use the 'Table' extension method to transform the accounts source from an IObservable<IReadOnlyDictionary<TKey, TValue>>
// into an IObservable<TableUpdate<TKey, TValue>> (the latter includes incremental update properties).
/// <see cref="TableUpdate{T, TKey}.added"/>
/// <see cref="TableUpdate{T, TKey}.changes"/>
/// <see cref="TableUpdate{T, TKey}.removed"/>
IObservable<TableUpdate<NameIDPair, uint>> incremental_accounts_source =
accounts_source.Table(account => account.id);
// subscribe to the incremental account updates,
// and print the changes in the console window.
return incremental_accounts_source
.ObserveOn(ThreadPoolScheduler.Instance)
.Subscribe(upd => UpdateUtils.print_incremental_changes(upd, acc => acc.id, acc => acc.name, "Accounts"));
}
static IReadOnlyDictionary<uint, NameIDPair> _map_accounts(AdminDataImmutable admin_data) =>
admin_data.Accounts.ToDictionary(acc => acc.Id, acc => new NameIDPair(acc.Name, acc.Id));
}
}
@@ -0,0 +1,48 @@
using bts.md;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using SecurityKey = System.UInt32;
namespace bts.api.csharp.watch {
static class WatchBooks {
/// <summary>
/// Uses the <see cref="MarketDataService"/> to observe book updates
/// for an optionally filtered list of securities in the system,
/// and organize them into a dictionary.
/// </summary>
public static IDisposable run(MarketDataClient md_client, Func<SecurityImmutable, bool> security_predicate = null) {
var books = new Dictionary<SecurityKey, IMDBook>();
object locker = new object();
if (md_client is null)
throw new ArgumentNullException(nameof(md_client));
if (security_predicate is null)
security_predicate = new Func<SecurityImmutable, bool>(sec => true);
/// unlike the other api subscriptions, which publish <see cref="core.Update{T, TKey}"/>
/// this subscription publishes an <see cref="IEnumerable{T}"/>, so the updates are not incremental
/// (i.e., don't have added/removed/changed items).
return md_client.subscribe_book(security_predicate)
.ObserveOn(ThreadPoolScheduler.Instance)
.Subscribe(on_update);
void on_update(IEnumerable<IMDBook> new_books) {
lock (locker) {
foreach (var book in new_books) {
Console.WriteLine($"{book.security} book updated: {_fmt_bbo(book)}.");
books[book.security.Id] = book;
}
}
}
}
static string _fmt_bbo(IMDBook book) {
return $"{_fmt_price_size(book.bids.FirstOrDefault())} <--> {_fmt_price_size(book.asks.FirstOrDefault())}";
}
static string _fmt_price_size(PriceSize price_size) {
return price_size == null ? "N/A" : $"{price_size.size} @ {price_size.price}";
}
}
}
@@ -0,0 +1,109 @@
using bts.api.core;
using bts.api.orderman;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using AccountKey = System.UInt32;
using OrderKey = System.UInt32;
namespace bts.api.csharp.watch {
static class WatchOrders {
/// <summary>
/// Uses the <see cref="OrderService"/> to observe order updates, and group them by account.
/// Fills and status changes are displayed in the console window.
/// </summary>
public static IDisposable run(OrderService order_service, Func<OrderRecordImmutable, bool> predicate) {
/// 'OrderService' has 2 methods which can be used to subscribe to order updates.
/// -------------------------------------------------------------------------------------
/// 1) <see cref="OrderService.subscribe_orders(string)"/>.
/// 2) <see cref="OrderService.subscribei_orders(string)"/>.
/// This method (no. 2) is typically preferrable because the data it provides is immutable.
/// --------------------------------------------------------------------------------------
var orders_source = order_service.subscribei_orders();
if (predicate != null)
orders_source = orders_source.Filter(predicate);
return run(orders_source.ObserveOn(ThreadPoolScheduler.Instance));
}
public static IDisposable run(IObservable<Update<OrderRecordImmutable, uint>> orders_source) {
object locker = new object();
var account_order_map = new Dictionary<AccountKey, Dictionary<OrderKey, OrderRecordImmutable>>();
return orders_source.Subscribe(on_update);
void on_order_added(OrderRecordImmutable new_order) {
if (!account_order_map.TryGetValue(new_order.Def.AccountId, out var account_orders)) {
// this is the first order for this account.
// initialize the account orders dictionary, and add it to our data.
account_orders = new Dictionary<AccountKey, OrderRecordImmutable>();
account_order_map.Add(new_order.Def.AccountId, account_orders);
}
_print_order_info(new_order, $"observed for the first time.");
// now add the order to account_orders
account_orders.Add(new_order.Key, new_order);
}
void on_order_changed(Change<OrderRecordImmutable> change) {
// ch.New and ch.Old have the same key.
Debug.Assert(change.New.Key == change.Old.Key);
var key = change.New.Key;
// detect fills
if (change.New.State.FilledQuantity != change.Old.State.FilledQuantity) {
if (change.New.State.FilledQuantity > change.New.State.Quantity) {
// order has been overfilled
var num_over_fills = change.New.State.FilledQuantity - change.New.State.Quantity;
_print_order_info(change.New, $"overfilled by {num_over_fills}.");
} else if (change.New.State.FilledQuantity == change.New.State.Quantity) {
// order has been filled to completion
_print_order_info(change.New, "filled to completion");
} else if (change.New.State.Status == OrderStatus.Working) {
// order is still working.
var working_qty = change.New.State.Quantity - change.New.State.FilledQuantity;
_print_order_info(change.New, $"still working {working_qty}");
} else {
// order has stopped working prematurely (perhaps it was canceled)
_print_order_info(change.New, $"is inactive");
}
}
// Account will not change, so our dictionary will definitely have a key for this account.
Debug.Assert(account_order_map.ContainsKey(change.New.Def.AccountId));
var account_orders = account_order_map[change.New.Def.AccountId];
// The value for this key in account_orders should equal ch.Old.
Debug.Assert(account_orders[key].Equals(change.Old));
// update account orders with the changed order.
account_orders[key] = change.New;
}
void on_order_removed(OrderRecordImmutable old_val) {
// An entry for this order should be present in our data,
// as it is in the 'removed' items.
// Assert this is the case to illustrate the point.
Debug.Assert(account_order_map.ContainsKey(old_val.Def.AccountId));
var account_orders = account_order_map[old_val.Def.AccountId];
// the removed value should match the one currently in our data.
// Assert this is the case, then remove the order.
Debug.Assert(account_orders[old_val.Key].Equals(old_val));
account_orders.Remove(old_val.Key);
}
void on_update(Update<OrderRecordImmutable, OrderKey> update) {
lock (locker) {
foreach (OrderRecordImmutable new_order in update.added) {
on_order_added(new_order);
}
foreach (Change<OrderRecordImmutable> change in update.changes) {
on_order_changed(change);
}
foreach (OrderRecordImmutable old_order in update.removed) {
on_order_removed(old_order);
}
}
}
}
static void _print_order_info(OrderRecordImmutable order, string message) {
Console.WriteLine($"Order {order.Key} ({order.State.Status}, filled {order.State.FilledQuantity}/{order.State.Quantity}): {message}.");
}
}
}
@@ -0,0 +1,112 @@
using bts.api.core;
using bts.api.position;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
namespace bts.api.csharp.watch {
/// <summary>
/// A pair of security, and account id,
/// which uniquely identifies each PositionUnit.
/// </summary>
readonly struct PositionKey {
public PositionKey(uint security_id, uint account_id) {
this.security_id = security_id;
this.account_id = account_id;
}
public uint security_id { get; }
public uint account_id { get; }
public static PositionKey from_position(PositionUnitImmutable p) =>
new PositionKey(p.SecurityId, p.AccountId);
public override string ToString() =>
$"Sec {security_id}, Acc {account_id}";
}
static class WatchPositions {
/// <summary>
/// Uses the <see cref="PositionService"/> api to monitor positions
/// for every account/security pair, and organize them into a Dictionary.
/// </summary>
public static IDisposable run(PositionService position_service, Func<PositionUnitImmutable, bool> predicate) {
/// 'PositionService' has 2 methods which can be used to subscribe to position updates.
/// -------------------------------------------------------------------------------------
/// 1) <see cref="PositionService.subscribe_position_data(string)"/>.
/// 2) <see cref="PositionService.subscribei_position_data(string)"/>.
/// This method (no. 2) is typically preferrable because the data it provides is immutable.
/// --------------------------------------------------------------------------------------
return run(position_service.subscribei_position_data().Filter(predicate).ObserveOn(ThreadPoolScheduler.Instance));
}
public static IDisposable run(IObservable<Update<PositionUnitImmutable, int>> position_source) {
var data = new Dictionary<PositionKey, PositionUnitImmutable>();
return position_source.Subscribe(on_update);
void on_position_added(PositionUnitImmutable new_val) {
var k = PositionKey.from_position(new_val);
// this entry should NOT be present in our _data, as it is in the 'added' items.
// Assert this is the case to illustrate the point.
Debug.Assert(!data.ContainsKey(k));
print_position_info(k, $"initial position received. Day: Long {new_val.DayLongPos}, Short {new_val.DayShortPos}, Net {new_val.DayLongPos - new_val.DayShortPos}.");
// add the new position unit to _data
data.Add(k, new_val);
}
void on_position_changed(Change<PositionUnitImmutable> change) {
var k = PositionKey.from_position(change.New);
// ch.New and ch.Old have the same key (i.e., the Account/Security pair),
Debug.Assert(change.New.AccountId == change.Old.AccountId);
Debug.Assert(change.New.SecurityId == change.Old.SecurityId);
// of course, the value for this key in our dictionary should equal ch.Old.
Debug.Assert(data[k].Equals(change.Old));
// Display changes to the day position in the console window.
// 1) check if the day long changed
if (change.New.DayLongPos != change.Old.DayLongPos) {
print_position_info(k, $"day long changed from {change.Old.DayLongPos} to {change.New.DayLongPos}.");
}
// 2) check if day short changed
if (change.New.DayShortPos != change.Old.DayShortPos) {
print_position_info(k, $"day short changed from {change.Old.DayShortPos} to {change.New.DayShortPos}.");
}
// 3) check if net day position changed
var new_net_day_pos = change.New.DayLongPos - change.New.DayShortPos;
var old_net_day_pos = change.Old.DayLongPos - change.Old.DayShortPos;
if (new_net_day_pos != old_net_day_pos) {
print_position_info(k, $"day net changed from {old_net_day_pos} to {new_net_day_pos}.");
}
// update _data with the changed position.
data[k] = change.New;
}
void on_position_removed(PositionUnitImmutable old_val) {
var k = PositionKey.from_position(old_val);
// this entry should be present in our _data, as it is in the 'removed' items.
// Assert this is the case to illustrate the point.
Debug.Assert(data.ContainsKey(k));
// this should match our data. Assert this is the case.
Debug.Assert(data[k].Equals(old_val));
data.Remove(k);
}
void on_update(Update<PositionUnitImmutable, int> update) {
foreach (PositionUnitImmutable p in update.added) {
on_position_added(p);
}
foreach (Change<PositionUnitImmutable> ch in update.changes) {
on_position_changed(ch);
}
foreach (PositionUnitImmutable p in update.removed) {
on_position_removed(p);
}
}
void print_position_info(PositionKey position_key, string message) {
Console.WriteLine($"Position {position_key}: {message}.");
}
}
}
}
@@ -0,0 +1,22 @@
using bts.api.core;
using bts.api.csharp.utils;
using System;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
namespace bts.api.csharp.watch {
static class WatchSecurities {
public static IDisposable run(MarketDataService market_data_service, Func<SecurityImmutable, bool> predicate) {
// watch securities
var securities = market_data_service.subscribei_securities().Table();
if (predicate != null)
securities = securities.Filter(predicate);
return securities
.ObserveOn(ThreadPoolScheduler.Instance)
.Subscribe(upd => {
if (!upd.is_empty())
UpdateUtils.print_incremental_changes(upd, sec => sec.Id, null, "Securities");
});
}
}
}
@@ -0,0 +1,48 @@
using bts.api.core;
using System;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using SymbolTableGroup = bts.api.core.TableUpdateGrouping<bts.api.SecurityImmutable, uint, string>;
namespace bts.api.csharp.watch {
static class WatchSymbols {
/// <summary>
/// Observes securities via <see cref="MarketDataService.subscribei_securities(string)"/>, and prints newly received symbols to the console.
/// </summary>
/// <returns>An IDisposable to dispose the subscription to the securities source.</returns>
public static IDisposable run(MarketDataService market_data_service) {
return run(market_data_service.subscribei_securities().Table());
}
/// <summary>
/// Observes a security update sequence, and prints newly received symbols to the console.
/// </summary>
/// <returns>An IDisposable to dispose the subscription to the securities source.</returns>
public static IDisposable run(IObservable<TableUpdate<SecurityImmutable, uint>> securities_source) {
ushort update_count = 0;
object locker = new object();
return securities_source.GroupTable(sec => sec.Id, sec => sec.Symbol)
.ObserveOn(ThreadPoolScheduler.Instance)
.Subscribe(on_update);
void on_update(TableUpdate<SymbolTableGroup, string> group_update) {
lock (locker) {
var num_new_securities = group_update.added.Concat(group_update.changed).Sum(grp => grp.table.added.Count());
Console.WriteLine(
$"Security update number {++update_count} provided {num_new_securities} new securities " +
$"on {group_update.added.Count()} new symbols.");
var new_symbols = group_update.added
.Select(table_group => table_group.group_key)
.OrderBy(sym => sym);
foreach (var new_symbol in new_symbols) {
Console.WriteLine("\t" + new_symbol);
}
}
}
}
}
}
@@ -1,451 +1,421 @@
#include "ICEListener.h"
#include "lib/utils/datetime.h"
#include <vector>
#include "lib/utils/helper_macros.h"
#include "lib/config/protos/MarketData.pb.h"
#include "lib/md/protos/co2.pb.h"
using namespace std;
using namespace bts::co2;
using namespace bts::md::ice;
namespace bts {
namespace md {
namespace ice {
template <>
double double_of_price<onixs_ice::Rational>(const onixs_ice::Rational& price, int8_t denominator)
{
UNUSED(denominator);
double dbl = price.convertTo<double>();
return dbl;
}
template<>
void ICEListener::set_security_specific<onixs_ice::FuturesProductDefinition>(const onixs_ice::FuturesProductDefinition& def,
FixedSecurity& fsec)
{
//fsec.cash_per_point = double_of_price(def.tickValue, fsec.ice_price_denominator);
//fsec.cash_per_point = def.tickValue;
fsec.cash_per_point = def.unitQuantity;
}
ICEListener::ICEListener(ICESecurityIndex& index, MDProducer& producer, const std::vector<MDProducerChannel> channels) :
_index(index),
_producer(producer),
_symbol_filter(),
_channels(channels),
_concurrency_monitor(0)
{
}
ICEListener::~ICEListener() {}
std::string ICEListener::parse_contract_symbol(const std::string& sym)
{
auto end = sym.find(' ');
if(end == std::string::npos)
return sym;
else
return sym.substr(0, end);
}
//
//State change
void ICEListener::onHandlerStateChange(const onixs_ice::HandlerStateChange&)
{
}
void ICEListener::onMarketStateChange(const onixs_ice::MarketStateChange& , const onixs_ice::MessageInfo&)
{
}
//
// Warnings/Errors
void ICEListener::onError(const onixs_ice::Error& error)
{
std::string error_desc(error.description());
LOG(md,Warn, "OnixS ICE Error: {} - {}", error.toString(), error_desc );
}
void ICEListener::onFeedError(onixs_ice::FeedId feedid, onixs_ice::ErrorCode code, const std::string& desc)
{
LOG(md,Warn, "OnixS ICE FeedError: {} - {}", feedid, desc, code);
}
void ICEListener::onWarning(const onixs_ice::Warning& warning)
{
LOG(md,Warn, "OnixS ICE Warn: {} - {}", warning.toString(), std::string(warning.description()) );
}
//
// Security discovery
void ICEListener::onFuturesProductDefinition(const onixs_ice::FuturesProductDefinition& msg, bool& is_interested)
{
if(msg.isSpread)
on_future_strip(msg, is_interested);
else
on_future(msg, is_interested);
LOG(md,Info,"ICE_Future,{},{},{},{},{},{},{},{},{},{},{},{},{}",
is_interested,
msg.productName, // NG Swing GDD Futures,
msg.hubAlias, // Henry
msg.marketDesc, // NG Swing GDD Futures - Henry - 10 Jul 20,
msg.stripName, // 10 Jul 20
msg.contractSymbol, // CGS SDN1020.N1020,
msg.productId,
msg.marketId,
msg.requestMarketType,
msg.securitySubType,
msg.contractSymbolExtra,
msg.clearedAlias,
msg.isSpread);
}
void ICEListener::onNewExpiry(const onixs_ice::NewExpiry& msg, bool& is_interested, const onixs_ice::MessageInfo& )
{
on_future(msg, is_interested);
}
void ICEListener::onFuturesStrategyDefinition(const onixs_ice::FuturesStrategyDefinition& msg, bool& is_interested)
{
on_uds_future_spread(msg, is_interested);
LOG(md,Info,"ICE_Strategy,{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
is_interested,
msg.productName,
msg.hubAlias,
msg.marketDesc,
msg.stripName,
msg.contractSymbol,
msg.productId,
msg.marketId,
msg.requestMarketType,
msg.securitySubType,
log::CharPtrWrapper {""},
msg.clearedAlias,
msg.productName,
0);
}
void ICEListener::onNewFuturesStrategyDefinition(const onixs_ice::NewFuturesStrategyDefinition& msg, bool& is_interested, const onixs_ice::MessageInfo& )
{
on_uds_future_spread(msg, is_interested);
}
void ICEListener::onOptionsProductDefinition(const onixs_ice::OptionsProductDefinition& msg, bool& is_interested)
{
UNUSED(msg);
UNUSED(is_interested);
}
void ICEListener::onNewOptionsMarketDefinition(const onixs_ice::NewOptionsMarketDefinition& msg, bool& is_interested, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(is_interested);
UNUSED(msg_info);
}
void ICEListener::onOptionsStrategyDefinition(const onixs_ice::OptionsStrategyDefinition& msg, bool& is_interested)
{
UNUSED(msg);
UNUSED(is_interested);
}
void ICEListener::onNewOptionsStrategyDefinition(const onixs_ice::NewOptionsStrategyDefinition& msg, bool& is_interested, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(is_interested);
UNUSED(msg_info);
}
//
void ICEListener::onMarketSnapshot(const onixs_ice::MarketSnapshot& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg_info);
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
auto ts = utils::TimeStamp::now();
auto settle_denominator = data->handle.security().ice_settle_denominator;
if (msg.settlePriceDateTime > 0) {
data->handle.update_settlement( ts, convert_price(msg.settlementPrice, settle_denominator), date_of_timestamp(msg.settlePriceDateTime) );
}
LOG(md,Diag,"OnixSICE Got Market Snap : {} - {}", std::to_string(data->id), std::to_string(msg.settlementPrice));
auto deal_denominator = data->handle.security().ice_deal_denominator;
data->handle.update_opening( ts, double_of_price(msg.openingPrice, deal_denominator), datetime::today() );
data->handle.update_session_high( ts, double_of_price(msg.high, deal_denominator) );
data->handle.update_session_low( ts, double_of_price(msg.low, deal_denominator) );
data->handle.set_last_trade( ts, pricesize_of_price_size(msg.lastTradePrice, deal_denominator, msg.lastTradeQuantity),
ll_of_timestamp(msg.lastTradeDateTime), msg.volume );
}
}
void ICEListener::onMarketStatistics(const onixs_ice::MarketStatistics& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg_info);
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
data->handle.set_total_volume(msg.volume);
}
}
// Gap Recovery -- This is not necessary if we subscribe to the onOrderBookUpdated callback, they handle the breaks transparently
void ICEListener::onMarketSnapshotOrder(const onixs_ice::MarketSnapshotOrder& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(msg_info);
}
// Gap Recovery -- This is not necessary if we subscribe to the onOrderBookUpdated callback, they handle the breaks transparently
void ICEListener::onMarketSnapshotPriceLevel(const onixs_ice::MarketSnapshotPriceLevel& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(msg_info);
}
//
// Book
void ICEListener::onOrderBookUpdated(const onixs_ice::OrderBook& onixs_book, const onixs_ice::MessageInfo& msg_info)
{
//tracepoint (bts_trace,trace_order,(char*) "onBookUpdate", (uint32_t)msg_info.sequenceNumber, (uint16_t) msg_info.sequenceNumber, 0);
auto data = _index.get_security(onixs_book.marketId());
if (data == nullptr)
return;
bts_assert(data->id == onixs_book.marketId());
auto denominator = data->handle.security().ice_price_denominator;
// Overwrite all entries every time
size_t bids_size = onixs_book.bids().size();
size_t asks_size = onixs_book.offers().size();
if (!data->handle.has_depth())
{
bids_size = std::min(bids_size, size_t { 1 });
asks_size = std::min(asks_size, size_t { 1 });
}
std::array<PriceSize,COCONUT_MAX_DEPTH> dbids; dbids.fill(NO_MARKET);
std::array<PriceSize,COCONUT_MAX_IMPLIED_DEPTH> ibids; ibids.fill(NO_MARKET);
std::array<PriceSize,COCONUT_MAX_DEPTH> dasks; dasks.fill(NO_MARKET);
std::array<PriceSize,COCONUT_MAX_IMPLIED_DEPTH> iasks; iasks.fill(NO_MARKET);
for (size_t x = 0; x < bids_size; ++x)
{
auto& bid = onixs_book.bids()[x];
if(x < COCONUT_MAX_DEPTH)
dbids[x] = pricesize_of_price_size(bid.price, denominator, bid.quantity - bid.impliedQuantity);
if(x < COCONUT_MAX_IMPLIED_DEPTH)
ibids[x] = pricesize_of_price_size(bid.price, denominator, bid.impliedQuantity);
}
for (size_t x = 0; x < asks_size; ++x)
{
auto& ask = onixs_book.offers()[x];
if(x < COCONUT_MAX_DEPTH)
dasks[x] = pricesize_of_price_size(ask.price, denominator, ask.quantity - ask.impliedQuantity);
if(x < COCONUT_MAX_IMPLIED_DEPTH)
iasks[x] = pricesize_of_price_size(ask.price, denominator, ask.impliedQuantity);
}
data->handle.update_books(TimeStamp::now(), dbids, dasks, ibids, iasks, (uint32_t) msg_info.sequenceNumber);
}
//
// Trades
AggressorSide map_aggressor(onixs_ice::AggressorSide::Enum agside)
{
switch(agside) {
default:
case onixs_ice::AggressorSide::NoAggressor:
return AGGRESSOR_SIDE_NONE;
case onixs_ice::AggressorSide::Buy:
return AGGRESSOR_SIDE_BUY;
case onixs_ice::AggressorSide::Sell:
return AGGRESSOR_SIDE_SELL;
}
}
void on_trade(ICESecurityData* data, const onixs_ice::Price& price, int size,
onixs_ice::AggressorSide::Enum agside, const onixs_ice::DateTime& onixs_date, bool from_spread)
{
auto ts = utils::TimeStamp::now();
PriceSize ps = pricesize_of_price_size(price, data->handle.security().ice_deal_denominator, size);
auto date = ll_of_timestamp(onixs_date);
data->handle.add_trade(ts, ps, date, map_aggressor(agside), 0u, from_spread);
}
void ICEListener::onTrade(const onixs_ice::Trade& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != NULL)
{
bts_assert(data->id == msg.marketId);
auto size = msg.quantity;
auto price = msg.price;
onixs_ice::AggressorSide::Enum agside = msg.aggressorSide;
on_trade(data, price, size, agside, msg.transactDateTime, msg.isSystemPricedLeg);
}
}
//
// RFQ
void ICEListener::onRfq(const onixs_ice::Rfq& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
auto ts = utils::TimeStamp::now();
co2::QuoteRequestMessage message;
message.set_key(data->handle.key());
auto time = ll_of_timestamp(msg.messageTimestamp);
message.set_time(time);
long long rfqid = msg.rfqSystemId;
message.set_request_id(std::to_string(rfqid));
//https://ref.onixs.biz/cpp-ice-impact-handler-guide/struct_onix_s_1_1_i_c_e_1_1i_mpact_1_1_market_data_1_1_side.html#a8150b7776c2a1749101acf22e868d091
switch(msg.side)
{
case ' ':
message.set_side(co2::QuoteRequestMessage::UNDEFINED);
break;
case '1':
message.set_side(co2::QuoteRequestMessage::BUY);
break;
case '2':
message.set_side(co2::QuoteRequestMessage::SELL);
break;
}
message.set_size(msg.quantity);
data->handle.add_rfq(ts, message);
}
}
//
// Misc
void ICEListener::onSettlementPrice(const onixs_ice::SettlementPrice& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
auto ts = utils::TimeStamp::now();
auto price = msg.settlementPrice;
auto time = msg.dateTime;
auto denominator = data->handle.security().ice_settle_denominator;
LOG(md,Diag,"OnixSICE Got Settlement: {} - {}", std::to_string(data->id), std::to_string(price));
data->handle.update_settlement(ts, convert_price(price, denominator), date_of_timestamp(time));
}
}
void ICEListener::onOpenInterest(const onixs_ice::OpenInterest &msg, const onixs_ice::MessageInfo &msg_info)
{
UNUSED(msg);
UNUSED(msg_info);
}
void ICEListener::onOpenPrice(const onixs_ice::OpenPrice& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
onixs_ice::Price price = msg.openPrice;
onixs_ice::DateTime time = msg.dateTime;
auto denominator = data->handle.security().ice_deal_denominator;
data->handle.update_opening( utils::TimeStamp::now(), double_of_price(price, denominator), date_of_timestamp(time));
}
}
void ICEListener::onClosePrice(const onixs_ice::ClosePrice& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
onixs_ice::Price price = msg.closePrice;
onixs_ice::DateTime time = msg.dateTime;
auto denominator = data->handle.security().ice_deal_denominator;
data->handle.update_closing( utils::TimeStamp::now(), double_of_price(price, denominator), date_of_timestamp(time));
}
}
void ICEListener::onEndOfDayMarketSummary(const onixs_ice::EndOfDayMarketSummary& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
onixs_ice::DateTime time = msg.dateTime;
auto ts = utils::TimeStamp::now();
auto settle_denominator = data->handle.security().ice_settle_denominator;
data->handle.update_settlement( ts, convert_price(msg.settlementPrice, settle_denominator), date_of_timestamp(time) );
LOG(md,Warn,"OnixSICE Got EOD Market : {} - {}", std::to_string(data->id), std::to_string(msg.settlementPrice));
auto deal_denominator = data->handle.security().ice_deal_denominator;
data->handle.update_opening( ts, double_of_price(msg.openingPrice, deal_denominator), date_of_timestamp(time) );
data->handle.update_session_high( ts, double_of_price(msg.high, deal_denominator) );
data->handle.update_session_low( ts, double_of_price(msg.low, deal_denominator) );
}
}
//
// Feed
void ICEListener::onMulticastMessageBlockBegin(onixs_ice::FeedId feedId, const onixs_ice::MessageInfo& )
{
UNUSED(feedId);
}
void ICEListener::onMulticastMessageBlockEnd(onixs_ice::FeedId feedId, const onixs_ice::MarketIds& marketIds)
{
UNUSED(feedId);
UNUSED(marketIds);
}
void ICEListener::onFeedStarted(onixs_ice::FeedId, const onixs_ice::MarketIds&)
{}
void ICEListener::onFeedStopped(onixs_ice::FeedId)
{}
void ICEListener::onSessionNumberChange(onixs_ice::FeedId , short , short )
{}
//
// Non-onixs calls
void ICEListener::add_filter_symbol(const std::string& symbol)
{
LOG(md, Info, "ICEListener::add_filter_symbol: adding {}", symbol);
_symbol_filter.insert(symbol);
}
void ICEListener::heartbeat()
{
for (auto& c : _channels) { c.heartbeat(); }
}
//
// Private functions
void ICEListener::add_security(const FixedSecurity& security)
{
// Set up the shared memory for this security
auto handle = _producer.add_security(security);
bts_assert(security.numeric_id != 0);
// Add it to the index so it can receive messages
_index.add_security(security.numeric_id, handle);
}
void ICEListener::update_all_status(CoconutSecurityStatus status)
{
_index.walk_security_list([=](ICESecurityData& data)
{
data.handle.set_status(status);
});
}
}}} //namespace
#include "ICEListener.h"
#include "lib/utils/datetime.h"
#include <vector>
#include "lib/utils/helper_macros.h"
#include "lib/config/protos/MarketData.pb.h"
#include "lib/md/protos/co2.pb.h"
using namespace std;
using namespace bts::co2;
using namespace bts::md::ice;
namespace bts {
namespace md {
namespace ice {
template <>
double double_of_price<onixs_ice::Rational>(const onixs_ice::Rational& price, int8_t denominator)
{
UNUSED(denominator);
double dbl = price.convertTo<double>();
return dbl;
}
template<>
void ICEListener::set_security_specific<onixs_ice::FuturesProductDefinition>(const onixs_ice::FuturesProductDefinition& def,
FixedSecurity& fsec)
{
//fsec.cash_per_point = double_of_price(def.tickValue, fsec.ice_price_denominator);
//fsec.cash_per_point = def.tickValue;
fsec.cash_per_point = def.unitQuantity;
}
ICEListener::ICEListener(ICESecurityIndex& index, MDProducer& producer, const std::vector<MDProducerChannel> channels) :
_index(index),
_producer(producer),
_symbol_filter(),
_channels(channels),
_concurrency_monitor(0)
{
}
ICEListener::~ICEListener() {}
std::string ICEListener::parse_contract_symbol(const std::string& sym)
{
auto end = sym.find(' ');
if(end == std::string::npos)
return sym;
else
return sym.substr(0, end);
}
//
//State change
void ICEListener::onHandlerStateChange(const onixs_ice::HandlerStateChange&)
{
}
void ICEListener::onMarketStateChange(const onixs_ice::MarketStateChange& , const onixs_ice::MessageInfo&)
{
}
//
// Warnings/Errors
void ICEListener::onError(const onixs_ice::Error& error)
{
std::string error_desc(error.description());
LOG(md,Warn, "OnixS ICE Error: {} - {}", error.toString(), error_desc );
}
void ICEListener::onFeedError(onixs_ice::FeedId feedid, onixs_ice::ErrorCode code, const std::string& desc)
{
LOG(md,Warn, "OnixS ICE FeedError: {} - {}", feedid, desc, code);
}
void ICEListener::onWarning(const onixs_ice::Warning& warning)
{
LOG(md,Warn, "OnixS ICE Warn: {} - {}", warning.toString(), std::string(warning.description()) );
}
//
// Security discovery
void ICEListener::onFuturesProductDefinition(const onixs_ice::FuturesProductDefinition& msg, bool& is_interested)
{
if(msg.isSpread)
on_future_strip(msg, is_interested);
else
on_future(msg, is_interested);
}
void ICEListener::onNewExpiry(const onixs_ice::NewExpiry& msg, bool& is_interested, const onixs_ice::MessageInfo& )
{
on_future(msg, is_interested);
}
void ICEListener::onFuturesStrategyDefinition(const onixs_ice::FuturesStrategyDefinition& def, bool& is_interested)
{
on_uds_future_spread(def, is_interested);
}
//This is only for UDS
void ICEListener::onNewFuturesStrategyDefinition(const onixs_ice::NewFuturesStrategyDefinition& msg, bool& is_interested, const onixs_ice::MessageInfo& )
{
on_uds_future_spread(msg, is_interested);
}
void ICEListener::onOptionsProductDefinition(const onixs_ice::OptionsProductDefinition& msg, bool& is_interested)
{
UNUSED(msg);
UNUSED(is_interested);
}
void ICEListener::onNewOptionsMarketDefinition(const onixs_ice::NewOptionsMarketDefinition& msg, bool& is_interested, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(is_interested);
UNUSED(msg_info);
}
void ICEListener::onOptionsStrategyDefinition(const onixs_ice::OptionsStrategyDefinition& msg, bool& is_interested)
{
UNUSED(msg);
UNUSED(is_interested);
}
void ICEListener::onNewOptionsStrategyDefinition(const onixs_ice::NewOptionsStrategyDefinition& msg, bool& is_interested, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(is_interested);
UNUSED(msg_info);
}
//
void ICEListener::onMarketSnapshot(const onixs_ice::MarketSnapshot& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg_info);
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
auto ts = utils::TimeStamp::now();
auto settle_denominator = data->handle.security().ice_settle_denominator;
if (msg.settlePriceDateTime > 0) {
data->handle.update_settlement( ts, convert_price(msg.settlementPrice, settle_denominator), date_of_timestamp(msg.settlePriceDateTime) );
}
LOG(md,Diag,"OnixSICE Got Market Snap : {} - {}", std::to_string(data->id), std::to_string(msg.settlementPrice));
auto deal_denominator = data->handle.security().ice_deal_denominator;
data->handle.update_opening( ts, double_of_price(msg.openingPrice, deal_denominator), datetime::today() );
data->handle.update_session_high( ts, double_of_price(msg.high, deal_denominator) );
data->handle.update_session_low( ts, double_of_price(msg.low, deal_denominator) );
data->handle.set_last_trade( ts, pricesize_of_price_size(msg.lastTradePrice, deal_denominator, msg.lastTradeQuantity),
ll_of_timestamp(msg.lastTradeDateTime), msg.volume );
}
}
void ICEListener::onMarketStatistics(const onixs_ice::MarketStatistics& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg_info);
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
data->handle.set_total_volume(msg.volume);
}
}
// Gap Recovery -- This is not necessary if we subscribe to the onOrderBookUpdated callback, they handle the breaks transparently
void ICEListener::onMarketSnapshotOrder(const onixs_ice::MarketSnapshotOrder& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(msg_info);
}
// Gap Recovery -- This is not necessary if we subscribe to the onOrderBookUpdated callback, they handle the breaks transparently
void ICEListener::onMarketSnapshotPriceLevel(const onixs_ice::MarketSnapshotPriceLevel& msg, const onixs_ice::MessageInfo& msg_info)
{
UNUSED(msg);
UNUSED(msg_info);
}
//
// Book
void ICEListener::onOrderBookUpdated(const onixs_ice::OrderBook& onixs_book, const onixs_ice::MessageInfo& msg_info)
{
//tracepoint (bts_trace,trace_order,(char*) "onBookUpdate", (uint32_t)msg_info.sequenceNumber, (uint16_t) msg_info.sequenceNumber, 0);
auto data = _index.get_security(onixs_book.marketId());
if (data == nullptr)
return;
bts_assert(data->id == onixs_book.marketId());
auto denominator = data->handle.security().ice_price_denominator;
// Overwrite all entries every time
size_t bids_size = onixs_book.bids().size();
size_t asks_size = onixs_book.offers().size();
if (!data->handle.has_depth())
{
bids_size = std::min(bids_size, size_t { 1 });
asks_size = std::min(asks_size, size_t { 1 });
}
std::array<PriceSize,COCONUT_MAX_DEPTH> dbids; dbids.fill(NO_MARKET);
std::array<PriceSize,COCONUT_MAX_IMPLIED_DEPTH> ibids; ibids.fill(NO_MARKET);
std::array<PriceSize,COCONUT_MAX_DEPTH> dasks; dasks.fill(NO_MARKET);
std::array<PriceSize,COCONUT_MAX_IMPLIED_DEPTH> iasks; iasks.fill(NO_MARKET);
for (size_t x = 0; x < bids_size; ++x)
{
auto& bid = onixs_book.bids()[x];
if(x < COCONUT_MAX_DEPTH)
dbids[x] = pricesize_of_price_size(bid.price, denominator, bid.quantity - bid.impliedQuantity);
if(x < COCONUT_MAX_IMPLIED_DEPTH)
ibids[x] = pricesize_of_price_size(bid.price, denominator, bid.impliedQuantity);
}
for (size_t x = 0; x < asks_size; ++x)
{
auto& ask = onixs_book.offers()[x];
if(x < COCONUT_MAX_DEPTH)
dasks[x] = pricesize_of_price_size(ask.price, denominator, ask.quantity - ask.impliedQuantity);
if(x < COCONUT_MAX_IMPLIED_DEPTH)
iasks[x] = pricesize_of_price_size(ask.price, denominator, ask.impliedQuantity);
}
data->handle.update_books(TimeStamp::now(), dbids, dasks, ibids, iasks, (uint32_t) msg_info.sequenceNumber);
}
//
// Trades
AggressorSide map_aggressor(onixs_ice::AggressorSide::Enum agside)
{
switch(agside) {
default:
case onixs_ice::AggressorSide::NoAggressor:
return AGGRESSOR_SIDE_NONE;
case onixs_ice::AggressorSide::Buy:
return AGGRESSOR_SIDE_BUY;
case onixs_ice::AggressorSide::Sell:
return AGGRESSOR_SIDE_SELL;
}
}
void on_trade(ICESecurityData* data, const onixs_ice::Price& price, int size,
onixs_ice::AggressorSide::Enum agside, const onixs_ice::DateTime& onixs_date, bool from_spread)
{
auto ts = utils::TimeStamp::now();
PriceSize ps = pricesize_of_price_size(price, data->handle.security().ice_deal_denominator, size);
auto date = ll_of_timestamp(onixs_date);
data->handle.add_trade(ts, ps, date, map_aggressor(agside), 0u, from_spread);
}
void ICEListener::onTrade(const onixs_ice::Trade& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != NULL)
{
bts_assert(data->id == msg.marketId);
auto size = msg.quantity;
auto price = msg.price;
onixs_ice::AggressorSide::Enum agside = msg.aggressorSide;
on_trade(data, price, size, agside, msg.transactDateTime, msg.isSystemPricedLeg);
}
}
//
// RFQ
void ICEListener::onRfq(const onixs_ice::Rfq& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
auto ts = utils::TimeStamp::now();
co2::QuoteRequestMessage message;
message.set_key(data->handle.key());
auto time = ll_of_timestamp(msg.messageTimestamp);
message.set_time(time);
long long rfqid = msg.rfqSystemId;
message.set_request_id(std::to_string(rfqid));
//https://ref.onixs.biz/cpp-ice-impact-handler-guide/struct_onix_s_1_1_i_c_e_1_1i_mpact_1_1_market_data_1_1_side.html#a8150b7776c2a1749101acf22e868d091
switch(msg.side)
{
case ' ':
message.set_side(co2::QuoteRequestMessage::UNDEFINED);
break;
case '1':
message.set_side(co2::QuoteRequestMessage::BUY);
break;
case '2':
message.set_side(co2::QuoteRequestMessage::SELL);
break;
}
message.set_size(msg.quantity);
data->handle.add_rfq(ts, message);
}
}
//
// Misc
void ICEListener::onSettlementPrice(const onixs_ice::SettlementPrice& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
auto ts = utils::TimeStamp::now();
auto price = msg.settlementPrice;
auto time = msg.dateTime;
auto denominator = data->handle.security().ice_settle_denominator;
LOG(md,Diag,"OnixSICE Got Settlement: {} - {}", std::to_string(data->id), std::to_string(price));
data->handle.update_settlement(ts, convert_price(price, denominator), date_of_timestamp(time));
}
}
void ICEListener::onOpenInterest(const onixs_ice::OpenInterest &msg, const onixs_ice::MessageInfo &msg_info)
{
UNUSED(msg);
UNUSED(msg_info);
}
void ICEListener::onOpenPrice(const onixs_ice::OpenPrice& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
onixs_ice::Price price = msg.openPrice;
onixs_ice::DateTime time = msg.dateTime;
auto denominator = data->handle.security().ice_deal_denominator;
data->handle.update_opening( utils::TimeStamp::now(), double_of_price(price, denominator), date_of_timestamp(time));
}
}
void ICEListener::onClosePrice(const onixs_ice::ClosePrice& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
onixs_ice::Price price = msg.closePrice;
onixs_ice::DateTime time = msg.dateTime;
auto denominator = data->handle.security().ice_deal_denominator;
data->handle.update_closing( utils::TimeStamp::now(), double_of_price(price, denominator), date_of_timestamp(time));
}
}
void ICEListener::onEndOfDayMarketSummary(const onixs_ice::EndOfDayMarketSummary& msg, const onixs_ice::MessageInfo& )
{
auto* data = _index.get_security(msg.marketId);
if (data != nullptr)
{
bts_assert(data->id == msg.marketId);
onixs_ice::DateTime time = msg.dateTime;
auto ts = utils::TimeStamp::now();
auto settle_denominator = data->handle.security().ice_settle_denominator;
data->handle.update_settlement( ts, convert_price(msg.settlementPrice, settle_denominator), date_of_timestamp(time) );
LOG(md,Warn,"OnixSICE Got EOD Market : {} - {}", std::to_string(data->id), std::to_string(msg.settlementPrice));
auto deal_denominator = data->handle.security().ice_deal_denominator;
data->handle.update_opening( ts, double_of_price(msg.openingPrice, deal_denominator), date_of_timestamp(time) );
data->handle.update_session_high( ts, double_of_price(msg.high, deal_denominator) );
data->handle.update_session_low( ts, double_of_price(msg.low, deal_denominator) );
}
}
//
// Feed
void ICEListener::onMulticastMessageBlockBegin(onixs_ice::FeedId feedId, const onixs_ice::MessageInfo& )
{
UNUSED(feedId);
}
void ICEListener::onMulticastMessageBlockEnd(onixs_ice::FeedId feedId, const onixs_ice::MarketIds& marketIds)
{
UNUSED(feedId);
UNUSED(marketIds);
}
void ICEListener::onFeedStarted(onixs_ice::FeedId, const onixs_ice::MarketIds&)
{}
void ICEListener::onFeedStopped(onixs_ice::FeedId)
{}
void ICEListener::onSessionNumberChange(onixs_ice::FeedId , short , short )
{}
//
// Non-onixs calls
void ICEListener::add_filter_symbol(const std::string& symbol)
{
_symbol_filter.insert(symbol);
}
void ICEListener::heartbeat()
{
for (auto& c : _channels) { c.heartbeat(); }
}
//
// Private functions
void ICEListener::add_security(const FixedSecurity& security)
{
// Set up the shared memory for this security
auto handle = _producer.add_security(security);
bts_assert(security.numeric_id != 0);
// Add it to the index so it can receive messages
_index.add_security(security.numeric_id, handle);
}
void ICEListener::update_all_status(CoconutSecurityStatus status)
{
_index.walk_security_list([=](ICESecurityData& data)
{
data.handle.set_status(status);
});
}
}}} //namespace
@@ -119,25 +119,7 @@ private:
enum SecurityFilterAction { ADD, SKIP, DELETE };
std::string parse_contract_symbol(const std::string& sym);
template<typename Definition>
std::string get_contract_symbol(const Definition& def){
const auto& sym = def.contractSymbol;
// TODO : this is only exception right now but for HHD sstrips
// def.contractSymbol is empty !
if (sym.size() == 0 &&
(
def.marketDesc.rfind("NG Swing GDD Futures - Henry", 0) == 0 ||
def.marketDesc.rfind("NG GDD for LD1 Futures Spr - Henry", 0) == 0
))
return "HHD";
auto end = sym.find(' ');
if(end == std::string::npos)
return sym;
else
return sym.substr(0, end);
}
template<typename Definition>
SecurityFilterAction filter_security(int exchange_id, const Definition& def)
{
@@ -148,7 +130,7 @@ private:
}
if(_symbol_filter.count("*") > 0)
return ADD;
return _symbol_filter.count(get_contract_symbol(def)) > 0 ? ADD : SKIP;
return _symbol_filter.count( parse_contract_symbol(def.contractSymbol) ) > 0 ? ADD : SKIP;
}
void flush_books();
void update_all_status(co2::CoconutSecurityStatus status);
@@ -164,7 +146,7 @@ template<typename Definition>
void ICEListener::set_security_common(const Definition& def, FixedSecurity& fsec)
{
fsec.numeric_id = def.marketId;
std::string sym = get_contract_symbol(def);
std::string sym = parse_contract_symbol(def.contractSymbol);
if(!sym.empty())
{
fsec.symbol( sym );
@@ -183,7 +165,7 @@ void ICEListener::set_security_common(const Definition& def, FixedSecurity& fsec
fsec.ice_settle_denominator = (int8_t)(def.settlePriceDenominator - '0');
double min_tick = double_of_price(def.incrementPrice, fsec.ice_price_denominator);
fsec.low_tick = fsec.high_tick = min_tick;
fsec.price_break = 0.0;
fsec.price_break = 0.0;
fsec.ice_market_description(def.marketDesc);
fsec.ice_cleared_alias(def.clearedAlias);
@@ -225,12 +207,12 @@ void ICEListener::on_uds_future_spread(const Definition& def, bool& is_intereste
// auto expiration = date_of_maturity(sec);
// if (expiration < datetime::today()) return;
FixedSpreadSecurityXL fsec;
FixedSpreadSecurityL fsec;
set_security_common(def, fsec);
fsec.alt_symbol(def.strategySymbol);
const auto& legs = def.legs;
if (legs.size() > 0 && legs.size() <= 512) {
const auto& legs = def.legs;
if (legs.size() > 0) {
fsec.set_leg_count(legs.size());
for(size_t x = 0; x < fsec.leg_count(); ++x)
{
@@ -256,8 +238,6 @@ void ICEListener::on_uds_future_spread(const Definition& def, bool& is_intereste
add_security(fsec);
is_interested = true;
}
else
LOG(md, Error, "Ignoring security {} with {} legs", def.marketDesc, legs.size());
}
@@ -268,7 +248,7 @@ void ICEListener::on_future_strip(const FutureDefinition& def, bool& is_interest
auto exchange_id = def.marketId;
if (filter_security(exchange_id, def) != ADD) return;
FixedSpreadSecurityXL fsec;
FixedSpreadSecurityXS fsec;
set_security_common(def, fsec);
fsec.alt_symbol(def.stripName);
@@ -73,17 +73,31 @@ onixs_ice::MarketSubscriptions market_subcription_for_channel(const ICEChannel&
onixs_ice::MarketSubscriptions subs;
for(const auto& x : chan.market_types)
{
onixs_ice::MarketSubscription futures(x, onixs_ice::MarketSubType::NonImplied, onixs_ice::SecurityType::Futures, onixs_ice::BookDepth::FOD);
subs.push_back(futures);
onixs_ice::MarketSubscription spreads(x, onixs_ice::MarketSubType::NonImplied, onixs_ice::SecurityType::UdsFuturesMarkets, onixs_ice::BookDepth::FOD);
subs.push_back(spreads);
//onixs_ice::MarketSubType::NonImplied
if(x == 26)
{
onixs_ice::MarketSubscription futures(x, onixs_ice::MarketSubType::NonImplied, onixs_ice::SecurityType::Futures, onixs_ice::BookDepth::FOD);
subs.push_back(futures);
}
else
{
onixs_ice::MarketSubscription futures(x, onixs_ice::MarketSubType::FullImplied, onixs_ice::SecurityType::Futures, onixs_ice::BookDepth::FOD);
subs.push_back(futures);
}
}
for(const auto& x : chan.uds_market_types)
{
onixs_ice::MarketSubscription spreads(x, onixs_ice::MarketSubType::NonImplied, onixs_ice::SecurityType::UdsFuturesMarkets, onixs_ice::BookDepth::FOD);
subs.push_back(spreads);
}
if(x == 26)
{
onixs_ice::MarketSubscription spreads(x, onixs_ice::MarketSubType::NonImplied, onixs_ice::SecurityType::UdsFuturesMarkets, onixs_ice::BookDepth::FOD);
subs.push_back(spreads);
}
else
{
onixs_ice::MarketSubscription spreads(x, onixs_ice::MarketSubType::FullImplied, onixs_ice::SecurityType::UdsFuturesMarkets, onixs_ice::BookDepth::FOD);
subs.push_back(spreads);
}
}
return subs;
}
@@ -138,7 +138,6 @@ struct PositionServicePNLHelper::PImpl {
void PositionServicePNLHelper::compute_pnl(
const std::map<int, PositionUnit const*>& in_pos,
const std::map<std::string,uint32_t>& account_map,
const std::map<uint32_t,double>& cpp_override_map,
std::vector<PNLUnit>& out_pnl,
std::vector<AccountPNL>& out_account_pnl){
out_account_pnl.clear();
@@ -146,8 +145,6 @@ void PositionServicePNLHelper::compute_pnl(
const PositionUnit& pu = *(p.second);
uint32_t sec_key = pu.security_id();
double cpp = me->handle_index.get_cpp(SecurityKey(sec_key));
auto it = cpp_override_map.find(sec_key);
cpp = (it != cpp_override_map.end()) ? it->second : cpp;
double bid, ask, last, settle;
me->handle_index.get_md(SecurityKey(sec_key),bid,ask,last,settle);
PNLUnit pnlu;
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff. Show more