Compare commits

...
Author SHA1 Message Date
John Colvin 8761f313f5 remove with_reguard_to_price param from tick 'format' extension 2020-07-20 10:50:15 -05:00
John Colvin 3906ef6507 fix typo in ConvertUtils.update_value_type (was missing negation '!') 2020-07-19 13:28:09 -05:00
John Colvin 78055d79c6 fix tick unit tests, and reorganize security tests a bit 2020-07-19 13:27:12 -05:00
John Colvin 885a9dd939 add precision param (epsilon) to tickup/down methods 2020-07-19 13:20:00 -05:00
John Colvin 20c449de60 TickFactors are cached in a ThreadStatic dictionary rather than concurrent (no locks required) 2020-07-15 11:22:11 -05:00
John Colvin 52b487946a ticksize extension handles invalid tick props gracefully 2020-07-15 10:50:25 -05:00
John Colvin 98144ea939 remove unused parameter from TickTestHelper.full_ticks_from_zero 2020-07-14 15:46:06 -05:00
John Colvin 5e27a5fa7c fix ladder session low/high grid animation (was not using the ticks per row setting) 2020-07-14 15:39:55 -05:00
John Colvin 6cfc15ccd8 LadderGridHost invalidates '_spec' on connect_vm (not just on settings updates) 2020-07-14 15:38:21 -05:00
John Colvin 2de08c2c48 fix typo in LadderGridHost._spec getter (was accidentally returning the fallback value) 2020-07-14 15:27:03 -05:00
John Colvin fb6e05560b make tick unit tests more robust (test tick_up/down, use these methods to covalidate results) 2020-07-14 12:48:21 -05:00
John Colvin 1abf84337f include Sim ContractSpecs in tick unit tests 2020-07-14 11:53:57 -05:00
John Colvin d691e0aeee ladder settings dialog - fix 'when implied is > 0' label alignment 2020-07-14 11:01:32 -05:00
John Colvin 5e0f1b59fb added tick unit test (namely for 'full_ticks_from_zero' method) 2020-07-14 10:53:30 -05:00
John Colvin a234ecbf26 added 'full_ticks_from' method which accepts an origin price (instead of just an arbitrary 0) 2020-07-14 10:53:27 -05:00
John Colvin 062cabf075 TickUtils.full_ticks_from_zero checks pricebreak 2020-07-13 13:52:29 -05:00
John Colvin 39e1fd4a1a move LadderUtils methods to TickUtils (all were related to ticks) 2020-07-09 12:05:17 -05:00
John Colvin 4d902747df move tick data\utils to spark_core 2020-07-09 11:58:19 -05:00
John Colvin 50d0078556 add 'ticks per row' ladder setting 2020-07-09 11:45:31 -05:00
John Colvin d86b7c6c50 add TickUp/DownCommand dependency props to TickUpDownUInt 2020-07-08 12:48:45 -05:00
33 changed files with 1195 additions and 586 deletions

No files matched your search

