Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b5fe5699 | ||
|
|
2fccceb91d | ||
|
|
fa49a0de07 | ||
|
|
bd28a0fa54 | ||
|
|
14ebe5ef51 | ||
|
|
a9b4049307 | ||
|
|
234d499731 | ||
|
|
324b9c1cc7 | ||
|
|
6aad329e09 | ||
|
|
0641cf45d9 | ||
|
|
1fe0efe5c3 | ||
|
|
93938b6ef2 | ||
|
|
818806a4e1 | ||
|
|
261bf3e475 | ||
|
|
dbe2194ed3 | ||
|
|
45e10ca377 | ||
|
|
4de77ce339 | ||
|
|
96f71290a4 | ||
|
|
e2b80942df | ||
|
|
51fb068469 | ||
|
|
58d39c4833 | ||
|
|
367d9be724 | ||
|
|
99f2ab7c8a | ||
|
|
8b091b39eb | ||
|
|
a357d978ca | ||
|
|
dfb673bd63 | ||
|
|
24f366491b | ||
|
|
55134bf172 | ||
|
|
b6b33f14ef | ||
|
|
12d532a17f | ||
|
|
09679ff494 | ||
|
|
4180b9a8ec |
No files matched your search
@@ -6,9 +6,14 @@ insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.14.0
|
||||
### User Visible Changes
|
||||
- All ladders can now be recentered by (shift or alt) + double clicking over the Price column in a ladder
|
||||
- Added a Max Order Quantity setting to the Safety Checks tab on the Ladder settings. This setting allows for a user-defined maximum order quantity.
|
||||
- Added a Price Display Format setting to the Market Price and Size Display tab on the Ladder settings. This setting allows for price levels in the ladder to be displayed without a decimal separator.
|
||||
- Synthetic instrument creation with 3 legs no longer defaults to butterfly quantites
|
||||
### Bug Fixes
|
||||
- Fixed an issue where market transition were not observed by IAlgoOrder
|
||||
- Fixed a bug in error reporting
|
||||
- Fixed a bug in the PnL window where specifying a certain grouping order would lead to a UI failure
|
||||
|
||||
## 2.13.0
|
||||
### User Visible Changes
|
||||
|
||||
@@ -357,19 +357,13 @@ namespace mbts.spark {
|
||||
if (leg_tuples.Count == 2) {
|
||||
_is_setting_price_mults = true;
|
||||
_price_mult_model.override_value(leg_tuples[1], -1);
|
||||
} else if (leg_tuples.Count == 3) {
|
||||
_is_setting_price_mults = true;
|
||||
_price_mult_model.override_value(leg_tuples[1], -2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _deduce_quantities() {
|
||||
if (leg_tuples.Count == 2) {
|
||||
_is_setting_quantities = true;
|
||||
_quantity_model.override_value(leg_tuples[1], -1);
|
||||
} else if (leg_tuples.Count == 3) {
|
||||
_is_setting_quantities = true;
|
||||
_quantity_model.override_value(leg_tuples[1], -2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ namespace mbts.spark {
|
||||
const double _default_order_entry_panel_width = 110.0;
|
||||
const double _default_zoom_factor = 1.0;
|
||||
const bool _default_show_warn_on_cross = true;
|
||||
const bool _default_enforce_max_clip = false;
|
||||
public static readonly IEqualityComparer<LadderSettings> profile_equality_comparer =
|
||||
EqualityComparer.Create<LadderSettings>(profile_equals);
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
@@ -282,6 +283,7 @@ namespace mbts.spark {
|
||||
use_defaults = true;
|
||||
favorite_algos = new LadderFavoriteAlgosSettings(null, 2, null);
|
||||
show_warn_on_cross = _default_show_warn_on_cross;
|
||||
enforce_max_clip = _default_enforce_max_clip;
|
||||
}
|
||||
|
||||
public static LadderSettings Default {
|
||||
@@ -347,6 +349,8 @@ namespace mbts.spark {
|
||||
zoom_factor = _default_zoom_factor,
|
||||
net_change = NetChangeTerms.None,
|
||||
mini_ob = new LadderMiniOrderBookSettings(bts.shell.orderbook.OrderBookStartupData.default_mini_grid.general),
|
||||
max_clip = 0,
|
||||
price_display_format = PriceDisplayFormat.Default,
|
||||
};
|
||||
rv.row_height = LadderRender.measure_row_height(rv);
|
||||
return rv;
|
||||
@@ -365,6 +369,13 @@ namespace mbts.spark {
|
||||
Ticks
|
||||
}
|
||||
|
||||
public enum PriceDisplayFormat {
|
||||
[Description("Default")]
|
||||
Default,
|
||||
[Description("Integer")]
|
||||
Integer
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public double row_height { get; set; }
|
||||
[ProtoMember(2)]
|
||||
@@ -499,6 +510,15 @@ namespace mbts.spark {
|
||||
[ProtoMember(57)]
|
||||
public double left_panel_scroll_position { get; set; }
|
||||
|
||||
[ProtoMember(58), DefaultValue(_default_enforce_max_clip)]
|
||||
public bool enforce_max_clip { get; set; }
|
||||
|
||||
[ProtoMember(59)]
|
||||
public uint max_clip { get; set; }
|
||||
|
||||
[ProtoMember(60)]
|
||||
public PriceDisplayFormat price_display_format { get; set; }
|
||||
|
||||
public void sanitize() =>
|
||||
sanitize(this);
|
||||
|
||||
@@ -600,7 +620,10 @@ namespace mbts.spark {
|
||||
a.favorite_algos == b.favorite_algos &&
|
||||
a.net_change == b.net_change &&
|
||||
a.show_warn_on_cross == b.show_warn_on_cross &&
|
||||
a.mini_ob == b.mini_ob;
|
||||
a.mini_ob == b.mini_ob &&
|
||||
a.enforce_max_clip == b.enforce_max_clip &&
|
||||
a.max_clip == b.max_clip &&
|
||||
a.price_display_format == b.price_display_format;
|
||||
|
||||
public bool Equals(LadderSettings other) =>
|
||||
profile_equals(this, other) &&
|
||||
|
||||
@@ -29,7 +29,7 @@ using TextUtils = mbts.ui.utility.text.TextUtils;
|
||||
namespace mbts.spark {
|
||||
class LadderSettingsVM : DisposableViewModelBase, IWindowSettingsVM<LadderSettings>, IValueFactory<LadderSettings>, IObservable<LadderSettings>, ITemplateSelectorKey {
|
||||
readonly Property<LadderSettings.BadgeStyle> _badge_style;
|
||||
readonly Property<uint> _alternate_order_size;
|
||||
readonly Property<uint> _alternate_order_size, _max_clip;
|
||||
readonly DelegateProperty<double> _row_height;
|
||||
readonly UserProperty<AccountTuple?> _default_account;
|
||||
readonly UserProperty<AlgoDefinitionVM> _default_algo;
|
||||
@@ -41,8 +41,9 @@ namespace mbts.spark {
|
||||
AlgoID? _desired_algo;
|
||||
TimeInForce _default_time_in_force;
|
||||
LadderSettings.NetChangeTerms _price_format;
|
||||
LadderSettings.PriceDisplayFormat _price_display_format;
|
||||
double _order_entry_panel_width, _zoom_factor;
|
||||
bool _auto_clear, _show_warn_on_cross;
|
||||
bool _auto_clear, _show_warn_on_cross, _enforce_max_clip;
|
||||
LadderSettings _current;
|
||||
LadderColumnSettingsVM
|
||||
_orders = new OrdersColumnSettingsVM(),
|
||||
@@ -91,6 +92,7 @@ namespace mbts.spark {
|
||||
_alternate_order_size = backing<uint>(nameof(alternate_order_size));
|
||||
_default_account = user_backing<AccountTuple?>(nameof(default_account));
|
||||
_default_algo = user_backing<AlgoDefinitionVM>(nameof(default_algo));
|
||||
_max_clip = backing<uint>(nameof(max_clip));
|
||||
// be sure to initialize backings before this measure_row_height call
|
||||
_row_height = backing(nameof(row_height), () => LadderRender.measure_row_height(get_result()));
|
||||
_orders.font_size_changed += on_font_size_changed;
|
||||
@@ -166,7 +168,9 @@ namespace mbts.spark {
|
||||
public SimpleGridSettingsVM mini_ob { get; } = new SimpleGridSettingsVM(OrderBookGridSettingsManager.hard_column_flags);
|
||||
// price format
|
||||
public LadderSettings.NetChangeTerms price_format { get => _price_format; set => Set(ref _price_format, value); }
|
||||
public LadderSettings.PriceDisplayFormat price_display_format { get => _price_display_format; set => Set(ref _price_display_format, value); }
|
||||
public IEnumerable<LadderSettings.NetChangeTerms> price_format_options => EnumHelper.get_values<LadderSettings.NetChangeTerms>();
|
||||
public IEnumerable<LadderSettings.PriceDisplayFormat> price_display_format_options => EnumHelper.get_values<LadderSettings.PriceDisplayFormat>();
|
||||
// time in force
|
||||
public TimeInForce default_time_in_force { get => _default_time_in_force; set => Set(ref _default_time_in_force, value); }
|
||||
public IEnumerable<TimeInForce> time_in_force_options => EnumHelper.get_values_except(TimeInForce.Undefined);
|
||||
@@ -182,6 +186,8 @@ namespace mbts.spark {
|
||||
public double zoom_factor { get => _zoom_factor; set => Set(ref _zoom_factor, value); }
|
||||
// safety checks
|
||||
public bool show_warn_on_cross { get => _show_warn_on_cross; set => Set(ref _show_warn_on_cross, value); }
|
||||
public bool enforce_max_clip { get => _enforce_max_clip; set => Set(ref _enforce_max_clip, value); }
|
||||
public uint max_clip { get => _max_clip.Value; set => _max_clip.Value = value; }
|
||||
|
||||
// favorites
|
||||
public FavoriteAlgosVM favorite_algos_vm { get; }
|
||||
@@ -354,8 +360,11 @@ namespace mbts.spark {
|
||||
_cancel_sells_visibility.show = settings.cancel_sells_visible;
|
||||
_cancel_all_visibility.show = settings.cancel_all_visible;
|
||||
price_format = settings.net_change;
|
||||
price_display_format = settings.price_display_format;
|
||||
// safety checks
|
||||
show_warn_on_cross = settings.show_warn_on_cross;
|
||||
enforce_max_clip = settings.enforce_max_clip;
|
||||
max_clip = settings.max_clip;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,8 +437,11 @@ namespace mbts.spark {
|
||||
clone.cancel_all_visible = _cancel_all_visibility.show;
|
||||
clone.zoom_slider_visible = _zoom_slider_visbility.show;
|
||||
clone.net_change = price_format;
|
||||
clone.price_display_format = price_display_format;
|
||||
// safety checks
|
||||
clone.show_warn_on_cross = show_warn_on_cross;
|
||||
clone.enforce_max_clip = enforce_max_clip;
|
||||
clone.max_clip = max_clip;
|
||||
return clone;
|
||||
}
|
||||
|
||||
|
||||
@@ -397,6 +397,8 @@ namespace mbts.spark {
|
||||
public uint volume_column_width { get => _volume_column_width.Value; set => _volume_column_width.set(value); }
|
||||
public uint alternate_order_size_from_settings => current_settings.alternate_order_size;
|
||||
public bool show_warn_on_cross_from_settings => current_settings.show_warn_on_cross;
|
||||
public bool enforce_max_clip_from_settings => current_settings.enforce_max_clip;
|
||||
public uint max_clip_from_settings => current_settings.max_clip;
|
||||
internal LadderModel model => _model;
|
||||
internal IObservable<LadderSettings> settings { get; }
|
||||
|
||||
@@ -447,6 +449,9 @@ namespace mbts.spark {
|
||||
public void key_down(KeyEventArgs e) {
|
||||
if (order_properties_vm == null)
|
||||
return;
|
||||
if (KeyboardExtensions.IsDown(Key.Space)) {
|
||||
_bus?.post(SparkTags.RequestRecenterLadder, null);
|
||||
}
|
||||
// the ctrl modifier requirement helps avoid keyboard-input collisions w/ algo-templates popup
|
||||
if (!KeyboardExtensions.IsCtrlDown()) return;
|
||||
var new_algo = order_properties_vm.algo_definitions.FirstOrDefault(o => o.algo_root.key_match(e));
|
||||
@@ -462,6 +467,10 @@ namespace mbts.spark {
|
||||
settings_invalidated?.Invoke();
|
||||
}
|
||||
|
||||
public void recenter_all_ladders() {
|
||||
_bus?.post(SparkTags.RequestRecenterLadder, null);
|
||||
}
|
||||
|
||||
public Task send_order_async((double price, Side? side, MouseButton button) args) {
|
||||
if (order_properties_vm != null && args.button != MouseButton.Middle) {
|
||||
var size_from_alternate = args.button == MouseButton.Right;
|
||||
@@ -470,6 +479,10 @@ namespace mbts.spark {
|
||||
size_calculator.flash_error();
|
||||
} else {
|
||||
uint size = size_from_alternate ? current_settings.alternate_order_size : (uint)Math.Abs(size_calculator.value ?? 0);
|
||||
// enforce max clip
|
||||
if (enforce_max_clip_from_settings && size > max_clip_from_settings) {
|
||||
size = max_clip_from_settings;
|
||||
}
|
||||
return send_order_async(args.side.Value, size, args.price);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,17 @@ namespace mbts.spark {
|
||||
}
|
||||
} else if (_vm != null) {
|
||||
uint size = e.ChangedButton == MouseButton.Right ? _vm.alternate_order_size_from_settings : _vm.size_calculator.value ?? 0;
|
||||
|
||||
// does the size violate the local max clip
|
||||
if (_vm.enforce_max_clip_from_settings && size > _vm.max_clip_from_settings) {
|
||||
// note this size is only used for the cross market message, not the actual order command
|
||||
size = _vm.max_clip_from_settings;
|
||||
// only update the calculator value when it's a standard left-click order
|
||||
if (e.ChangedButton == MouseButton.Left) {
|
||||
_vm.size_calculator.value = _vm.max_clip_from_settings;
|
||||
}
|
||||
}
|
||||
|
||||
// if warning enabled && (buy above opposite inside market || ask below opposite inside market)
|
||||
// when 1-sided market, _get_top_price will return NaN, and so the price comparison is false
|
||||
// meaning show no warning == correct
|
||||
@@ -479,8 +490,14 @@ namespace mbts.spark {
|
||||
|
||||
protected override void OnMouseDoubleClick(MouseButtonEventArgs e) {
|
||||
var column = _column_for_point(e.GetPosition(this));
|
||||
if (column.column_type == ColumnType.Price)
|
||||
_scroll_to_center();
|
||||
if (column.column_type == ColumnType.Price) {
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) ||
|
||||
Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)) {
|
||||
_vm?.recenter_all_ladders();
|
||||
} else {
|
||||
_scroll_to_center();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Row Utility Methods
|
||||
@@ -588,7 +605,8 @@ namespace mbts.spark {
|
||||
_settings.row_height,
|
||||
_price_list,
|
||||
column.settings,
|
||||
security.ContractSpec);
|
||||
security.ContractSpec,
|
||||
_settings.price_display_format);
|
||||
}
|
||||
break;
|
||||
case ColumnType.Ask: render_market_col(_asks); break;
|
||||
@@ -911,7 +929,7 @@ namespace mbts.spark {
|
||||
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);
|
||||
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);
|
||||
_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
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace mbts.spark {
|
||||
// Note: the orders-column has a separate "row_padding" value: Badge.DefaultMargin
|
||||
// (see how it is incorporated in measure_row_height() below)
|
||||
public static readonly Thickness row_padding = new Thickness(6);
|
||||
const string tick_separator_period = ".";
|
||||
const string tick_separator_dash = "-";
|
||||
|
||||
// IMPORTANT: this method ONLY WORKS if Badges use the BadgeRender.margin/border_thickness/padding values.
|
||||
// TODO: elminate the dependency on BadgeRender.margin/border_thickness/padding by integrating them into settings.
|
||||
@@ -52,8 +54,11 @@ namespace mbts.spark {
|
||||
public static int index_of(double[] prices, double price, IComparer<double> comparer) =>
|
||||
Array.BinarySearch(prices, price, comparer);
|
||||
|
||||
public static GlyphRun format_text(string text, int row, double row_height, LadderColumnSettings settings, GlyphContext glyph_context) =>
|
||||
glyph_context.get_glyph_run(text, settings.font_size, settings.width, row_height, HorizontalAlignment.Center, 0, VerticalAlignment.Center, row * row_height, true);
|
||||
public static GlyphRun format_text(string text, int row, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, LadderSettings.PriceDisplayFormat price_display_format = LadderSettings.PriceDisplayFormat.Default) =>
|
||||
glyph_context.get_glyph_run(
|
||||
// long term there is probably a better way to get rid of the separator, such as in the TickExtensions._format_tick
|
||||
price_display_format == LadderSettings.PriceDisplayFormat.Integer ? text.Replace(tick_separator_period, string.Empty).Replace(tick_separator_dash, string.Empty) : text,
|
||||
settings.font_size, settings.width, row_height, HorizontalAlignment.Center, 0, VerticalAlignment.Center, row * row_height, true);
|
||||
|
||||
public static double[] gen_prices(double mid, int row_count, ContractSpecificationImmutable spec) {
|
||||
// require odd number
|
||||
@@ -79,10 +84,10 @@ namespace mbts.spark {
|
||||
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) {
|
||||
public static GlyphRun[] format_prices(double[] prices, double row_height, LadderColumnSettings settings, GlyphContext glyph_context, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
|
||||
var result = new GlyphRun[prices.Length];
|
||||
Parallel.For(0, prices.Length, i =>
|
||||
result[i] = format_text(spec.format(prices[i]), i, row_height, settings, glyph_context));
|
||||
result[i] = format_text(spec.format(prices[i]), i, row_height, settings, glyph_context, price_display_format));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -104,9 +109,9 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public static double[] render_prices(DrawingVisual price, GlyphContext glyph_context, double mid, int row_count, double row_height,
|
||||
LadderColumnSettings settings, ContractSpecificationImmutable spec) {
|
||||
LadderColumnSettings settings, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
|
||||
var dprices = gen_prices(mid, row_count, spec);
|
||||
render_prices(price, glyph_context, row_count, row_height, dprices, settings, spec);
|
||||
render_prices(price, glyph_context, row_count, row_height, dprices, settings, spec, price_display_format);
|
||||
return dprices;
|
||||
}
|
||||
|
||||
@@ -122,10 +127,10 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, double[] dprices, LadderColumnSettings settings, ContractSpecificationImmutable spec) {
|
||||
public static void render_prices(DrawingVisual price, GlyphContext glyph_context, int row_count, double row_height, double[] dprices, LadderColumnSettings settings, ContractSpecificationImmutable spec, LadderSettings.PriceDisplayFormat price_display_format) {
|
||||
var foreground = settings.foreground.rgb_brush();
|
||||
//var background = new SolidColorBrush(settings.background.rgb_color());
|
||||
var fprices = format_prices(dprices, row_height, settings, glyph_context, spec);
|
||||
var fprices = format_prices(dprices, row_height, settings, glyph_context, spec, price_display_format);
|
||||
using (var dc = price.RenderOpen())
|
||||
using (_draw_with_clip(dc, settings, row_height, row_count)) {
|
||||
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, row_count * row_height));
|
||||
|
||||
@@ -178,6 +178,14 @@
|
||||
<Setter Property="VerticalAlignment" Value="Bottom"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="EnableDisableFeatureText" TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.GrayTextBrushKey}}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<u:KeyedTemplateSelector x:Key="LadderColumnTemplateSelector">
|
||||
<u:KeyedTemplateSelector.Default>
|
||||
<DataTemplate>
|
||||
@@ -630,7 +638,11 @@
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<GroupBox Header="Size Display" DataContext="{Binding size_display}" Padding="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<GroupBox Header="Size Display" DataContext="{Binding size_display}" Padding="4" Grid.Row="0" Grid.RowSpan="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -710,6 +722,7 @@
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Top"
|
||||
Header="Price Display"
|
||||
Visibility="{Binding is_synthetic, Converter={StaticResource NetChangeGroupBoxIsVisibleVisibilityConverter}}"
|
||||
@@ -717,6 +730,15 @@
|
||||
>
|
||||
<c:RadioButtonsControl ItemsSource="{Binding price_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_format}"/>
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Stretch"
|
||||
Header="Price Display Format"
|
||||
Padding="4"
|
||||
>
|
||||
<c:RadioButtonsControl ItemsSource="{Binding price_display_format_options}" ItemTemplate="{StaticResource EnumDescriptionTemplate}" SelectedItem="{Binding price_display_format}"/>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<!-- Size Presets Tab-->
|
||||
@@ -991,21 +1013,45 @@
|
||||
<Grid HorizontalAlignment="Left" Margin="6">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush1}}"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="50"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Content="Warn on Market Cross" FlowDirection="RightToLeft" IsChecked="{Binding show_warn_on_cross}">
|
||||
<!-- market cross -->
|
||||
<CheckBox Name="warn" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding show_warn_on_cross}">
|
||||
<CheckBox.Resources>
|
||||
<Style TargetType="{x:Type Path}">
|
||||
<Setter Property="FlowDirection" Value="LeftToRight"/>
|
||||
</Style>
|
||||
</CheckBox.Resources>
|
||||
</CheckBox>
|
||||
<TextBlock Grid.Column="1"
|
||||
Style="{StaticResource EnableDisableFeatureText}"
|
||||
IsEnabled="{Binding ElementName=warn, Path=IsChecked}"
|
||||
Text="Warn on Market Cross" />
|
||||
<!-- max order qty -->
|
||||
<CheckBox Name="enforce_max_clip" Grid.Row="1" Grid.Column="0" FlowDirection="RightToLeft" IsChecked="{Binding enforce_max_clip}">
|
||||
<CheckBox.Resources>
|
||||
<Style TargetType="{x:Type Path}">
|
||||
<Setter Property="FlowDirection" Value="LeftToRight"/>
|
||||
</Style>
|
||||
</CheckBox.Resources>
|
||||
</CheckBox>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
||||
Style="{StaticResource EnableDisableFeatureText}"
|
||||
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
|
||||
MinWidth="{Binding ElementName=warn, Path=ActualWidth}"
|
||||
Text="Max Order Quantity"/>
|
||||
<c:TickUpDownUInt Grid.Row="1" Grid.Column="2"
|
||||
IsEnabled="{Binding ElementName=enforce_max_clip, Path=IsChecked}"
|
||||
Value="{Binding max_clip}" />
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="Mini Order Book">
|
||||
|
||||
@@ -1,220 +1,223 @@
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.expression;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLSettingsVM : DisposableViewModelBase, IWindowSettingsVM<PNLSettings>, IValueFactory<PNLSettings>, IObservable<PNLSettings>, ITemplateSelectorKey {
|
||||
readonly IReadOnlyDictionary<ColumnKeys, AnchorItemVM> _anchor_cache;
|
||||
readonly DeferrableActionWrapper _sync_anchors;
|
||||
|
||||
PNLCalculationType _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display;
|
||||
PNLSettings _last_loaded;
|
||||
|
||||
public PNLSettingsVM(PNLSettings settings) {
|
||||
// init anchor cache, deferrable resync
|
||||
_anchor_cache = PNLSettings.anchorable_columns.ToDictionary(k => k, k => new AnchorItemVM(this, k));
|
||||
_sync_anchors = new DeferrableActionWrapper(() => {
|
||||
for (int i = 0; i < anchor_items.Count; i++) {
|
||||
anchor_items[i].anchor_index = i + 1;
|
||||
anchor_items[i].refresh();
|
||||
}
|
||||
});
|
||||
core_settings_vm = new SimpleGridSettingsVM(column_factory: col_settings => {
|
||||
var col_vm = new PNLColumnVM(core_settings_vm, EnumHelper.parse<ColumnKeys>(col_settings.id));
|
||||
col_vm.load_from(col_settings);
|
||||
return col_vm;
|
||||
});
|
||||
core_settings_vm.selection_highlight.bind_opacity_to_alpha = true;
|
||||
_dispose = Disposable.Create(
|
||||
anchor_items.Subscribe((s, e) => _sync_anchors.execute()));
|
||||
load(settings);
|
||||
}
|
||||
|
||||
public IEnumerable<AnchorItemVM> anchor_item_headers => _anchor_cache.Values;
|
||||
public ObservableCollection<AnchorItemVM> anchor_items { get; } = new ObservableCollection<AnchorItemVM>();
|
||||
public SimpleGridSettingsVM core_settings_vm { get; }
|
||||
public PNLGroupRowAppearanceVM group_row_defaults { get; } = new PNLGroupRowAppearanceVM();
|
||||
public PNLCellAppearanceVM header_row_defaults { get; } = new PNLCellAppearanceVM();
|
||||
public PNLCalculationType calculation_type { get => _calculation_type; set => Set(ref _calculation_type, value); }
|
||||
public IEnumerable<PNLCalculationType> calculation_types => EnumHelper.get_values_except(PNLCalculationType.None, PNLCalculationType.Same, PNLCalculationType.Opposite);
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public IEnumerable<PNLHeaderRowDisplay> header_row_display_options => PNLSettings.header_row_display_options;
|
||||
|
||||
string ITemplateSelectorKey.key => GridSettingsTemplateKeys.PNL;
|
||||
|
||||
public PNLSettings get_result() {
|
||||
var s = _last_loaded == null ? new PNLSettings() : SparkUtils.clone_proto(_last_loaded);
|
||||
s.calculation_type = _calculation_type;
|
||||
s.grid = core_settings_vm.create();
|
||||
// move anchor cols to the left, order wrt grouping hierarchy
|
||||
var new_cols = new Lazy<List<GridColumnSettings>>(() => s.grid.columns.ToList());
|
||||
for (int i = 0; i < s.grid.columns.Count; i++) {
|
||||
var key = s.grid.columns[i].strong_id<ColumnKeys>();
|
||||
if (_anchor_cache.TryGetValue(key, out var a) && a.anchor_index.HasValue) {
|
||||
// anchor index is 1 based, subtract 1 to get 0 based
|
||||
int anchor_index = a.anchor_index.Value - 1;
|
||||
if (anchor_index != i && anchor_index >= 0 && anchor_index < s.grid.columns.Count) {
|
||||
new_cols.Value.RemoveAt(i);
|
||||
new_cols.Value.Insert(anchor_index, s.grid.columns[i]);
|
||||
}
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.collections;
|
||||
using mbts.expression;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
|
||||
|
||||
namespace bts.shell.pnl {
|
||||
class PNLSettingsVM : DisposableViewModelBase, IWindowSettingsVM<PNLSettings>, IValueFactory<PNLSettings>, IObservable<PNLSettings>, ITemplateSelectorKey {
|
||||
readonly IReadOnlyDictionary<ColumnKeys, AnchorItemVM> _anchor_cache;
|
||||
readonly DeferrableActionWrapper _sync_anchors;
|
||||
|
||||
PNLCalculationType _calculation_type;
|
||||
PNLHeaderRowDisplay _header_row_display;
|
||||
PNLSettings _last_loaded;
|
||||
|
||||
public PNLSettingsVM(PNLSettings settings) {
|
||||
// init anchor cache, deferrable resync
|
||||
_anchor_cache = PNLSettings.anchorable_columns.ToDictionary(k => k, k => new AnchorItemVM(this, k));
|
||||
_sync_anchors = new DeferrableActionWrapper(() => {
|
||||
for (int i = 0; i < anchor_items.Count; i++) {
|
||||
anchor_items[i].anchor_index = i + 1;
|
||||
anchor_items[i].refresh();
|
||||
}
|
||||
}
|
||||
// rule-based animations
|
||||
var trigger_anims = core_settings_vm.columns
|
||||
.OfType<PNLColumnVM>()
|
||||
.SelectMany(c => c.get_triggers())
|
||||
.GroupBy(tup => tup.expression)
|
||||
.Where(g => CoreExpressionEvaluator<PNLRowData>.validate(g.Key))
|
||||
.Select(g => (
|
||||
expression: g.Key,
|
||||
animations: g.ToArray()))
|
||||
.ToList();
|
||||
|
||||
var normalized_anims = trigger_anims
|
||||
.Where(tup => CoreExpressionEvaluator<PNLNormalizedData>.validate(tup.expression))
|
||||
.ToList();
|
||||
|
||||
s = s.with(anchors: anchor_items.Select(vm => vm.key),
|
||||
grid: new_cols.IsValueCreated ? s.grid.with(columns: new_cols.Value) : null,
|
||||
group_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.group_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
header_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.header_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
animations: trigger_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.row)
|
||||
}),
|
||||
group_header_animations: normalized_anims
|
||||
.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.group)
|
||||
}),
|
||||
header_row_animations: normalized_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.header)
|
||||
}));
|
||||
s.header_row_display = header_row_display;
|
||||
s.header_cell_appearance = PNLCellAppearanceProto.from_appearance(header_row_defaults.to_appearance());
|
||||
// group
|
||||
var (group_cell_appearance, group_row_background_colors) = group_row_defaults.create();
|
||||
s.group_cell_appearance = PNLCellAppearanceProto.from_appearance(group_cell_appearance);
|
||||
s.group_background_colors.SetItems(group_row_background_colors.Select(c => c.rgb_color()));
|
||||
//
|
||||
return s;
|
||||
}
|
||||
|
||||
public void load(PNLSettings value) {
|
||||
_last_loaded = value;
|
||||
header_row_defaults.load(value.header_cell_appearance.to_appearance());
|
||||
group_row_defaults.load_from((value.group_cell_appearance.to_appearance(), value.group_background_colors.WrapSelectorRO(c => c.argb_color())));
|
||||
var column_map = _map_column_data(value.grid.columns, value.animations);
|
||||
var group_column_map = _map_column_data(value.group_columns, value.group_header_animations);
|
||||
var header_column_map = _map_column_data(value.header_columns, value.header_row_animations);
|
||||
|
||||
core_settings_vm.load_from(value.grid);
|
||||
|
||||
for (int i = 0; i < core_settings_vm.columns.Count; i++) {
|
||||
var col_vm = core_settings_vm.columns[i] as PNLColumnVM ?? throw new InvalidOperationException($"In {nameof(PNLSettingsVM)}, all columns should be of type {typeof(PNLColumnVM)}.");
|
||||
var key = EnumHelper.parse<ColumnKeys>(col_vm.id);
|
||||
var (col_settings, trigger_animations) = column_map[key];
|
||||
var group_data = group_column_map.GetNullable(key);
|
||||
var header_data = header_column_map.GetNullable(key);
|
||||
col_vm.load_from(col_settings, trigger_animations, group_data?.settings, group_data?.triggers, header_data?.triggers, header_data?.settings);
|
||||
}
|
||||
using (_sync_anchors.defer_execution())
|
||||
anchor_items.WrapSelector(a => a.key, k => _anchor_cache[k]).sync_with(value.anchors.Intersect(PNLSettings.anchorable_columns));
|
||||
header_row_display = value.header_row_display;
|
||||
calculation_type = value.calculation_type;
|
||||
}
|
||||
|
||||
PNLSettings IValueFactory<PNLSettings>.create() => get_result();
|
||||
void IValueFactory<PNLSettings>.load_from(PNLSettings value) => load(value);
|
||||
IDisposable IObservable<PNLSettings>.Subscribe(IObserver<PNLSettings> observer) => throw new NotImplementedException();
|
||||
|
||||
static Dictionary<ColumnKeys, (GridColumnSettings settings, Dictionary<string, PNLAnimation> triggers)> _map_column_data(IEnumerable<GridColumnSettings> columns, IEnumerable<PNLAnimationTriggerData> triggers) {
|
||||
var map = columns.ToDictionary(
|
||||
c => c.strong_id<ColumnKeys>(),
|
||||
c => (settings: c, triggers: new Dictionary<string, PNLAnimation>()));
|
||||
foreach (var trig in triggers) {
|
||||
foreach (var anim in trig.animations) {
|
||||
if (map.TryGetValue(anim.column, out var col_data))
|
||||
col_data.triggers[trig.expression] = anim;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
void _append_or_remove_anchor(ColumnKeys key, bool include) {
|
||||
if (!_anchor_cache.TryGetValue(key, out var vm))
|
||||
throw new KeyNotFoundException($"Column key '{key}' is not anchorable (expected one of: {string.Join(", ", PNLSettings.anchorable_columns)}).");
|
||||
if (vm.include == include)
|
||||
return;
|
||||
if (include) {
|
||||
if (anchor_items.Any(a => a.key == key))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} already contains an element with key '{key}.'");
|
||||
anchor_items.Add(vm);
|
||||
vm.anchor_index = anchor_items.Count;
|
||||
} else {
|
||||
if (!anchor_items.Remove(vm))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} does not contain an element with key '{key}.'");
|
||||
vm.anchor_index = null;
|
||||
}
|
||||
_sync_anchors.execute();
|
||||
}
|
||||
|
||||
public class AnchorItemVM : ViewModelBase, IEquatable<AnchorItemVM> {
|
||||
readonly PNLSettingsVM _owner;
|
||||
int? _anchor_index;
|
||||
|
||||
public AnchorItemVM(PNLSettingsVM owner, ColumnKeys key) {
|
||||
_owner = owner;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public ColumnKeys key { get; }
|
||||
public bool include { get => anchor_index.HasValue; set => _owner._append_or_remove_anchor(key, value); }
|
||||
public int? anchor_index { get => _anchor_index; set => Set(ref _anchor_index, value); }
|
||||
|
||||
public void refresh() =>
|
||||
RaisePropertyChanged(nameof(include), nameof(anchor_index));
|
||||
|
||||
public bool Equals(AnchorItemVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as AnchorItemVM);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
EnumHelper.get_display_name(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
core_settings_vm = new SimpleGridSettingsVM(column_factory: col_settings => {
|
||||
var col_vm = new PNLColumnVM(core_settings_vm, EnumHelper.parse<ColumnKeys>(col_settings.id));
|
||||
col_vm.load_from(col_settings);
|
||||
return col_vm;
|
||||
});
|
||||
core_settings_vm.selection_highlight.bind_opacity_to_alpha = true;
|
||||
_dispose = Disposable.Create(
|
||||
anchor_items.Subscribe((s, e) => _sync_anchors.execute()));
|
||||
load(settings);
|
||||
}
|
||||
|
||||
public IEnumerable<AnchorItemVM> anchor_item_headers => _anchor_cache.Values;
|
||||
public ObservableCollection<AnchorItemVM> anchor_items { get; } = new ObservableCollection<AnchorItemVM>();
|
||||
public SimpleGridSettingsVM core_settings_vm { get; }
|
||||
public PNLGroupRowAppearanceVM group_row_defaults { get; } = new PNLGroupRowAppearanceVM();
|
||||
public PNLCellAppearanceVM header_row_defaults { get; } = new PNLCellAppearanceVM();
|
||||
public PNLCalculationType calculation_type { get => _calculation_type; set => Set(ref _calculation_type, value); }
|
||||
public IEnumerable<PNLCalculationType> calculation_types => EnumHelper.get_values_except(PNLCalculationType.None, PNLCalculationType.Same, PNLCalculationType.Opposite);
|
||||
public PNLHeaderRowDisplay header_row_display { get => _header_row_display; set => Set(ref _header_row_display, value); }
|
||||
public IEnumerable<PNLHeaderRowDisplay> header_row_display_options => PNLSettings.header_row_display_options;
|
||||
|
||||
string ITemplateSelectorKey.key => GridSettingsTemplateKeys.PNL;
|
||||
|
||||
public PNLSettings get_result() {
|
||||
var s = _last_loaded == null ? new PNLSettings() : SparkUtils.clone_proto(_last_loaded);
|
||||
s.calculation_type = _calculation_type;
|
||||
s.grid = core_settings_vm.create();
|
||||
// move anchor cols to the left, order wrt grouping hierarchy
|
||||
List<GridColumnSettings> new_cols = null;
|
||||
IEnumerable<GridColumnSettings> cols = s.grid.columns;
|
||||
var sorted_anchors = anchor_items.OrderBy(a => a.anchor_index).ToList();
|
||||
for (int i = 0; i < sorted_anchors.Count; i++) {
|
||||
if (GridSettingsHelper.try_find_column(cols, sorted_anchors[i].key, out var match, out int pos) && pos != i) {
|
||||
if (new_cols == null) {
|
||||
new_cols = s.grid.columns.ToList();
|
||||
// set cols to new_cols so subsequent position lookups
|
||||
// take previous column order manipulations into account.
|
||||
cols = new_cols;
|
||||
}
|
||||
new_cols.RemoveAt(pos);
|
||||
new_cols.Insert(i, match);
|
||||
}
|
||||
}
|
||||
// rule-based animations
|
||||
var trigger_anims = core_settings_vm.columns
|
||||
.OfType<PNLColumnVM>()
|
||||
.SelectMany(c => c.get_triggers())
|
||||
.GroupBy(tup => tup.expression)
|
||||
.Where(g => CoreExpressionEvaluator<PNLRowData>.validate(g.Key))
|
||||
.Select(g => (
|
||||
expression: g.Key,
|
||||
animations: g.ToArray()))
|
||||
.ToList();
|
||||
|
||||
var normalized_anims = trigger_anims
|
||||
.Where(tup => CoreExpressionEvaluator<PNLNormalizedData>.validate(tup.expression))
|
||||
.ToList();
|
||||
|
||||
s = s.with(anchors: anchor_items.Select(vm => vm.key),
|
||||
grid: new_cols == null ? null : s.grid.with(columns: new_cols),
|
||||
group_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.group_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
header_columns: core_settings_vm.columns.OfType<PNLColumnVM>().Select(c => {
|
||||
var col_settings = c.create();
|
||||
col_settings = c.header_column_vm.apply_to(col_settings);
|
||||
return col_settings;
|
||||
}),
|
||||
animations: trigger_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.row)
|
||||
}),
|
||||
group_header_animations: normalized_anims
|
||||
.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.group)
|
||||
}),
|
||||
header_row_animations: normalized_anims.Select(
|
||||
ta => new PNLAnimationTriggerData {
|
||||
expression = ta.expression,
|
||||
animations = Array.ConvertAll(ta.animations, t => t.header)
|
||||
}));
|
||||
s.header_row_display = header_row_display;
|
||||
s.header_cell_appearance = PNLCellAppearanceProto.from_appearance(header_row_defaults.to_appearance());
|
||||
// group
|
||||
var (group_cell_appearance, group_row_background_colors) = group_row_defaults.create();
|
||||
s.group_cell_appearance = PNLCellAppearanceProto.from_appearance(group_cell_appearance);
|
||||
s.group_background_colors.SetItems(group_row_background_colors.Select(c => c.rgb_color()));
|
||||
//
|
||||
return s;
|
||||
}
|
||||
|
||||
public void load(PNLSettings value) {
|
||||
_last_loaded = value;
|
||||
header_row_defaults.load(value.header_cell_appearance.to_appearance());
|
||||
group_row_defaults.load_from((value.group_cell_appearance.to_appearance(), value.group_background_colors.WrapSelectorRO(c => c.argb_color())));
|
||||
var column_map = _map_column_data(value.grid.columns, value.animations);
|
||||
var group_column_map = _map_column_data(value.group_columns, value.group_header_animations);
|
||||
var header_column_map = _map_column_data(value.header_columns, value.header_row_animations);
|
||||
|
||||
core_settings_vm.load_from(value.grid);
|
||||
|
||||
for (int i = 0; i < core_settings_vm.columns.Count; i++) {
|
||||
var col_vm = core_settings_vm.columns[i] as PNLColumnVM ?? throw new InvalidOperationException($"In {nameof(PNLSettingsVM)}, all columns should be of type {typeof(PNLColumnVM)}.");
|
||||
var key = EnumHelper.parse<ColumnKeys>(col_vm.id);
|
||||
var (col_settings, trigger_animations) = column_map[key];
|
||||
var group_data = group_column_map.GetNullable(key);
|
||||
var header_data = header_column_map.GetNullable(key);
|
||||
col_vm.load_from(col_settings, trigger_animations, group_data?.settings, group_data?.triggers, header_data?.triggers, header_data?.settings);
|
||||
}
|
||||
using (_sync_anchors.defer_execution())
|
||||
anchor_items.WrapSelector(a => a.key, k => _anchor_cache[k]).sync_with(value.anchors.Intersect(PNLSettings.anchorable_columns));
|
||||
header_row_display = value.header_row_display;
|
||||
calculation_type = value.calculation_type;
|
||||
}
|
||||
|
||||
PNLSettings IValueFactory<PNLSettings>.create() => get_result();
|
||||
void IValueFactory<PNLSettings>.load_from(PNLSettings value) => load(value);
|
||||
IDisposable IObservable<PNLSettings>.Subscribe(IObserver<PNLSettings> observer) => throw new NotImplementedException();
|
||||
|
||||
static Dictionary<ColumnKeys, (GridColumnSettings settings, Dictionary<string, PNLAnimation> triggers)> _map_column_data(IEnumerable<GridColumnSettings> columns, IEnumerable<PNLAnimationTriggerData> triggers) {
|
||||
var map = columns.ToDictionary(
|
||||
c => c.strong_id<ColumnKeys>(),
|
||||
c => (settings: c, triggers: new Dictionary<string, PNLAnimation>()));
|
||||
foreach (var trig in triggers) {
|
||||
foreach (var anim in trig.animations) {
|
||||
if (map.TryGetValue(anim.column, out var col_data))
|
||||
col_data.triggers[trig.expression] = anim;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
void _append_or_remove_anchor(ColumnKeys key, bool include) {
|
||||
if (!_anchor_cache.TryGetValue(key, out var vm))
|
||||
throw new KeyNotFoundException($"Column key '{key}' is not anchorable (expected one of: {string.Join(", ", PNLSettings.anchorable_columns)}).");
|
||||
if (vm.include == include)
|
||||
return;
|
||||
if (include) {
|
||||
if (anchor_items.Any(a => a.key == key))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} already contains an element with key '{key}.'");
|
||||
anchor_items.Add(vm);
|
||||
vm.anchor_index = anchor_items.Count;
|
||||
} else {
|
||||
if (!anchor_items.Remove(vm))
|
||||
throw new InvalidOperationException($"{nameof(anchor_items)} does not contain an element with key '{key}.'");
|
||||
vm.anchor_index = null;
|
||||
}
|
||||
_sync_anchors.execute();
|
||||
}
|
||||
|
||||
public class AnchorItemVM : ViewModelBase, IEquatable<AnchorItemVM> {
|
||||
readonly PNLSettingsVM _owner;
|
||||
int? _anchor_index;
|
||||
|
||||
public AnchorItemVM(PNLSettingsVM owner, ColumnKeys key) {
|
||||
_owner = owner;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public ColumnKeys key { get; }
|
||||
public bool include { get => anchor_index.HasValue; set => _owner._append_or_remove_anchor(key, value); }
|
||||
public int? anchor_index { get => _anchor_index; set => Set(ref _anchor_index, value); }
|
||||
|
||||
public void refresh() =>
|
||||
RaisePropertyChanged(nameof(include), nameof(anchor_index));
|
||||
|
||||
public bool Equals(AnchorItemVM other) =>
|
||||
other != null && key == other.key;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as AnchorItemVM);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
key.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
EnumHelper.get_display_name(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,12 +90,11 @@ namespace bts.shell.pnl {
|
||||
}
|
||||
|
||||
int? _try_compare_groups(CollectionViewGroup x, CollectionViewGroup y) {
|
||||
if (x == null || y == null)
|
||||
return null;
|
||||
var xcell = (IPNLCellData)x.Name;
|
||||
var ycell = (IPNLCellData)y.Name;
|
||||
var comp = _get_column_comparer(xcell.column);
|
||||
return comp.value_comparer.Compare(xcell.value, ycell.value);
|
||||
if (x?.Name is IPNLCellData xcell && y?.Name is IPNLCellData ycell) {
|
||||
var comp = _get_column_comparer(xcell.column);
|
||||
return comp.value_comparer.Compare(xcell.value, ycell.value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -698,8 +698,8 @@
|
||||
c:DataGridFilterProperties.FilterActive="{Binding DataContext.security_filter.active, Source={u:DirectReference _grid}}"
|
||||
c:DataGridFilterProperties.FilterOpen="{Binding DataContext.security_filter.open, Source={u:DirectReference _grid}}"
|
||||
key="Security"
|
||||
sort_member_path="security_cell.cell_data"
|
||||
data_member_path="security"
|
||||
data_member_path="security_cell.cell_data"
|
||||
sort_member_path="security"
|
||||
HeaderTemplate="{StaticResource AnchorableColumnHeaderTemplate}"
|
||||
/>
|
||||
<!--Security Key-->
|
||||
|
||||
@@ -102,14 +102,14 @@ namespace mbts.utils
|
||||
string identifier = ((felem.Name != null) && (felem.Name.Length > 0)) ?
|
||||
felem.Name :
|
||||
felem.GetType().ToString();
|
||||
return "$FrameworkElement [{identifier}]";
|
||||
return $"FrameworkElement [{identifier}]";
|
||||
} else {
|
||||
// Maybe a FrameworkContentElement has focus
|
||||
if (elem is FrameworkContentElement fcelem) {
|
||||
string identifier = ((fcelem.Name != null) && (fcelem.Name.Length > 0)) ?
|
||||
fcelem.Name :
|
||||
fcelem.GetType().ToString();
|
||||
return "$FrameworkContentElement [{identifier}]";
|
||||
return $"FrameworkContentElement [{identifier}]";
|
||||
}
|
||||
return $"Element of type [{elem.GetType().ToString()}] has focus";
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using Google.Protobuf;
|
||||
using security_key = System.UInt32;
|
||||
using bts.co2;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.co2
|
||||
{
|
||||
@@ -87,7 +85,6 @@ namespace mbts.co2
|
||||
|
||||
[DllImport("libspark")]
|
||||
internal unsafe static extern void post_on_scan_security_thread(Action callback);
|
||||
|
||||
}
|
||||
|
||||
internal static class Co2NativeTestMethods
|
||||
@@ -136,59 +133,5 @@ namespace mbts.co2
|
||||
[DllImport("libspark")]
|
||||
internal static extern void co2_test_get_constants(out ulong max_depth, out ulong max_implied_depth, out sbyte nan_exponent);
|
||||
|
||||
internal unsafe delegate void raw_receive_pb_or_string(byte* buffer, int buffer_size);
|
||||
[DllImport("libspark")]
|
||||
private static unsafe extern void raw_co2_compute_synthetic_book(
|
||||
byte[] security_buffer,
|
||||
int security_length,
|
||||
IntPtr[] leg_books,
|
||||
int[] leg_lengths,
|
||||
int leg_books_length,
|
||||
raw_receive_pb_or_string callback);
|
||||
|
||||
internal static unsafe BookMessageImmutable co2_compute_synthetic_book(SecurityImmutable synthetic, params BookMessageImmutable[] books) {
|
||||
BookMessage result = null;
|
||||
string error = null;
|
||||
|
||||
var security_buffer = synthetic.ToByteArray();
|
||||
// var h = GCHandle.Alloc(null, GCHandleType.Pinned).
|
||||
var book_buffers =
|
||||
books
|
||||
.Select(b => b.ToByteArray())
|
||||
.ToArray();
|
||||
GCHandle[] book_buffer_handles =
|
||||
book_buffers
|
||||
.Select(b => GCHandle.Alloc(b,GCHandleType.Pinned))
|
||||
.ToArray();
|
||||
IntPtr[] book_buffer_pointers =
|
||||
book_buffer_handles
|
||||
.Select(h => h.AddrOfPinnedObject())
|
||||
.ToArray();
|
||||
var lengths =
|
||||
book_buffers
|
||||
.Select(b => b.Length)
|
||||
.ToArray();
|
||||
|
||||
raw_co2_compute_synthetic_book(
|
||||
security_buffer,
|
||||
security_buffer.Length,
|
||||
book_buffer_pointers,
|
||||
lengths,
|
||||
book_buffers.Length,
|
||||
(byte* buffer, int length) => {
|
||||
if (length < 0) {
|
||||
error = new string((char*)buffer,0,-1 * length);
|
||||
}
|
||||
else {
|
||||
result = Co2NativeMethods.deserialize<BookMessage>(buffer, (ulong)length);
|
||||
}
|
||||
});
|
||||
|
||||
if(error != null) {
|
||||
// C# style multiple return values
|
||||
throw new Exception(error);
|
||||
}
|
||||
return new BookMessageImmutable(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,17 +52,6 @@ namespace mbts.co2 {
|
||||
return _mantissa.GetHashCode();
|
||||
}
|
||||
|
||||
public static IntPrice from_double(double price, sbyte precision) {
|
||||
if (double.IsNaN(price)) return NaN;
|
||||
if (Math.Abs(price%1) != 0) {
|
||||
double rounding = 0.5 * Math.Pow(10,-precision);
|
||||
price += (price > 0) ? rounding : -rounding;
|
||||
price *= Math.Pow(10,precision);
|
||||
return new IntPrice((long)price, (sbyte)-precision);
|
||||
}
|
||||
return new IntPrice((long)price, 0);
|
||||
}
|
||||
|
||||
public static IntPrice NaN = new IntPrice(0,127);
|
||||
}
|
||||
|
||||
|
||||
@@ -286,46 +286,6 @@ namespace mbts.co2.tests
|
||||
assert_about_equal(dt_utc_now.Second, DateTime.UtcNow.Second);
|
||||
}
|
||||
|
||||
[Test,Ignore("Sample code")]
|
||||
public void merge_synthetic_book() {
|
||||
Security security = new Security();
|
||||
PriceSizeMessage mkPriceSize(double price, UInt32 size) {
|
||||
var iprice = IntPrice.from_double(price, 11);
|
||||
return new PriceSizeMessage() {
|
||||
Mantissa = iprice.mantissa,
|
||||
Exponent = iprice.exponent,
|
||||
Size = size
|
||||
};
|
||||
}
|
||||
IEnumerable<PriceSizeMessage> mkPrices(params (double,uint)[] args) {
|
||||
return args.Select((ps) => mkPriceSize(ps.Item1,ps.Item2));
|
||||
}
|
||||
var leg1 = new BookMessage() {
|
||||
Bids = {
|
||||
mkPriceSize(2000, 10),
|
||||
mkPriceSize(1999.75, 5),
|
||||
mkPriceSize(1999.5, 7)
|
||||
},
|
||||
Asks = {
|
||||
mkPriceSize(2000.25,15),
|
||||
mkPriceSize(2000.5, 5),
|
||||
mkPriceSize(2001,10)
|
||||
}
|
||||
}.immutable();
|
||||
var leg2 = new BookMessage() {
|
||||
Bids = {
|
||||
mkPrices( (2003, 5), (2002.75, 15), (2002.5, 12))
|
||||
},
|
||||
Asks = {
|
||||
mkPrices( (2003.25, 12), (2003.5, 3), (2003.75,5))
|
||||
}
|
||||
}.immutable();
|
||||
|
||||
var result = Co2NativeTestMethods.co2_compute_synthetic_book(security.immutable(), leg1, leg2);
|
||||
Assert.IsTrue(result.Bids.First().Size == 5);
|
||||
Assert.IsTrue(result.Asks.First().Size == 2);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,10 @@ Symbols {
|
||||
future: @SymbolLists.list[GLBX].symbol
|
||||
spread: @SymbolLists.list[GLBX].symbol
|
||||
}
|
||||
simice {
|
||||
future: @SymbolLists.list[ICE].symbol
|
||||
spread: @SymbolLists.list[ICE].symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +110,10 @@ Symbols {
|
||||
future: @SymbolLists.list[GLBX].symbol
|
||||
spread: @SymbolLists.list[GLBX].symbol
|
||||
}
|
||||
ice {
|
||||
future: @SymbolLists.list[ICE].symbol
|
||||
spread: @SymbolLists.list[ICE].symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "lib/config/config.h"
|
||||
#include "lib/md/protos/co2.pb.h"
|
||||
#include "lib/utils/TimeStamp.h"
|
||||
#include "lib/md/test/md_test_utils.h"
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
@@ -16,7 +15,7 @@ using namespace co2;
|
||||
|
||||
std::vector<MDProducerHandle> create_test_securities(MDProducer& producer, const char* postfix = "M");
|
||||
|
||||
// void assign_ids(std::vector<MDProducerHandle>::iterator begin, std::vector<MDProducerHandle>::iterator end, security_key start_key = 1000);
|
||||
void assign_ids(std::vector<MDProducerHandle>::iterator begin, std::vector<MDProducerHandle>::iterator end, security_key start_key = 1000);
|
||||
|
||||
extern "C" BTS_EXPORT void co2_get_1234_handle(size_t& size, MDConsumerHandle& handle, uintptr_t* hotaddr, uintptr_t* coldaddr, uintptr_t* headeraddr) {
|
||||
// Test that marshaling is working
|
||||
@@ -170,72 +169,5 @@ extern "C" BTS_EXPORT void co2_get_sample_datetime(datetime::LLDateTime& now, da
|
||||
utc_now = datetime::ll_now();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Book compute_synthetic_book(const api::Security& security, const std::vector<Book>& leg_books);
|
||||
|
||||
typedef void(*raw_receive_pb_or_string)(const char*, int);
|
||||
extern "C" BTS_EXPORT void raw_co2_compute_synthetic_book(
|
||||
const char* security_buffer,
|
||||
int security_len,
|
||||
const char** book_buffers,
|
||||
const int* book_lengths,
|
||||
int book_buffers_length,
|
||||
raw_receive_pb_or_string cb) {
|
||||
|
||||
// Deserialize Security
|
||||
api::Security security;
|
||||
if (!security.ParseFromArray(security_buffer, security_len)) {
|
||||
const char* err = "Security parse error";
|
||||
cb(err, -1 * strlen(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Deserialize BookMessage and convert to Book
|
||||
std::vector<Book> books;
|
||||
for (int x = 0; x < book_buffers_length; ++x) {
|
||||
BookMessage bm;
|
||||
if (!bm.ParseFromArray(book_buffers[x], book_lengths[x])) {
|
||||
auto err = fmt::format("Book parse error: {}", x);
|
||||
cb(err.c_str(), -1 * err.size());
|
||||
return;
|
||||
}
|
||||
Book book;
|
||||
for (const auto& bid : bm.bids()) {
|
||||
IntPrice ip { bid.mantissa(), (int8_t)bid.exponent() };
|
||||
book.bids.push_back({ ip.price(), bid.size() });
|
||||
}
|
||||
for (const auto& ask : bm.asks()) {
|
||||
IntPrice ip { ask.mantissa(), (int8_t)ask.exponent() };
|
||||
book.asks.push_back({ ip.price(), ask.size() });
|
||||
}
|
||||
books.push_back(book);
|
||||
}
|
||||
|
||||
// Make the call we wanted to make in the first place
|
||||
Book bres = compute_synthetic_book(security, books);
|
||||
|
||||
// Copy back to a BookMessage
|
||||
BookMessage bmres;
|
||||
for (const auto & bid : bres.bids) {
|
||||
PriceSizeMessage& ps = *bmres.add_bids();
|
||||
auto ip = IntPrice_from_double<11>(bid.price);
|
||||
ps.set_mantissa(ip.mantissa());
|
||||
ps.set_exponent(ip.exponent());
|
||||
ps.set_size(bid.size);
|
||||
}
|
||||
for (const auto & ask : bres.asks) {
|
||||
PriceSizeMessage& ps = *bmres.add_asks();
|
||||
auto ip = IntPrice_from_double<11>(ask.price);
|
||||
ps.set_mantissa(ip.mantissa());
|
||||
ps.set_exponent(ip.exponent());
|
||||
ps.set_size(ask.size);
|
||||
}
|
||||
|
||||
// serialize to string
|
||||
auto str = bmres.SerializeAsString();
|
||||
cb(str.c_str(), str.size());
|
||||
}
|
||||
|
||||
}}}//namespace
|
||||
#endif
|
||||
@@ -1,8 +1,5 @@
|
||||
#include "md_test_utils.h"
|
||||
#include "../MDProducer.h"
|
||||
#include "lib/utils/sane_algorithm.h"
|
||||
#include "../MDConsumerHandle.h"
|
||||
#include "Co2TestInstrument.h"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
@@ -41,49 +38,4 @@ void assign_ids(std::vector<MDProducerHandle>::iterator begin, std::vector<MDPro
|
||||
}
|
||||
}
|
||||
|
||||
Book compute_synthetic_book(const api::Security& security, const std::vector<Book>& leg_books) {
|
||||
auto synthetic = std::make_shared<SyntheticStrategy>();
|
||||
synthetic->security.CopyFrom(security);
|
||||
std::vector<Co2TestInstrument> leg_instruments;
|
||||
for (const auto& leg : synthetic->security.synthetic().legs()) {
|
||||
const auto& security = leg.security();
|
||||
Co2TestInstrument instr(security.symbol(), securitytype_of(security));
|
||||
convert_security(security, instr.cold().security, true);
|
||||
leg_instruments.push_back(instr);
|
||||
synthetic->legs.push_back(instr.consumer());
|
||||
}
|
||||
|
||||
// Write leg books
|
||||
for (size_t x = 0; x < leg_books.size(); ++x) {
|
||||
std::array<PriceSize,COCONUT_MAX_DEPTH> bids = {}, asks = {};
|
||||
std::transform(
|
||||
leg_books[x].bids.begin(),
|
||||
leg_books[x].bids.end(),
|
||||
bids.begin(),
|
||||
[](const PS& ps) {
|
||||
return PriceSize_from_double<7>(ps.price, ps.size);
|
||||
});
|
||||
std::transform(
|
||||
leg_books[x].asks.begin(),
|
||||
leg_books[x].asks.end(),
|
||||
asks.begin(),
|
||||
[](const PS& ps) {
|
||||
return PriceSize_from_double<7>(ps.price, ps.size);
|
||||
});
|
||||
auto ts = TimeStamp::now();
|
||||
leg_instruments[x].producer().update_direct(ts, bids, asks);
|
||||
}
|
||||
|
||||
// Read synthetic book back
|
||||
MDConsumerHandle synthetic_handle { synthetic };
|
||||
std::array<PriceSize,COCONUT_MAX_DEPTH> raw_bids, raw_asks;
|
||||
TimeStamp t;
|
||||
synthetic_handle.get_direct_book(t, COCONUT_MAX_DEPTH, raw_bids.data(), raw_asks.data());
|
||||
Book result {};
|
||||
sane::map(raw_bids, result.bids, [](PriceSize ps) -> PS { return {ps.price(), ps.size()}; });
|
||||
sane::map(raw_asks, result.asks, [](PriceSize ps) -> PS { return {ps.price(), ps.size()}; });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}}}//namespace
|
||||
@@ -142,6 +142,50 @@ void set_leg_params(SyntheticStrategy& strat, uint32_t security_key, const spark
|
||||
}
|
||||
|
||||
|
||||
Book compute_synthetic_book(const api::Security& security, const std::vector<Book>& leg_books) {
|
||||
auto synthetic = std::make_shared<SyntheticStrategy>();
|
||||
synthetic->security.CopyFrom(security);
|
||||
vector<Co2TestInstrument> leg_instruments;
|
||||
for (const auto& leg : synthetic->security.synthetic().legs()) {
|
||||
const auto& security = leg.security();
|
||||
Co2TestInstrument instr(security.symbol(), securitytype_of(security));
|
||||
convert_security(security, instr.cold().security, true);
|
||||
leg_instruments.push_back(instr);
|
||||
synthetic->legs.push_back(instr.consumer());
|
||||
}
|
||||
|
||||
// Write leg books
|
||||
for (size_t x = 0; x < leg_books.size(); ++x) {
|
||||
std::array<PriceSize,COCONUT_MAX_DEPTH> bids = {}, asks = {};
|
||||
std::transform(
|
||||
leg_books[x].bids.begin(),
|
||||
leg_books[x].bids.end(),
|
||||
bids.begin(),
|
||||
[](const PS& ps) {
|
||||
return PriceSize_from_double<7>(ps.price, ps.size);
|
||||
});
|
||||
std::transform(
|
||||
leg_books[x].asks.begin(),
|
||||
leg_books[x].asks.end(),
|
||||
asks.begin(),
|
||||
[](const PS& ps) {
|
||||
return PriceSize_from_double<7>(ps.price, ps.size);
|
||||
});
|
||||
auto ts = TimeStamp::now();
|
||||
leg_instruments[x].producer().update_direct(ts, bids, asks);
|
||||
}
|
||||
|
||||
// Read synthetic book back
|
||||
MDConsumerHandle synthetic_handle { synthetic };
|
||||
std::array<PriceSize,COCONUT_MAX_DEPTH> raw_bids, raw_asks;
|
||||
TimeStamp t;
|
||||
synthetic_handle.get_direct_book(t, COCONUT_MAX_DEPTH, raw_bids.data(), raw_asks.data());
|
||||
Book result {};
|
||||
sane::map(raw_bids, result.bids, [](PriceSize ps) -> PS { return {ps.price(), ps.size()}; });
|
||||
sane::map(raw_asks, result.asks, [](PriceSize ps) -> PS { return {ps.price(), ps.size()}; });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// Test Cases
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
1. Verify you can place orders
|
||||
Launch a Ladder
|
||||
Place a 10 lot order away from the market and make sure the order is not rejected
|
||||
Cancel the order
|
||||
|
||||
2. Verify the Max Order Quantity limit works
|
||||
Click on the Settings button
|
||||
Click on the Safety Checks tab
|
||||
Click on the checkbox next to Max Order Quantity and specify 5
|
||||
You should additionally observe that the text has gone from a disabled appearance to active
|
||||
The ticker box is also now active
|
||||
Hit apply
|
||||
Back in the ladder, attempt to place another 10 lot order away from the market. You should observe the following:
|
||||
You are now working a 5 lot order
|
||||
The qty in the qty box additionally reads 5
|
||||
Cancel the 5 lot order
|
||||
|
||||
3. Verify the Max Order Quantity when disabled allows you to place a 10 lot order
|
||||
This is essentially the same as step 1, except now we're explicitly unchecking the checkbox, ergo...
|
||||
Uncheck the Max Order Quantity chechbox and observe that:
|
||||
The Max order quantity text is disabled
|
||||
The ticker box is disabled
|
||||
Click Apply
|
||||
In the ladder, click the 5 button so that your qty box reads 10, which is a quantity greater than the disabled 5
|
||||
A 10 lot order should now be working
|
||||
Cancel the 10 lot order.
|
||||
|
||||
4. Right click / Alternate order qty respects the limit
|
||||
Re-enable the Max Order Quantity (5 is still fine)
|
||||
On the Order Quantities tab, specify 7 as your 'Single Click' Quantity
|
||||
In the ladder, Right click to work an order attempting to use your single click quantity
|
||||
You should be working a a 5 lot order now i.e. your max order quantity
|
||||
Observe that the qty box did NOT update with your alternate order quantity
|
||||
|
||||
5. Cross market alert communicates respects the limit
|
||||
Back on the Safety Checks tab, make sure Warn on Market Cross is checked
|
||||
Attempt to place a 10 lot bid order (higher than your max of 5) on the sell side of the market
|
||||
You should observe the message respects your max order quantity and warns you about buying 5
|
||||
Feel free to click whichever button you desire on the warning dialog
|
||||
|
||||
FIN
|
||||
@@ -0,0 +1,12 @@
|
||||
Launch an ES Ladder (i.e. an instrument whose separator is the .)
|
||||
Observe the ladder prices are in fact separated by a decimal point
|
||||
Launch a ZT ladder (i.e. an instrument whose separator is the -)
|
||||
Observe the ladder prices are in fact separated by a dash
|
||||
Perform the following in each:
|
||||
Click Settings
|
||||
Go to Market Price and Size Display tab
|
||||
Click on Integer under Price Display Format
|
||||
Click Apply
|
||||
Observe that the separator disappears when you do this
|
||||
Feel free to go back and forth several times
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
1. Verify pressing the shift or alt key while double clicking in the Price column in a Ladder Window recenters ALL ladders
|
||||
Launch 2 ladders
|
||||
Scroll both ladders so they are no longer centered at mid market
|
||||
Press the shift + double click while over the Price column
|
||||
You should observe both scrolled ladders are now centered once again
|
||||
The countdown timer is also reset via this spacebar action. This is more informational; I don't expect you to wait to verify that the timer is reset.
|
||||
Reference in new issue
Block a user