Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8761f313f5 | ||
|
|
3906ef6507 | ||
|
|
78055d79c6 | ||
|
|
885a9dd939 | ||
|
|
20c449de60 | ||
|
|
52b487946a | ||
|
|
98144ea939 | ||
|
|
5e27a5fa7c | ||
|
|
6cfc15ccd8 | ||
|
|
2de08c2c48 | ||
|
|
fb6e05560b | ||
|
|
1abf84337f | ||
|
|
d691e0aeee | ||
|
|
5e0f1b59fb | ||
|
|
a234ecbf26 | ||
|
|
062cabf075 | ||
|
|
39e1fd4a1a | ||
|
|
4d902747df | ||
|
|
50d0078556 | ||
|
|
d86b7c6c50 |
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 "<paramref name="tick_base"/> * 10^n," where 'n' is a natural number (including 0).
|
||||
/// <para>Note: If <paramref name="display_digits_override"/> is non-zero, 'n' will be "<paramref name="display_digits_override"/> - Math.Ceiling(Log10(<paramref name="tick_base"/>))"</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 "reduced_target_base * 10^n," 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
-2
@@ -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);
|
||||
File renamed without changes.
File renamed without changes.
+1
-6
@@ -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
-5
@@ -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; }
|
||||
}
|
||||
+36
-18
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -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 "<paramref name="tick_base"/> * 10^n," where 'n' is a natural number (including 0).
|
||||
/// <para>Note: If <paramref name="display_digits_override"/> is non-zero, 'n' will be "<paramref name="display_digits_override"/> - Math.Ceiling(Log10(<paramref name="tick_base"/>))"</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 "reduced_target_base * 10^n," 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));
|
||||
|
||||
@@ -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)}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user