@@ -334,6 +334,7 @@ namespace mbts.spark {
// this class is 100% mutable (there are literally 0 readonly members),
// so GetHashCode must return the same hash for every instance.
static readonly int _type_hash = typeof(LadderSettings).GetHashCode();
uint _ticks_per_row;
// Default proto constructor
// Added this so that when clients upgrade to a version with new settings,
@@ -603,6 +604,12 @@ namespace mbts.spark {
[ProtoMember(61), DefaultValue(true)]
public bool status_message_visible { get; set; }
[ProtoMember(62)]
public uint ticks_per_row {
get => Math.Max(_ticks_per_row, 1);
set => _ticks_per_row = value;
}
[ProtoMember(900)]
public LadderOrderLimits ladder_order_limits { get; set; }
@@ -712,7 +719,8 @@ namespace mbts.spark {
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.ladder_order_limits.safe_equals(b.ladder_order_limits) &&
a._ticks_per_row == b._ticks_per_row;
public bool Equals(LadderSettings other) =>
profile_equals(this, other) &&
@@ -1,13 +1,16 @@
using bts.api;
using bts.api.orderman;
using bts.shell.orderbook;
using bts.utils;
using mbts.persist;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
@@ -52,9 +55,9 @@ namespace mbts.spark {
public event Action piq_changed;
LadderModel model => _owner.model;
Dictionary<uint, (OrderRecordImmutable order, uint? p, AlgoData? algo, AlgoDefinitionMetadata? meta)> _orders = new Dictionary<uint, (OrderRecordImmutable order, uint? p, AlgoData? algo, AlgoDefinitionMetadata? meta)>();
ImmutableDictionary<uint, OrderSecurity> _orders = ImmutableDictionary<uint, OrderSecurity>.Empty;
public LadderOrderVM((OrderRecordImmutable, uint?, AlgoData?, AlgoDefinitionMetadata?) op, double real_price, LadderVM owner, IBus bus, IObservable<LadderSettings.BadgeStyle> bs) {
public LadderOrderVM(ImmutableDictionary<uint, OrderSecurity> orders, double real_price, LadderVM owner, IBus bus, IObservable<LadderSettings.BadgeStyle> bs) {
_owner = owner;
_bus = bus;
_bs = bs;
@@ -78,8 +81,7 @@ namespace mbts.spark {
double get_font_size(LadderSettings s) => s.orders_column.font_size;
Brush get_foreground(LadderSettings s) => s.orders_column.foreground.rgb_brush();
double get_row_height(LadderSettings s) => s.row_height;
//
add_order(op);
_orders = orders;
_dispose = Disposable.Create(
_bs.Subscribe(badge => {
@@ -175,41 +177,6 @@ namespace mbts.spark {
RaisePropertyChanged(nameof(text_lines));
}
/// <summary>
/// Add Order performs an add the first time it's called via the constructor. When it's called, directly
/// it really serves as an update/replace. Also note that since the min_piq is a separate piece of state representing
/// the lowest piq of the orders the badge represents, we must reevaluate if that has changed on said update.
/// </summary>
public void add_order((OrderRecordImmutable order, uint? p, AlgoData? algo, AlgoDefinitionMetadata? meta) new_op) {
_orders[new_op.order.Key] = new_op;
// reevaluate min piq if this order's piq is less
if (new_op.p.HasValue) {
// if this lovm only represents 1 order and the piq is now different, regardless of what it is that's our piq
if (_orders.Count == 1 && new_op.p.Value != min_piq) {
min_piq = new_op.p.Value;
} else if (new_op.p.Value < min_piq) {
min_piq = new_op.p.Value;
}
}
}
public void remove_order(uint order_key) {
_orders.Remove(order_key);
// on a remove, potentially the order w/ the lowest piq has been removed, so
// if the min() is now > than the current min_piq, update the min_piq
var prospective_piq = _orders.Select(op => op.Value.p).Where(piq => piq.HasValue).Min();
if (prospective_piq > min_piq) {
min_piq = prospective_piq;
}
}
//public void update(OrderRecordImmutable neworder) {
// Log.assert(neworder.Key == id, $"OrderVM.update was called with an order with a different Key({neworder.Key}) from original({id})");
// using (detect_property_changes()) {
// _init(neworder);
// }
//}
public uint security_key => _owner.security.Id;
public double price { get => double.IsNaN(_phantom_price) ? _real_price : _phantom_price; }
public uint? min_piq {
@@ -261,9 +228,13 @@ namespace mbts.spark {
public CustomOrderDefinition.CustomOneofCase algo_type { get => _algo_type.Value; private set => _algo_type.Value = value; }
public string algo_studio_abbreviation { get => _algo_studio_abbreviation.Value; private set => _algo_studio_abbreviation.Value = value; }
public IReadOnlyList<uint> order_keys => _orders.Keys.ToReadOnlyList();
public IEnumerable<OrderRecordImmutable> root_orders => _root_nodes();
public void sync_orders(ImmutableDictionary<uint, OrderSecurity> new_orders) {
_orders = new_orders;
min_piq = new_orders.Count == 0 ? null : new_orders.Values.Select(o => o.piq).Min();
}
void _cancel_order() {
foreach (var order in _root_nodes()) {
model.order_gateway.cancel_order(order).ignore();
@@ -296,14 +267,16 @@ namespace mbts.spark {
// I checked with Eric and he guesses that what is happening is that the Popup is throwing away the grid, but the binding doesn't remove the columns from that grid
//
// I think returning a new OrderbookVM is okay at the moment since this Popup window will not be saving any state at this time
var price_filter = new filters.DoubleMinMaxState();
price_filter.minimum.value = real_price;
price_filter.maximum.value = real_price;
var ticks = _owner.spec.full_ticks_from_zero(real_price);
if (!ticks.HasValue) {
// this is an unexpected code path, but not necessarily a reason to crash, so log a warning and return null
return null;
}
var min_price = _owner.spec.tickup(0, ticks.Value);
var max_price = _owner.spec.tickup(min_price, 1);
var state = new bts.shell.orderbook.OrderBookStartupData(
new bts.shell.orderbook.FilterStates {
security_criteria = new SecurityCriteria(_owner.security),
price = price_filter,
statuses = new[] { OrderStatus.Working },
order_types = AlgoID.exchange_algo_keys
},
@@ -311,7 +284,8 @@ namespace mbts.spark {
use_defaults: false
);
var settings_man = new WindowSettingsManager<OrderBookGridSettings>.DisconnectedImpl(state.orderbook_grid);
var obvm = new OrderBookVM(_owner.bus, null, _owner.model.order_gateway, settings_man, true);
var obvm = new OrderBookVM(_owner.bus, null, _owner.model.order_gateway, settings_man, true,
global_filter: o => o.order.order.ui_price() >= min_price - MathUtils.epsilon && o.order.order.ui_price() <= max_price - MathUtils.epsilon);
_owner.settings.Subscribe(s => settings_man.instance_settings = settings_man.instance_settings.with(s.mini_ob.value));
obvm.columns.Insert(0, new OrderBookVM.AttachDetachColumn(typeof(MiniOrderBookView)));
obvm.columns.Insert(0, new OrderBookVM.CommitColumn());
@@ -40,6 +40,7 @@ namespace mbts.spark {
readonly ObservableKeyedCollection<AccountTuple?, uint> _accounts = new ObservableKeyedCollection<AccountTuple?, uint>(AccountTuple.nullable_key_fn);
readonly ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey> _algo_definitions = new ObservableKeyedCollection<AlgoDefinitionVM, AlgoKey>(d => d.key);
readonly OrdersModel _model;
readonly SecurityImmutable _security;
uint? _desired_account;
AlgoID? _desired_algo;
TimeInForce _default_time_in_force;
@@ -48,6 +49,7 @@ namespace mbts.spark {
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;
uint _ticks_per_row;
LadderSettings _current;
LadderColumnSettingsVM
_orders = new OrdersColumnSettingsVM(),
@@ -88,7 +90,7 @@ namespace mbts.spark {
public LadderSettingsVM(OrdersModel model, SecurityImmutable security) {
_model = model;
is_synthetic = security.ContractCase == Security.ContractOneofCase.Synthetic;
_security = security;
favorite_algos_vm = new FavoriteAlgosVM(_algo_definitions);
size_presets = new ObservableEditableCollection<IMutableValueWrapperVM<uint>>(_create_size_preset_vm);
size_presets.CollectionChanged += (s, e) => RaisePropertyChanged(nameof(size_presets_text));
@@ -110,6 +112,9 @@ namespace mbts.spark {
accounts_view = new ListCollectionView<AccountTuple?>(_accounts) {
CustomSort = Comparer<AccountTuple>.Default.to_nullable().to_non_generic()
};
// cmds
tick_up_ticks_per_row_cmd = new DelegateCommand(increment_ticks_per_row);
tick_down_ticks_per_row_cmd = new DelegateCommand(decrement_ticks_per_row);
// update
_update = new DeferrableActionWrapper(() => result_changed?.Invoke());
mini_ob.result_changed += _update.execute;
@@ -155,6 +160,26 @@ namespace mbts.spark {
}
AccountTuple? account_factory(uint key, string name) => new AccountTuple(key, name);
AlgoDefinitionVM algo_factory(AlgoData data) => AlgoDefinitionVM.create(_model, data);
void increment_ticks_per_row() {
var tpr = ticks_per_row;
for (uint i = 1; i < 100; i++) {
var new_tpr = TickUtils.fit_ticks_per_row_to_dollars(security.ContractSpec, ticks_per_row + i);
if (new_tpr != tpr) {
ticks_per_row = new_tpr;
break;
}
}
}
void decrement_ticks_per_row() {
var tpr = ticks_per_row;
for (uint i = 1; i < tpr; i++) {
var new_tpr = TickUtils.fit_ticks_per_row_to_dollars(security.ContractSpec, tpr - i);
if (new_tpr != tpr) {
ticks_per_row = new_tpr;
break;
}
}
}
}
public event Action result_changed;
@@ -177,6 +202,16 @@ namespace mbts.spark {
get => _alternate_order_size.Value;
set => _alternate_order_size.Value = value;
}
public uint ticks_per_row {
get => _ticks_per_row;
set => Set(ref _ticks_per_row, value, dependent_props: nameof(rounded_ticks_per_row));
}
public ICommand tick_up_ticks_per_row_cmd { get; }
public ICommand tick_down_ticks_per_row_cmd { get; }
// since settings profiles may be shared among ladders
// of different securities/tick sizes, the ticks per row setting
// may need to be rounded to a number that is a factor of $1.00
public uint? rounded_ticks_per_row => TickUtils.fit_ticks_per_row_to_dollars(_security.ContractSpec, ticks_per_row);
public MarketSizeDisplayVM size_display { get; } = new MarketSizeDisplayVM();
public SimpleGridSettingsVM mini_ob { get; } = new SimpleGridSettingsVM(OrderBookGridSettingsManager.hard_column_flags);
@@ -209,7 +244,7 @@ namespace mbts.spark {
public FavoriteAlgosVM favorite_algos_vm { get; }
public double row_height => _row_height.Value;
public bool is_synthetic { get; }
public bool is_synthetic => _security.ContractCase == Security.ContractOneofCase.Synthetic;
public IEnumerable<LadderColumnSettingsVM> column_items {
get {
@@ -326,6 +361,7 @@ namespace mbts.spark {
_current = settings;
badge_style = settings.badge_style;
alternate_order_size = settings.alternate_order_size;
ticks_per_row = settings.ticks_per_row;
favorite_algos_vm.load_from(settings.favorite_algos);
size_display.load_from(settings.size_display);
size_presets.SetItems(settings.calculator_buttons.empty_if_null().Select(s => _create_size_preset_vm((uint)s)));
@@ -407,6 +443,7 @@ namespace mbts.spark {
}
clone.badge_style = badge_style;
clone.alternate_order_size = alternate_order_size;
clone.ticks_per_row = ticks_per_row;
clone.favorite_algos = favorite_algos_vm.create();
clone.size_display = size_display.create();
clone.calculator_buttons = size_presets.Select(vm => (int)vm.value).ToArray();
@@ -1,4 +1,5 @@
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.proto;
using bts.shell;
@@ -8,7 +9,7 @@ using mbts.co2;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
@@ -28,7 +29,6 @@ namespace mbts.spark {
readonly IBus _bus;
readonly LadderModel _model;
readonly Property<double> _high, _low, _change, _open, _settlement, _left_panel_width, _zoom_factor;
//readonly Property<double> _window_width, _window_height;
readonly Property<uint> _orders_column_width, _bid_column_width, _price_column_width, _ask_column_width, _last_column_width, _volume_column_width, _piq_column_width;
readonly Property<ulong> _volume;
readonly Property<bool> _auto_clear;
@@ -49,8 +49,10 @@ namespace mbts.spark {
ulong _last_trade_time = 0;
IEqualityComparer<double> _equality;
Dictionary<double, LadderOrderVM> _slot_price_vs_orders;
ImmutableSortedDictionary<double, LadderOrderVM> _order_price_index;
IReadOnlyDictionary<uint, OrderSecurity> _cache = DictionaryHelper.empty<uint, OrderSecurity>();
ImmutableDictionary<AlgoKey, LadderFavoriteAlgoItem> _favorite_algo_map = ImmutableDictionary<AlgoKey, LadderFavoriteAlgoItem>.Empty;
ContractSpecificationImmutable? _spec;
public static LadderVM create(IBus bus, LadderModel model, LadderSettings init, IObservable<LadderSettings> settings) =>
new LadderVM(bus, model, init, settings, new OrderPropertiesEditorVM(model.order_gateway, Observable.Return(model.security_md), settings.Select(s => new InlineOrderProperties(model.order_gateway, s))));
@@ -65,6 +67,7 @@ namespace mbts.spark {
_bus = bus;
_model = model;
current_settings = init;
_order_price_index = ImmutableSortedDictionary.Create<double, LadderOrderVM>(spec.price_comparer());
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);
@@ -122,6 +125,8 @@ namespace mbts.spark {
(_show_zoom = bool_prop(nameof(show_zoom), s => s.zoom_slider_visible)).set,
(_show_status_message = bool_prop(nameof(show_status_message), s => s.status_message_visible)).set,
s => {
// invalidate spec in case 'ticks per row' changed
_spec = null;
if (
Set(ref _show_account_position, s.account_position_visible, nameof(show_account_position)) |
Set(ref _show_day_position, s.day_position_visible, nameof(show_day_position)) |
@@ -159,71 +164,40 @@ namespace mbts.spark {
settings_command = new DelegateCommand(() => request_settings_dialog?.Invoke());
orders = new ObservableCollection<LadderOrderVM>();
var orders_source = settings.Select(s => s.ticks_per_row).DistinctUntilChanged()
.SelectManyInSeries(tpr => {
var spec = TickUtils.multiply_tick_size(security.ContractSpec, tpr);
var price_comp = Comparer<double>.Create((a, b) =>
CompareUtils.nullable_compare(spec.full_ticks_from_zero(a), spec.full_ticks_from_zero(b)));
return _order_price_source(model.orders, price_comp, p => TickUtils.normalize_price(spec, p));
});
_dispose = Disposable.Create(
settings.OnDispatcher().Subscribe(settings_observer),
get_order_properties_editor_hooks(),
bus.subscribe(SparkTags.LogActivity).OnDispatcher().Subscribe(status_update),
// we need to hook this up to model.orders which is filtered for just the security
model.orders.OnDispatcher().Subscribe(working => {
orders_source.OnDispatcher().Subscribe(tup => {
// Here we manage N orders to 1 price slot in the ladder
Dictionary<double, HashSet<uint>> orders_used_in_this_update = new Dictionary<double, HashSet<uint>>(_equality);
_cache = working.state;
foreach (var os in _cache.Values) {
var wo = os.order;
var slot_price_target = wo.ui_price();
var order_type = wo.order_type();
var filled = wo.filled();
if (_slot_price_vs_orders.TryGetValue(slot_price_target, out LadderOrderVM slot_vm)) {
slot_vm.add_order((wo, os.piq, os.algo, os.meta));
} else {
slot_vm = new LadderOrderVM((wo, os.piq, os.algo, os.meta), slot_price_target, this, _bus, settings.Select(s => s.badge_style).OnDispatcher());
_slot_price_vs_orders[slot_price_target] = slot_vm;
}
// keep track of slot prices vs orders; we use this below to remove orders from the LadderOrderVMs
// that are not found in this callback run
if (!orders_used_in_this_update.TryGetValue(slot_price_target, out HashSet<uint> orders_at_level)) {
orders_at_level = new HashSet<uint>();
}
orders_at_level.Add(wo.Key);
orders_used_in_this_update[slot_price_target] = orders_at_level;
_cache = tup.map;
/// remove empty price slots (the vm will be disposed automatically when it is removed from <see cref="orders"/>)
var removed_prices = _slot_price_vs_orders.Keys.Except(tup.price_index.Keys).ToList();
foreach (var p in removed_prices)
_slot_price_vs_orders.Remove(p);
// add or update current price slots
foreach (var kv in tup.price_index) {
_slot_price_vs_orders.AddOrUpdate(kv.Key,
order_vm => order_vm.sync_orders(kv.Value),
() => new LadderOrderVM(kv.Value, kv.Key, this, _bus, settings.Select(s => s.badge_style).OnDispatcher()));
}
// remove any orders from the lovm that aren't in here
foreach(var kvp in orders_used_in_this_update) {
var slot_vm = _slot_price_vs_orders[kvp.Key];
foreach(var existing_order_key in slot_vm.order_keys) {
if (!kvp.Value.Contains(existing_order_key)) {
slot_vm.remove_order(existing_order_key);
}
}
}
// update dirty brushes and text
foreach (var lovm in _slot_price_vs_orders.Values) {
lovm.update();
}
// complete removal of slots that were not updated
// orders_used_in_this_update.Keys are the price levels in the update
foreach (var slot_price in _slot_price_vs_orders.Keys.ToList()) {
if (!orders_used_in_this_update.Keys.Contains(slot_price)) {
_slot_price_vs_orders.TryGetValue(slot_price, out LadderOrderVM removal_candidate);
if (removal_candidate != null) {
removal_candidate.Dispose();
}
_slot_price_vs_orders.Remove(slot_price);
}
}
// sync the collection
orders.sync_with(_slot_price_vs_orders.Values);
// update badge colors, text, etc.
foreach (var lovm in orders)
lovm.update();
}),
WorkLoop.Start($"Ladder '{security}'", _tick),
ObservableAutoWrapper.dispose_on_removed(orders, true),
Disposable.Create(() => _on_disposed_impl?.Invoke(true))
);
IDisposable get_order_properties_editor_hooks() {
@@ -299,6 +273,8 @@ namespace mbts.spark {
remove => _on_disposed_impl -= value;
}
public SecurityImmutable security => _model.security;
public ContractSpecificationImmutable spec => _spec ?? (
_spec = TickUtils.multiply_tick_size(security.ContractSpec, current_settings?.ticks_per_row ?? 1)).Value;
public uint display_quantity_divisor { get; }
public LadderSettings current_settings { get; private set; }
public double high => _high.Value;
@@ -409,11 +385,11 @@ namespace mbts.spark {
double _max_left_panel_scroll_position =>
Math.Max(0, _left_panel_extent_height - _left_panel_view_port_height);
internal OrderRecordImmutable get_root_at_price((OrderRecordImmutable order, uint? _, AlgoData? __, AlgoDefinitionMetadata? meta) op) {
internal OrderRecordImmutable get_root_at_price(OrderSecurity op) {
var order = op.order;
double p = order.ui_price();
double p = _normalize_price(order.ui_price());
foreach (var o in lineage(order.ParentKey)) {
if (_equality.Equals(p, o.ui_price()))
if (_equality.Equals(p, _normalize_price(o.ui_price())))
order = o;
}
return order;
@@ -542,6 +518,64 @@ namespace mbts.spark {
}
}
static IObservable<(ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderSecurity>> price_index, IReadOnlyDictionary<uint, OrderSecurity> map)> _order_price_source(
IObservable<TableUpdate<OrderSecurity, uint>> source,
IComparer<double> price_comp,
Func<double, double> normalize_price) {
//
return Observable.Create<(ImmutableSortedDictionary<double, ImmutableDictionary<uint, OrderSecurity>>, IReadOnlyDictionary<uint, OrderSecurity>)>(o => {
var state = ImmutableSortedDictionary.Create<double, ImmutableDictionary<uint, OrderSecurity>>(price_comp);
var sub = source.Subscribe(table => {
var bd = state.ToBuilder();
// removes
var removes = table.removed.Concat(table.changes.Where(price_changed).Old());
foreach (var old_order in removes) {
var price = get_price(old_order);
if (bd.TryGetValue(price, out var index)) {
var new_index = index.Remove(old_order.order.Key);
if (new_index.Count == 0) {
bd.Remove(price);
} else {
bd[price] = new_index;
}
}
}
// changes
var changes = table.changes.Where(ch => !price_changed(ch)).New();
foreach (var changed_order in changes) {
var price = get_price(changed_order);
bd[price] = bd[price].SetItem(changed_order.order.Key, changed_order);
}
// adds
var adds = table.added.Concat(table.changes.Where(price_changed).New());
foreach (var new_order in adds) {
var price = get_price(new_order);
if (bd.TryGetValue(price, out var index)) {
bd[price] = index.Add(new_order.order.Key, new_order);
} else {
bd[price] = ImmutableDictionary.CreateRange(
new[] { new KeyValuePair<uint, OrderSecurity>(new_order.order.Key, new_order) });
}
}
o.OnNext((state = bd.ToImmutable(), table.state));
});
return sub;
});
bool price_changed(Change<OrderSecurity> ch) =>
price_comp.Compare(ch.New.order.ui_price(), ch.Old.order.ui_price()) != 0;
double get_price(OrderSecurity order) {
var p = order.order.ui_price();
if (!(normalize_price is null))
p = normalize_price(p);
return p;
}
}
double _normalize_price(double price) =>
TickUtils.normalize_price(spec, price);
bool _validate_price(Side side, double price, out string msg) {
msg = null;
if (!price.is_real()) {
@@ -296,6 +296,18 @@ namespace mbts.spark {
Column[] columns { get; set; } = new Column[0];
IEnumerable<Column> _visible_columns => columns.Where(c => c.settings.show);
ContractSpecificationImmutable? _spec_backing;
ContractSpecificationImmutable _spec {
get {
if (_spec_backing.HasValue) {
return _spec_backing.Value;
} else if (_vm != null) {
_spec_backing = TickUtils.multiply_tick_size(security.ContractSpec, _settings?.ticks_per_row ?? 1);
return _spec_backing.Value;
}
return SecurityDataHelper.Defaults.ContractSpecificationImmutable;
}
}
public void drag_out_of_bounds() =>
_tactile_adorner?.drag_out_of_bounds();
@@ -303,7 +315,7 @@ namespace mbts.spark {
public void scroll_to_price(double price) {
if (_scrollViewer == null || _price_list.is_null_or_empty())
return;
if (security.ContractSpec.tickdiff(_price_list[0], price) is int target_row) {
if (_spec.tickdiff(_price_list[0], price) is int target_row) {
int visible_row_count = (int)(_scrollViewer.ActualHeight / _settings.row_height);
int curr_row = visible_row_count + (int)(_scrollViewer.VerticalOffset / _settings.row_height);
if ((target_row > curr_row && target_row < _row_count) || (target_row < (curr_row - visible_row_count) && target_row >= 0))
@@ -533,10 +545,10 @@ namespace mbts.spark {
_price_list == null ? double.NaN : _price_list[row];
uint? _bid_size_from_row(int row) =>
_size_from_row(row, _price_list, _bids, _vm.security.ContractSpec);
_size_from_row(row, _price_list, _bids, _spec);
uint? _ask_size_from_row(int row) =>
_size_from_row(row, _price_list, _asks, _vm.security.ContractSpec);
_size_from_row(row, _price_list, _asks, _spec);
uint _volume_from_row(int row) {
const int max_tries = 3;
@@ -634,7 +646,7 @@ namespace mbts.spark {
_vm.md.settlement_price,
_settings.net_change == LadderSettings.NetChangeTerms.Ticks,
column.settings,
security.ContractSpec);
_spec);
} else {
LadderRender.render_prices(
column.visual_tup.visual,
@@ -643,7 +655,7 @@ namespace mbts.spark {
_settings.row_height,
_price_list,
column.settings,
security.ContractSpec,
_spec,
_settings.price_display_format);
}
break;
@@ -661,7 +673,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, _spec, column.settings, _fmt_mkt_depth_item);
}
}
_apply_column_visual_offset();
@@ -696,7 +708,12 @@ namespace mbts.spark {
} else {
_vm.OnDisposed += _on_vm_disposed;
_prices_updated = 0;
_price_comparer = Comparer<double>.Create((x, y) => security.ContractSpec.tick_equals(x, y) ? 0 : y.CompareTo(x));
// invalidate the contract spec here, as well as in the 'settings update' callback.
// While it is assumed that the settings IObservable will always fire at least once (on subscribe),
// this serves as a redundancy/fail-safe; it ensures that the vm's security's
// contract spec will be used, even if the settings sequence is empty.
_invalidate_spec();
_price_comparer = Comparer<double>.Create((x, y) => _spec.tick_equals(_normalize_price(x), _normalize_price(y)) ? 0 : y.CompareTo(x));
_price_dirty = true;
_piq_dirty = true;
// **use local vm to protect against re-assignment,
@@ -916,14 +933,14 @@ namespace mbts.spark {
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, _spec, _settings.piq_column, _fmt_mkt_quantity);
}
void _refresh_sizes() {
Log.assert_d(_price_list != null);
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _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, _spec, _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, _spec, _settings.ask_column, _fmt_mkt_depth_item);
}
/// <summary>
@@ -951,7 +968,7 @@ namespace mbts.spark {
_price_list,
vapls,
_settings.row_height,
security.ContractSpec,
_spec,
_settings.volume_column,
last_index,
_get_last_arrow_string(),
@@ -988,8 +1005,8 @@ namespace mbts.spark {
void _refresh_prices() {
var prev_price_list = _price_list;
_price_list = (!(_settings.net_change == LadderSettings.NetChangeTerms.None)) ?
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _vm.md.settlement_price, _settings.net_change == LadderSettings.NetChangeTerms.Ticks, _row_count, _settings.row_height, _settings.price_column, security.ContractSpec) :
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _row_count, _settings.row_height, _settings.price_column, security.ContractSpec, _settings.price_display_format);
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _vm.md.settlement_price, _settings.net_change == LadderSettings.NetChangeTerms.Ticks, _row_count, _settings.row_height, _settings.price_column, _spec) :
LadderRender.render_prices(_price_vis_tup.visual, _price_vis_tup.glyph_context, _mkt_break_lines.origin, _row_count, _settings.row_height, _settings.price_column, _spec, _settings.price_display_format);
_ordershost.update_settings(_price_list, _price_comparer, _settings.row_height, _settings.orders_column);
if (prev_price_list != null) {
// maintain previous visible price range if possible
@@ -1017,8 +1034,8 @@ namespace mbts.spark {
SparkUtils.lazy_warn(!double.IsNaN(_mkt_break_lines.origin) && _mkt_break_lines.origin >= low && _mkt_break_lines.origin <= high,
() => fmt_log_msg($"market break ({_mkt_break_lines.origin}) is unacceptable for the market range ({low} - {high})"));
int prev_row_count = _row_count;
int n_ticks_to_low = security.ContractSpec.tickdiff(_mkt_break_lines.origin, low) ?? 0;
int n_ticks_to_high = security.ContractSpec.tickdiff(high, _mkt_break_lines.origin) ?? 0;
int n_ticks_to_low = _spec.tickdiff(_mkt_break_lines.origin, low) ?? 0;
int n_ticks_to_high = _spec.tickdiff(high, _mkt_break_lines.origin) ?? 0;
int desired_row_count = Math.Max(n_ticks_to_low, n_ticks_to_high) * 2 + 1;
_row_count = Math.Min(_max_row_count, desired_row_count);
if (_row_count != prev_row_count) {
@@ -1034,7 +1051,7 @@ namespace mbts.spark {
_last_price_range_calc_time = now;
//
string fmt_log_msg(string msg) =>
$"Ladder {security} (id:{security.Id}, low/high tick:{security.ContractSpec.LowTick}/{security.ContractSpec.HighTick}): {msg}.";
$"Ladder {security} (id:{security.Id}, low/high tick:{_spec.LowTick}/{_spec.HighTick}): {msg}.";
}
return prices_changed;
}
@@ -1080,31 +1097,46 @@ namespace mbts.spark {
prices != null && prices.Length > 0 ? prices[0].price : double.NaN;
double _get_bid_market_break() {
var raw_bid_break = _get_bid_market_break_raw();
return _normalize_price(raw_bid_break);
}
double _normalize_price(double price) =>
TickUtils.normalize_price(_spec, price);
double _get_bid_market_break_raw() {
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.settlement : _spec.tickdown(best_ask) :
best_bid;
}
double _get_ask_market_break() {
var raw_ask_break = _get_ask_market_break_raw();
return _normalize_price(raw_ask_break);
}
double _get_ask_market_break_raw() {
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 :
security.ContractSpec.tickdown(best_ask);
_spec.tickdown(best_ask);
}
double _get_mid_market_break() {
double best_bid = _get_top_price(_bids);
double best_ask = _get_top_price(_asks);
return double.IsNaN(best_bid) ?
double.IsNaN(best_ask) ? _vm.settlement : security.ContractSpec.tickdown(best_ask) :
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(security.ContractSpec, best_bid, best_ask);
double.IsNaN(best_ask) ? _vm.settlement : _spec.tickdown(best_ask) :
double.IsNaN(best_ask) ? best_bid : TickUtils.mid_tick(_spec, best_bid, best_ask);
}
double _get_session_low() => _vm.md.session_low;
double _get_session_high() => _vm.md.session_high;
double _get_session_low() => _normalize_price(_get_session_low_raw());
double _get_session_high() => _normalize_price(_get_session_high_raw());
double _get_session_low_raw() => _vm.md.session_low;
double _get_session_high_raw() => _vm.md.session_high;
void _render_mkt_break(DrawingVisual vis, double price, uint color) =>
LadderRender.render_mkt_break(vis, _row_for_price(price), _settings.row_height, color);
@@ -1113,6 +1145,9 @@ namespace mbts.spark {
LadderRender.render_single_grid_line(vis, row, _settings.row_height, color, _price_column.x_left, _settings.price_column.width);
#endregion
void _invalidate_spec() =>
_spec_backing = null;
void _settings_updated(LadderSettings s) {
bool init;
double prev_row_height;
@@ -1126,6 +1161,7 @@ namespace mbts.spark {
/// The viewmodel will always have the most up-to-date settings, so ignore <see cref="s"/> and just get it from <see cref="_vm"/>.
/// This is a workaround for the global settings-defaults issue: when defaults are updated, some properties are inadvertantly overwritten, such as column-widths, etc.
_settings = _vm.get_save_state();
_invalidate_spec();
_bid_vis_tup.glyph_context = glyph_context(s.bid_column);
_ask_vis_tup.glyph_context = glyph_context(s.ask_column);
_price_vis_tup.glyph_context = glyph_context(s.price_column);
@@ -1318,7 +1354,7 @@ namespace mbts.spark {
}
int _get_session_range_size() =>
_owner.security.spec_or_default().tickdiff(_session_high, _session_low) ?? 0;
_owner._spec.tickdiff(_session_high, _session_low) ?? 0;
LadderSettings _settings => _owner._settings;
@@ -130,6 +130,8 @@ namespace mbts.spark {
if (e.ChangedButton == MouseButton.Middle) {
// create the new popup window in here
var obvm = order.create_orderbook_vm();
if (obvm is null)
return;
await Task.Delay(100); // Wait for obvm to load rows collection before showing UI to avoid a flash
var view = new MiniOrderBookView(obvm);
var popup = new System.Windows.Controls.Primitives.Popup {
@@ -85,7 +85,7 @@ namespace mbts.spark {
public static GlyphRun[] format_prices(double[] prices, double reference_price, bool format_in_tick_terms, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, ContractSpecificationImmutable spec) {
var result = new GlyphRun[prices.Length];
Parallel.For(0, prices.Length, i =>
result[i] = format_text(spec.format(prices[i], reference_price, format_in_tick_terms), i, row_height, settings, glyph_context));
result[i] = format_text(spec.format(prices[i] - reference_price, format_in_tick_terms), i, row_height, settings, glyph_context));
return result;
}
public static GlyphRun[] format_prices(double[] prices, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
@@ -181,15 +181,28 @@ namespace mbts.spark {
var foreground = settings.foreground.rgb_brush();
int curr_index = origin_index;
double curr_price = prices[curr_index];
for (int i = 0; i < rungs.Length; i++) {
if (rungs[i].is_nan) break;
double price = rungs[i].price;
NativePriceSize curr_size = NativePriceSize.NO_MARKET;
var row_tick_groups = rungs
.TakeWhile(nps => !nps.is_nan)
.AggregateConsecutiveGroups(() => NativePriceSize.NO_MARKET,
(aggr, rung) => rung.with(
direct_size: aggr.direct_size + rung.direct_size,
implied_size: aggr.implied_size + rung.implied_size),
rung => {
//spec.round_to_tick(rung.price);
var ticks = spec.full_ticks_from_zero(rung.price);
return ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.price;
});
foreach (var rung in row_tick_groups) {
//double price = spec.round_to_tick(rung.price);
var ticks = spec.full_ticks_from_zero(rung.price);
var price = ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.price;
// prices are in descending order, so get diff from curr_price - price
int tick_diff = spec.tickdiff_unchecked(curr_price, price);
curr_index += tick_diff;
curr_price = price;
if (curr_index >= 0 && curr_index < prices.Length) {
var gr = format_text(fmt(rungs[i]), curr_index, row_height, settings, glyph_context);
var gr = format_text(fmt(rung), curr_index, row_height, settings, glyph_context);
dc.DrawGlyphRun(foreground, gr);
} else break;
}
@@ -656,7 +656,7 @@
ItemsSource="{Binding format_options}"
SelectedItem="{Binding selected_format}"
/>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal">
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal" VerticalAlignment="Top">
<TextBlock
Margin="{StaticResource MarginIndentChild}"
Text="when implied is > 0"
@@ -670,6 +670,7 @@
IsEnabled="{Binding can_choose_alternate_implied_format}"
ItemsSource="{Binding alternate_implied_format_options}"
SelectedItem="{Binding selected_alternate_implied_format}"
VerticalAlignment="Top"
/>
</StackPanel>
<Border Grid.Column="2" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="12,4,0,0" Background="Transparent" BorderBrush="DimGray" BorderThickness="1" ToolTip="Sample Output">
@@ -728,7 +729,38 @@
Visibility="{Binding is_synthetic, Converter={StaticResource NetChangeGroupBoxIsVisibleVisibilityConverter}}"
Padding="4"
>
<c:RadioButtonsControl ItemsSource="{Binding price_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_format}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush2}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock>Ticks Per Row</TextBlock>
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<c:TickUpDownUInt
Maximum="100"
TickUpCommand="{Binding DataContext.tick_up_ticks_per_row_cmd, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}}"
TickDownCommand="{Binding DataContext.tick_down_ticks_per_row_cmd, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}}"
Value="{Binding DataContext.ticks_per_row, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}}"
/>
<TextBlock
Grid.Column="1"
Margin="2,0,0,0"
Text="{Binding DataContext.rounded_ticks_per_row, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}, StringFormat='({0})'}"
ToolTip="The ticks per row parameter may need to be adjusted to fit evenly into dollars."
/>
</Grid>
<c:RadioButtonsControl Grid.Row="2" Grid.ColumnSpan="3" ItemsSource="{Binding price_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_format}"/>
</Grid>
</GroupBox>
<GroupBox
Grid.Row="1"
@@ -52,6 +52,7 @@ namespace mbts.spark {
readonly CommonValueMonitor<SecurityImmutable?> _selected_security_mon = new CommonValueMonitor<SecurityImmutable?>();
readonly RightPanelModel _right_panel = new RightPanelModel();
readonly CustomStateSynchronizer _custom_state_sync;
readonly Func<OrderSecurityRootPair, bool> _global_filter;
OrderBookFillsGridSettings _cached_fills_settings = OrderBookStartupData.DefaultFillsGrid;
double
_raw_right_panel_width = OrderBookStartupData.Default.right_panel_width,
@@ -71,8 +72,8 @@ namespace mbts.spark {
FilterData _cached_filter_data;
public OrderBookVM(
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false)
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded) { }
IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool rows_are_initially_expanded = false, Func<OrderSecurityRootPair, bool> global_filter = null)
: this(bus, notification_service, model, OrdersModel.piq_enabled ? model.on_order_tuples_with_root_and_piq : model.on_order_tuples_with_root, settings_manager, rows_are_initially_expanded, global_filter) { }
public OrderBookVM(
IBus bus,
@@ -80,10 +81,12 @@ namespace mbts.spark {
OrdersModel model,
IObservable<TableUpdate<OrderSecurityRootPair, uint>> orders_source,
IWindowSettingsManager<OrderBookGridSettings> settings_manager,
bool rows_are_initially_expanded = false) {
bool rows_are_initially_expanded = false,
Func<OrderSecurityRootPair, bool> global_filter = null) {
_bus = bus;
_model = model;
_global_filter = global_filter;
_selected_params_mon = new OrderParamsAggregator(model.dynamic_helper, _selection_change_throttle, k => _cached_orders.TryGetValue(k, out var o) ? o.order : (OrderSecurityAlgo?)null);
ladder_vm = new LadderVMContainer(bus, model);
order_mod_vm = new OrderParamsVM(this, AlgoEditingPermissions.AllowEditValues);
@@ -611,7 +614,7 @@ namespace mbts.spark {
// resume column filter updates
_columns.filter_changed += _on_filter_changed;
grid_settings_manager.algo_tree_display_mode_changed += _on_filter_changed;
var new_filter = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states());
var new_filter = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states(), _global_filter);
var new_sort = _columns.get_sorting_data();
if (!new_filter.Equals(_cached_filter_data)) {
_cached_filter_data = new_filter;
@@ -686,7 +689,7 @@ namespace mbts.spark {
}
void _on_filter_changed() {
var fd = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states());
var fd = new FilterData(grid_settings_manager.algo_tree_display_mode, _columns.create_filter_states(), _global_filter);
if (!fd.Equals(_cached_filter_data)) {
_cached_filter_data = fd;
_row_collection.filter = fd.build_predicate();
@@ -970,13 +973,25 @@ namespace mbts.spark {
struct FilterData : IEquatable<FilterData> {
public readonly AlgoTreeDisplayMode algo_tree_display_mode;
public readonly FilterStates column_filters;
public readonly Func<OrderSecurityRootPair, bool> global_filter;
public FilterData(AlgoTreeDisplayMode algo_tree_display_mode, FilterStates column_filters) {
public FilterData(AlgoTreeDisplayMode algo_tree_display_mode, FilterStates column_filters, Func<OrderSecurityRootPair, bool> global_filter) {
this.algo_tree_display_mode = algo_tree_display_mode;
this.column_filters = column_filters;
this.global_filter = global_filter;
}
public Func<OrderSecurityRootPair, bool> build_predicate() {
if (global_filter == null)
return _build_predicate_core();
var core_pred = _build_predicate_core();
if (core_pred == null)
return global_filter;
var glbf = global_filter;
return new Func<OrderSecurityRootPair, bool>(o => glbf(o) && core_pred(o));
}
Func<OrderSecurityRootPair, bool> _build_predicate_core() {
var pred = column_filters.build_predicate();
if (algo_tree_display_mode == AlgoTreeDisplayMode.RootAndLeafNodes) {
if (pred == null) {
@@ -112,6 +112,14 @@ namespace mbts.ui.shell {
protected bool Set<T>(ref T backing, T value, [CallerMemberName]string name = "") =>
Set(ref backing, value, EqualityComparer<T>.Default, name);
protected bool Set<T>(ref T backing, T value, [CallerMemberName] string name = "", params string[] dependent_props) {
if (Set(ref backing, value, EqualityComparer<T>.Default, name)) {
RaisePropertyChanged(dependent_props);
return true;
}
return false;
}
protected bool Set<T>(ref T backing, T value, IEqualityComparer<T> comparer, [CallerMemberName]string name = "") {
if (!comparer.Equals(backing, value)) {
backing = value;
@@ -93,6 +93,25 @@ namespace mbts.ui.controls {
public uint Increment { get { return (uint)GetValue(IncrementProperty); } set { SetValue(IncrementProperty, value); } }
#region TickUpCommand
public static readonly DependencyProperty TickUpCommandProperty =
TickUpDown.TickUpCommandProperty.AddOwner(typeof(TickUpDownUInt));
public ICommand TickUpCommand {
get => GetValue(TickUpCommandProperty) as ICommand;
set => SetValue(TickUpCommandProperty, value);
}
#endregion
#region TickDownCommand
public static readonly DependencyProperty TickDownCommandProperty =
TickUpDown.TickDownCommandProperty.AddOwner(typeof(TickUpDownUInt));
public ICommand TickDownCommand {
get => GetValue(TickDownCommandProperty) as ICommand;
set => SetValue(TickDownCommandProperty, value);
}
#endregion
// explicit props
public bool EnableArrowKeyTick { get => _enable_arrow_key_tick.value; set => _enable_arrow_key_tick.value = value; }
public ModifierKeys ArrowKeyTickModifiers { get => _arrow_key_tick_modifiers.value; set => _arrow_key_tick_modifiers.value = value; }
@@ -120,13 +139,19 @@ namespace mbts.ui.controls {
}
void _tick_up() {
if (TickUpDownHelper.tick_up(Value, Increment, out uint result))
if (TickUpCommand is ICommand cmd) {
cmd.ExecuteIfCan(null);
} else if (TickUpDownHelper.tick_up(Value, Increment, out uint result)) {
SetCurrentValue(ValueProperty, result);
}
}
void _tick_down() {
if (TickUpDownHelper.tick_down(Value, Increment, out uint result))
if (TickDownCommand is ICommand cmd) {
cmd.ExecuteIfCan(null);
} else if (TickUpDownHelper.tick_down(Value, Increment, out uint result)) {
SetCurrentValue(ValueProperty, result);
}
}
}
}
@@ -1,388 +0,0 @@
using bts.api;
using bts.utils;
using mbts.co2;
using mbts.user.settings;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
static class TickUtils {
internal static ContractSpecificationImmutable spec_or_default(this ContractSpecificationImmutable? spec) =>
spec.HasValue ? spec.Value : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
internal static ContractSpecificationImmutable spec_or_default(this SecurityImmutable security) =>
security.HasContractSpec ? security.ContractSpec : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
internal static ContractSpecificationImmutable spec_or_default(this SecurityImmutable? security) =>
security.HasValue && security.Value.HasContractSpec ? security.Value.ContractSpec : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
public static ContractSpecificationImmutable spec_or_default(this SecurityMD? security_md) =>
(security_md?.security).spec_or_default();
public static double ticksize(this ContractSpecificationImmutable spec, double price) =>
Math.Abs(price) > spec.PriceBreak ? spec.HighTick : spec.LowTick;
public static string format(this ContractSpecificationImmutable spec, double price, double with_regard_to_price, bool in_tick_terms) {
double net_change = price - with_regard_to_price;
return (in_tick_terms) ?
TickStore.get(spec).format_without_decimal_part((net_change) / spec.ticksize(net_change)) :
TickStore.get(spec).format(net_change);
}
public static string format(this ContractSpecificationImmutable spec, double price) =>
TickStore.get(spec).format(price);
public static string format(this ContractSpecificationImmutable? spec, double price) =>
spec.spec_or_default().format(price);
public static string format(this ContractSpecificationImmutable spec, double price, double display_multiplier, bool show_commas, string nan_text) =>
TickStore.get(spec).format(price, display_multiplier, show_commas, nan_text);
public static string format(this ContractSpecificationImmutable? spec, double price, double display_multiplier, bool show_commas, string nan_text) =>
spec.spec_or_default().format(price, display_multiplier, show_commas, nan_text);
public static double parse(this ContractSpecificationImmutable spec, string text, double display_multiplier, string nan_text) =>
TickStore.get(spec).parse(text, display_multiplier, nan_text);
public static double parse(this ContractSpecificationImmutable? spec, string text, double display_multiplier, string nan_text) =>
spec.spec_or_default().parse(text, display_multiplier, nan_text);
public static bool is_integer_tick(ContractSpecificationImmutable spec) =>
spec.MainFraction == 0 &&
spec.PriceDisplayFormat == 0 &&
spec.LowTick > 0 &&
MathUtils.is_int(spec.LowTick) &&
spec.HighTick > 0 &&
MathUtils.is_int(spec.HighTick);
public static ContractSpecification create_integer_tick_spec(int low_tick) =>
create_integer_tick_spec(low_tick, low_tick);
public static ContractSpecification create_integer_tick_spec(int low_tick, int high_tick) {
var rv = SecurityDataHelper.Defaults.ContractSpecification;
rv.MainFraction = 0;
rv.SubFraction = 0;
rv.PriceDisplayFormat = 0;
rv.PriceBreak = 0;
rv.LowTick = low_tick;
rv.HighTick = high_tick;
return rv;
}
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double low = bids[bids.Length - 1].price;
if (double.IsNaN(low))
low = bids.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(low))
low = asks[0].price;
return low;
}
public static double high_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double high = asks[asks.Length - 1].price;
if (double.IsNaN(high))
high = asks.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(high))
high = bids[0].price;
return high;
}
public static int? tickdiff(this ContractSpecificationImmutable spec, double x, double y) {
if (get_regional_ticks(spec).try_get_tickdiff(x, y, out int n_low, out int n_high))
return n_low + n_high;
return null;
}
public static int tickdiff_unchecked(this ContractSpecificationImmutable spec, double x, double y) {
(int n_low, int n_high) = get_regional_ticks(spec).tickdiff_unchecked(x, y);
return n_low + n_high;
}
public static double mid_tick(ContractSpecificationImmutable spec, double bid, double ask) {
double raw_mid;
if (double.IsNaN(bid)) {
if (double.IsNaN(ask)) return double.NaN;
raw_mid = ask;
} else if (double.IsNaN(ask)) {
raw_mid = bid;
} else {
raw_mid = (bid + ask) / 2.0;
}
double tick_size = spec.ticksize(raw_mid);
return raw_mid < 0 ?
raw_mid + (raw_mid % tick_size) :
raw_mid - (raw_mid % tick_size);
}
public static TickFactors deduce_ticks(IEnumerable<ContractSpecification> contract_specs) {
var ticks = new HashSet<TickFactors>(contract_specs.Select(TickStore.get).SelectMany(rtf => new[] { rtf.low_tick, rtf.high_tick }));
switch (ticks.Count) {
case 0: return TickStore.default_rtf.low_tick;
case 1: return ticks.First();
default:
double tick_base_lcm = MathUtils.LCM(ticks.Select(t => (int)t.tick_base));
double tick_unit_gcd = MathUtils.GCD(ticks.Select(t => t.tick_unit * (tick_base_lcm / t.tick_base)));
return new TickFactors(tick_base_lcm, tick_unit_gcd);
}
}
public static RegionalTickFactors get_tick_factors(ContractSpecificationImmutable spec) =>
TickStore.get(spec);
public static double tickup(this ContractSpecificationImmutable spec, double value) =>
TickStore.get(spec).tickup(value);
public static double tickup(this ContractSpecificationImmutable spec, double value, int count) {
if (count == 0) return value;
if (count < 0) return TickStore.get(spec).tickdown(value, (uint)(-count));
else return TickStore.get(spec).tickup(value, (uint)count);
}
public static double tickdown(this ContractSpecificationImmutable spec, double value) =>
TickStore.get(spec).tickdown(value);
public static double tickdown(this ContractSpecificationImmutable spec, double value, int count) {
if (count == 0) return value;
if (count < 0) return TickStore.get(spec).tickup(value, (uint)(-count));
else return TickStore.get(spec).tickdown(value, (uint)count);
}
public static bool tick_equals(this ContractSpecificationImmutable contract, double x, double y) =>
MathUtils.RoundFractionalUnits(x, contract.LowTick) == MathUtils.RoundFractionalUnits(y, contract.LowTick);
public static IEqualityComparer<double> tick_equality_comparer(this ContractSpecificationImmutable contract) =>
EqualityComparer.Create((double a, double b) => tick_equals(contract, a, b), x => MathUtils.RoundFractionalUnits(x, contract.LowTick));
public static int compare_tick_fallback_to_double(this ContractSpecificationImmutable contract, double x, double y) =>
tick_equals(contract, x, y) ? 0 : x.CompareTo(y);
public static double round_to_tick(this ContractSpecificationImmutable spec, double value) {
double intpart = (long)value;
double floatpart = value % 1;
double tick = spec.ticksize(value);
floatpart = ((int)(floatpart / tick)) * tick;
return intpart + floatpart;
}
public static decimal round_to_tick(this ContractSpecificationImmutable spec, decimal value) {
decimal intpart = (long)value;
decimal floatpart = value % 1;
decimal tick = (decimal)spec.ticksize((double)value);
floatpart = ((int)(floatpart / tick)) * tick;
return intpart + floatpart;
}
public static RegionalTickFactors get_regional_ticks(ContractSpecificationImmutable? spec) =>
spec.HasValue ? TickStore.get(spec.Value) : TickStore.default_rtf;
public static RegionalTickFactors get_regional_ticks(SecurityImmutable security) =>
get_regional_ticks(security.spec_or_default());
static TickFactors _inflate_to_base(TickFactors tick_factors, double new_base) =>
new TickFactors(new_base, tick_factors.tick_unit * new_base / tick_factors.tick_base);
public static class CompareUtils {
public static IComparer<double> base_comparer =>
Comparer<double>.Create(_compare_base);
public static IComparer<double> unit_comparer =>
Comparer<double>.Create(_compare_unit);
public static IComparer<TickFactors> base_unit_comparer =>
Comparer<TickFactors>.Create(bts.utils.CompareUtils.safe_comparison<TickFactors>(compare_base_unit));
public static int compare_base_unit(TickFactors a, TickFactors b) {
int comp = _compare_base(a.tick_base, b.tick_base);
return comp == 0 ? a.tick_unit.CompareTo(b.tick_unit) : comp;
}
static int _compare_base(double a, double b) {
double reduced_a = MathUtils.factor_out(a, 10);
double reduced_b = MathUtils.factor_out(b, 10);
int comp = reduced_a.CompareTo(reduced_b);
return comp == 0 ? a.CompareTo(b) : comp;
}
static int _compare_unit(double a, double b) =>
_compare_unit(a, b, 0, 0);
static int _compare_unit(double a, double b, int a_mult10, int b_mult10) {
// fractional-coins are a special case, as they may not be an integer to begin with
if (!MathUtils.is_int(a))
return _compare_unit(a * 10, b, ++a_mult10, b_mult10);
if (!MathUtils.is_int(b))
return _compare_unit(a, b * 10, a_mult10, ++b_mult10);
double reduced_a = a_mult10 > 0 ? a : MathUtils.factor_out(a, 10, 5);
double reduced_b = b_mult10 > 0 ? b : MathUtils.factor_out(b, 10, 5);
int comp = reduced_a.CompareTo(reduced_b);
return comp == 0 ? (a * Math.Pow(10, -a_mult10)).CompareTo(b * Math.Pow(10, -b_mult10)) : comp;
}
}
static class TickStore {
public static readonly RegionalTickFactors default_rtf = new RegionalTickFactors(new TickFactors(1000, 1), new TickFactors(1000, 1), double.MaxValue);
static readonly ConcurrentDictionary<SpecKey, RegionalTickFactors> _exchange_ticks = new ConcurrentDictionary<SpecKey, RegionalTickFactors>();
static readonly bool _som_ticks_fix_enabled = config.Configuration.get<bts.config.toggles.SOMTicksFix>().enabled;
public static RegionalTickFactors get(ContractSpecification spec) =>
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
public static RegionalTickFactors get(ContractSpecificationImmutable spec) =>
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
public static RegionalTickFactors get(SecurityImmutable security) =>
security.HasContractSpec ? get(security.ContractSpec) : default_rtf;
public static RegionalTickFactors get(SecurityImmutable? security) =>
security.HasValue ? get(security.Value) : default_rtf;
static RegionalTickFactors _create_rtf(SpecKey key) {
double tick_base = key.denominator;
if (tick_base == 0) {
// indicates tick is a decimal
tick_base = 100;
}
return _create_rtf(key.low_tick, key.high_tick, key.price_break, tick_base, key.display_digits);
}
static RegionalTickFactors _create_rtf(double low_tick, double high_tick, double price_break, double reduced_target_base, uint display_digits_override) {
var low_tf = _try_create_tf(low_tick, reduced_target_base, display_digits_override);
var high_tf = _try_create_tf(high_tick, reduced_target_base, display_digits_override);
if (low_tf == null) {
low_tf = high_tf ?? (high_tf = default_rtf.low_tick);
} else if (high_tf == null)
high_tf = low_tf;
else {
// if low/high tick-bases do not match, inflate the smaller base so they do
// Note: this assumes the tick-bases are related by factors of 10 (i.e. they have all the same prime factors except for a different number of pairs of 5 and 2, e.g. 64 and 640)
if (low_tf.tick_base > high_tf.tick_base) {
high_tf = _inflate_to_base(high_tf, low_tf.tick_base);
} else if (low_tf.tick_base < high_tf.tick_base) {
low_tf = _inflate_to_base(low_tf, high_tf.tick_base);
}
}
return new RegionalTickFactors(low_tf, high_tf, price_break);
}
/// <summary>
/// Creates TickFactors with a base equal to &quot;<paramref name="tick_base"/> * 10^n,&quot; where 'n' is a natural number (including 0).
/// <para>Note: If <paramref name="display_digits_override"/> is non-zero, 'n' will be &quot;<paramref name="display_digits_override"/> - Math.Ceiling(Log10(<paramref name="tick_base"/>))&quot;</para>
/// </summary>
/// <param name="tick_size">The raw tick-size.</param>
/// <param name="tick_base">The raw tick-base.</param>
/// <returns>Well-defined TickFactors</returns>
static TickFactors _try_create_tf(double tick_size, double tick_base, uint display_digits_override) {
var tf = _try_create_tf(tick_size, tick_base);
if (tf != null && display_digits_override > 0) {
int display_digit_diff = (int)display_digits_override - tf.display_digits();
if (_som_ticks_fix_enabled) {
// we can always add display_digits (i.e. dislpay_digit_diff > 0), of course the ticks can be expressed precisely with extra 0s.
// but if we are going to remove display_digits (i.e display_digit_diff < 0), we must make sure the tick_unit is large enough to be expressed precisely with that many fewer digits
// - "-display_digit_diff <= Math.Ceiling(Math.Log10(tf.tick_unit))" will tell us if the digits can be removed without compromising precision
// - unfortunately, we cannot rely on the exchange's ContractSpec for this, there is 1 known case where the data they give us is incorrect
// SOM spreads ContractSpec (from the exchange):
// LowTick: 0.0025 (aka 1/400, or 2/800)
// MainFraction: 8
// SubFraction: 0
// PriceDisplayFormat: 1
// - as you can see, this cannot possibly be formatted with 1 display digit, it ticks in increments of 1/400
// - in practice, to maintain the '8' base, we format this in increments of 2/800 (e.g. 1-000, 1-002, 1-004, and so on up to 1-778, 2-000)
if (display_digit_diff > 0 || (display_digit_diff < 0 && -display_digit_diff <= Math.Ceiling(Math.Log10(tf.tick_unit))))
tf = _inflate_to_base(tf, tf.tick_base * Math.Pow(10, display_digit_diff));
} else if (display_digit_diff != 0) {
tf = _inflate_to_base(tf, tf.tick_base * Math.Pow(10, display_digit_diff));
}
}
return tf;
}
/// <summary>
/// Creates TickFactors with a base equal to &quot;reduced_target_base * 10^n,&quot; where 'n' is a natural number (including 0).
/// </summary>
/// <param name="tick_size">The raw tick-size.</param>
/// <param name="reduced_target_base">The smallest acceptable tick-base.</param>
/// <returns>Well-defined TickFactors</returns>
static TickFactors _try_create_tf(double tick_size, double reduced_target_base) {
if (!_check_tick_size(tick_size))
return null;
double raw_base = 1 / tick_size;
double raw_base_log10 = Math.Log10(raw_base);
double reduced_target_base_log10 = Math.Log10(reduced_target_base);
double log10_diff = raw_base_log10 - reduced_target_base_log10;
if (MathUtils.is_int(log10_diff))
return raw_base > reduced_target_base ? new TickFactors(raw_base, 1) : new TickFactors(reduced_target_base, Math.Pow(10, -log10_diff));
int count = 0;
double factored_base = log10_diff > 0 ? reduced_target_base * Math.Pow(10, Math.Ceiling(log10_diff)) : reduced_target_base;
do {
// Note that the modulus operator is not used here due to rounding errors.
// Such a rounding errror was the cause of the bug https://bluetradesys.atlassian.net/browse/SP-283.
if (MathUtils.is_int(factored_base / raw_base))
return new TickFactors(factored_base, factored_base / raw_base);
factored_base *= 10;
} while (++count < 10);
return null;
}
static TickFactors _create_tf(double tick_size) {
if (!_check_tick_size(tick_size))
return default_rtf.low_tick;
double matched_unit = double.MinValue;
TickFactors matched_tf = null;
foreach (var kv in _exchange_ticks) {
if (kv.Key.high_tick == tick_size) {
return kv.Value.high_tick;
} else if (kv.Key.low_tick == tick_size) {
return kv.Value.low_tick;
} else if (is_better_match(kv.Key.high_tick)) {
matched_unit = kv.Key.high_tick;
matched_tf = kv.Value.high_tick;
} else if (is_better_match(kv.Key.low_tick)) {
matched_unit = kv.Key.low_tick;
matched_tf = kv.Value.low_tick;
}
}
return matched_tf ?? new TickFactors(1.0 / tick_size, 1);
bool is_better_match(double known_unit) =>
known_unit < tick_size && known_unit > matched_unit && tick_size % known_unit == 0;
}
static bool _check_tick_size(double tick_size) =>
tick_size > 0 && (tick_size <= 0.5 || MathUtils.is_int(tick_size));
struct SpecKey : IEquatable<SpecKey> {
public readonly uint denominator, display_digits;
public readonly double low_tick, high_tick, price_break;
public SpecKey(ContractSpecification spec)
: this(spec.MainFraction, spec.PriceDisplayFormat, spec.LowTick, spec.HighTick, spec.PriceBreak) { }
public SpecKey(ContractSpecificationImmutable spec)
: this(spec.MainFraction, spec.PriceDisplayFormat, spec.LowTick, spec.HighTick, spec.PriceBreak) { }
public SpecKey(uint denominator, uint display_digits, double low_tick, double high_tick, double price_break) {
this.denominator = denominator;
this.display_digits = display_digits;
this.low_tick = low_tick;
this.high_tick = high_tick;
this.price_break = price_break;
}
public bool Equals(SpecKey other) =>
denominator == other.denominator &&
display_digits == other.display_digits &&
low_tick == other.low_tick &&
high_tick == other.high_tick &&
price_break == other.price_break;
public override bool Equals(object obj) =>
obj is SpecKey sk && Equals(sk);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash((int)denominator, (int)display_digits, low_tick.GetHashCode(), high_tick.GetHashCode(), price_break.GetHashCode());
}
}
}
}
+5 -1
View File
@@ -112,5 +112,9 @@ namespace mbts.co2 {
return !a.Equals(b);
}
}
static class NativePriceSizeExtensions {
public static NativePriceSize with(this NativePriceSize nps, uint? direct_size = null, uint? implied_size = null) =>
new NativePriceSize(nps.int_price, direct_size ?? nps.direct_size, implied_size ?? nps.implied_size);
}
}
+41
View File
@@ -23,6 +23,9 @@ namespace bts.utils {
public static bool epsilon_equals(double x, double y) =>
Math.Abs(x - y) < epsilon;
public static bool marginal_equals(double x, double y, double margin = epsilon) =>
Math.Abs(x - y) < margin;
public static double GCD(IEnumerable<double> nums) {
var no_zeros = nums.Where(n => n != 0);
if (!no_zeros.Any()) return 0;
@@ -102,6 +105,44 @@ namespace bts.utils {
public static int RoundFractionalUnits(double value, double fractional_unit) =>
(int)Math.Round(value / fractional_unit);
public static double round_down_abs(double value, double margin = epsilon) {
if (value < 0) {
return round_up(value, margin);
} else if (value > 0) {
return round_down(value, margin);
}
return 0;
}
public static double round_down(double value, double margin = epsilon) {
if (double.IsNaN(value) || double.IsInfinity(value))
return value;
var result = Math.Floor(value);
if (marginal_equals(value - result, 1, margin)) {
result = Math.Ceiling(value);
}
return result;
}
public static double round_up_abs(double value, double margin = epsilon) {
if (value < 0) {
return round_down(value, margin);
} else if (value > 0) {
return round_up(value, margin);
}
return 0;
}
public static double round_up(double value, double margin = epsilon) {
if (double.IsNaN(value) || double.IsInfinity(value))
return value;
var result = Math.Ceiling(value);
if (marginal_equals(result - value, 1, margin)) {
result = Math.Floor(value);
}
return result;
}
public static bool is_int(double d) =>
epsilon_equals(d, Math.Round(d));
@@ -83,6 +83,49 @@ namespace System.Collections.Generic {
return rv;
}
public static IEnumerable<TAccumulate> AggregateConsecutiveGroups<T, TGroupKey, TAccumulate>(
this IEnumerable<T> source,
Func<TAccumulate> seed_factory,
Func<TAccumulate, T, TAccumulate> aggregate,
Func<T, TGroupKey> group_key) =>
AggregateConsecutiveGroups(source, seed_factory, aggregate, group_key, EqualityComparer<TGroupKey>.Default);
public static IEnumerable<TAccumulate> AggregateConsecutiveGroups<T, TGroupKey, TAccumulate>(
this IEnumerable<T> source,
Func<TAccumulate> seed_factory,
Func<TAccumulate, T, TAccumulate> aggregate,
Func<T, TGroupKey> group_key,
IEqualityComparer<TGroupKey> group_key_comparer) {
//
if (source is null)
throw new ArgumentNullException(nameof(source));
if (seed_factory is null)
throw new ArgumentNullException(nameof(seed_factory));
if (aggregate is null)
throw new ArgumentNullException(nameof(aggregate));
if (group_key is null)
throw new ArgumentNullException(nameof(group_key));
if (group_key_comparer is null)
throw new ArgumentNullException(nameof(group_key_comparer));
using (var en = source.GetEnumerator()) {
if (en.MoveNext()) {
var curr_group = group_key(en.Current);
var agg = aggregate(seed_factory(), en.Current);
while (en.MoveNext()) {
var group = group_key(en.Current);
if (group_key_comparer.Equals(group, curr_group))
agg = aggregate(agg, en.Current);
else {
yield return agg;
curr_group = group;
agg = aggregate(seed_factory(), en.Current);
}
}
yield return agg;
}
}
}
public static bool CountAtLeast<T>(this IEnumerable<T> source, int count) {
if (count < 0)
throw new ArgumentException(ExceptionBuilder.Format.LessThan(0), nameof(count));
@@ -1,9 +1,7 @@
using bts.api;
using System.ComponentModel.Composition;
namespace mbts.user.settings
{
[InheritedExport]
internal interface ISecurityBasedSettingsFactory
{
SecurityBasedSettings get(Venue exchange, SecurityType type, string symbol);
@@ -1,6 +1,4 @@
using bts.utils;
using mbts.spark.utilities;
using mbts.ui;
using ProtoBuf;
using System;
using System.ComponentModel;
@@ -24,11 +22,8 @@ namespace mbts.user.settings {
_low_tick_inclusive_end = low_tick_inclusive_end;
}
[DisplayInfo("Low")]
public TickFactors low_tick => _low_tick;
[DisplayInfo("High")]
public TickFactors high_tick => _high_tick;
[DisplayInfo("Inclusive Low Range End Pt")]
public double low_tick_inclusive_end => _low_tick_inclusive_end;
public bool Equals(RegionalTickFactors other) =>
@@ -60,4 +55,4 @@ namespace mbts.user.settings {
public static bool operator !=(RegionalTickFactors x, RegionalTickFactors y) => !(x == y);
}
}
}
@@ -1,9 +1,7 @@
using mbts.ui;
using ProtoBuf;
using ProtoBuf;
namespace mbts.user.settings
{
[DisplayInfo("Tick Settings")]
[ProtoContract]
class SecurityTickSettings : SecurityBasedSettings
{
@@ -12,10 +10,8 @@ namespace mbts.user.settings
theo_tick = new TickFactors();
}
[DisplayInfo("Price", style = DisplayInfo.DisplayStyle.GroupBox)]
[ProtoMember(1)]
public RegionalTickFactors price_tick { get; set; }
[DisplayInfo("Theo")]
[ProtoMember(2)]
public TickFactors theo_tick { get; set; }
}
@@ -1,10 +1,11 @@
using mbts.user.settings;
using System;
using System.Globalization;
using bts;
using static bts.utils.MathUtils;
using bts;
using bts.api;
using bts.utils;
using mbts.spark;
using mbts.user.settings;
using System;
using System.Globalization;
using static bts.utils.MathUtils;
namespace mbts {
static class TickExtensions {
@@ -18,6 +19,9 @@ namespace mbts {
public static bool is_valid(this RegionalTickFactors regional_tick) =>
regional_tick.low_tick.is_valid() && regional_tick.high_tick.is_valid();
public static bool is_integer(this TickFactors tick) =>
tick.tick_unit == tick.tick_base;
public static double tick_unit_frac(this TickFactors tick) =>
tick.tick_unit / tick.tick_base;
@@ -88,25 +92,33 @@ namespace mbts {
return _round_tick(tuf, val);
}
public static double tickup(this RegionalTickFactors regional_tick, double input) { return regional_tick.tickup(input, 1); }
public static double tickup(this RegionalTickFactors regional_tick, double input, uint count) {
public static double tickup(this RegionalTickFactors regional_tick, double input) =>
regional_tick.tickup(input, 1);
public static double tickup(this RegionalTickFactors regional_tick, double input, uint count) =>
regional_tick.tickup(input, count, TickUtils.precision_const);
public static double tickup(this RegionalTickFactors regional_tick, double input, uint count, double precision, bool bling = true) {
if (!precision.is_real_positive())
throw new ArgumentException(nameof(precision), "Must be a real positive number.");
if (DoubleHelper.IsNaNOrInfinity(input) || count == 0) return input;
double val, tuf;
if (input < regional_tick.low_tick_inclusive_end) {
tuf = regional_tick.low_tick.tick_unit_frac();
double ieeer = Math.IEEERemainder(input, tuf);
input -= ieeer;
if (ieeer < -_precision_const) count--;
if (ieeer < -precision) count--;
val = input + tuf * count;
if (val > regional_tick.low_tick_inclusive_end) {
count -= (uint)Math.Ceiling((regional_tick.low_tick_inclusive_end - input) / tuf);
count -= (uint)MathUtils.round_up((regional_tick.low_tick_inclusive_end - input) / tuf, precision);
tuf = regional_tick.high_tick.tick_unit_frac();
val = regional_tick.low_tick_inclusive_end + tuf * count;
}
} else {
tuf = regional_tick.high_tick.tick_unit_frac();
double ieeer = Math.IEEERemainder(input, tuf);
if (Math.Abs(ieeer) <= _precision_const) input -= ieeer;
input -= ieeer;
if (ieeer < -precision) count--;
val = input + tuf * count;
}
return _round_tick(tuf, val);
@@ -176,8 +188,6 @@ namespace mbts {
static int _count_ticks(double tick_unit_frac, double value) =>
(int)Math.Round(value / tick_unit_frac);
private const double _precision_const = 0.000000001;
public static double tickdown(this TickFactors tick, double input) { return tick.tickdown(input, 1); }
public static double tickdown(this TickFactors tick, double input, uint count) {
if (DoubleHelper.IsNaNOrInfinity(input)) return input;
@@ -188,25 +198,33 @@ namespace mbts {
return _round_tick(tuf, val);
}
public static double tickdown(this RegionalTickFactors regional_tick, double input) { return regional_tick.tickdown(input, 1); }
public static double tickdown(this RegionalTickFactors regional_tick, double input, uint count) {
public static double tickdown(this RegionalTickFactors regional_tick, double input) =>
regional_tick.tickdown(input, 1);
public static double tickdown(this RegionalTickFactors regional_tick, double input, uint count) =>
tickdown(regional_tick, input, count, TickUtils.precision_const);
public static double tickdown(this RegionalTickFactors regional_tick, double input, uint count, double precision) {
if (!precision.is_real_positive())
throw new ArgumentException(nameof(precision), "Must be a real positive number.");
if (DoubleHelper.IsNaNOrInfinity(input) || count == 0) return input;
double val, tuf;
if (input > regional_tick.low_tick_inclusive_end) {
tuf = regional_tick.high_tick.tick_unit_frac();
double ieeer = Math.IEEERemainder(input, tuf);
input -= ieeer;
if (ieeer > _precision_const) count--;
if (ieeer > precision) count--;
val = input - count * tuf;
if (val < regional_tick.low_tick_inclusive_end) {
count -= (uint)Math.Ceiling((input - regional_tick.low_tick_inclusive_end) / tuf);
count -= (uint)MathUtils.round_up((input - regional_tick.low_tick_inclusive_end) / tuf, precision);
tuf = regional_tick.low_tick.tick_unit_frac();
val = regional_tick.low_tick_inclusive_end - tuf * count;
}
} else {
tuf = regional_tick.low_tick.tick_unit_frac();
double ieeer = Math.IEEERemainder(input, tuf);
if (Math.Abs(ieeer) <= _precision_const) input -= ieeer;
input -= ieeer;
if (ieeer > precision) count--;
val = input - tuf * count;
}
return _round_tick(tuf, val);
@@ -337,4 +355,4 @@ namespace mbts {
}
#endregion
}
}
}
@@ -1,5 +1,4 @@
using mbts.ui;
using ProtoBuf;
using ProtoBuf;
using System;
using System.ComponentModel;
@@ -19,9 +18,7 @@ namespace mbts.user.settings {
_tick_unit = tick_unit;
}
[DisplayInfo("Base")]
public double tick_base => _tick_base;
[DisplayInfo("Unit")]
public double tick_unit => _tick_unit;
public bool Equals(TickFactors other) =>
@@ -48,4 +45,4 @@ namespace mbts.user.settings {
public static bool operator !=(TickFactors x, TickFactors y) => !(x == y);
}
}
}
@@ -0,0 +1,144 @@
using bts.api;
using bts.utils;
using mbts;
using mbts.user.settings;
using System;
using System.Collections.Generic;
namespace bts.data.ticks {
class TickFactorsFactory {
static readonly RegionalTickFactors _default_rtf = new RegionalTickFactors(new TickFactors(1000, 1), new TickFactors(1000, 1), double.MaxValue);
readonly Dictionary<SpecKey, RegionalTickFactors> _exchange_ticks = new Dictionary<SpecKey, RegionalTickFactors>();
public RegionalTickFactors default_rtf => _default_rtf;
public RegionalTickFactors get(ContractSpecification spec) =>
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
public RegionalTickFactors get(ContractSpecificationImmutable spec) =>
_exchange_ticks.GetOrAdd(new SpecKey(spec), _create_rtf);
public RegionalTickFactors get(SecurityImmutable security) =>
security.HasContractSpec ? get(security.ContractSpec) : _default_rtf;
public RegionalTickFactors get(SecurityImmutable? security) =>
security.HasValue ? get(security.Value) : _default_rtf;
static RegionalTickFactors _create_rtf(SpecKey key) {
double tick_base = key.denominator;
if (tick_base == 0) {
// indicates tick is a decimal
tick_base = 100;
}
return _create_rtf(key.low_tick, key.high_tick, key.price_break, tick_base, key.display_digits);
}
static RegionalTickFactors _create_rtf(double low_tick, double high_tick, double price_break, double reduced_target_base, uint display_digits_override) {
var low_tf = _try_create_tf(low_tick, reduced_target_base, display_digits_override);
var high_tf = _try_create_tf(high_tick, reduced_target_base, display_digits_override);
if (low_tf == null) {
low_tf = high_tf ?? (high_tf = _default_rtf.low_tick);
} else if (high_tf == null)
high_tf = low_tf;
else {
// if low/high tick-bases do not match, inflate the smaller base so they do
// Note: this assumes the tick-bases are related by factors of 10 (i.e. they have all the same prime factors except for a different number of pairs of 5 and 2, e.g. 64 and 640)
if (low_tf.tick_base > high_tf.tick_base) {
high_tf = _inflate_to_base(high_tf, low_tf.tick_base);
} else if (low_tf.tick_base < high_tf.tick_base) {
low_tf = _inflate_to_base(low_tf, high_tf.tick_base);
}
}
// if price break is not a multiple of both low/high tick, set it to nan
// (much simpler than trying to interpret data that is incorrect)
if (!price_break.is_real() || !MathUtils.is_int(price_break / low_tick) || !MathUtils.is_int(price_break / high_tick)) {
price_break = double.NaN;
high_tf = low_tf;
}
return new RegionalTickFactors(low_tf, high_tf, price_break);
}
/// <summary>
/// Creates TickFactors with a base equal to &quot;<paramref name="tick_base"/> * 10^n,&quot; where 'n' is a natural number (including 0).
/// <para>Note: If <paramref name="display_digits_override"/> is non-zero, 'n' will be &quot;<paramref name="display_digits_override"/> - Math.Ceiling(Log10(<paramref name="tick_base"/>))&quot;</para>
/// </summary>
/// <param name="tick_size">The raw tick-size.</param>
/// <param name="tick_base">The raw tick-base.</param>
/// <returns>Well-defined TickFactors</returns>
static TickFactors _try_create_tf(double tick_size, double tick_base, uint display_digits_override) {
var tf = _try_create_tf(tick_size, tick_base);
if (tf != null && display_digits_override > 0) {
int display_digit_diff = (int)display_digits_override - tf.display_digits();
if (display_digit_diff != 0) {
tf = _inflate_to_base(tf, tf.tick_base * Math.Pow(10, display_digit_diff));
}
}
return tf;
}
/// <summary>
/// Creates TickFactors with a base equal to &quot;reduced_target_base * 10^n,&quot; where 'n' is a natural number (including 0).
/// </summary>
/// <param name="tick_size">The raw tick-size.</param>
/// <param name="reduced_target_base">The smallest acceptable tick-base.</param>
/// <returns>Well-defined TickFactors</returns>
static TickFactors _try_create_tf(double tick_size, double reduced_target_base) {
if (!_check_tick_size(tick_size))
return null;
double raw_base = 1 / tick_size;
double raw_base_log10 = Math.Log10(raw_base);
double reduced_target_base_log10 = Math.Log10(reduced_target_base);
double log10_diff = raw_base_log10 - reduced_target_base_log10;
if (MathUtils.is_int(log10_diff))
return raw_base > reduced_target_base ? new TickFactors(raw_base, 1) : new TickFactors(reduced_target_base, Math.Pow(10, -log10_diff));
int count = 0;
double factored_base = log10_diff > 0 ? reduced_target_base * Math.Pow(10, Math.Ceiling(log10_diff)) : reduced_target_base;
do {
// Note that the modulus operator is not used here due to rounding errors.
// Such a rounding errror was the cause of the bug https://bluetradesys.atlassian.net/browse/SP-283.
if (MathUtils.is_int(factored_base / raw_base))
return new TickFactors(factored_base, factored_base / raw_base);
factored_base *= 10;
} while (++count < 10);
return null;
}
static TickFactors _inflate_to_base(TickFactors tick_factors, double new_base) =>
new TickFactors(new_base, tick_factors.tick_unit * new_base / tick_factors.tick_base);
static bool _check_tick_size(double tick_size) =>
tick_size > 0 && (tick_size <= 0.5 || MathUtils.is_int(tick_size));
struct SpecKey : IEquatable<SpecKey> {
public readonly uint denominator, display_digits;
public readonly double low_tick, high_tick, price_break;
public SpecKey(ContractSpecification spec)
: this(spec.MainFraction, spec.PriceDisplayFormat, spec.LowTick, spec.HighTick, spec.PriceBreak) { }
public SpecKey(ContractSpecificationImmutable spec)
: this(spec.MainFraction, spec.PriceDisplayFormat, spec.LowTick, spec.HighTick, spec.PriceBreak) { }
public SpecKey(uint denominator, uint display_digits, double low_tick, double high_tick, double price_break) {
this.denominator = denominator;
this.display_digits = display_digits;
this.low_tick = low_tick;
this.high_tick = high_tick;
this.price_break = price_break;
}
public bool Equals(SpecKey other) =>
denominator == other.denominator &&
display_digits == other.display_digits &&
low_tick == other.low_tick &&
high_tick == other.high_tick &&
price_break == other.price_break;
public override bool Equals(object obj) =>
obj is SpecKey sk && Equals(sk);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash((int)denominator, (int)display_digits, low_tick.GetHashCode(), high_tick.GetHashCode(), price_break.GetHashCode());
}
}
}
@@ -0,0 +1,325 @@
using bts.api;
using bts.data.ticks;
using bts.utils;
using mbts.co2;
using mbts.user.settings;
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
static class TickUtils {
//const double _precision_const = 0.000000001;
const double _precision_const = 0.00000001;
public static double precision_const => _precision_const;
[ThreadStatic]
static TickFactorsFactory _exchange_ticks_backing;
static TickFactorsFactory _exchange_ticks =>
_exchange_ticks_backing ?? (_exchange_ticks_backing = new TickFactorsFactory());
internal static ContractSpecificationImmutable spec_or_default(this ContractSpecificationImmutable? spec) =>
spec.HasValue ? spec.Value : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
internal static ContractSpecificationImmutable spec_or_default(this SecurityImmutable security) =>
security.HasContractSpec ? security.ContractSpec : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
internal static ContractSpecificationImmutable spec_or_default(this SecurityImmutable? security) =>
security.HasValue && security.Value.HasContractSpec ? security.Value.ContractSpec : SecurityDataHelper.Defaults.ContractSpecificationImmutable;
public static ContractSpecificationImmutable spec_or_default(this SecurityMD? security_md) =>
(security_md?.security).spec_or_default();
public static IComparer<double> price_comparer(this ContractSpecificationImmutable spec) =>
MathUtils.epsilon_comparer.wrap<double, double>(p => spec.round_to_tick(p));
public static double ticksize(this ContractSpecificationImmutable spec, double price) {
if (!spec.LowTick.is_real_positive()) {
return spec.HighTick;
} else if (!spec.HighTick.is_real_positive()) {
return spec.LowTick;
} else if (!price.is_real() || !spec.PriceBreak.is_real()) {
return spec.LowTick;
} else {
return price > spec.PriceBreak ? spec.HighTick : spec.LowTick;
}
}
public static string format(this ContractSpecificationImmutable spec, double price, bool in_tick_terms) =>
in_tick_terms ?
_get(spec).format_without_decimal_part(price / spec.ticksize(price)) :
_get(spec).format(price);
public static string format(this ContractSpecificationImmutable spec, double price) =>
_get(spec).format(price);
public static string format(this ContractSpecificationImmutable? spec, double price) =>
spec.spec_or_default().format(price);
public static string format(this ContractSpecificationImmutable spec, double price, double display_multiplier, bool show_commas, string nan_text) =>
_get(spec).format(price, display_multiplier, show_commas, nan_text);
public static string format(this ContractSpecificationImmutable? spec, double price, double display_multiplier, bool show_commas, string nan_text) =>
spec.spec_or_default().format(price, display_multiplier, show_commas, nan_text);
public static double parse(this ContractSpecificationImmutable spec, string text, double display_multiplier, string nan_text) =>
_get(spec).parse(text, display_multiplier, nan_text);
public static double parse(this ContractSpecificationImmutable? spec, string text, double display_multiplier, string nan_text) =>
spec.spec_or_default().parse(text, display_multiplier, nan_text);
public static bool is_integer_tick(ContractSpecificationImmutable spec) =>
spec.MainFraction == 0 &&
spec.PriceDisplayFormat == 0 &&
spec.LowTick > 0 &&
MathUtils.is_int(spec.LowTick) &&
spec.HighTick > 0 &&
MathUtils.is_int(spec.HighTick);
public static ContractSpecification create_integer_tick_spec(int low_tick) =>
create_integer_tick_spec(low_tick, low_tick);
public static ContractSpecification create_integer_tick_spec(int low_tick, int high_tick) {
var rv = SecurityDataHelper.Defaults.ContractSpecification;
rv.MainFraction = 0;
rv.SubFraction = 0;
rv.PriceDisplayFormat = 0;
rv.PriceBreak = 0;
rv.LowTick = low_tick;
rv.HighTick = high_tick;
return rv;
}
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double low = bids[bids.Length - 1].price;
if (double.IsNaN(low))
low = bids.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(low))
low = asks[0].price;
return low;
}
public static double high_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
double high = asks[asks.Length - 1].price;
if (double.IsNaN(high))
high = asks.LastOrDefault(nps => nps.price, p => !double.IsNaN(p), double.NaN);
if (double.IsNaN(high))
high = bids[0].price;
return high;
}
public static int? tickdiff(this ContractSpecificationImmutable spec, double x, double y) {
if (get_regional_ticks(spec).try_get_tickdiff(x, y, out int n_low, out int n_high))
return n_low + n_high;
return null;
}
public static int tickdiff_unchecked(this ContractSpecificationImmutable spec, double x, double y) {
(int n_low, int n_high) = get_regional_ticks(spec).tickdiff_unchecked(x, y);
return n_low + n_high;
}
public static double mid_tick(ContractSpecificationImmutable spec, double bid, double ask) {
double raw_mid;
if (double.IsNaN(bid)) {
if (double.IsNaN(ask)) return double.NaN;
raw_mid = ask;
} else if (double.IsNaN(ask)) {
raw_mid = bid;
} else {
raw_mid = (bid + ask) / 2.0;
}
double tick_size = spec.ticksize(raw_mid);
return raw_mid < 0 ?
raw_mid + (raw_mid % tick_size) :
raw_mid - (raw_mid % tick_size);
}
public static TickFactors deduce_ticks(IEnumerable<ContractSpecification> contract_specs) {
var ticks = new HashSet<TickFactors>(contract_specs.Select(_get).SelectMany(rtf => new[] { rtf.low_tick, rtf.high_tick }));
switch (ticks.Count) {
case 0: return _exchange_ticks.default_rtf.low_tick;
case 1: return ticks.First();
default:
double tick_base_lcm = MathUtils.LCM(ticks.Select(t => (int)t.tick_base));
double tick_unit_gcd = MathUtils.GCD(ticks.Select(t => t.tick_unit * (tick_base_lcm / t.tick_base)));
return new TickFactors(tick_base_lcm, tick_unit_gcd);
}
}
public static RegionalTickFactors get_tick_factors(ContractSpecificationImmutable spec) =>
_get(spec);
public static double tickup(this ContractSpecificationImmutable spec, double value) =>
_get(spec).tickup(value);
public static double tickup(this ContractSpecificationImmutable spec, double value, int count) {
if (count == 0) return value;
if (count < 0) return _get(spec).tickdown(value, (uint)(-count));
else return _get(spec).tickup(value, (uint)count);
}
public static double tickdown(this ContractSpecificationImmutable spec, double value) =>
_get(spec).tickdown(value);
public static double tickdown(this ContractSpecificationImmutable spec, double value, int count) {
if (count == 0) return value;
if (count < 0) return _get(spec).tickup(value, (uint)(-count));
else return _get(spec).tickdown(value, (uint)count);
}
public static bool tick_equals(this ContractSpecificationImmutable contract, double x, double y) =>
MathUtils.RoundFractionalUnits(x, contract.LowTick) == MathUtils.RoundFractionalUnits(y, contract.LowTick);
public static IEqualityComparer<double> tick_equality_comparer(this ContractSpecificationImmutable contract) =>
EqualityComparer.Create((double a, double b) => tick_equals(contract, a, b), x => MathUtils.RoundFractionalUnits(x, contract.LowTick));
public static int compare_tick_fallback_to_double(this ContractSpecificationImmutable contract, double x, double y) =>
tick_equals(contract, x, y) ? 0 : x.CompareTo(y);
public static double round_to_tick(this ContractSpecificationImmutable spec, double value) {
double intpart = (long)value;
double floatpart = value % 1;
double tick = spec.ticksize(value);
floatpart = ((int)Math.Round(floatpart / tick)) * tick;
return intpart + floatpart;
}
public static decimal round_to_tick(this ContractSpecificationImmutable spec, decimal value) {
decimal intpart = (long)value;
decimal floatpart = value % 1;
decimal tick = (decimal)spec.ticksize((double)value);
floatpart = ((int)(floatpart / tick)) * tick;
return intpart + floatpart;
}
public static int? full_ticks_from_zero(this ContractSpecificationImmutable spec, double value) =>
full_ticks_from(spec, 0, value);
public static int? full_ticks_from(this ContractSpecificationImmutable spec, double origin, double value) {
if (!value.is_real() || !origin.is_real())
return null;
if (value < origin) {
if (spec.LowTick.is_real_positive()) {
if (spec.HighTick.is_real_positive() && spec.PriceBreak < origin && spec.PriceBreak > value) {
var ticks_to_break = MathUtils.round_up((spec.PriceBreak - origin) / spec.HighTick, precision_const);
var low_ticks_remainder = MathUtils.round_up((value - spec.PriceBreak) / spec.LowTick, precision_const);
return (int)(ticks_to_break + low_ticks_remainder);
} else {
return (int)MathUtils.round_up((value - origin) / spec.LowTick, precision_const);
}
} else if (spec.HighTick.is_real_positive()) {
return (int)MathUtils.round_up((value - origin) / spec.HighTick, precision_const);
}
} else {
if (spec.HighTick.is_real_positive()) {
if (spec.LowTick.is_real_positive() && spec.PriceBreak > origin && spec.PriceBreak < value) {
var ticks_to_break = MathUtils.round_down((spec.PriceBreak - origin) / spec.LowTick, precision_const);
var high_ticks_remainder = MathUtils.round_down((value - spec.PriceBreak) / spec.HighTick, precision_const);
return (int)(ticks_to_break + high_ticks_remainder);
} else {
return (int)MathUtils.round_down(value / spec.HighTick, precision_const);
}
} else if (spec.LowTick.is_real_positive()) {
return (int)MathUtils.round_down(value / spec.LowTick, precision_const);
}
}
return null;
}
public static RegionalTickFactors get_regional_ticks(ContractSpecificationImmutable? spec) =>
spec.HasValue ? _get(spec.Value) : _exchange_ticks.default_rtf;
public static RegionalTickFactors get_regional_ticks(SecurityImmutable security) =>
get_regional_ticks(security.spec_or_default());
public static ContractSpecificationImmutable multiply_tick_size(ContractSpecificationImmutable spec, uint factor) {
factor = fit_ticks_per_row_to_dollars(spec, factor);
return spec.with(low_tick: spec.LowTick * factor, high_tick: spec.HighTick * factor);
}
public static double normalize_price(ContractSpecificationImmutable spec, double price) {
var ticks = spec.full_ticks_from_zero(price);
return ticks.HasValue ? spec.tickup(0, ticks.Value) : price;
}
public static uint fit_ticks_per_row_to_dollars(ContractSpecificationImmutable spec, uint ticks_per_row) =>
_fit_ticks_per_row_to_dollars(spec, ticks_per_row) ?? 1;
static uint? _fit_ticks_per_row_to_dollars(ContractSpecificationImmutable spec, uint ticks_per_row) {
if (ticks_per_row == 0)
return null;
// it is assumed that low tick will be the same as high tick;
// however, best to double check - return null if this isn't the case (let the caller decide what to do).
var tf = get_tick_factors(spec);
if (!tf.low_tick.Equals(tf.high_tick) || !tf.is_valid() || tf.low_tick.is_integer())
return null;
var ticks_per_dollar = (uint)(tf.low_tick.tick_base / tf.low_tick.tick_unit);
// if ticks_per_row is greater than ticks_per_dollar, round to a multiple of ticks_per_dollar
if (ticks_per_row > ticks_per_dollar) {
return (uint)Math.Floor((double)(ticks_per_row / ticks_per_dollar));
} else if (ticks_per_row == ticks_per_dollar) {
return ticks_per_row;
} else {
uint max_decrease_adj = ticks_per_row;
uint max_increase_adj = ticks_per_dollar - ticks_per_row - 1;
for (uint adj = 0; adj < Math.Max(max_decrease_adj, max_increase_adj); adj++) {
if (adj < max_decrease_adj) {
var result = ticks_per_row - adj;
if (ticks_per_dollar % result == 0)
return result;
}
if (adj < max_increase_adj) {
var result = ticks_per_row + adj;
if (ticks_per_dollar % result == 0)
return result;
}
}
}
return null;
}
static RegionalTickFactors _get(ContractSpecification spec) => _exchange_ticks.get(spec);
static RegionalTickFactors _get(ContractSpecificationImmutable spec) => _exchange_ticks.get(spec);
public static class CompareUtils {
public static IComparer<double> base_comparer =>
Comparer<double>.Create(_compare_base);
public static IComparer<double> unit_comparer =>
Comparer<double>.Create(_compare_unit);
public static IComparer<TickFactors> base_unit_comparer =>
Comparer<TickFactors>.Create(bts.utils.CompareUtils.safe_comparison<TickFactors>(compare_base_unit));
public static int compare_base_unit(TickFactors a, TickFactors b) {
int comp = _compare_base(a.tick_base, b.tick_base);
return comp == 0 ? a.tick_unit.CompareTo(b.tick_unit) : comp;
}
static int _compare_base(double a, double b) {
double reduced_a = MathUtils.factor_out(a, 10);
double reduced_b = MathUtils.factor_out(b, 10);
int comp = reduced_a.CompareTo(reduced_b);
return comp == 0 ? a.CompareTo(b) : comp;
}
static int _compare_unit(double a, double b) =>
_compare_unit(a, b, 0, 0);
static int _compare_unit(double a, double b, int a_mult10, int b_mult10) {
// fractional-coins are a special case, as they may not be an integer to begin with
if (!MathUtils.is_int(a))
return _compare_unit(a * 10, b, ++a_mult10, b_mult10);
if (!MathUtils.is_int(b))
return _compare_unit(a, b * 10, a_mult10, ++b_mult10);
double reduced_a = a_mult10 > 0 ? a : MathUtils.factor_out(a, 10, 5);
double reduced_b = b_mult10 > 0 ? b : MathUtils.factor_out(b, 10, 5);
int comp = reduced_a.CompareTo(reduced_b);
return comp == 0 ? (a * Math.Pow(10, -a_mult10)).CompareTo(b * Math.Pow(10, -b_mult10)) : comp;
}
}
}
}
@@ -10,6 +10,11 @@ namespace bts.utils {
public static int safe_compare<T>(this T a, T b) where T : IComparable<T> =>
a == null ? (b == null ? 0 : -1) : (b == null ? 1 : a.CompareTo(b));
public static int nullable_compare<T>(T? a, T? b) where T : struct, IComparable<T> =>
a.HasValue ?
(b.HasValue ? a.Value.CompareTo(b.Value) : 1) :
(b.HasValue ? -1 : 0);
#region Equality
public static bool NullCheckEquals<T>(T x, T y) =>
NullCheckEquals(x, y, (_x, _y) => _x.Equals(_y));
+2 -2
View File
@@ -96,7 +96,7 @@
}
public static bool update_value_type<T>(ref T field, T newval) where T : struct, IEquatable<T> {
bool rv = field.Equals(newval);
bool rv = !field.Equals(newval);
field = newval;
return rv;
}
@@ -106,4 +106,4 @@
v => v is TResult r ? r : throw new InvalidCastException($"Failed to cast element in source collection from type {typeof(T)} to {typeof(TResult)}.");
}
}
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace bts.tests.security {
public static class ContractSpecificationFiles {
public const string
Sim = "SimContractSpecifications.contract_spec_proto";
public static IEnumerable<string> all() {
yield return Sim;
}
}
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace bts.tests.security {
public static class SecurityFiles {
public const string
KrandallOrderBookCrash = "KrandallOrderBookCrash.security_proto",
NonDeterministicICEFutures = "NonDeterministicICEFutures.security_proto";
public static IEnumerable<string> all() {
yield return KrandallOrderBookCrash;
yield return NonDeterministicICEFutures;
}
}
}
+111 -12
View File
@@ -1,12 +1,16 @@
using bts.api;
using bts.collections;
using bts.mcore;
using bts.resources;
using bts.test;
using bts.utils;
using bts.utils.proto;
using mbts.spark;
using System;
using System.Collections.Generic;
using System.Linq;
namespace bts.mcore {
namespace bts.tests.security {
[TestGroup]
class SecurityTests {
[Test]
@@ -33,6 +37,18 @@ namespace bts.mcore {
_test_security_compare(_read_securities(file_path));
}
[Test]
public void test_ticks() {
var from_sec_files = SecurityFiles.all().SelectMany(_read_securities).Select(s => s.ContractSpec);
var from_spec_files = ContractSpecificationFiles.all().SelectMany(TickTestHelper.read_contract_specs);
var distinct_specs = new HashSet<ContractSpecificationImmutable>(from_sec_files.Concat(from_spec_files));
foreach (var spec in distinct_specs)
_test_ticks(spec);
}
static IEnumerable<SecurityImmutable> _read_securities(string file_path) =>
StreamHelper.ReadProtos<Security>(ResourceHelper.LoadResource(file_path)).Select(s => new SecurityImmutable(s));
void _test_security_compare(IEnumerable<SecurityImmutable> securities) {
const int max_count = 2048;
var sec_list = securities as IList<SecurityImmutable> ?? securities.ToList();
@@ -42,18 +58,101 @@ namespace bts.mcore {
ComparisonTests.identity_comparison(sec_list, SecurityHelper.Comparer.InstanceImmutable);
}
IEnumerable<SecurityImmutable> _read_securities(string file_path) =>
StreamHelper.ReadProtos<Security>(ResourceHelper.LoadResource(file_path)).Select(s => new SecurityImmutable(s));
static class SecurityFiles {
public const string
KrandallOrderBookCrash = "KrandallOrderBookCrash.security_proto",
NonDeterministicICEFutures = "NonDeterministicICEFutures.security_proto";
public static IEnumerable<string> all() {
yield return KrandallOrderBookCrash;
yield return NonDeterministicICEFutures;
void _test_ticks(ContractSpecificationImmutable tick) {
// the prices straddle a particular number (the radius determines how far above/below the straddle the price range spans)
const int min_tick_radius = 200;
// some ticks can have more than 200 prices between integers (n and n+1)
const int min_int_radius = 2;
// test price range about zero
var prices_about_zero = TickTestHelper.generate_test_prices(tick.LowTick, radius: get_radius(tick.LowTick), origin_price: 0);
_test_ticks(tick, prices_about_zero);
// test price range about the price break
if (tick.PriceBreak.is_real()) {
var prices_about_break = TickTestHelper.generate_test_prices(tick.HighTick, radius: get_radius(tick.HighTick), origin_price: tick.PriceBreak);
_test_ticks(tick, prices_about_break);
}
int get_radius(double tick_size) {
Assert.That(tick_size.is_real_positive());
return Math.Max(min_tick_radius, (int)(min_int_radius / tick_size));
}
}
void _test_ticks(ContractSpecificationImmutable tick, IEnumerable<double> prices) {
Assert.That(!(tick.LowTick < 0));
Assert.That(!(tick.HighTick < 0));
foreach (var p in prices) {
var expected_ticks_from_zero = TickTestHelper.full_ticks_from_zero(tick, p);
var actual_ticks_from_zero = TickUtils.full_ticks_from_zero(tick, p);
if (expected_ticks_from_zero != actual_ticks_from_zero) {
expected_ticks_from_zero = TickTestHelper.full_ticks_from_zero(tick, p);
actual_ticks_from_zero = TickUtils.full_ticks_from_zero(tick, p);
}
Assert.That(expected_ticks_from_zero == actual_ticks_from_zero,
$"Expected ticks from zero to be '{expected_ticks_from_zero}', actually '{actual_ticks_from_zero}' " +
$"(low/high/pricebreak: {tick.LowTick}/{tick.HighTick}/{tick.PriceBreak}, price: {p}).");
if (expected_ticks_from_zero.HasValue) {
Assert.That(p.is_real());
_test_tick_up(tick, p - 1, p);
if (tick.PriceBreak.is_real() && tick.PriceBreak != double.MaxValue && tick.PriceBreak != double.MinValue) {
// test both sides of the break with a few adjustments
foreach (var adjustment in new[] { 1, tick.LowTick, tick.HighTick }) {
_test_tick_up(tick, p, tick.PriceBreak + adjustment);
_test_tick_up(tick, p, tick.PriceBreak - adjustment);
}
}
}
}
}
void _test_tick_up(ContractSpecificationImmutable spec, double from, double to) {
if (!from.is_real() || !to.is_real())
return;
var (min, max) = MathUtils.get_outliers(from, to);
if (spec.PriceBreak.is_real()) {
if (min >= spec.PriceBreak) {
//_test_single_tick(spec.HighTick, min, max);
} else if (max <= spec.PriceBreak) {
_test_single_tick(spec, spec.LowTick, min, max);
} else {
//if (round_ticks(spec.LowTick, min, out var ))
_test_dual_tick(spec, min, max);
}
} else {
_test_single_tick(spec, spec.LowTick, min, max);
}
}
void _test_dual_tick(ContractSpecificationImmutable spec, double min, double max) {
Assert.That(min < spec.PriceBreak);
Assert.That(max > spec.PriceBreak);
// 2 tick counts are used to determine the number of ticks between min and max,
// 1) number of ticks from min to the price break
// 2) number of ticks from the price break to max
// if either min/max is beyond epsilon away from a real tick,
// a partial tick is used to correct the rounding error.
Assert.That(TickTestHelper.round_ticks(spec.LowTick, spec.PriceBreak - min, out var rounded_min, out var min_ticks_to_break));
Assert.That(TickTestHelper.round_ticks(spec.HighTick, max - spec.PriceBreak, out var rounded_max, out var max_ticks_to_break));
rounded_min = spec.PriceBreak - rounded_min;
rounded_max = spec.PriceBreak + rounded_max;
int min_partial_tick = TickTestHelper.check_partial_tick(spec, min, rounded_min, true);
int max_partial_tick = TickTestHelper.check_partial_tick(spec, max, rounded_max, false);
// assert that ticking up/down between min/max behaves expectedly.
var batch_tick_up_result = spec.tickup(min, min_ticks_to_break + max_ticks_to_break + min_partial_tick);
Assert.That(MathUtils.marginal_equals(batch_tick_up_result, rounded_max, TickUtils.precision_const));
var batch_tick_down_result = spec.tickdown(max, min_ticks_to_break + max_ticks_to_break + max_partial_tick);
Assert.That(MathUtils.marginal_equals(batch_tick_down_result, rounded_min, TickUtils.precision_const));
}
void _test_single_tick(ContractSpecificationImmutable spec, double tick_size, double min, double max) {
// min round/partial tick
Assert.That(TickTestHelper.round_ticks(tick_size, min, out var rounded_min, out var rounded_min_ticks));
var min_partial_tick = TickTestHelper.check_partial_tick(spec, min, rounded_min, true);
// max round/partial tick
Assert.That(TickTestHelper.round_ticks(tick_size, max, out var rounded_max, out var rounded_max_ticks));
var max_partial_tick = TickTestHelper.check_partial_tick(spec, max, rounded_max, false);
var full_ticks = rounded_max_ticks - rounded_min_ticks;
Assert.That(MathUtils.marginal_equals(rounded_max, spec.tickup(min, full_ticks + min_partial_tick), TickUtils.precision_const));
Assert.That(MathUtils.marginal_equals(rounded_min, spec.tickup(max, -(full_ticks + max_partial_tick)), TickUtils.precision_const));
}
}
}
@@ -0,0 +1,121 @@
using bts.api;
using bts.resources;
using bts.test;
using bts.utils;
using bts.utils.proto;
using mbts.spark;
using System;
using System.Collections.Generic;
using System.Linq;
namespace bts.tests.security {
static class TickTestHelper {
public static IEnumerable<double> generate_test_prices(double tick_size, int radius = 200, double origin_price = 0) {
Assert.That(radius > 0);
var price_base_set = Enumerable.Range(-radius, radius * 2).Select(
n => origin_price + tick_size * n);
return generate_test_prices(price_base_set, new[] {
0,
MathUtils.epsilon,
-MathUtils.epsilon,
MathUtils.epsilon * 100,
-MathUtils.epsilon * 100,
tick_size * 0.02,
-tick_size * 0.02,
tick_size * 0.49,
-tick_size * 0.49,
tick_size * 0.99,
-tick_size * 0.99,
});
}
public static IEnumerable<double> generate_test_prices(IEnumerable<double> price_base_set, IEnumerable<double> adjustments) =>
price_base_set.Concat(
adjustments.SelectMany(a => price_base_set.Select(p => p + a)));
public static IEnumerable<ContractSpecificationImmutable> read_contract_specs(string file_path) =>
StreamHelper.ReadProtos<ContractSpecification>(ResourceHelper.LoadResource(file_path)).Select(s => new ContractSpecificationImmutable(s));
public static int? full_ticks_from_zero(ContractSpecificationImmutable tick, double value) {
if (!value.is_real())
return null;
if (MathUtils.marginal_equals(value, 0, TickUtils.precision_const))
return 0;
bool low_tick_valid = tick.LowTick.is_real_positive();
bool high_tick_valid = tick.HighTick.is_real_positive();
if (low_tick_valid) {
if (high_tick_valid) {
if (value < 0) {
if (tick.PriceBreak > value) {
if (tick.PriceBreak >= 0) {
return (int)MathUtils.round_up(value / tick.LowTick, TickUtils.precision_const);
} else {
var high_ticks = (int)MathUtils.round_up(tick.PriceBreak / tick.HighTick, TickUtils.precision_const);
var low_ticks = (int)MathUtils.round_up((value - tick.PriceBreak) / tick.LowTick, TickUtils.precision_const);
return high_ticks + low_ticks;
}
} else {
return (int)MathUtils.round_up(value / (tick.PriceBreak.is_real() ? tick.HighTick : tick.LowTick), TickUtils.precision_const);
}
} else {
if (tick.PriceBreak < value) {
if (tick.PriceBreak <= 0) {
return (int)MathUtils.round_down(value / tick.HighTick, TickUtils.precision_const);
} else {
var low_ticks = (int)MathUtils.round_down(tick.PriceBreak / tick.LowTick, TickUtils.precision_const);
var high_ticks = (int)MathUtils.round_down((value - tick.PriceBreak) / tick.HighTick, TickUtils.precision_const);
return low_ticks + high_ticks;
}
} else {
return (int)MathUtils.round_down(value / (tick.PriceBreak.is_real() ? tick.LowTick : tick.HighTick), TickUtils.precision_const);
}
}
} else {
return (int)MathUtils.round_down_abs(value / tick.LowTick, TickUtils.precision_const);
}
} else if (high_tick_valid) {
return (int)MathUtils.round_down_abs(value / tick.HighTick, TickUtils.precision_const);
} else {
return null;
}
}
public static bool round_ticks(double tick_size, double value, out double rounded_value, out int rounded_ticks) {
if (!value.is_real() || !tick_size.is_real_positive()) {
rounded_value = double.NaN;
rounded_ticks = 0;
return false;
}
var ticks_to_val = value / tick_size;
ticks_to_val = ticks_to_val > 0 ? Math.Floor(ticks_to_val) : Math.Ceiling(ticks_to_val);
var rounded_down = ticks_to_val * tick_size;
var one_tick_up_abs = value > 0 ? rounded_down + tick_size : rounded_down - tick_size;
if (MathUtils.marginal_equals(one_tick_up_abs, value, TickUtils.precision_const)) {
rounded_value = one_tick_up_abs;
rounded_ticks = (int)(value > 0 ? ticks_to_val + 1 : ticks_to_val - 1);
} else {
rounded_value = rounded_down;
rounded_ticks = (int)ticks_to_val;
}
return true;
}
/// <summary>
/// Determines if the first tick in the specified direction should be a partial tick (to correct a rounding discrepency).
/// If this is so, it asserts that ticking up/down once in that direction corrects the rounding discrepency as expected.
/// </summary>
public static int check_partial_tick(ContractSpecificationImmutable spec, double value, double rounded_value, bool ticking_up) {
var partial_tick = count_partial_tick(value, rounded_value, ticking_up);
if (partial_tick != 0)
Assert.That(MathUtils.marginal_equals(spec.tickup(value, ticking_up ? partial_tick : -partial_tick), rounded_value, TickUtils.precision_const));
return partial_tick;
}
/// <summary>
/// Determines if the first tick in the specified direction should be a partial tick (to correct a rounding discrepency).
/// </summary>
public static int count_partial_tick(double value, double rounded_value, bool ticking_up) =>
(ticking_up ? (rounded_value - value) : (value - rounded_value)) > TickUtils.precision_const ?
1 : 0;
}
}
+2 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@@ -17,5 +17,6 @@
<ItemGroup>
<EmbeddedResource Include="Resources\KrandallOrderBookCrash.security_proto" />
<EmbeddedResource Include="Resources\NonDeterministicICEFutures.security_proto" />
<EmbeddedResource Include="Resources\SimContractSpecifications.contract_spec_proto" />
</ItemGroup>
</Project>