Compare commits
195
Commits
ServiceDriver
...
1.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd052979e | ||
|
|
3b3d44d6f6 | ||
|
|
2b2eaac979 | ||
|
|
484ad656f9 | ||
|
|
0e92ee15f4 | ||
|
|
0deb4fbcf4 | ||
|
|
b8b375eca5 | ||
|
|
4a42238cfb | ||
|
|
d4081420ae | ||
|
|
27d38cf443 | ||
|
|
4dad98a8b2 | ||
|
|
a2b8d53ebb | ||
|
|
33a1762cb7 | ||
|
|
f4f1bb7914 | ||
|
|
908958ea3c | ||
|
|
0a8bee2c31 | ||
|
|
2dd8e64272 | ||
|
|
aa704b8c96 | ||
|
|
8673b857a7 | ||
|
|
f83f2c7964 | ||
|
|
86d8d7dafb | ||
|
|
5363fdd0db | ||
|
|
d3e7870b9a | ||
|
|
f05692e441 | ||
|
|
053c460ff6 | ||
|
|
af7e9dd3a8 | ||
|
|
3e3b6acfd7 | ||
|
|
b75496e665 | ||
|
|
03864caa09 | ||
|
|
1558b79c16 | ||
|
|
57bfe5a85e | ||
|
|
3eaeaf0f35 | ||
|
|
ab3121cd5d | ||
|
|
ec5bd4a164 | ||
|
|
3877aec09a | ||
|
|
fb17596304 | ||
|
|
b36bcc66fb | ||
|
|
73189fbbd8 | ||
|
|
484a9dba12 | ||
|
|
83f2866604 | ||
|
|
37f40046da | ||
|
|
2401b9db77 | ||
|
|
a45546690c | ||
|
|
bc394ee3b5 | ||
|
|
b917fba19a | ||
|
|
63106eb866 | ||
|
|
fa79bddb6b | ||
|
|
83851c1494 | ||
|
|
373f7ee3a1 | ||
|
|
2ed8b9caf0 | ||
|
|
470eee431e | ||
|
|
e5e6723b3a | ||
|
|
71be28911b | ||
|
|
8ab3182c21 | ||
|
|
508e2d4fcf | ||
|
|
027e434ac9 | ||
|
|
e9be6d9859 | ||
|
|
82e63f691c | ||
|
|
cb83ad08a6 | ||
|
|
e2e78d91c3 | ||
|
|
427d03d340 | ||
|
|
32e5111c54 | ||
|
|
ef12f81b73 | ||
|
|
c0f255f5a7 | ||
|
|
f6861820e4 | ||
|
|
6a3960b33f | ||
|
|
e3858fd203 | ||
|
|
3f1d9307e9 | ||
|
|
5f7c2330f3 | ||
|
|
315b2a8b26 | ||
|
|
29ebfebcbc | ||
|
|
4fbaca6d05 | ||
|
|
4d4f706e7c | ||
|
|
241396e292 | ||
|
|
4e68c98cfb | ||
|
|
30c1dfd9a7 | ||
|
|
a64ca702c9 | ||
|
|
0a2b6ce17e | ||
|
|
660f203132 | ||
|
|
27f27dbcf1 | ||
|
|
1ac6c8cd40 | ||
|
|
a13cececa1 | ||
|
|
01d2e88097 | ||
|
|
8f96cd07e3 | ||
|
|
b515e45b23 | ||
|
|
a37e2ead87 | ||
|
|
92ae592d31 | ||
|
|
db1cbf589f | ||
|
|
a561ff3aae | ||
|
|
3eeab708d2 | ||
|
|
082056ea3f | ||
|
|
e9f3e92f82 | ||
|
|
152c79a4a9 | ||
|
|
db877c859b | ||
|
|
1ec2423ad2 | ||
|
|
1a88e52a08 | ||
|
|
a1b52d083d | ||
|
|
f364317cfa | ||
|
|
f474af1925 | ||
|
|
03ac5035fc | ||
|
|
e78d8208fa | ||
|
|
7f8c591f75 | ||
|
|
7d9685f995 | ||
|
|
699736ff52 | ||
|
|
8d0e7bc467 | ||
|
|
df784e9961 | ||
|
|
af39aab153 | ||
|
|
885c1ad9f8 | ||
|
|
91fdd6ec58 | ||
|
|
3adfd7056a | ||
|
|
5f845f1355 | ||
|
|
5d4bb1d421 | ||
|
|
9836e46dd9 | ||
|
|
bf9d5c2c6d | ||
|
|
ef1c3a7959 | ||
|
|
b07f6aba59 | ||
|
|
a7b3b0fa6d | ||
|
|
f7a3b3ebc1 | ||
|
|
d291e4a022 | ||
|
|
292e5575d0 | ||
|
|
1ad57dbf45 | ||
|
|
b25f5556af | ||
|
|
c10a4be411 | ||
|
|
de312c3023 | ||
|
|
93ff56c5cd | ||
|
|
5254b7367a | ||
|
|
6d2efb7dc9 | ||
|
|
50214741e7 | ||
|
|
56950b682d | ||
|
|
83b1bbf087 | ||
|
|
203998e201 | ||
|
|
59d88d3cc1 | ||
|
|
13f9717195 | ||
|
|
855e0d7ef9 | ||
|
|
27a1cb277c | ||
|
|
3107d5f382 | ||
|
|
e1940faeac | ||
|
|
a2f22482f6 | ||
|
|
ccea4571a6 | ||
|
|
3a72a4a82e | ||
|
|
ed6affb6b5 | ||
|
|
2fc208ef74 | ||
|
|
64f86db86b | ||
|
|
5e960479c7 | ||
|
|
837aa51adf | ||
|
|
d7a7b480ad | ||
|
|
d94afe9867 | ||
|
|
410e17aec5 | ||
|
|
a8bd99af56 | ||
|
|
e11b5928ce | ||
|
|
6bcf352813 | ||
|
|
eeec7b0076 | ||
|
|
bacdb1c4cd | ||
|
|
1e4ad28088 | ||
|
|
e013f93169 | ||
|
|
002c051c06 | ||
|
|
42fd7b3f40 | ||
|
|
f73d163312 | ||
|
|
ee124401b9 | ||
|
|
ec7417a9c6 | ||
|
|
536f4703ab | ||
|
|
5f848a36e2 | ||
|
|
a7c714591a | ||
|
|
4655a68593 | ||
|
|
9677d8996e | ||
|
|
3f0a9e442e | ||
|
|
f3387b57af | ||
|
|
abed4bbff6 | ||
|
|
bb90429ada | ||
|
|
1efbc2fd45 | ||
|
|
04027ed980 | ||
|
|
5a850c314c | ||
|
|
b566668d83 | ||
|
|
6679695931 | ||
|
|
23a8112dd3 | ||
|
|
b9a07a8e21 | ||
|
|
86deaf2edb | ||
|
|
fc347250e3 | ||
|
|
aae867a555 | ||
|
|
37a454cf51 | ||
|
|
5162b9f0c0 | ||
|
|
ad757495bd | ||
|
|
e473c18a30 | ||
|
|
640ec0b1ed | ||
|
|
c3e2622a22 | ||
|
|
7d05f76fe4 | ||
|
|
7a8a38f89d | ||
|
|
1e54cb1daf | ||
|
|
7b714018d2 | ||
|
|
c3371d0971 | ||
|
|
322e3d81ba | ||
|
|
4ada5a89f4 | ||
|
|
15aa016aa0 | ||
|
|
e77f3169de | ||
|
|
660c5d3a06 |
No files matched your search
@@ -21,3 +21,5 @@ src/client/sparkcmd/Properties/launchSettings.json
|
||||
node_modules
|
||||
paket-files
|
||||
project.lock.json
|
||||
src/server/sparkd/orderman/perf_analysis/data
|
||||
src/server/sparkd/orderman/perf_analysis/trace_data/
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
<Message Importance="High" Text="Generating service %(Proto3.FileName)" />
|
||||
<Exec Command="
|
||||
set PATH=$(spark_build_python_path);%path% &
|
||||
set PYTHONPATH=$(Externals)\python &
|
||||
$(protoc) ^
|
||||
%(Proto3.FullPath) ^
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace bts.ui {
|
||||
public AboutDaemon(IBus bus, [Import("username")]string username, IBrowserWindowManager browser) {
|
||||
_user = username;
|
||||
_browser = browser;
|
||||
_dispose = bus.subscribe(SparkTags.About).ObserveOnDispatcher().Subscribe(_ => show_about());
|
||||
_dispose = bus.subscribe(SparkTags.About).OnDispatcher().Subscribe(_ => show_about());
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace bts {
|
||||
public DeleteSyntheticDaemon(IBus bus, OrdersModel orders_model) {
|
||||
_orders_model = orders_model;
|
||||
_dispose = bus.subscribe(SparkTags.SyntheticDeleteTag)
|
||||
.ObserveOnDispatcher()
|
||||
.OnDispatcher()
|
||||
.Subscribe(_delete_synthetic);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using mbts.ui;
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using System.Windows;
|
||||
|
||||
namespace bts.spark
|
||||
@@ -22,18 +22,21 @@ namespace bts.spark
|
||||
public ErrorReporter(ErrorReportService svc, IBus bus, [Import("username")] string username) {
|
||||
_svc = svc;
|
||||
_username = username;
|
||||
_dispose = bus.subscribe(SparkTags.Support).ObserveOnDispatcher().Subscribe(msg => show_support_window(msg, "Report a problem"));
|
||||
_dispose = bus.subscribe(SparkTags.Support).OnDispatcher().Subscribe(msg => show_support_window(msg, "Report a problem"));
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Disposable.dispose(ref _dispose);
|
||||
}
|
||||
|
||||
public async Task report_error(string message, string title = null, MessageBoxImage? image = null) {
|
||||
var result = await BTSMessageBox.ShowAsync(message, title = "Error", BTSMessageBoxButtons.OK | BTSMessageBoxButtons.Support, image ?? MessageBoxImage.Error);
|
||||
if (result == BTSMessageBoxButtons.Support) {
|
||||
show_support_window(message, title);
|
||||
}
|
||||
public void report_error(string message, string title = null, MessageBoxImage? image = null) {
|
||||
// Error reports are likely to arrive the api thread (which is not an STA thread)
|
||||
Application.Current.Dispatcher.BeginInvoke(async () => {
|
||||
var result = await BTSMessageBox.ShowAsync(message, title = "Error", BTSMessageBoxButtons.OK | BTSMessageBoxButtons.Support, image ?? MessageBoxImage.Error);
|
||||
if (result == BTSMessageBoxButtons.Support) {
|
||||
show_support_window(message, title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void show_support_window(string message, string title) {
|
||||
@@ -70,11 +73,11 @@ namespace bts.spark
|
||||
return (get_successful_rv(result.value), string.Empty);
|
||||
else {
|
||||
string error_msg = template.fmt(error);
|
||||
error_reporter.report_error(error_msg).ignore();
|
||||
error_reporter.report_error(error_msg);
|
||||
return (get_unsuccessful_rv(), error_msg);
|
||||
}
|
||||
} else {
|
||||
error_reporter.report_error(result.error_message).ignore();
|
||||
error_reporter.report_error(result.error_message);
|
||||
return (get_unsuccessful_rv(), result.error_message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ namespace bts.shell {
|
||||
_model = model;
|
||||
_error_reporter = error_reporter;
|
||||
_dispose = Disposable.Create(
|
||||
model.on_orders.ObserveOnDispatcher().Subscribe(o => _orders = o),
|
||||
bus.subscribe(SparkTags.SecuritiesTag).ObserveOnDispatcher().Subscribe(m =>_md_map = m),
|
||||
bus.subscribe(SparkTags.ShowOrderTicketFromSecurityTag).ObserveOnDispatcher().Subscribe(_show_order_ticket),
|
||||
bus.subscribe(SparkTags.ShowOrderTicketTag).ObserveOnDispatcher().Subscribe(_show_order_ticket),
|
||||
bus.subscribe(SparkTags.ShowModifyOrderTag).ObserveOnDispatcher().Subscribe(_show_modify_order)
|
||||
model.on_orders.OnDispatcher().Subscribe(o => _orders = o),
|
||||
bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(m =>_md_map = m),
|
||||
bus.subscribe(SparkTags.ShowOrderTicketFromSecurityTag).OnDispatcher().Subscribe(_show_order_ticket),
|
||||
bus.subscribe(SparkTags.ShowOrderTicketTag).OnDispatcher().Subscribe(_show_order_ticket),
|
||||
bus.subscribe(SparkTags.ShowModifyOrderTag).OnDispatcher().Subscribe(_show_modify_order)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ namespace bts {
|
||||
public SecurityPickerDaemon(IBus bus) {
|
||||
_bus = bus;
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.ShowPickOne).ObserveOnDispatcher().Subscribe(show_pick_one),
|
||||
bus.subscribe(SparkTags.FilterPickOne).ObserveOnDispatcher().Subscribe(show_filtered_pick_one),
|
||||
bus.subscribe(SparkTags.ShowPickMultiple).ObserveOnDispatcher().Subscribe(show_pick_multiple),
|
||||
bus.subscribe(SparkTags.ShowAndEditPickMultiple).ObserveOnDispatcher().Subscribe(show_and_edit_pick_multiple)
|
||||
bus.subscribe(SparkTags.ShowPickOne).OnDispatcher().Subscribe(show_pick_one),
|
||||
bus.subscribe(SparkTags.FilterPickOne).OnDispatcher().Subscribe(show_filtered_pick_one),
|
||||
bus.subscribe(SparkTags.ShowPickMultiple).OnDispatcher().Subscribe(show_pick_multiple),
|
||||
bus.subscribe(SparkTags.ShowAndEditPickMultiple).OnDispatcher().Subscribe(show_and_edit_pick_multiple)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,11 +78,11 @@ namespace mbts.spark {
|
||||
_animation.AnimationsChanged += on_animations_changed;
|
||||
void on_animations_changed() => RaisePropertyChanged(nameof(animations));
|
||||
_dispose = Disposable.Create(
|
||||
_orders_model.on_security_orders(security.Id).ObserveOnDispatcher().Subscribe(_on_orders),
|
||||
_position.ObserveOnDispatcher().Subscribe(v => _animation.animate_position(v.newval > v.oldval)),
|
||||
_inside_bids.ObserveOnDispatcher().Subscribe(v => _animation.animate_working_bids_inside(v.newval)),
|
||||
_inside_asks.ObserveOnDispatcher().Subscribe(v => _animation.animate_working_asks_inside(v.newval)),
|
||||
_day_change.ObserveOnDispatcher().Subscribe(v => _animation.animate_day_change(v.newval > v.oldval)),
|
||||
_orders_model.on_security_orders(security.Id).OnDispatcher().Subscribe(_on_orders),
|
||||
_position.OnDispatcher().Subscribe(v => _animation.animate_position(v.newval > v.oldval)),
|
||||
_inside_bids.OnDispatcher().Subscribe(v => _animation.animate_working_bids_inside(v.newval)),
|
||||
_inside_asks.OnDispatcher().Subscribe(v => _animation.animate_working_asks_inside(v.newval)),
|
||||
_day_change.OnDispatcher().Subscribe(v => _animation.animate_day_change(v.newval > v.oldval)),
|
||||
Disposable.Create(() => _animation.AnimationsChanged -= on_animations_changed)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace mbts.spark {
|
||||
}
|
||||
//
|
||||
_resources.AddRange(
|
||||
state.ObserveOnDispatcher().Subscribe(s => {
|
||||
state.OnDispatcher().Subscribe(s => {
|
||||
using (rows_view.DeferRefresh()) {
|
||||
rows_view.SetSortingCriteria(s.sorting_criteria);
|
||||
var filters = s.filters;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace mbts.spark {
|
||||
SparkCommands.ShowTimeAndSales.complete(_vm.selected_securities_observable(), _ => true, secs => secs.Count > 0 ? new SecurityCriteria(secs) : _state.security_criteria),
|
||||
SparkCommands.ShowSyntheticCreate.complete(_vm.selected_securities_observable(), secs => secs.Count > 0 && secs.Count < 3, secs => new SecurityCriteria(secs))
|
||||
};
|
||||
_subscriptions = _bus.subscribe(update_securities_tag).ObserveOnDispatcher().Subscribe(c =>
|
||||
_subscriptions = _bus.subscribe(update_securities_tag).OnDispatcher().Subscribe(c =>
|
||||
_state_changed?.Invoke(_state = new MarketViewWindowFactory.StartupData(c, _vm.rows_view.GetSortingCriteria(), _vm.get_filter_states())));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace bts.shell {
|
||||
public MarketViewWindowFactory(MarketShellModule module, IBus bus, OrdersModel orders_model) : base(module, "MarketView") {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_dispose = bus.subscribe(SparkTags.ShowMarketViewTag).ObserveOnDispatcher().Subscribe(_show);
|
||||
_dispose = bus.subscribe(SparkTags.ShowMarketViewTag).OnDispatcher().Subscribe(_show);
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(StartupData state) =>
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace mbts.spark {
|
||||
_vm = new SingleValuePump<TimeAndSalesVM>();
|
||||
var update_securities_tag = new SparkTags.SparkTag<SecurityCriteria>("TimeAndSales Callback");
|
||||
var update_securities_cmd = SparkCommand.create("Update Time and Sales", Icon, update_securities_tag);
|
||||
var current_smd_source = _vm.observable.SelectMany(vm => vm.current_security_md_observable(_bus)).ObserveOnDispatcher();
|
||||
var selected_secs_source = _vm.observable.SelectMany(vm => vm.selected_securities_observable()).ObserveOnDispatcher();
|
||||
var current_smd_source = _vm.observable.SelectMany(vm => vm.current_security_md_observable(_bus)).OnDispatcher();
|
||||
var selected_secs_source = _vm.observable.SelectMany(vm => vm.selected_securities_observable()).OnDispatcher();
|
||||
_commands = new SparkCommand[] {
|
||||
SparkCommand.create("Change Securities", Icons.Admin, SparkTags.ShowAndEditPickMultiple, () => (update_securities_cmd, _state.security_criteria)),
|
||||
SparkCommands.ShowLadder.complete(current_smd_source, md => md.HasValue, ObjectHelper.ValueOrDefault),
|
||||
@@ -56,7 +56,7 @@ namespace mbts.spark {
|
||||
SparkCommands.ShowFills.complete(selected_secs_source, always_true, get_fills_startup_data),
|
||||
SparkCommands.ShowMarketView.complete(selected_secs_source, always_true, secs => secs.Count > 0 ? new SecurityCriteria(secs) : _state.security_criteria)
|
||||
};
|
||||
_subscriptions = _bus.subscribe(update_securities_tag).ObserveOnDispatcher().Subscribe(update_criteria);
|
||||
_subscriptions = _bus.subscribe(update_securities_tag).OnDispatcher().Subscribe(update_criteria);
|
||||
_initialize();
|
||||
|
||||
bool always_true<T>(T _) => true;
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace bts.shell {
|
||||
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
bus.subscribe(SparkTags.ShowTimeAndSalesTag).ObserveOnDispatcher().Subscribe(show)
|
||||
bus.subscribe(SparkTags.ShowTimeAndSalesTag).OnDispatcher().Subscribe(show)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,34 +15,33 @@
|
||||
<ResourceDictionary Source="/view/Controls/Popup/PopupControl.xaml"/>
|
||||
<ResourceDictionary Source="/Modules/Spreader/ParameterSectionView.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<sys:Double x:Key="GroupLevelIndent">18</sys:Double>
|
||||
<sys:Double x:Key="RowHeaderWidth">285</sys:Double>
|
||||
<sys:Double x:Key="RowHeight">16</sys:Double>
|
||||
<sys:Double x:Key="NegativeRowHeight">-16</sys:Double>
|
||||
<sys:Double x:Key="ItemsHostCanBeDisabledCollapsedHeight">15</sys:Double>
|
||||
<Thickness
|
||||
x:Key="GroupLevelMargin"
|
||||
Left="{StaticResource GroupLevelIndent}"
|
||||
Left="{x:Static usp:ExecutionStrategyEditor.GroupLevelIndent}"
|
||||
/>
|
||||
<Thickness
|
||||
x:Key="GroupContentAlignedWithHeaderMargin"
|
||||
Left="{StaticResource GroupLevelIndent}"
|
||||
Left="{x:Static usp:ExecutionStrategyEditor.GroupLevelIndent}"
|
||||
Top="{StaticResource NegativeRowHeight}"
|
||||
/>
|
||||
<cv:SyntheticPriceCalcDisplayTextConverter x:Key="SyntheticPriceCalcDisplayConverter"/>
|
||||
<cv:TickFactorsTextConverter x:Key="TickFactorsTextConverter"/>
|
||||
<usp:RowHeaderGroupIndentConverter
|
||||
x:Key="RowHeaderGroupIndentConverter"
|
||||
Indent="{StaticResource GroupLevelIndent}"
|
||||
Indent="{x:Static usp:ExecutionStrategyEditor.GroupLevelIndent}"
|
||||
/>
|
||||
<usp:RowHeaderGroupIndentConverter
|
||||
x:Key="ReverseRowHeaderGroupIndentConverter"
|
||||
Indent="{StaticResource GroupLevelIndent}"
|
||||
Indent="{x:Static usp:ExecutionStrategyEditor.GroupLevelIndent}"
|
||||
Reverse="True"
|
||||
/>
|
||||
<usp:RowGroupExpanderWidthConverter
|
||||
x:Key="RowGroupExpanderWidthConverter"
|
||||
Indent="{StaticResource GroupLevelIndent}"
|
||||
Indent="{x:Static usp:ExecutionStrategyEditor.GroupLevelIndent}"
|
||||
Offset="{StaticResource RowHeight}"
|
||||
RowHeaderWidth="{StaticResource RowHeaderWidth}"
|
||||
/>
|
||||
@@ -293,7 +292,7 @@
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<DataGridTextColumn x:Key="AddColumnPlaceholder" x:Shared="False" IsReadOnly="True">
|
||||
<DataGridTextColumn x:Key="{x:Static usp:ExecutionStrategyEditor.AddColumnPlaceholderKey}" x:Shared="False" IsReadOnly="True">
|
||||
<DataGridTextColumn.Header>
|
||||
<c:PopupControl
|
||||
ShowExpanderArrow="False" PopupPlacement="Right" Header="Add Leg" Padding="6,0"
|
||||
@@ -308,7 +307,7 @@
|
||||
</c:PopupControl>
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataTemplate x:Key="RemovableColumnHeaderTemplate">
|
||||
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.RemovableColumnHeaderTemplateKey}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -321,7 +320,9 @@
|
||||
<TextBlock Grid.Column="1" Margin="4,0,0,0" Text="{Binding Converter={StaticResource SecurityTextConverter}}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.NonRemovableColumnHeaderTemplateKey}">
|
||||
<TextBlock Text="{Binding Converter={StaticResource SecurityTextConverter}}"/>
|
||||
</DataTemplate>
|
||||
<ControlTemplate x:Key="ExecutionStrategyEditorTemplate" TargetType="usp:ExecutionStrategyEditor">
|
||||
<Border
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
|
||||
@@ -25,7 +25,13 @@ namespace mbts.ui.spark {
|
||||
/// <summary>
|
||||
/// Interaction logic for ExecutionStrategyEditor.xaml
|
||||
/// </summary>
|
||||
partial class ExecutionStrategyEditor : Control {
|
||||
partial class ExecutionStrategyEditor : Control {
|
||||
public const string
|
||||
RemovableColumnHeaderTemplateKey = "RemovableColumnHeaderTemplate",
|
||||
NonRemovableColumnHeaderTemplateKey = "ReadOnlyColumnHeaderTemplate",
|
||||
AddColumnPlaceholderKey = "AddColumnPlaceholder";
|
||||
public const double GroupLevelIndent = 18;
|
||||
|
||||
static ExecutionStrategyEditor() =>
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExecutionStrategyEditor), new FrameworkPropertyMetadata(typeof(ExecutionStrategyEditor)));
|
||||
|
||||
@@ -196,7 +202,7 @@ namespace mbts.ui.spark {
|
||||
if (dg != null) {
|
||||
var legs = LegTuples;
|
||||
dg.Columns.SetItems(legs.Select(_column_factory));
|
||||
if (IsNew) dg.Columns.Add((DataGridColumn)FindResource("AddColumnPlaceholder"));
|
||||
if (IsNew) dg.Columns.Add((DataGridColumn)FindResource(AddColumnPlaceholderKey));
|
||||
_dg_line_adorner = AdornerHelper.attach_continuously_on_loaded(() => new ExecutionStrategyGridLineAdorner(dg), dg);
|
||||
_legs_collection_changed_sub = (legs as INotifyCollectionChanged)?.Subscribe((s, e) => dg.Columns.ReflectChange(legs, _column_factory, e));
|
||||
}
|
||||
@@ -204,7 +210,7 @@ namespace mbts.ui.spark {
|
||||
|
||||
DataGridColumn _column_factory(LegSpreaderParamsTuple l) {
|
||||
var col = new DataGridColumnInternal(l.leg.Security, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter) { Width = 75, };
|
||||
if (IsNew) col.HeaderTemplate = (DataTemplate)FindResource("RemovableColumnHeaderTemplate");
|
||||
col.HeaderTemplate = (DataTemplate)FindResource(IsNew ? RemovableColumnHeaderTemplateKey : NonRemovableColumnHeaderTemplateKey);
|
||||
DataGridEx.SetRightBorder(col, new DataGridExVerticalLine(false, true, Colors.DarkGray, 1));
|
||||
return col;
|
||||
}
|
||||
@@ -240,7 +246,7 @@ namespace mbts.ui.spark {
|
||||
FrameworkElement _generate_element(IValueConverter cell_template_converter) {
|
||||
var rv = new ContentControl();
|
||||
rv.SetBinding(ContentControl.ContentProperty, new Binding());
|
||||
rv.SetBinding(ContentControl.ContentTemplateProperty, new Binding("data_type") { Converter = cell_template_converter });
|
||||
rv.SetBinding(ContentControl.ContentTemplateProperty, new Binding(nameof(ICell.data_type)) { Converter = cell_template_converter });
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ namespace mbts.ui.spark
|
||||
void _items_presenter_SizeChanged(object sender, SizeChangedEventArgs e) { InvalidateVisual(); }
|
||||
|
||||
protected override void draw_vertical_line(DrawingContext dc, double x, DataGridExVerticalLine line) {
|
||||
if (_items_presenter != null) dc.DrawLine(line.pen.Value, new Point(x - 10, 58), new Point(x - 10, _items_presenter.ActualHeight + 58));
|
||||
x -= GroupLevelIndent;
|
||||
var start = _grid.TranslatePoint(new Point(x, _grid.super_header_height + _col_header_presenter.ActualHeight), this);
|
||||
var stop = _grid.TranslatePoint(new Point(x, _grid.ActualHeight), this);
|
||||
dc.DrawLine(line.pen.Value, start, stop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ namespace bts.shell {
|
||||
void IInitializable.initialize() {
|
||||
Disposable.dispose(ref _dispose);
|
||||
_dispose = Disposable.Create(
|
||||
_bus.subscribe(SparkTags.ShowSyntheticCreateTag).ObserveOnDispatcher().Subscribe(_show_synthetic_create),
|
||||
_bus.subscribe(SparkTags.ShowSyntheticStrategyTag).ObserveOnDispatcher().Subscribe(_show_synthetic_strategy),
|
||||
_bus.subscribe(SparkTags.ShowSyntheticCloneTag).ObserveOnDispatcher().Subscribe(_show_synthetic_clone)
|
||||
_bus.subscribe(SparkTags.ShowSyntheticCreateTag).OnDispatcher().Subscribe(_show_synthetic_create),
|
||||
_bus.subscribe(SparkTags.ShowSyntheticStrategyTag).OnDispatcher().Subscribe(_show_synthetic_strategy),
|
||||
_bus.subscribe(SparkTags.ShowSyntheticCloneTag).OnDispatcher().Subscribe(_show_synthetic_clone)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@ namespace mbts.spark {
|
||||
SparkCommand.create("Add", Icons.AddSynthetic, SparkTags.ShowSyntheticCreateTag).complete(
|
||||
() => new SecurityCriteria(), () => true
|
||||
), SparkCommand.create("Remove", Icons.RemoveSynthetic, SparkTags.SyntheticDeleteTag).complete(
|
||||
_vm.observable.SelectMany(vm => vm.current_row_observable),
|
||||
can_row_change, r => r == null ? 0 : r.security_key
|
||||
_vm.observable.SelectMany(vm => vm.current_row_observable.SelectMany(observe_row_mod_params)),
|
||||
can_execute, select_security_id
|
||||
), SparkCommand.create("Change", Icons.ModifySynthetic, SparkTags.ShowSyntheticStrategyTag).complete(
|
||||
_vm.observable.SelectMany(vm => vm.current_row_observable),
|
||||
can_row_change, r => r == null ? SecurityDataHelper.Defaults.SecurityImmutable : r.security
|
||||
r => r != null, r => r?.security ?? SecurityDataHelper.Defaults.SecurityImmutable
|
||||
), SparkCommand.create("Clone", Icons.CloneSynthetic, SparkTags.ShowSyntheticCloneTag).complete(
|
||||
_vm.observable.SelectMany(vm => vm.current_row_observable),
|
||||
r => r != null && r.strategy.HasValue, r => r == null ? default : (r.security, r.strategy.Value)
|
||||
_vm.observable.SelectMany(vm => vm.current_row_observable.SelectMany(observe_row_strategy)),
|
||||
rs => rs.HasValue, select_row_strategy_tuple
|
||||
), SparkCommands.ShowLadder.complete(
|
||||
_vm.observable.SelectMany(vm => vm.current_security_md), md => md.HasValue, ObjectHelper.ValueOrDefault
|
||||
), SparkCommands.ShowOrders.complete(() => new OrderBookWindowFactory.OrderBookStartupData {
|
||||
@@ -49,10 +49,19 @@ namespace mbts.spark {
|
||||
})
|
||||
};
|
||||
|
||||
bool can_row_change(SyntheticManagementDialogVM.Row r) =>
|
||||
r != null && r.n_working_orders == 0;
|
||||
}
|
||||
IObservable<RowModificationParams?> observe_row_mod_params(SyntheticManagementDialogVM.Row r) =>
|
||||
r == null ? Observable.Return(new RowModificationParams?()) :
|
||||
_orders_model.security_has_working_orders(r.security_key).Select(hwo => RowModificationParams.create(r.security, hwo));
|
||||
|
||||
IObservable<RowStrategy?> observe_row_strategy(SyntheticManagementDialogVM.Row r) =>
|
||||
r == null ? Observable.Return(new RowStrategy?()) :
|
||||
_orders_model.spreader.security_strategy(r.security.Id).Select(strategy => RowStrategy.create(r.security, strategy));
|
||||
|
||||
uint select_security_id(RowModificationParams? rmp) => rmp.HasValue ? rmp.Value.security.Id : 0;
|
||||
(SecurityImmutable, ExecutionStrategyImmutable) select_row_strategy_tuple(RowStrategy? rs) => rs.HasValue ? (rs.Value.security, rs.Value.strategy) : default;
|
||||
bool can_execute(RowModificationParams? rmp) => rmp.HasValue && !rmp.Value.has_working_orders;
|
||||
}
|
||||
|
||||
public void initialize(StartupData state) {
|
||||
Logger.notnull(state);
|
||||
_state = state;
|
||||
@@ -71,5 +80,31 @@ namespace mbts.spark {
|
||||
Disposable.dispose(ref _vm);
|
||||
Commands.OfType<IDisposable>().DisposeAll();
|
||||
}
|
||||
|
||||
struct RowModificationParams {
|
||||
public readonly SecurityImmutable security;
|
||||
public readonly bool has_working_orders;
|
||||
|
||||
RowModificationParams(SecurityImmutable security, bool has_working_orders) {
|
||||
this.security = security;
|
||||
this.has_working_orders = has_working_orders;
|
||||
}
|
||||
|
||||
public static RowModificationParams? create(SecurityImmutable security, bool has_working_orders) =>
|
||||
new RowModificationParams(security, has_working_orders);
|
||||
}
|
||||
|
||||
struct RowStrategy {
|
||||
public readonly SecurityImmutable security;
|
||||
public readonly ExecutionStrategyImmutable strategy;
|
||||
|
||||
RowStrategy(SecurityImmutable security, ExecutionStrategyImmutable strategy) {
|
||||
this.security = security;
|
||||
this.strategy = strategy;
|
||||
}
|
||||
|
||||
public static RowStrategy? create(SecurityImmutable security, ExecutionStrategyImmutable strategy) =>
|
||||
new RowStrategy(security, strategy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace bts.shell {
|
||||
public SyntheticManagementWindowFactory(SpreaderShellModule module, IBus bus, OrdersModel orders_model) : base(module, "SyntheticManagement") {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_dispose = bus.subscribe(SparkTags.ShowSyntheticManagementTag).ObserveOnDispatcher().Subscribe(show);
|
||||
_dispose = bus.subscribe(SparkTags.ShowSyntheticManagementTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(StartupData state) =>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace mbts.spark {
|
||||
readonly Property<bool> _enabled;
|
||||
|
||||
protected CellBase(IRowModel model, LegSpreaderParamsTuple leg_tup) =>
|
||||
_enabled = backing(nameof(enabled), model.observe_enabled(leg_tup).ObserveOnDispatcher());
|
||||
_enabled = backing(nameof(enabled), model.observe_enabled(leg_tup).OnDispatcher());
|
||||
|
||||
public bool enabled => _enabled.Value;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace mbts.spark {
|
||||
: base(model, leg_tup) {
|
||||
_model = model;
|
||||
_leg_tup = leg_tup;
|
||||
_override_value_sub = model.observe_override_value(leg_tup).ObserveOnDispatcher().Subscribe(v => {
|
||||
_override_value_sub = model.observe_override_value(leg_tup).OnDispatcher().Subscribe(v => {
|
||||
value = v;
|
||||
RaisePropertyChanged(nameof(value));
|
||||
});
|
||||
|
||||
@@ -44,10 +44,10 @@ namespace mbts.spark {
|
||||
_tick_factors = TickUtils.get_regional_ticks(synthetic.ContractSpec).low_tick;
|
||||
title = $"Edit '{synthetic.Synthetic.Name}'";
|
||||
icon = Icons.ModifySynthetic;
|
||||
_has_working_orders = backing(nameof(has_working_orders), security_has_working_orders.ObserveOnDispatcher());
|
||||
_has_working_orders = backing(nameof(has_working_orders), security_has_working_orders.OnDispatcher());
|
||||
var commit = new ObservantAsyncCommand(commit_async);
|
||||
_resources.Add(security_has_working_orders.Select(v => !v).ObserveOnDispatcher().Subscribe(commit));
|
||||
_resources.Add(_has_working_orders.ObserveOnDispatcher().Subscribe(vals => {
|
||||
_resources.Add(security_has_working_orders.Select(v => !v).OnDispatcher().Subscribe(commit));
|
||||
_resources.Add(_has_working_orders.OnDispatcher().Subscribe(vals => {
|
||||
if (vals.newval)
|
||||
set_error(nameof(has_working_orders), "Security has working orders");
|
||||
else clear_error(nameof(has_working_orders));
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace mbts.spark {
|
||||
.Select(t => (uint)(t.state
|
||||
.Where(kv => kv.Value.is_active())
|
||||
.Count())
|
||||
).ObserveOnDispatcher());
|
||||
).OnDispatcher());
|
||||
name = security.Synthetic.Name;
|
||||
n_legs = security.Synthetic.Legs.Count();
|
||||
description = SecurityFormatter.format_contract_legs(security.Synthetic.Legs);
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace bts.shell
|
||||
}
|
||||
|
||||
public void thread_init() {
|
||||
_dispose = _message_src.ObserveOnDispatcher().Subscribe(_messages.AddLog);
|
||||
_dispose = _message_src.OnDispatcher().Subscribe(_messages.AddLog);
|
||||
_content =
|
||||
new LogViewer {
|
||||
ItemsSource = _messages,
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace bts.shell {
|
||||
public HelpShellModule(IBus bus, IBrowserWindowManager browser) {
|
||||
_awf = new ActivityWindowFactory(this, bus);
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.ShowUri).ObserveOnDispatcher().Subscribe(u => browser.Pages.Add(u))
|
||||
bus.subscribe(SparkTags.ShowUri).OnDispatcher().Subscribe(u => browser.Pages.Add(u))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,16 @@ namespace mbts.spark {
|
||||
public FillsVM(IBus bus, OrdersModel model, StartupData state)
|
||||
: this(bus, model, _get_observable(bus, model, state), state.sorting_criteria, state.filters) { }
|
||||
|
||||
FillsVM(IBus bus, OrdersModel model, IObservable<ITableUpdateTransform<FillTuple, string>> fills_source, IReadOnlyCollection<SortingCriteria> sorting_criteria, FilterStates filters)
|
||||
: base(RXCollection.CreateTableUpdate(fills_source), sorting_criteria) {
|
||||
FillsVM(IBus bus, OrdersModel model, IObservable<IIncrementalUpdate<FillTuple, FillTupleKey>> fills_source, IReadOnlyCollection<SortingCriteria> sorting_criteria, FilterStates filters)
|
||||
: base(RXCollection.CreateIncrementalUpdate(fills_source), sorting_criteria) {
|
||||
_model = model;
|
||||
account_filter = DiscreteValuesFilterVM.Create<Row, string>(_model.factory.subscribe_accounts(), r => r.account, nameof(Row.account));
|
||||
user_filter = DiscreteValuesFilterVM.Create<Row, string>(_model.factory.subscribe_users(), r => r.user, nameof(Row.user));
|
||||
security_filter = DiscreteValuesFilterVM.Create<Row, SecurityImmutable, SecurityCriteria>(
|
||||
fills_source.with_respect_to(ft => ft.security ?? SecurityDataHelper.Defaults.SecurityImmutable, s => s.Id, ft => ft.key).Select(t => (t.added, t.removed)),
|
||||
fills_source.with_respect_to(
|
||||
ft => ft.security_md.HasValue ? ft.security_md.Value.security : SecurityDataHelper.Defaults.SecurityImmutable,
|
||||
s => s.Id, ft => ft.key
|
||||
).Select(t => (t.added.Values, t.removed.Values)),
|
||||
r => r.security,
|
||||
SecurityCriteria.from_securities,
|
||||
SecurityCriteria.get_to_securities(bus),
|
||||
@@ -126,8 +129,8 @@ namespace mbts.spark {
|
||||
protected override Row row_factory(FillTuple row_model) => new Row(row_model, _model);
|
||||
protected override IEnumerable<SortingCriteria> get_default_sorting() { yield return new SortingCriteria(nameof(Row.fill_time), false); }
|
||||
|
||||
static IObservable<ITableUpdateTransform<FillTuple, string>> _get_observable(IBus bus, OrdersModel model, StartupData state) {
|
||||
if (state.orders != null && state.orders.Length == 0) {
|
||||
static IObservable<IIncrementalUpdate<FillTuple, FillTupleKey>> _get_observable(IBus bus, OrdersModel model, StartupData state) {
|
||||
if (!state.orders.is_null_or_empty()) {
|
||||
var orders_hash = new HashSet<uint>(state.orders);
|
||||
return model.on_fill_tuples.filter(f => orders_hash.Contains(f.fill.OrderKey));
|
||||
} else return model.on_fill_tuples;
|
||||
@@ -138,7 +141,7 @@ namespace mbts.spark {
|
||||
readonly FillImmutable _fill;
|
||||
readonly OrderRecordImmutable _order;
|
||||
|
||||
public Row(FillTuple fill_tuple, OrdersModel model) : base(fill_tuple.security ?? SecurityDataHelper.Defaults.Security) {
|
||||
public Row(FillTuple fill_tuple, OrdersModel model) : base(fill_tuple.security_md.HasValue ? fill_tuple.security_md.Value.security : SecurityDataHelper.Defaults.Security) {
|
||||
_fill = fill_tuple.fill;
|
||||
_order = fill_tuple.order ?? OrderDataHelper.Defaults.OrderRecordImmutable;
|
||||
_model = model;
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace bts.shell {
|
||||
: base(module, "Fills") {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_dispose = bus.subscribe(SparkTags.ShowFillsTag).ObserveOnDispatcher().Subscribe(show);
|
||||
_dispose = bus.subscribe(SparkTags.ShowFillsTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(StartupData state) =>
|
||||
|
||||
@@ -30,12 +30,11 @@ namespace bts.shell {
|
||||
_wm = wm;
|
||||
_settings_hub = settings_hub;
|
||||
_orders_model = orders_model;
|
||||
var window_props = Configuration.get<bts.config.IShellWindowProps>();
|
||||
_max_ladders_open = (int)(window_props?.ladder?.max_total_open ?? 75);
|
||||
_max_ladders_open = (int)(Configuration.get<bts.config.LadderKnobs>().max_open_ladders);
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
bus.subscribe(SparkTags.ShowLadderTag).ObserveOnDispatcher().Subscribe(s => show_windows(new[] { s })),
|
||||
bus.subscribe(SparkTags.ShowLaddersTag).ObserveOnDispatcher().Subscribe(show_windows)
|
||||
bus.subscribe(SparkTags.ShowLadderTag).OnDispatcher().Subscribe(s => show_windows(new[] { s })),
|
||||
bus.subscribe(SparkTags.ShowLaddersTag).OnDispatcher().Subscribe(show_windows)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,6 +76,7 @@ namespace bts.shell {
|
||||
s.ask_break_line = LadderSettings.Default.ask_break_line;
|
||||
if (s.mid_break_line == null)
|
||||
s.mid_break_line = LadderSettings.Default.mid_break_line;
|
||||
s.row_height = LadderRender.measure_row_height(s);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace bts.shell {
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
|
||||
_dispose = bus.subscribe(SparkTags.ShowOrderBookTag).ObserveOnDispatcher().Subscribe(show);
|
||||
_dispose = bus.subscribe(SparkTags.ShowOrderBookTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(OrderBookStartupData state) {
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace bts.shell {
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
|
||||
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).ObserveOnDispatcher().Subscribe(show);
|
||||
_dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace mbts.spark {
|
||||
var security_source = security_md_source.Select(smd => smd.security);
|
||||
//
|
||||
_security = backing(nameof(security), security_source, SecurityDataHelper.Defaults.SecurityImmutable);
|
||||
properties_vm = new OrderPropertiesEditorVM(model, security_source.ObserveOnDispatcher(), order_source.Select(ot => new InlineOrderProperties(model, ot.order.Def)));
|
||||
properties_vm = new OrderPropertiesEditorVM(model, security_source.OnDispatcher(), order_source.Select(ot => new InlineOrderProperties(model, ot.order.Def)));
|
||||
_price = backing(nameof(price), double.NaN);
|
||||
size_calculator = new CalculatorLogic(null, calculator_buttons);
|
||||
_current_price = backing(nameof(current_price), order_source.Select(ort => ort.order.State.Price));
|
||||
@@ -83,15 +83,15 @@ namespace mbts.spark {
|
||||
size_calculator,
|
||||
properties_vm,
|
||||
bbo = new BBOVM(security_md_source),
|
||||
order_source.ObserveOnDispatcher().Subscribe(ort => {
|
||||
order_source.OnDispatcher().Subscribe(ort => {
|
||||
key = ort.order.Key;
|
||||
if (DoubleHelper.IsNaNOrInfinity(price))
|
||||
price = ort.order.State.Price;
|
||||
if (!size_calculator.value.HasValue || !size_calculator.value_setter_called)
|
||||
size_calculator.bypass_value_setter(ort.order.State.Quantity);
|
||||
}),
|
||||
Observable.Merge(_price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty).ObserveOnDispatcher().Subscribe(_ => RaisePropertyChanged(nameof(dirty_items_text))),
|
||||
_price_dirty.observe_current(true).CombineLatest(_size_dirty.observe_current(true), (pd, sd) => pd || sd).DistinctUntilChanged().ObserveOnDispatcher().Subscribe(modify_order));
|
||||
Observable.Merge(_price_dirty, _size_dirty, _account_dirty, _time_in_force_dirty, _order_type_dirty).OnDispatcher().Subscribe(_ => RaisePropertyChanged(nameof(dirty_items_text))),
|
||||
_price_dirty.observe_current(true).CombineLatest(_size_dirty.observe_current(true), (pd, sd) => pd || sd).DistinctUntilChanged().OnDispatcher().Subscribe(modify_order));
|
||||
this.modify_order = modify_order;
|
||||
set_price_command = new DelegateCommand<double>(_price.set);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
namespace mbts.spark {
|
||||
class FillTuple : IEquatable<FillTuple> {
|
||||
public readonly FillImmutable fill;
|
||||
public readonly OrderRecordImmutable? order;
|
||||
public readonly SecurityImmutable? security;
|
||||
public readonly string key;
|
||||
public readonly SecurityMD? security_md;
|
||||
public readonly FillTupleKey key;
|
||||
|
||||
public FillTuple(FillImmutable fill, OrderRecordImmutable? order, SecurityImmutable? security) {
|
||||
public FillTuple(FillImmutable fill, OrderRecordImmutable? order, SecurityMD? security_md) {
|
||||
this.fill = fill;
|
||||
this.order = order;
|
||||
this.security = security;
|
||||
// a FillTuple that DOES NOT HAVE associated order/security needs to be distinguishable
|
||||
// from a FillTuple (of the same underlying FillImmutable) that DOES HAVE associated order/security
|
||||
key = fill.FillKey;
|
||||
if (!order.HasValue) key += "!o";
|
||||
if (!security.HasValue) key += "!s";
|
||||
this.security_md = security_md;
|
||||
key = new FillTupleKey(fill.FillKey, order.HasValue ? order.Value.Key : 0u, security_md.HasValue ? security_md.Value.security.Id : 0u);
|
||||
}
|
||||
|
||||
public bool Equals(FillTuple other) => other != null
|
||||
&& other.order.HasValue == order.HasValue
|
||||
&& other.security.HasValue == security.HasValue
|
||||
&& other.security_md.HasValue == security_md.HasValue
|
||||
&& other.fill.Equals(fill);
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as FillTuple);
|
||||
public override int GetHashCode() => key.GetHashCode();
|
||||
}
|
||||
|
||||
struct FillTupleKey : IEquatable<FillTupleKey> {
|
||||
public readonly string fill_key;
|
||||
public readonly uint order_key, security_key;
|
||||
|
||||
public FillTupleKey(string fill_key, uint order_key, uint security_key) {
|
||||
this.fill_key = fill_key;
|
||||
this.order_key = order_key;
|
||||
this.security_key = security_key;
|
||||
}
|
||||
|
||||
public bool Equals(FillTupleKey other) =>
|
||||
other.order_key == order_key &&
|
||||
other.security_key == security_key &&
|
||||
other.fill_key == fill_key;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is FillTupleKey k && Equals(k);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
fill_key.GetHashCode(),
|
||||
(int)order_key,
|
||||
(int)security_key);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using mbts.collections;
|
||||
using mbts.utils;
|
||||
@@ -7,23 +6,25 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
|
||||
namespace mbts.spark {
|
||||
class FillTupleSource : IDisposable {
|
||||
public static IObservable<ITableUpdateTransform<FillTuple, string>> create_observable(
|
||||
class FillTupleSource : IObservable<IIncrementalUpdate<FillTuple, FillTupleKey>>, IDisposable {
|
||||
// returns a ref-count Observable.Create-wrapper that initializes/destroys the [hot-observable] FillTupleSource.
|
||||
// (initializes when the first subscription is made, and destroys when the last subscription is disposed).
|
||||
public static IObservable<IIncrementalUpdate<FillTuple, FillTupleKey>> create_observable(
|
||||
IObservable<IReadOnlyDictionary<uint, SecurityMD>> securities,
|
||||
IObservable<TableUpdate<OrderRecordImmutable, uint>> orders,
|
||||
IObservable<TableUpdate<FillImmutable, string>> fills
|
||||
) {
|
||||
IObservable<TableUpdate<FillImmutable, string>> fills) {
|
||||
FillTupleSource source = null;
|
||||
int ref_count = 0;
|
||||
return Observable.Create<TableUpdateTransform<FillTuple, string>>(observer => {
|
||||
return Observable.Create<IIncrementalUpdate<FillTuple, FillTupleKey>>(observer => {
|
||||
if (Interlocked.Increment(ref ref_count) == 1) {
|
||||
SparkUtils.lazy_assert(source == null, () => fmt_assert_msg("source should be null, there must be a leak."));
|
||||
source = new FillTupleSource(securities, orders, fills);
|
||||
}
|
||||
IDisposable sub = source.fill_tuples.Subscribe(observer);
|
||||
IDisposable sub = source.Subscribe(observer);
|
||||
return Disposable.Create(() => {
|
||||
Disposable.dispose_once(ref sub);
|
||||
if (Interlocked.Decrement(ref ref_count) == 0)
|
||||
@@ -33,153 +34,157 @@ namespace mbts.spark {
|
||||
string fmt_assert_msg(string msg) => $"{nameof(FillTupleSource)}.{nameof(create_observable)}: {msg}";
|
||||
}
|
||||
|
||||
readonly Subject<IIncrementalUpdate<FillTuple, FillTupleKey>> _subject = new Subject<IIncrementalUpdate<FillTuple, FillTupleKey>>();
|
||||
readonly Dictionary<FillTupleKey, FillTuple> _state = new Dictionary<FillTupleKey, FillTuple>();
|
||||
readonly SetMap<uint, string> _order_fills = new SetMap<uint, string>();
|
||||
readonly SetMap<uint, string> _security_fills = new SetMap<uint, string>();
|
||||
IReadOnlyDictionary<string, FillImmutable> _fills_cache = new Dictionary<string, FillImmutable>(0);
|
||||
IReadOnlyDictionary<uint, OrderRecordImmutable> _orders_cache = new Dictionary<uint, OrderRecordImmutable>(0);
|
||||
IReadOnlyDictionary<uint, SecurityMD> _security_md_cache = new Dictionary<uint, SecurityMD>(0);
|
||||
// _n_write_ops should never be greater than 1, bc all 3 input observables fire 'OnDispatcher'
|
||||
int _n_read_ops, _n_write_ops;
|
||||
IDisposable _dispose;
|
||||
|
||||
FillTupleSource(
|
||||
IObservable<IReadOnlyDictionary<uint, SecurityMD>> securities,
|
||||
IObservable<TableUpdate<OrderRecordImmutable, uint>> orders,
|
||||
IObservable<TableUpdate<FillImmutable, string>> fills) {
|
||||
State state = new State();
|
||||
var src = Observable.FromEvent<Action<TableUpdateTransform<FillTuple, string>>, TableUpdateTransform<FillTuple, string>>(cb => _updated += cb, cb => _updated -= cb);
|
||||
fill_tuples = Observable.Create<TableUpdateTransform<FillTuple, string>>(observer => {
|
||||
var init_state = state.fill_tuples.state.ToDictionary();
|
||||
observer.OnNext(new TableUpdateTransform<FillTuple, string>(init_state, init_state.Values.ToList(), new List<FillTuple>(0)));
|
||||
return src.Subscribe(observer);
|
||||
});
|
||||
IObservable<TableUpdate<FillImmutable, string>> fills) =>
|
||||
//
|
||||
_dispose = Disposable.Create(
|
||||
securities.ObserveOnDispatcher().Subscribe(s => update(state.update_securities(s))),
|
||||
orders.ObserveOnDispatcher().Subscribe(o => update(state.update_orders(o))),
|
||||
fills.ObserveOnDispatcher().Subscribe(f => update(state.update_fills(f)))
|
||||
);
|
||||
void update(State new_state) {
|
||||
state = new_state;
|
||||
_updated?.Invoke(state.fill_tuples);
|
||||
securities.OnDispatcher().Subscribe(_update_securities),
|
||||
orders.OnDispatcher().Subscribe(_update_orders),
|
||||
fills.OnDispatcher().Subscribe(_update_fills));
|
||||
|
||||
public IDisposable Subscribe(IObserver<IIncrementalUpdate<FillTuple, FillTupleKey>> observer) {
|
||||
using (_read()) {
|
||||
observer.OnNext(new IncrementalUpdate<FillTuple, FillTupleKey>(new Dictionary<FillTupleKey, FillTuple>(_state), new Dictionary<FillTupleKey, FillTuple>(0)));
|
||||
return _subject.Subscribe(observer);
|
||||
}
|
||||
}
|
||||
|
||||
event Action<TableUpdateTransform<FillTuple, string>> _updated;
|
||||
public void Dispose() => Disposable.dispose(ref _dispose);
|
||||
|
||||
IObservable<TableUpdateTransform<FillTuple, string>> fill_tuples { get; }
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
|
||||
class State {
|
||||
readonly IReadOnlyDictionary<string, FillImmutable> _fills;
|
||||
readonly IReadOnlyDictionary<uint, OrderRecordImmutable> _orders;
|
||||
readonly IReadOnlyDictionary<uint, SecurityMD> _securities;
|
||||
readonly TableUpdateTransform<FillTuple, string> _fill_tuples;
|
||||
readonly SetMap<uint, string> _order_fills;
|
||||
readonly SetMap<uint, string> _security_fills;
|
||||
|
||||
public State() : this(
|
||||
new Dictionary<string, FillImmutable>(),
|
||||
new Dictionary<uint, OrderRecordImmutable>(),
|
||||
new Dictionary<uint, SecurityMD>(),
|
||||
new TableUpdateTransform<FillTuple, string>(new Dictionary<string, FillTuple>(), new List<FillTuple>(), new List<FillTuple>()),
|
||||
new SetMap<uint, string>(),
|
||||
new SetMap<uint, string>()
|
||||
) { }
|
||||
|
||||
State(
|
||||
IReadOnlyDictionary<string, FillImmutable> fills,
|
||||
IReadOnlyDictionary<uint, OrderRecordImmutable> orders,
|
||||
IReadOnlyDictionary<uint, SecurityMD> securities,
|
||||
TableUpdateTransform<FillTuple, string> fill_tuples,
|
||||
SetMap<uint, string> order_fill_tuples,
|
||||
SetMap<uint, string> security_fill_tuples
|
||||
) {
|
||||
_fills = fills;
|
||||
_orders = orders;
|
||||
_securities = securities;
|
||||
_fill_tuples = fill_tuples;
|
||||
_order_fills = order_fill_tuples;
|
||||
_security_fills = security_fill_tuples;
|
||||
void _update_fills(TableUpdate<FillImmutable, string> fills) {
|
||||
using (_write()) {
|
||||
Dictionary<FillTupleKey, FillTuple> removed = new Dictionary<FillTupleKey, FillTuple>(), added = new Dictionary<FillTupleKey, FillTuple>();
|
||||
foreach (FillImmutable fill in fills.removed) {
|
||||
FillTuple old_ft = new FillTuple(fill, _get_order(fill.OrderKey), _get_security_md(fill.SecurityKey));
|
||||
removed.Add(old_ft.key, old_ft);
|
||||
_security_fills.remove(fill.SecurityKey, fill.FillKey);
|
||||
_order_fills.remove(fill.OrderKey, fill.FillKey);
|
||||
}
|
||||
foreach (FillImmutable fill in fills.added) {
|
||||
FillTuple new_ft = new FillTuple(fill, _get_order(fill.OrderKey), _get_security_md(fill.SecurityKey));
|
||||
added.Add(new_ft.key, new_ft);
|
||||
_security_fills.add(fill.SecurityKey, fill.FillKey);
|
||||
_order_fills.add(fill.OrderKey, fill.FillKey);
|
||||
}
|
||||
_fills_cache = fills.state;
|
||||
_apply_changes(added, removed);
|
||||
}
|
||||
}
|
||||
|
||||
public TableUpdateTransform<FillTuple, string> fill_tuples => _fill_tuples;
|
||||
|
||||
public State update_fills(TableUpdate<FillImmutable, string> fills) {
|
||||
var removed = fills.removed.Select(get_fill_tuple).ToList();
|
||||
var added = fills.added.Select(get_fill_tuple).ToList();
|
||||
var order_fills = _order_fills.clone();
|
||||
var security_fills = _security_fills.clone();
|
||||
HashSet<string> added_keys = new HashSet<string>(fills.added.Select(f => f.FillKey)),
|
||||
removed_keys = new HashSet<string>(fills.removed.Select(f => f.FillKey));
|
||||
foreach (var ft in removed) {
|
||||
string k = ft.fill.FillKey;
|
||||
if (!added_keys.Contains(k)) {
|
||||
order_fills.remove(ft.fill.OrderKey, k);
|
||||
security_fills.remove(ft.fill.SecurityKey, k);
|
||||
void _update_orders(TableUpdate<OrderRecordImmutable, uint> orders) {
|
||||
using (_write()) {
|
||||
Dictionary<FillTupleKey, FillTuple> added = new Dictionary<FillTupleKey, FillTuple>(), removed = new Dictionary<FillTupleKey, FillTuple>();
|
||||
HashSet<uint> changed_keys = new HashSet<uint>();
|
||||
foreach (var o in orders.removed) {
|
||||
if (_order_fills.try_lookup(o.Key, out ISet<string> fill_keys)) {
|
||||
IEnumerable<FillImmutable> fills = fill_keys.Select(fk => _fills_cache[fk]);
|
||||
SecurityMD? sec_md = _get_security_md(o.security_key());
|
||||
foreach (FillImmutable fill in fills) {
|
||||
FillTuple old_ft = new FillTuple(fill, o, sec_md);
|
||||
removed.Add(old_ft.key, old_ft);
|
||||
}
|
||||
OrderRecordImmutable? curr_order = null;
|
||||
if (orders.try_get(o.Key, out OrderRecordImmutable newval)) {
|
||||
curr_order = newval;
|
||||
changed_keys.Add(o.Key);
|
||||
}
|
||||
foreach (FillImmutable fill in fills) {
|
||||
FillTuple new_ft = new FillTuple(fill, curr_order, sec_md);
|
||||
added.Add(new_ft.key, new_ft);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var ft in added) {
|
||||
string k = ft.fill.FillKey;
|
||||
if (!removed_keys.Contains(k)) {
|
||||
order_fills.add(ft.fill.OrderKey, k);
|
||||
security_fills.add(ft.fill.SecurityKey, k);
|
||||
foreach (var o in orders.added) {
|
||||
if (!changed_keys.Contains(o.Key) && _order_fills.try_lookup(o.Key, out ISet<string> fill_keys)) {
|
||||
IEnumerable<FillImmutable> fills = fill_keys.Select(fk => _fills_cache[fk]);
|
||||
SecurityMD? sec_md = _get_security_md(o.security_key());
|
||||
foreach (FillImmutable fill in fills) {
|
||||
FillTuple old_ft = new FillTuple(fill, null, sec_md);
|
||||
removed.Add(old_ft.key, old_ft);
|
||||
FillTuple new_ft = new FillTuple(fill, o, sec_md);
|
||||
added.Add(new_ft.key, new_ft);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new State(fills.state, _orders, _securities, _fill_tuples.update(removed, added, f => f.key), order_fills, security_fills);
|
||||
//
|
||||
FillTuple get_fill_tuple(FillImmutable fill) =>
|
||||
new FillTuple(fill, _orders.TryGetValue(fill.OrderKey, out OrderRecordImmutable o) ? o : new OrderRecordImmutable?(),
|
||||
_securities.TryGetValue(fill.SecurityKey, out SecurityMD s) ? s.security : new SecurityImmutable?());
|
||||
_orders_cache = orders.state;
|
||||
_apply_changes(added, removed);
|
||||
}
|
||||
}
|
||||
|
||||
public State update_orders(TableUpdate<OrderRecordImmutable, uint> orders) {
|
||||
List<FillTuple> added = new List<FillTuple>(), removed = new List<FillTuple>();
|
||||
orders.detect_changed_keys(o => o.Key, out HashSet<uint> removed_keys, out HashSet<uint> added_keys, out HashSet<uint> changed_keys);
|
||||
foreach (var k in removed_keys) {
|
||||
if (_order_fills.try_lookup(k, out ISet<string> fill_keys)) {
|
||||
removed.AddRange(get_fill_tuples(_orders[k], fill_keys));
|
||||
added.AddRange(get_fill_tuples(null, fill_keys));
|
||||
}
|
||||
}
|
||||
foreach (var k in added_keys) {
|
||||
if (_order_fills.try_lookup(k, out ISet<string> fill_keys)) {
|
||||
removed.AddRange(get_fill_tuples(null, fill_keys));
|
||||
added.AddRange(get_fill_tuples(orders.get(k), fill_keys));
|
||||
}
|
||||
}
|
||||
foreach (var k in changed_keys) {
|
||||
if (_order_fills.try_lookup(k, out ISet<string> fill_keys)) {
|
||||
removed.AddRange(get_fill_tuples(_orders[k], fill_keys));
|
||||
added.AddRange(get_fill_tuples(orders.get(k), fill_keys));
|
||||
}
|
||||
}
|
||||
return new State(_fills, orders.state, _securities, _fill_tuples.update(removed, added, ft => ft.key), _order_fills, _security_fills);
|
||||
//
|
||||
IEnumerable<FillTuple> get_fill_tuples(OrderRecordImmutable? o, ISet<string> fill_keys) =>
|
||||
fill_keys.Select(k => {
|
||||
var fill = _fills[k];
|
||||
return new FillTuple(fill, o, _securities.TryGetValue(fill.SecurityKey, out SecurityMD s) ? s.security : new SecurityImmutable?());
|
||||
});
|
||||
}
|
||||
|
||||
public State update_securities(IReadOnlyDictionary<uint, SecurityMD> securities) {
|
||||
List<FillTuple> added = new List<FillTuple>(), removed = new List<FillTuple>();
|
||||
_securities.Keys.difference(securities.Keys, out IEnumerable<uint> added_keys, out IEnumerable<uint> removed_keys);
|
||||
void _update_securities(IReadOnlyDictionary<uint, SecurityMD> securities) {
|
||||
using (_write()) {
|
||||
Dictionary<FillTupleKey, FillTuple> added = new Dictionary<FillTupleKey, FillTuple>(), removed = new Dictionary<FillTupleKey, FillTuple>();
|
||||
_security_md_cache.Keys.difference(securities.Keys, out IEnumerable<uint> added_keys, out IEnumerable<uint> removed_keys);
|
||||
foreach (var k in removed_keys) {
|
||||
if (_security_fills.try_lookup(k, out ISet<string> fill_keys)) {
|
||||
removed.AddRange(get_fill_tuples(_securities[k].security, fill_keys));
|
||||
added.AddRange(get_fill_tuples(null, fill_keys));
|
||||
IEnumerable<FillImmutable> fills = fill_keys.Select(fk => _fills_cache[fk]);
|
||||
SecurityMD old_sec_md = _security_md_cache[k];
|
||||
foreach (FillImmutable fill in fills) {
|
||||
OrderRecordImmutable? order = _get_order(fill.OrderKey);
|
||||
FillTuple old_ft = new FillTuple(fill, order, old_sec_md);
|
||||
removed.Add(old_ft.key, old_ft);
|
||||
FillTuple new_ft = new FillTuple(fill, order, null);
|
||||
added.Add(new_ft.key, new_ft);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var k in added_keys) {
|
||||
if (_security_fills.try_lookup(k, out ISet<string> fill_keys)) {
|
||||
removed.AddRange(get_fill_tuples(null, fill_keys));
|
||||
added.AddRange(get_fill_tuples(securities[k].security, fill_keys));
|
||||
IEnumerable<FillImmutable> fills = fill_keys.Select(fk => _fills_cache[fk]);
|
||||
SecurityMD new_sec_md = securities[k];
|
||||
foreach (FillImmutable fill in fills) {
|
||||
OrderRecordImmutable? order = _get_order(fill.OrderKey);
|
||||
FillTuple old_ft = new FillTuple(fill, order, null);
|
||||
removed.Add(old_ft.key, old_ft);
|
||||
FillTuple new_ft = new FillTuple(fill, order, new_sec_md);
|
||||
added.Add(new_ft.key, new_ft);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new State(_fills, _orders, securities, _fill_tuples.update(removed, added, f => f.key), _order_fills, _security_fills);
|
||||
//
|
||||
IEnumerable<FillTuple> get_fill_tuples(SecurityImmutable? s, ISet<string> fill_keys) =>
|
||||
fill_keys.Select(k => {
|
||||
var fill = _fills[k];
|
||||
return new FillTuple(fill, _orders.TryGetValue(fill.OrderKey, out OrderRecordImmutable o) ? o : new OrderRecordImmutable?(), s);
|
||||
});
|
||||
_security_md_cache = securities;
|
||||
_apply_changes(added, removed);
|
||||
}
|
||||
}
|
||||
|
||||
SecurityMD? _get_security_md(uint key) =>
|
||||
_security_md_cache.TryGetValue(key, out SecurityMD smd) ? smd : new SecurityMD?();
|
||||
|
||||
OrderRecordImmutable? _get_order(uint key) =>
|
||||
_orders_cache.TryGetValue(key, out OrderRecordImmutable o) ? o : new OrderRecordImmutable?();
|
||||
|
||||
void _apply_changes(Dictionary<FillTupleKey, FillTuple> added, Dictionary<FillTupleKey, FillTuple> removed) {
|
||||
foreach (var kv in removed)
|
||||
_state.Remove(kv.Key);
|
||||
foreach (var kv in added)
|
||||
_state.Add(kv.Key, kv.Value);
|
||||
_subject.OnNext(new IncrementalUpdate<FillTuple, FillTupleKey>(added, removed));
|
||||
}
|
||||
|
||||
IDisposable _read() {
|
||||
while (_n_write_ops > 0)
|
||||
Thread.Yield();
|
||||
Interlocked.Increment(ref _n_read_ops);
|
||||
return Disposable.Create(() => Interlocked.Decrement(ref _n_read_ops));
|
||||
}
|
||||
|
||||
IDisposable _write() {
|
||||
while (_n_read_ops > 0)
|
||||
Thread.Yield();
|
||||
Interlocked.Increment(ref _n_write_ops);
|
||||
return Disposable.Create(() => Interlocked.Decrement(ref _n_write_ops));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ namespace mbts.spark {
|
||||
set_price_command = new DelegateCommand<double>(set_price);
|
||||
_dispose = Disposable.Create(
|
||||
size_calculator = new CalculatorLogic(0, calculator_buttons),
|
||||
props_source.ObserveOnDispatcher().Subscribe(p => on_props(p.price, p.size))
|
||||
props_source.OnDispatcher().Subscribe(p => on_props(p.price, p.size))
|
||||
);
|
||||
|
||||
void on_props(InlineUserProperty<double> price_prop, InlineUserProperty<uint> size_prop) {
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace bts.spark {
|
||||
(var security, var spec) = get_security(neworder.Def.SecurityKey);
|
||||
//
|
||||
// description
|
||||
var mod_desc = modified_description(oldorder, neworder, spec.Value);
|
||||
var mod_desc = modified_description(oldorder, neworder, spec);
|
||||
if (!string.IsNullOrEmpty(mod_desc)) {
|
||||
log_status(ActivityLogCategory.modify, security, mod_desc);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ namespace mbts.ui.spark {
|
||||
public DataTemplate StopLimitOrderTemplate { get; set; }
|
||||
public DataTemplate StopMarketOrderTemplate { get; set; }
|
||||
public DataTemplate TimeBoxOrderTemplate { get; set; }
|
||||
public DataTemplate TickOrderTemplate { get; set; }
|
||||
public DataTemplate LeanOrderTemplate { get; set; }
|
||||
public DataTemplate JoinOrderTemplate { get; set; }
|
||||
public DataTemplate TopOrderTemplate { get; set; }
|
||||
public DataTemplate IcebergOrderTemplate { get; set; }
|
||||
public DataTemplate SpreaderTemplate { get; set; }
|
||||
public DataTemplate BTSStopOrderTemplate { get; set; }
|
||||
@@ -33,6 +37,10 @@ namespace mbts.ui.spark {
|
||||
case CustomOrderDefinition.CustomOneofCase.Stop: return StopMarketOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.StopLimit: return StopLimitOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.TimeBox: return TimeBoxOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.Tick: return TickOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.Lean: return LeanOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.Join: return JoinOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.Top: return TopOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.Iceberg: return IcebergOrderTemplate;
|
||||
case CustomOrderDefinition.CustomOneofCase.Spreader: return SpreaderTemplate;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace mbts.spark {
|
||||
|
||||
public OrderPropertiesEditorVM(OrdersModel model, IObservable<SecurityImmutable> security_source, IObservable<InlineOrderProperties> order_properties) {
|
||||
_model = model;
|
||||
_security = backing(nameof(security), security_source.ObserveOnDispatcher(), SecurityDataHelper.Defaults.SecurityImmutable);
|
||||
_security = backing(nameof(security), security_source.OnDispatcher(), SecurityDataHelper.Defaults.SecurityImmutable);
|
||||
// guarentee that "this.security" will always be set before observers are notified
|
||||
// this is important bc some observers will reference "this.security" in their callbacks
|
||||
// - namely the [SelectorProperty]order_type's "get_default" function,
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace mbts.spark {
|
||||
: base(model) {
|
||||
_security_source = security_source;
|
||||
_dispose = Disposable.Create(
|
||||
security_source.ObserveOnDispatcher()
|
||||
security_source.OnDispatcher()
|
||||
.Subscribe(s => _security = s),
|
||||
inline_properties.ObserveOnDispatcher()
|
||||
inline_properties.OnDispatcher()
|
||||
.Subscribe(p => {
|
||||
if (_order_types.Count == 0 || p.child_order_types.force_override) {
|
||||
_order_types.SetItems(p.child_order_types.value);
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using static bts.api.orderman.CustomOrderDefinition;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
static partial class OrderTypeVMFactory
|
||||
{
|
||||
class InlineJoinOrderProperties
|
||||
{
|
||||
public InlineJoinOrderProperties (CustomOneofCase selected_child_order_type, IReadOnlyCollection<CustomOneofCase> child_order_types)
|
||||
{
|
||||
ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types");
|
||||
this.selected_child_order_type = new InlineUserProperty<CustomOneofCase>(selected_child_order_type, true);
|
||||
this.child_order_types = new InlineUserProperty<IReadOnlyCollection<CustomOneofCase>>(child_order_types, true);
|
||||
|
||||
var hedgetype = CustomOneofCase.Lean;
|
||||
if(!child_order_types.Contains(hedgetype))
|
||||
hedgetype = CustomOneofCase.Limit;
|
||||
|
||||
//ExceptionAssert.InvalidOperation.That(child_order_types.Contains(hedgetype), $"Hedge order type {hedgetype} is not in hedge_order_types");
|
||||
this.selected_hedge_order_type = new InlineUserProperty<CustomOneofCase>(hedgetype, true);
|
||||
this.hedge_order_types = new InlineUserProperty<IReadOnlyCollection<CustomOneofCase>>(child_order_types, true);
|
||||
|
||||
tick_offset_on_deploy = new InlineUserProperty<uint>(0, false);
|
||||
join_threshold = new InlineUserProperty<bts.spark.QuantityMeasure>(
|
||||
new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1}, false);
|
||||
enable_hedge = new InlineUserProperty<bool>(false, false);
|
||||
hedge_ticks = new InlineUserProperty<int>(1, false);
|
||||
}
|
||||
public readonly InlineUserProperty<CustomOneofCase> selected_child_order_type;
|
||||
public readonly InlineUserProperty<IReadOnlyCollection<CustomOneofCase>> child_order_types;
|
||||
public readonly InlineUserProperty<CustomOneofCase> selected_hedge_order_type;
|
||||
public readonly InlineUserProperty<IReadOnlyCollection<CustomOneofCase>> hedge_order_types;
|
||||
|
||||
|
||||
public readonly InlineUserProperty<uint> tick_offset_on_deploy;
|
||||
public readonly InlineUserProperty<bts.spark.QuantityMeasure> join_threshold;
|
||||
|
||||
public readonly InlineUserProperty<bool> enable_hedge;
|
||||
public readonly InlineUserProperty<int> hedge_ticks;
|
||||
|
||||
}
|
||||
|
||||
class JoinOrderVM : OrderTypeVMBase<JoinOrderDefinition>, IHierarchicalOrderTypeVM, IDisposable
|
||||
{
|
||||
readonly IObservable<SecurityImmutable> _security_source;
|
||||
SecurityImmutable? _security;
|
||||
ObservableCollection<CustomOneofCase> _order_types = new ObservableCollection<CustomOneofCase>();
|
||||
ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase> _child_order_choices;
|
||||
InlineJoinOrderProperties _inline_props;
|
||||
IOrderTypeVM _child_order;
|
||||
IDisposable _dispose, _child_order_errors_sub;
|
||||
|
||||
ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase> _hedge_order_choices;
|
||||
IOrderTypeVM _hedge_order;
|
||||
IDisposable _hedge_order_errors_sub;
|
||||
|
||||
public uint tick_offset_on_deploy { get; set; } = 0;
|
||||
public bts.spark.QuantityMeasure join_threshold { get; set; } = new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1 };
|
||||
bool _enable_hedge = false;
|
||||
public bool enable_hedge
|
||||
{
|
||||
get { return _enable_hedge; }
|
||||
set
|
||||
{
|
||||
_enable_hedge = value;
|
||||
RaisePropertyChanged(nameof(enable_hedge));
|
||||
}
|
||||
}
|
||||
public int hedge_ticks { get; set; } = 1;
|
||||
|
||||
public JoinOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source)
|
||||
: this(model, security_source, security_source.Select(s =>
|
||||
new InlineJoinOrderProperties(model.factory.default_order_type(s), model.factory.default_order_types(s).ToList()))) { }
|
||||
|
||||
public JoinOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source,
|
||||
IObservable<InlineJoinOrderProperties> inline_properties) : base(model)
|
||||
{
|
||||
_security_source = security_source;
|
||||
_dispose = Disposable.Create(
|
||||
security_source.OnDispatcher()
|
||||
.Subscribe(s => _security = s),
|
||||
inline_properties.OnDispatcher()
|
||||
.Subscribe(p => {
|
||||
if (_order_types.Count == 0 || p.child_order_types.force_override)
|
||||
{
|
||||
_order_types.SetItems(p.child_order_types.value);
|
||||
}
|
||||
if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override))
|
||||
{
|
||||
_select_child_order(p.selected_child_order_type.value);
|
||||
}
|
||||
if (_hedge_order_choices != null && (_hedge_order == null || p.selected_hedge_order_type.force_override))
|
||||
{
|
||||
_select_hedge_order(p.selected_hedge_order_type.value);
|
||||
}
|
||||
if (p.tick_offset_on_deploy.force_override)
|
||||
{
|
||||
tick_offset_on_deploy = p.tick_offset_on_deploy.value;
|
||||
RaisePropertyChanged(nameof(tick_offset_on_deploy));
|
||||
}
|
||||
if (p.join_threshold.force_override)
|
||||
{
|
||||
join_threshold = p.join_threshold.value;
|
||||
RaisePropertyChanged(nameof(join_threshold));
|
||||
}
|
||||
if (p.enable_hedge.force_override)
|
||||
{
|
||||
enable_hedge = p.enable_hedge.value;
|
||||
RaisePropertyChanged(nameof(enable_hedge));
|
||||
}
|
||||
if (p.hedge_ticks.force_override)
|
||||
{
|
||||
hedge_ticks = p.hedge_ticks.value;
|
||||
RaisePropertyChanged(nameof(hedge_ticks));
|
||||
}
|
||||
_inline_props = p;
|
||||
_validate();
|
||||
})
|
||||
);
|
||||
_validate();
|
||||
}
|
||||
|
||||
public override void assign_to(CustomOrderDefinition value)
|
||||
{
|
||||
if (_child_order == null)
|
||||
_select_child_order(model.factory.default_order_type(_security.Value, true));
|
||||
if (_hedge_order == null && enable_hedge)
|
||||
_select_hedge_order(model.factory.default_order_type(_security.Value, true));
|
||||
_validate();
|
||||
if (!HasErrors)
|
||||
{
|
||||
var i = factory.join_order();
|
||||
i.TickOffsetOnDeploy = tick_offset_on_deploy;
|
||||
i.JoinThreshold = join_threshold;
|
||||
i.EnableHedge = enable_hedge;
|
||||
i.HedgeTicks = hedge_ticks;
|
||||
child_order.assign_to(i.Child);
|
||||
if(enable_hedge && hedge_order != null)
|
||||
hedge_order.assign_to(i.Hedge);
|
||||
value.Join = i;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<IOrderTypeVM> child_order_types
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_child_order_choices == null)
|
||||
_child_order_choices = new ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase>(_order_types, ot => create(ot, model, _security_source));
|
||||
return _child_order_choices;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<IOrderTypeVM> hedge_order_types
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hedge_order_choices == null)
|
||||
_hedge_order_choices = new ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase>(_order_types, ot => create(ot, model, _security_source));
|
||||
return _hedge_order_choices;
|
||||
}
|
||||
}
|
||||
|
||||
public IOrderTypeVM child_order
|
||||
{
|
||||
get
|
||||
{
|
||||
init();
|
||||
return _child_order;
|
||||
}
|
||||
set
|
||||
{
|
||||
Disposable.dispose(ref _child_order_errors_sub);
|
||||
_child_order = value;
|
||||
_child_order?.init();
|
||||
if (_child_order is INotifyDataErrorInfo ndei)
|
||||
{
|
||||
ndei.ErrorsChanged += on_errors_changed;
|
||||
void on_errors_changed(object s, DataErrorsChangedEventArgs e) =>
|
||||
_validate();
|
||||
_child_order_errors_sub = Disposable.Create(() => ndei.ErrorsChanged -= on_errors_changed);
|
||||
}
|
||||
_validate();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
}
|
||||
|
||||
public IOrderTypeVM hedge_order
|
||||
{
|
||||
get
|
||||
{
|
||||
init();
|
||||
return _hedge_order;
|
||||
}
|
||||
set
|
||||
{
|
||||
Disposable.dispose(ref _hedge_order_errors_sub);
|
||||
_hedge_order = value;
|
||||
_hedge_order?.init();
|
||||
if (_hedge_order is INotifyDataErrorInfo ndei)
|
||||
{
|
||||
ndei.ErrorsChanged += on_errors_changed;
|
||||
void on_errors_changed(object s, DataErrorsChangedEventArgs e) =>
|
||||
_validate();
|
||||
_hedge_order_errors_sub = Disposable.Create(() => ndei.ErrorsChanged -= on_errors_changed);
|
||||
}
|
||||
_validate();
|
||||
RaisePropertyChanged(nameof(hedge_order));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposable.dispose(ref _dispose);
|
||||
if (_child_order_choices != null)
|
||||
_child_order_choices.OfType<IDisposable>().DisposeAll();
|
||||
if (_hedge_order_choices != null)
|
||||
_hedge_order_choices.OfType<IDisposable>().DisposeAll();
|
||||
}
|
||||
|
||||
public override void init()
|
||||
{
|
||||
if (_child_order == null && _security.HasValue)
|
||||
_select_child_order();
|
||||
if (_hedge_order == null && _security.HasValue && enable_hedge)
|
||||
_select_hedge_order();
|
||||
_validate();
|
||||
}
|
||||
|
||||
void _select_child_order()
|
||||
{
|
||||
CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ?
|
||||
_inline_props.selected_child_order_type.value :
|
||||
model.factory.default_order_type(_security.Value, true);
|
||||
_select_child_order(ot);
|
||||
}
|
||||
|
||||
void _select_child_order(CustomOneofCase ot)
|
||||
{
|
||||
_child_order = child_order_types.FirstOrDefault(vm => vm.order_type == ot);
|
||||
if (_child_order == null)
|
||||
_child_order = child_order_types.FirstOrDefault();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
|
||||
void _select_hedge_order()
|
||||
{
|
||||
CustomOneofCase ot = _inline_props != null && _inline_props.selected_hedge_order_type.has_value ?
|
||||
_inline_props.selected_hedge_order_type.value :
|
||||
model.factory.default_order_type(_security.Value, true);
|
||||
_select_hedge_order(ot);
|
||||
}
|
||||
|
||||
void _select_hedge_order(CustomOneofCase ot)
|
||||
{
|
||||
_hedge_order = hedge_order_types.FirstOrDefault(vm => vm.order_type == ot);
|
||||
if (_hedge_order == null)
|
||||
_hedge_order = hedge_order_types.FirstOrDefault();
|
||||
RaisePropertyChanged(nameof(hedge_order));
|
||||
}
|
||||
|
||||
|
||||
void _validate()
|
||||
{
|
||||
if (_child_order == null || (_child_order is INotifyDataErrorInfo ndei && ndei.HasErrors))
|
||||
set_error(nameof(child_order), string.Empty, true);
|
||||
else clear_error(nameof(child_order));
|
||||
|
||||
if ( (_hedge_order == null && enable_hedge) || (_hedge_order is INotifyDataErrorInfo hdei && hdei.HasErrors))
|
||||
set_error(nameof(hedge_order), string.Empty, true);
|
||||
else clear_error(nameof(hedge_order));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using static bts.api.orderman.CustomOrderDefinition;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
static partial class OrderTypeVMFactory
|
||||
{
|
||||
class InlineLeanOrderProperties
|
||||
{
|
||||
public InlineLeanOrderProperties (CustomOneofCase selected_child_order_type, IReadOnlyCollection<CustomOneofCase> child_order_types)
|
||||
{
|
||||
ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types");
|
||||
this.selected_child_order_type = new InlineUserProperty<CustomOneofCase>(selected_child_order_type, true);
|
||||
this.child_order_types = new InlineUserProperty<IReadOnlyCollection<CustomOneofCase>>(child_order_types, true);
|
||||
cross_threshold = new InlineUserProperty<bts.spark.QuantityMeasure>(
|
||||
new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1}, false);
|
||||
max_chase_ticks = new InlineUserProperty<uint>(10, false);
|
||||
}
|
||||
public readonly InlineUserProperty<CustomOneofCase> selected_child_order_type;
|
||||
public readonly InlineUserProperty<IReadOnlyCollection<CustomOneofCase>> child_order_types;
|
||||
|
||||
public readonly InlineUserProperty<bts.spark.QuantityMeasure> cross_threshold;
|
||||
public readonly InlineUserProperty<uint> max_chase_ticks;
|
||||
}
|
||||
|
||||
class LeanOrderVM : OrderTypeVMBase<LeanOrderDefinition>, IHierarchicalOrderTypeVM, IDisposable
|
||||
{
|
||||
readonly IObservable<SecurityImmutable> _security_source;
|
||||
SecurityImmutable? _security;
|
||||
ObservableCollection<CustomOneofCase> _order_types = new ObservableCollection<CustomOneofCase>();
|
||||
ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase> _child_order_choices;
|
||||
InlineLeanOrderProperties _inline_props;
|
||||
IOrderTypeVM _child_order;
|
||||
IDisposable _dispose, _child_order_errors_sub;
|
||||
|
||||
public bts.spark.QuantityMeasure cross_threshold { get; set; } = new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1 };
|
||||
public uint max_chase_ticks { get; set; } = 10;
|
||||
|
||||
public LeanOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source)
|
||||
: this(model, security_source, security_source.Select(s =>
|
||||
new InlineLeanOrderProperties(model.factory.default_order_type(s), model.factory.default_order_types(s).ToList()))) { }
|
||||
|
||||
public LeanOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source,
|
||||
IObservable<InlineLeanOrderProperties> inline_properties) : base(model)
|
||||
{
|
||||
_security_source = security_source;
|
||||
_dispose = Disposable.Create(
|
||||
security_source.OnDispatcher()
|
||||
.Subscribe(s => _security = s),
|
||||
inline_properties.OnDispatcher()
|
||||
.Subscribe(p => {
|
||||
if (_order_types.Count == 0 || p.child_order_types.force_override)
|
||||
{
|
||||
_order_types.SetItems(p.child_order_types.value);
|
||||
}
|
||||
if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override))
|
||||
{
|
||||
_select_child_order(p.selected_child_order_type.value);
|
||||
}
|
||||
if (p.cross_threshold.force_override)
|
||||
{
|
||||
cross_threshold = p.cross_threshold.value;
|
||||
RaisePropertyChanged(nameof(cross_threshold));
|
||||
}
|
||||
if (p.max_chase_ticks.force_override)
|
||||
{
|
||||
max_chase_ticks = p.max_chase_ticks.value;
|
||||
RaisePropertyChanged(nameof(max_chase_ticks));
|
||||
}
|
||||
_inline_props = p;
|
||||
_validate();
|
||||
})
|
||||
);
|
||||
_validate();
|
||||
}
|
||||
|
||||
public override void assign_to(CustomOrderDefinition value)
|
||||
{
|
||||
if (_child_order == null)
|
||||
_select_child_order(model.factory.default_order_type(_security.Value, true));
|
||||
_validate();
|
||||
if (!HasErrors)
|
||||
{
|
||||
var i = factory.lean_order();
|
||||
i.CrossThreshold = cross_threshold;
|
||||
i.MaxChaseTicks = max_chase_ticks;
|
||||
child_order.assign_to(i.Child);
|
||||
value.Lean = i;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<IOrderTypeVM> child_order_types
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_child_order_choices == null)
|
||||
_child_order_choices = new ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase>(_order_types, ot => create(ot, model, _security_source));
|
||||
return _child_order_choices;
|
||||
}
|
||||
}
|
||||
|
||||
public IOrderTypeVM child_order
|
||||
{
|
||||
get
|
||||
{
|
||||
init();
|
||||
return _child_order;
|
||||
}
|
||||
set
|
||||
{
|
||||
Disposable.dispose(ref _child_order_errors_sub);
|
||||
_child_order = value;
|
||||
_child_order?.init();
|
||||
if (_child_order is INotifyDataErrorInfo ndei)
|
||||
{
|
||||
ndei.ErrorsChanged += on_errors_changed;
|
||||
void on_errors_changed(object s, DataErrorsChangedEventArgs e) =>
|
||||
_validate();
|
||||
_child_order_errors_sub = Disposable.Create(() => ndei.ErrorsChanged -= on_errors_changed);
|
||||
}
|
||||
_validate();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposable.dispose(ref _dispose);
|
||||
if (_child_order_choices != null)
|
||||
_child_order_choices.OfType<IDisposable>().DisposeAll();
|
||||
}
|
||||
|
||||
public override void init()
|
||||
{
|
||||
if (_child_order == null && _security.HasValue)
|
||||
_select_child_order();
|
||||
_validate();
|
||||
}
|
||||
|
||||
void _select_child_order()
|
||||
{
|
||||
CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ?
|
||||
_inline_props.selected_child_order_type.value :
|
||||
model.factory.default_order_type(_security.Value, true);
|
||||
_select_child_order(ot);
|
||||
}
|
||||
|
||||
void _select_child_order(CustomOneofCase ot)
|
||||
{
|
||||
_child_order = child_order_types.FirstOrDefault(vm => vm.order_type == ot);
|
||||
if (_child_order == null)
|
||||
_child_order = child_order_types.FirstOrDefault();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
|
||||
void _validate()
|
||||
{
|
||||
if (_child_order == null || (_child_order is INotifyDataErrorInfo ndei && ndei.HasErrors))
|
||||
set_error(nameof(child_order), string.Empty, true);
|
||||
else clear_error(nameof(child_order));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@ namespace mbts.spark {
|
||||
public static IOrderTypeVM stop(OrdersModel model) => new StopOrderVM(model);
|
||||
public static IOrderTypeVM stop_limit(OrdersModel model) => new StopLimitOrderVM(model);
|
||||
public static IOrderTypeVM time_box(OrdersModel model, IObservable<SecurityImmutable> security_source) => new TimeBoxOrderVM(model, security_source);
|
||||
public static IOrderTypeVM tick(OrdersModel model, IObservable<SecurityImmutable> security_source) => new TickOrderVM(model, security_source);
|
||||
public static IOrderTypeVM lean(OrdersModel model, IObservable<SecurityImmutable> security_source) => new LeanOrderVM(model, security_source);
|
||||
public static IOrderTypeVM join(OrdersModel model, IObservable<SecurityImmutable> security_source) => new JoinOrderVM(model, security_source);
|
||||
public static IOrderTypeVM top(OrdersModel model, IObservable<SecurityImmutable> security_source) => new TopOrderVM(model, security_source);
|
||||
public static IOrderTypeVM iceberg(OrdersModel model, IObservable<SecurityImmutable> security_source) => new IcebergOrderVM(model, security_source);
|
||||
public static IOrderTypeVM spreader(OrdersModel model, IObservable<SecurityImmutable> security_source) => new SpreaderOrderVM(model, security_source);
|
||||
|
||||
@@ -27,6 +31,10 @@ namespace mbts.spark {
|
||||
case CustomOneofCase.Stop: return stop(model);
|
||||
case CustomOneofCase.StopLimit: return stop_limit(model);
|
||||
case CustomOneofCase.TimeBox: return time_box(model, security_source);
|
||||
case CustomOneofCase.Tick: return tick(model, security_source);
|
||||
case CustomOneofCase.Lean: return lean(model, security_source);
|
||||
case CustomOneofCase.Join: return join(model, security_source);
|
||||
case CustomOneofCase.Top: return top(model, security_source);
|
||||
case CustomOneofCase.Iceberg: return iceberg(model, security_source);
|
||||
case CustomOneofCase.Spreader: return spreader(model, security_source);
|
||||
default: throw new NotImplementedException();
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace mbts.spark {
|
||||
static partial class OrderTypeVMFactory {
|
||||
private class SpreaderOrderVM : OrderTypeVMBase<SpreaderOrderDefinition>, IOrderTypeVM, IDisposable {
|
||||
readonly Property<SpreaderOrderDefinitionImmutable?> _template;
|
||||
readonly StrategyEditorWrapper _strategy_editor_wrapper;
|
||||
IDisposable _dispose;
|
||||
SyntheticEditorVM _strategy_editor;
|
||||
|
||||
public SpreaderOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source)
|
||||
: this(model, security_source, security_source.SelectMany(s => model.spreader.security_strategy(s.Id)
|
||||
@@ -26,15 +28,15 @@ namespace mbts.spark {
|
||||
IObservable<InlineUserProperty<SpreaderOrderDefinitionImmutable>> order_template_source)
|
||||
: base(model) {
|
||||
_template = backing(nameof(template), new SpreaderOrderDefinitionImmutable?());
|
||||
_strategy_editor_wrapper = new StrategyEditorWrapper(this);
|
||||
_dispose = Disposable.Create(
|
||||
_template.ObserveOnDispatcher().Subscribe(vals => {
|
||||
_strategy_editor_wrapper,
|
||||
_template.OnDispatcher().Subscribe(vals => {
|
||||
_validate();
|
||||
}),
|
||||
order_template_source.ObserveOnDispatcher().Subscribe(prop => {
|
||||
order_template_source.OnDispatcher().Subscribe(prop => {
|
||||
if (prop.force_override || (!template.HasValue && prop.has_value)) {
|
||||
_template.Value = prop.value;
|
||||
if (_strategy_editor != null)
|
||||
_strategy_editor.set_synthetic(prop.value.Security, prop.value.ExecutionStrategy);
|
||||
_validate();
|
||||
}
|
||||
})
|
||||
@@ -43,23 +45,14 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public SpreaderOrderDefinitionImmutable? template => _template.Value;
|
||||
|
||||
public SyntheticEditorVM strategy_editor {
|
||||
get {
|
||||
if (_strategy_editor == null && template.HasValue) {
|
||||
var def = template.Value;
|
||||
_strategy_editor = new SyntheticEditorVM(model.spreader, def.Security, def.ExecutionStrategy, model.security_has_working_orders(def.Security.Id));
|
||||
}
|
||||
return _strategy_editor;
|
||||
}
|
||||
}
|
||||
|
||||
public SyntheticEditorVM strategy_editor => _strategy_editor_wrapper.vm;
|
||||
public IEnumerable<DialogSkirtItem> skirt_items => _strategy_editor_wrapper.skirt_items;
|
||||
public override bool editable_properties_open {
|
||||
get => base.editable_properties_open;
|
||||
set {
|
||||
if (!value && template.HasValue && _strategy_editor != null) {
|
||||
if (!value && template.HasValue && strategy_editor != null) {
|
||||
var mutable_template = template.Value.CloneToMutable();
|
||||
mutable_template.ExecutionStrategy = _strategy_editor.get_strategy().CloneToMutable();
|
||||
mutable_template.ExecutionStrategy = _strategy_editor_wrapper.vm.get_strategy().CloneToMutable();
|
||||
_template.Value = new SpreaderOrderDefinitionImmutable(mutable_template);
|
||||
}
|
||||
base.editable_properties_open = value;
|
||||
@@ -80,6 +73,39 @@ namespace mbts.spark {
|
||||
set_error(nameof(template), "Waiting for security and/or strategy.", true);
|
||||
else clear_error(nameof(template));
|
||||
}
|
||||
|
||||
// lazily initializes SyntheticEditorVM and associated dialog skirt items
|
||||
class StrategyEditorWrapper : IDisposable {
|
||||
readonly SpreaderOrderVM _owner;
|
||||
SyntheticEditorVM _vm;
|
||||
DialogSkirtItem _save_strategy_item;
|
||||
|
||||
public StrategyEditorWrapper(SpreaderOrderVM owner) {
|
||||
_owner = owner;
|
||||
_owner._template.Changed += _order_template_changed;
|
||||
}
|
||||
|
||||
public SyntheticEditorVM vm { get { _init(); return _vm; } }
|
||||
public IEnumerable<DialogSkirtItem> skirt_items { get { _init(); yield return _save_strategy_item; } }
|
||||
|
||||
public void Dispose() {
|
||||
Disposable.dispose(ref _vm);
|
||||
_owner._template.Changed -= _order_template_changed;
|
||||
}
|
||||
|
||||
void _init() {
|
||||
if (_vm == null && _owner.template.HasValue) {
|
||||
var def = _owner.template.Value;
|
||||
_vm = new SyntheticEditorVM(_owner.model.spreader, def.Security, def.ExecutionStrategy, _owner.model.security_has_working_orders(def.Security.Id));
|
||||
_save_strategy_item = new DialogSkirtItem("Save Strategy", _vm.commit);
|
||||
}
|
||||
}
|
||||
|
||||
void _order_template_changed(SpreaderOrderDefinitionImmutable? oldval, SpreaderOrderDefinitionImmutable? newval) {
|
||||
if (_vm != null && newval.HasValue)
|
||||
_vm.set_synthetic(newval.Value.Security, newval.Value.ExecutionStrategy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using static bts.api.orderman.CustomOrderDefinition;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
static partial class OrderTypeVMFactory
|
||||
{
|
||||
class InlineTickOrderProperties
|
||||
{
|
||||
public InlineTickOrderProperties (CustomOneofCase selected_child_order_type, IReadOnlyCollection<CustomOneofCase> child_order_types)
|
||||
{
|
||||
ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types");
|
||||
this.selected_child_order_type = new InlineUserProperty<CustomOneofCase>(selected_child_order_type, true);
|
||||
this.child_order_types = new InlineUserProperty<IReadOnlyCollection<CustomOneofCase>>(child_order_types, true);
|
||||
tick_offset_on_deploy = new InlineUserProperty<uint>(0, false);
|
||||
cross_threshold = new InlineUserProperty<bts.spark.QuantityMeasure>(
|
||||
new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1}, false);
|
||||
cross_ticks = new InlineUserProperty<uint>(0, false);
|
||||
max_chase_ticks = new InlineUserProperty<uint>(10, false);
|
||||
detach_on_toward = new InlineUserProperty<bool>(true, false);
|
||||
}
|
||||
public readonly InlineUserProperty<CustomOneofCase> selected_child_order_type;
|
||||
public readonly InlineUserProperty<IReadOnlyCollection<CustomOneofCase>> child_order_types;
|
||||
|
||||
public readonly InlineUserProperty<uint> tick_offset_on_deploy;
|
||||
public readonly InlineUserProperty<bts.spark.QuantityMeasure> cross_threshold;
|
||||
public readonly InlineUserProperty<uint> cross_ticks;
|
||||
public readonly InlineUserProperty<uint> max_chase_ticks;
|
||||
public readonly InlineUserProperty<bool> detach_on_toward;
|
||||
}
|
||||
|
||||
class TickOrderVM : OrderTypeVMBase<TickOrderDefinition>, IHierarchicalOrderTypeVM, IDisposable
|
||||
{
|
||||
readonly IObservable<SecurityImmutable> _security_source;
|
||||
SecurityImmutable? _security;
|
||||
ObservableCollection<CustomOneofCase> _order_types = new ObservableCollection<CustomOneofCase>();
|
||||
ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase> _child_order_choices;
|
||||
InlineTickOrderProperties _inline_props;
|
||||
IOrderTypeVM _child_order;
|
||||
IDisposable _dispose, _child_order_errors_sub;
|
||||
|
||||
public uint tick_offset_on_deploy { get; set; } = 0;
|
||||
public bts.spark.QuantityMeasure cross_threshold { get; set; } = new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1 };
|
||||
public uint cross_ticks { get; set; } = 0;
|
||||
public uint max_chase_ticks { get; set; } = 10;
|
||||
public bool detach_on_toward { get; set; } = true;
|
||||
|
||||
public TickOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source)
|
||||
: this(model, security_source, security_source.Select(s =>
|
||||
new InlineTickOrderProperties(model.factory.default_order_type(s), model.factory.default_order_types(s).ToList()))) { }
|
||||
|
||||
public TickOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source,
|
||||
IObservable<InlineTickOrderProperties> inline_properties) : base(model)
|
||||
{
|
||||
_security_source = security_source;
|
||||
_dispose = Disposable.Create(
|
||||
security_source.OnDispatcher()
|
||||
.Subscribe(s => _security = s),
|
||||
inline_properties.OnDispatcher()
|
||||
.Subscribe(p => {
|
||||
if (_order_types.Count == 0 || p.child_order_types.force_override)
|
||||
{
|
||||
_order_types.SetItems(p.child_order_types.value);
|
||||
}
|
||||
if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override))
|
||||
{
|
||||
_select_child_order(p.selected_child_order_type.value);
|
||||
}
|
||||
if (p.tick_offset_on_deploy.force_override)
|
||||
{
|
||||
tick_offset_on_deploy = p.tick_offset_on_deploy.value;
|
||||
RaisePropertyChanged(nameof(tick_offset_on_deploy));
|
||||
}
|
||||
if (p.cross_threshold.force_override)
|
||||
{
|
||||
cross_threshold = p.cross_threshold.value;
|
||||
RaisePropertyChanged(nameof(cross_threshold));
|
||||
}
|
||||
if (p.cross_ticks.force_override)
|
||||
{
|
||||
cross_ticks = p.cross_ticks.value;
|
||||
RaisePropertyChanged(nameof(cross_ticks));
|
||||
}
|
||||
if (p.max_chase_ticks.force_override)
|
||||
{
|
||||
max_chase_ticks = p.max_chase_ticks.value;
|
||||
RaisePropertyChanged(nameof(max_chase_ticks));
|
||||
}
|
||||
if (p.detach_on_toward.force_override)
|
||||
{
|
||||
detach_on_toward = p.detach_on_toward.value;
|
||||
RaisePropertyChanged(nameof(detach_on_toward));
|
||||
}
|
||||
_inline_props = p;
|
||||
_validate();
|
||||
})
|
||||
);
|
||||
_validate();
|
||||
}
|
||||
|
||||
public override void assign_to(CustomOrderDefinition value)
|
||||
{
|
||||
if (_child_order == null)
|
||||
_select_child_order(model.factory.default_order_type(_security.Value, true));
|
||||
_validate();
|
||||
if (!HasErrors)
|
||||
{
|
||||
var i = factory.tick_order();
|
||||
i.TickOffsetOnDeploy = tick_offset_on_deploy;
|
||||
i.CrossThreshold = cross_threshold;
|
||||
i.CrossTicks = cross_ticks;
|
||||
i.MaxChaseTicks = max_chase_ticks;
|
||||
i.DetachOnToward = detach_on_toward;
|
||||
child_order.assign_to(i.Child);
|
||||
value.Tick = i;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<IOrderTypeVM> child_order_types
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_child_order_choices == null)
|
||||
_child_order_choices = new ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase>(_order_types, ot => create(ot, model, _security_source));
|
||||
return _child_order_choices;
|
||||
}
|
||||
}
|
||||
|
||||
public IOrderTypeVM child_order
|
||||
{
|
||||
get
|
||||
{
|
||||
init();
|
||||
return _child_order;
|
||||
}
|
||||
set
|
||||
{
|
||||
Disposable.dispose(ref _child_order_errors_sub);
|
||||
_child_order = value;
|
||||
_child_order?.init();
|
||||
if (_child_order is INotifyDataErrorInfo ndei)
|
||||
{
|
||||
ndei.ErrorsChanged += on_errors_changed;
|
||||
void on_errors_changed(object s, DataErrorsChangedEventArgs e) =>
|
||||
_validate();
|
||||
_child_order_errors_sub = Disposable.Create(() => ndei.ErrorsChanged -= on_errors_changed);
|
||||
}
|
||||
_validate();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposable.dispose(ref _dispose);
|
||||
if (_child_order_choices != null)
|
||||
_child_order_choices.OfType<IDisposable>().DisposeAll();
|
||||
}
|
||||
|
||||
public override void init()
|
||||
{
|
||||
if (_child_order == null && _security.HasValue)
|
||||
_select_child_order();
|
||||
_validate();
|
||||
}
|
||||
|
||||
void _select_child_order()
|
||||
{
|
||||
CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ?
|
||||
_inline_props.selected_child_order_type.value :
|
||||
model.factory.default_order_type(_security.Value, true);
|
||||
_select_child_order(ot);
|
||||
}
|
||||
|
||||
void _select_child_order(CustomOneofCase ot)
|
||||
{
|
||||
_child_order = child_order_types.FirstOrDefault(vm => vm.order_type == ot);
|
||||
if (_child_order == null)
|
||||
_child_order = child_order_types.FirstOrDefault();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
|
||||
void _validate()
|
||||
{
|
||||
if (_child_order == null || (_child_order is INotifyDataErrorInfo ndei && ndei.HasErrors))
|
||||
set_error(nameof(child_order), string.Empty, true);
|
||||
else clear_error(nameof(child_order));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,9 +50,9 @@ namespace mbts.spark {
|
||||
: base(model) {
|
||||
_security_source = security_source;
|
||||
_dispose = Disposable.Create(
|
||||
security_source.ObserveOnDispatcher()
|
||||
security_source.OnDispatcher()
|
||||
.Subscribe(s => _security = s),
|
||||
inline_properties.ObserveOnDispatcher()
|
||||
inline_properties.OnDispatcher()
|
||||
.Subscribe(p => {
|
||||
if (_order_types.Count == 0 || p.child_order_types.force_override) {
|
||||
_order_types.SetItems(p.child_order_types.value);
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using static bts.api.orderman.CustomOrderDefinition;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
static partial class OrderTypeVMFactory
|
||||
{
|
||||
class InlineTopOrderProperties
|
||||
{
|
||||
public InlineTopOrderProperties(CustomOneofCase selected_child_order_type, IReadOnlyCollection<CustomOneofCase> child_order_types)
|
||||
{
|
||||
ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types");
|
||||
this.selected_child_order_type = new InlineUserProperty<CustomOneofCase>(selected_child_order_type, true);
|
||||
this.child_order_types = new InlineUserProperty<IReadOnlyCollection<CustomOneofCase>>(child_order_types, true);
|
||||
|
||||
var hedgetype = CustomOneofCase.Lean;
|
||||
if(!child_order_types.Contains(hedgetype))
|
||||
hedgetype = CustomOneofCase.Limit;
|
||||
this.selected_hedge_order_type = new InlineUserProperty<CustomOneofCase>(hedgetype, true);
|
||||
this.hedge_order_types = new InlineUserProperty<IReadOnlyCollection<CustomOneofCase>>(child_order_types, true);
|
||||
|
||||
watch_level_tick_offset = new InlineUserProperty<uint>(0, false);
|
||||
top_threshold = new InlineUserProperty<bts.spark.QuantityMeasure>(
|
||||
new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1 }, false);
|
||||
enable_hedge = new InlineUserProperty<bool>(false, false);
|
||||
hedge_ticks = new InlineUserProperty<int>(1, false);
|
||||
}
|
||||
public readonly InlineUserProperty<CustomOneofCase> selected_child_order_type;
|
||||
public readonly InlineUserProperty<IReadOnlyCollection<CustomOneofCase>> child_order_types;
|
||||
public readonly InlineUserProperty<CustomOneofCase> selected_hedge_order_type;
|
||||
public readonly InlineUserProperty<IReadOnlyCollection<CustomOneofCase>> hedge_order_types;
|
||||
|
||||
|
||||
public readonly InlineUserProperty<uint> watch_level_tick_offset;
|
||||
public readonly InlineUserProperty<bts.spark.QuantityMeasure> top_threshold;
|
||||
|
||||
public readonly InlineUserProperty<bool> enable_hedge;
|
||||
public readonly InlineUserProperty<int> hedge_ticks;
|
||||
|
||||
}
|
||||
|
||||
class TopOrderVM : OrderTypeVMBase<TopOrderDefinition>, IHierarchicalOrderTypeVM, IDisposable
|
||||
{
|
||||
readonly IObservable<SecurityImmutable> _security_source;
|
||||
SecurityImmutable? _security;
|
||||
ObservableCollection<CustomOneofCase> _order_types = new ObservableCollection<CustomOneofCase>();
|
||||
ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase> _child_order_choices;
|
||||
InlineTopOrderProperties _inline_props;
|
||||
IOrderTypeVM _child_order;
|
||||
IDisposable _dispose, _child_order_errors_sub;
|
||||
|
||||
ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase> _hedge_order_choices;
|
||||
IOrderTypeVM _hedge_order;
|
||||
IDisposable _hedge_order_errors_sub;
|
||||
|
||||
public uint watch_level_tick_offset { get; set; } = 0;
|
||||
public bts.spark.QuantityMeasure top_threshold { get; set; } = new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1 };
|
||||
bool _enable_hedge = false;
|
||||
public bool enable_hedge
|
||||
{
|
||||
get { return _enable_hedge; }
|
||||
set
|
||||
{
|
||||
_enable_hedge = value;
|
||||
RaisePropertyChanged(nameof(enable_hedge));
|
||||
}
|
||||
}
|
||||
public int hedge_ticks { get; set; } = 1;
|
||||
|
||||
public TopOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source)
|
||||
: this(model, security_source, security_source.Select(s =>
|
||||
new InlineTopOrderProperties(model.factory.default_order_type(s), model.factory.default_order_types(s).ToList())))
|
||||
{ }
|
||||
|
||||
public TopOrderVM(OrdersModel model, IObservable<SecurityImmutable> security_source,
|
||||
IObservable<InlineTopOrderProperties> inline_properties) : base(model)
|
||||
{
|
||||
_security_source = security_source;
|
||||
_dispose = Disposable.Create(
|
||||
security_source.OnDispatcher()
|
||||
.Subscribe(s => _security = s),
|
||||
inline_properties.OnDispatcher()
|
||||
.Subscribe(p =>
|
||||
{
|
||||
if (_order_types.Count == 0 || p.child_order_types.force_override)
|
||||
{
|
||||
_order_types.SetItems(p.child_order_types.value);
|
||||
}
|
||||
if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override))
|
||||
{
|
||||
_select_child_order(p.selected_child_order_type.value);
|
||||
}
|
||||
if (_hedge_order_choices != null && (_hedge_order == null || p.selected_hedge_order_type.force_override))
|
||||
{
|
||||
_select_hedge_order(p.selected_hedge_order_type.value);
|
||||
}
|
||||
if (p.watch_level_tick_offset.force_override)
|
||||
{
|
||||
watch_level_tick_offset = p.watch_level_tick_offset.value;
|
||||
RaisePropertyChanged(nameof(watch_level_tick_offset));
|
||||
}
|
||||
if (p.top_threshold.force_override)
|
||||
{
|
||||
top_threshold = p.top_threshold.value;
|
||||
RaisePropertyChanged(nameof(top_threshold));
|
||||
}
|
||||
if (p.enable_hedge.force_override)
|
||||
{
|
||||
enable_hedge = p.enable_hedge.value;
|
||||
RaisePropertyChanged(nameof(enable_hedge));
|
||||
}
|
||||
if (p.hedge_ticks.force_override)
|
||||
{
|
||||
hedge_ticks = p.hedge_ticks.value;
|
||||
RaisePropertyChanged(nameof(hedge_ticks));
|
||||
}
|
||||
_inline_props = p;
|
||||
_validate();
|
||||
})
|
||||
);
|
||||
_validate();
|
||||
}
|
||||
|
||||
public override void assign_to(CustomOrderDefinition value)
|
||||
{
|
||||
if (_child_order == null)
|
||||
_select_child_order(model.factory.default_order_type(_security.Value, true));
|
||||
if (_hedge_order == null && enable_hedge)
|
||||
_select_hedge_order(model.factory.default_order_type(_security.Value, true));
|
||||
_validate();
|
||||
if (!HasErrors)
|
||||
{
|
||||
var i = factory.top_order();
|
||||
i.WatchLevelTickOffset = watch_level_tick_offset;
|
||||
i.TopThreshold = top_threshold;
|
||||
i.EnableHedge = enable_hedge;
|
||||
i.HedgeTicks = hedge_ticks;
|
||||
child_order.assign_to(i.Child);
|
||||
if (enable_hedge && hedge_order != null)
|
||||
hedge_order.assign_to(i.Hedge);
|
||||
value.Top = i;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<IOrderTypeVM> child_order_types
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_child_order_choices == null)
|
||||
_child_order_choices = new ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase>(_order_types, ot => create(ot, model, _security_source));
|
||||
return _child_order_choices;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<IOrderTypeVM> hedge_order_types
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hedge_order_choices == null)
|
||||
_hedge_order_choices = new ObservableAutoWrapper<IOrderTypeVM, CustomOneofCase>(_order_types, ot => create(ot, model, _security_source));
|
||||
return _hedge_order_choices;
|
||||
}
|
||||
}
|
||||
|
||||
public IOrderTypeVM child_order
|
||||
{
|
||||
get
|
||||
{
|
||||
init();
|
||||
return _child_order;
|
||||
}
|
||||
set
|
||||
{
|
||||
Disposable.dispose(ref _child_order_errors_sub);
|
||||
_child_order = value;
|
||||
_child_order?.init();
|
||||
if (_child_order is INotifyDataErrorInfo ndei)
|
||||
{
|
||||
ndei.ErrorsChanged += on_errors_changed;
|
||||
void on_errors_changed(object s, DataErrorsChangedEventArgs e) =>
|
||||
_validate();
|
||||
_child_order_errors_sub = Disposable.Create(() => ndei.ErrorsChanged -= on_errors_changed);
|
||||
}
|
||||
_validate();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
}
|
||||
|
||||
public IOrderTypeVM hedge_order
|
||||
{
|
||||
get
|
||||
{
|
||||
init();
|
||||
return _hedge_order;
|
||||
}
|
||||
set
|
||||
{
|
||||
Disposable.dispose(ref _hedge_order_errors_sub);
|
||||
_hedge_order = value;
|
||||
_hedge_order?.init();
|
||||
if (_hedge_order is INotifyDataErrorInfo ndei)
|
||||
{
|
||||
ndei.ErrorsChanged += on_errors_changed;
|
||||
void on_errors_changed(object s, DataErrorsChangedEventArgs e) =>
|
||||
_validate();
|
||||
_hedge_order_errors_sub = Disposable.Create(() => ndei.ErrorsChanged -= on_errors_changed);
|
||||
}
|
||||
_validate();
|
||||
RaisePropertyChanged(nameof(hedge_order));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposable.dispose(ref _dispose);
|
||||
if (_child_order_choices != null)
|
||||
_child_order_choices.OfType<IDisposable>().DisposeAll();
|
||||
if (_hedge_order_choices != null)
|
||||
_hedge_order_choices.OfType<IDisposable>().DisposeAll();
|
||||
}
|
||||
|
||||
public override void init()
|
||||
{
|
||||
if (_child_order == null && _security.HasValue)
|
||||
_select_child_order();
|
||||
if (_hedge_order == null && _security.HasValue && enable_hedge)
|
||||
_select_hedge_order();
|
||||
_validate();
|
||||
}
|
||||
|
||||
void _select_child_order()
|
||||
{
|
||||
CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ?
|
||||
_inline_props.selected_child_order_type.value :
|
||||
model.factory.default_order_type(_security.Value, true);
|
||||
_select_child_order(ot);
|
||||
}
|
||||
|
||||
void _select_child_order(CustomOneofCase ot)
|
||||
{
|
||||
_child_order = child_order_types.FirstOrDefault(vm => vm.order_type == ot);
|
||||
if (_child_order == null)
|
||||
_child_order = child_order_types.FirstOrDefault();
|
||||
RaisePropertyChanged(nameof(child_order));
|
||||
}
|
||||
|
||||
void _select_hedge_order()
|
||||
{
|
||||
CustomOneofCase ot = _inline_props != null && _inline_props.selected_hedge_order_type.has_value ?
|
||||
_inline_props.selected_hedge_order_type.value :
|
||||
model.factory.default_order_type(_security.Value, true);
|
||||
_select_hedge_order(ot);
|
||||
}
|
||||
|
||||
void _select_hedge_order(CustomOneofCase ot)
|
||||
{
|
||||
_hedge_order = hedge_order_types.FirstOrDefault(vm => vm.order_type == ot);
|
||||
if (_hedge_order == null)
|
||||
_hedge_order = hedge_order_types.FirstOrDefault();
|
||||
RaisePropertyChanged(nameof(hedge_order));
|
||||
}
|
||||
|
||||
|
||||
void _validate()
|
||||
{
|
||||
if (_child_order == null || (_child_order is INotifyDataErrorInfo ndei && ndei.HasErrors))
|
||||
set_error(nameof(child_order), string.Empty, true);
|
||||
else clear_error(nameof(child_order));
|
||||
|
||||
if ((_hedge_order == null && enable_hedge) || (_hedge_order is INotifyDataErrorInfo hdei && hdei.HasErrors))
|
||||
set_error(nameof(hedge_order), string.Empty, true);
|
||||
else clear_error(nameof(hedge_order));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,11 @@ namespace mbts.spark {
|
||||
_service = service;
|
||||
_spreader = spreader;
|
||||
_error = error;
|
||||
on_orders = _service.subscribei_orders().Table().Replay(1).RefCount();
|
||||
on_orders = _service.subscribei_orders().Table().Cache();
|
||||
IReadOnlyDictionary<uint, OrderRecordImmutable> empty_orders = new Dictionary<uint, OrderRecordImmutable>();
|
||||
working_orders =
|
||||
on_orders.Scan(empty_orders, (worders, table) => _update_working_orders(worders, table)).Replay(1).RefCount();
|
||||
on_fills = _service.subscribei_fills().Table().Replay(1).RefCount();
|
||||
on_fills = _service.subscribei_fills().Table().Cache();
|
||||
_activity = new OrderActivitySummarizer(bus, on_orders);
|
||||
on_fill_tuples = FillTupleSource.create_observable(bus.subscribe(SparkTags.SecuritiesTag), on_orders, on_fills);
|
||||
_security_orders_demuxer = new TableUpdateDemuxer<OrderRecordImmutable, uint, uint>(o => o.Key, o => o.security_key());
|
||||
@@ -53,7 +53,7 @@ namespace mbts.spark {
|
||||
public SpreaderModel spreader => _spreader;
|
||||
public IObservable<TableUpdate<OrderRecordImmutable, uint>> on_orders { get; }
|
||||
public IObservable<TableUpdate<FillImmutable, string>> on_fills { get; }
|
||||
public IObservable<ITableUpdateTransform<FillTuple, string>> on_fill_tuples { get; }
|
||||
public IObservable<IIncrementalUpdate<FillTuple, FillTupleKey>> on_fill_tuples { get; }
|
||||
public IObservable<IReadOnlyDictionary<uint,OrderRecordImmutable>> working_orders { get; }
|
||||
|
||||
public IObservable<ITableUpdateTransform<OrderRecordImmutable, uint>> on_security_orders(uint security) =>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<StartupObject>mbts.ui.shell.App</StartupObject>
|
||||
<RootNamespace>bts</RootNamespace>
|
||||
<LangVersion>7.1</LangVersion>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
<Version>1.0.0</Version>
|
||||
|
||||
@@ -175,8 +175,8 @@ namespace bts.spark
|
||||
public SparkCommandParamObserver(string title, ImageSource icon, ITag<T> tag, IObservable<T> parameter, IObservable<bool> can_execute) : base(title, icon) {
|
||||
_tag = tag;
|
||||
_subs = Disposable.Create(
|
||||
parameter.ObserveOnDispatcher().Subscribe(p => _parameter = p),
|
||||
can_execute.ObserveOnDispatcher().Subscribe(ce => {
|
||||
parameter.OnDispatcher().Subscribe(p => _parameter = p),
|
||||
can_execute.OnDispatcher().Subscribe(ce => {
|
||||
_can_execute = ce;
|
||||
raiseCanExecuteChanged();
|
||||
})
|
||||
|
||||
@@ -199,10 +199,10 @@ namespace mbts.ui {
|
||||
public static BTSColor straddle_theovsmarket_positive { get { return BTSCoreColors.green_light; } }
|
||||
|
||||
// Order Types
|
||||
public static BTSColor tick_order { get { return new BTSColor(31, 4, 57);}}
|
||||
public static BTSColor lean_order { get { return new BTSColor(110, 77, 143);}}
|
||||
public static BTSColor join_order { get { return new BTSColor(15, 1, 29);}}
|
||||
public static BTSColor leader_order { get { return new BTSColor(40, 9, 72);}}
|
||||
public static BTSColor tick_order { get { return BTSColors.purple.rgb_color();}}
|
||||
public static BTSColor lean_order { get { return BTSColors.purple.rgb_color();}}
|
||||
public static BTSColor join_order { get { return BTSColors.purple.rgb_color();}}
|
||||
public static BTSColor top_order { get { return BTSColors.purple.rgb_color();}}
|
||||
}
|
||||
|
||||
public static class AW2 {
|
||||
|
||||
@@ -345,6 +345,17 @@ namespace System.Reactive.Linq
|
||||
|
||||
public static async Task<(TResult result, bool success)> FirstAsyncTask<T, TResult>(this IObservable<T> source, Func<T, bool> predicate, Func<T, TResult> selector, TimeSpan timeout) where T : class =>
|
||||
await source.Where(predicate).Select(val => (selector(val), true)).Merge(Observable.Return((default(TResult), false)).Delay(timeout)).FirstAsync();
|
||||
|
||||
// This is placed here to intentionally make ObserveOnDispatcher ambiguous with the one provided by Rx
|
||||
public static IObservable<T> ObserveOnDispatcher<T>(this IObservable<T> source) {
|
||||
throw new InvalidOperationException("This method can starve the UI of input messages. Please use OnDispatcher() instead");
|
||||
}
|
||||
|
||||
public static IObservable<T> OnDispatcher<T>(this IObservable<T> source) {
|
||||
return DispatcherObservable.ObserveOnDispatcher(source, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,5 +54,33 @@
|
||||
rv |= try_convert(args[1], out arg2);
|
||||
return rv;
|
||||
}
|
||||
|
||||
public static bool try_convert_args<T1, T2, T3>(object[] args, out T1 arg1, out T2 arg2, out T3 arg3) {
|
||||
if (args == null || args.Length != 3) {
|
||||
arg1 = default;
|
||||
arg2 = default;
|
||||
arg3 = default;
|
||||
return false;
|
||||
}
|
||||
bool rv = try_convert(args[0], out arg1);
|
||||
rv |= try_convert(args[1], out arg2);
|
||||
rv |= try_convert(args[2], out arg3);
|
||||
return rv;
|
||||
}
|
||||
|
||||
public static bool try_convert_args<T1, T2, T3, T4>(object[] args, out T1 arg1, out T2 arg2, out T3 arg3, out T4 arg4) {
|
||||
if (args == null || args.Length != 4) {
|
||||
arg1 = default;
|
||||
arg2 = default;
|
||||
arg3 = default;
|
||||
arg4 = default;
|
||||
return false;
|
||||
}
|
||||
bool rv = try_convert(args[0], out arg1);
|
||||
rv |= try_convert(args[1], out arg2);
|
||||
rv |= try_convert(args[2], out arg3);
|
||||
rv |= try_convert(args[3], out arg4);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ using bts.api.persistence;
|
||||
|
||||
namespace mbts.ui.settings
|
||||
{
|
||||
//
|
||||
// This class is used for tools like SettingsManager.exe
|
||||
//
|
||||
[Export(typeof(IUserPersistFactory))]
|
||||
class UISettingsFactory : IUserPersistFactory
|
||||
{
|
||||
@@ -34,7 +37,7 @@ namespace mbts.ui.settings
|
||||
|
||||
public IUserPersist get_persist_for(string name) {
|
||||
var user = _users.Result.FirstOrDefault(u => u.Login == name);
|
||||
return new UserUISettings(user.Id, _persist);
|
||||
return new UserUISettings(user.Id, _persist, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using bts.api.core;
|
||||
using bts.api.persistence;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Reactive.Linq;
|
||||
using bts.spark;
|
||||
|
||||
namespace mbts.persist
|
||||
{
|
||||
@@ -18,11 +19,13 @@ namespace mbts.persist
|
||||
protected abstract string user();
|
||||
string uns(string ns) { return $"{user()}#{ns}"; }
|
||||
PersistenceService _persist;
|
||||
ErrorReporter _errors;
|
||||
|
||||
private const string _settings_change_base = "/ui/settingschange/";
|
||||
|
||||
public UISettingsBase(PersistenceService persist) {
|
||||
public UISettingsBase(PersistenceService persist, ErrorReporter errors) {
|
||||
_persist = persist;
|
||||
_errors = errors;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +53,7 @@ namespace mbts.persist
|
||||
Ns = uns(ns),
|
||||
Key = { key }
|
||||
});
|
||||
check_error(ret.value.Result, ns, key, out bool empty);
|
||||
check_error(ret, ns, key, out bool empty);
|
||||
if (empty) return default(T);
|
||||
return deserialize<T>(ret.value.Record.Single().Value);
|
||||
}
|
||||
@@ -67,20 +70,28 @@ namespace mbts.persist
|
||||
return ProtoBuf.Serializer.DeserializeWithLengthPrefix<T>(stream, ProtoBuf.PrefixStyle.Fixed32);
|
||||
}
|
||||
|
||||
private void check_error(RetrieveResponse.Types.Result result, string ns, string key, out bool empty) {
|
||||
switch (result) {
|
||||
case RetrieveResponse.Types.Result.EmptyNs:
|
||||
case RetrieveResponse.Types.Result.NoSuchNs:
|
||||
case RetrieveResponse.Types.Result.NoSuchKey:
|
||||
empty = true;
|
||||
return;
|
||||
case RetrieveResponse.Types.Result.EmptyKey:
|
||||
case RetrieveResponse.Types.Result.DuplicateKey:
|
||||
throw new ArgumentException($"Invalid Key {result} '{key}'");
|
||||
default:
|
||||
case RetrieveResponse.Types.Result.Ok:
|
||||
empty = false;
|
||||
return;
|
||||
private void check_error(Result<RetrieveResponse> result, string ns, string key, out bool empty) {
|
||||
if (result.success) {
|
||||
switch (result.value.Result) {
|
||||
case RetrieveResponse.Types.Result.EmptyNs:
|
||||
case RetrieveResponse.Types.Result.NoSuchNs:
|
||||
case RetrieveResponse.Types.Result.NoSuchKey:
|
||||
empty = true;
|
||||
return;
|
||||
case RetrieveResponse.Types.Result.EmptyKey:
|
||||
case RetrieveResponse.Types.Result.DuplicateKey:
|
||||
_errors?.report_error($"Invalid Key {result} '{key}'", "Requested Invalid Settings Key", System.Windows.MessageBoxImage.Information);
|
||||
empty = true;
|
||||
return;
|
||||
default:
|
||||
case RetrieveResponse.Types.Result.Ok:
|
||||
empty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
_errors?.report_error($"There was an error communicating with the settings server: '{result.error_message}'", "Failed to retrieve settings from the server");
|
||||
empty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +101,7 @@ namespace mbts.persist
|
||||
new RetrieveAllRequest() {
|
||||
Ns = uns(ns)
|
||||
});
|
||||
check_error(results.value.Result, ns, "", out bool empty);
|
||||
check_error(results, ns, "", out bool empty);
|
||||
if (empty) return Enumerable.Empty<PersistResult<T>>();
|
||||
return results.value.Record
|
||||
.Where(r => re.IsMatch(r.Key))
|
||||
@@ -131,7 +142,7 @@ namespace mbts.persist
|
||||
protected override string user() { return "all_users"; }
|
||||
|
||||
[ImportingConstructor]
|
||||
public UISettings([Import("settings")]PersistenceService p) : base(p) { }
|
||||
public UISettings([Import("settings")]PersistenceService p, ErrorReporter err) : base(p,err) { }
|
||||
}
|
||||
|
||||
[Export(typeof(IUserPersist))]
|
||||
@@ -140,8 +151,8 @@ namespace mbts.persist
|
||||
string _user;
|
||||
|
||||
[ImportingConstructor]
|
||||
public UserUISettings([Import("userid")]uint user, [Import("settings")]PersistenceService persist)
|
||||
: base(persist) {
|
||||
public UserUISettings([Import("userid")]uint user, [Import("settings")]PersistenceService persist, ErrorReporter err)
|
||||
: base(persist,err) {
|
||||
_user = $"User {user}";
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
|
||||
namespace mbts.utils {
|
||||
public static class EqualityComparer {
|
||||
const int _hash_seed = 63607, _hash_multiplier = 6451;
|
||||
|
||||
public static IEqualityComparer<T> Create<T>(Func<T, T, bool> equals) =>
|
||||
new AnonymousEqualityComparer<T>(equals);
|
||||
|
||||
@@ -28,6 +30,31 @@ namespace mbts.utils {
|
||||
y != null &&
|
||||
non_null_equals(x, y);
|
||||
|
||||
public static int aggregate_hash(int item1_hash, int item2_hash, int item3_hash) {
|
||||
int rv = _hash_seed;
|
||||
unchecked {
|
||||
_accumulate_hash(ref rv, ref item1_hash);
|
||||
_accumulate_hash(ref rv, ref item2_hash);
|
||||
_accumulate_hash(ref rv, ref item3_hash);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Note: be wary of the performance cost this method may have, as frequently creating large arrays can be expensive.
|
||||
// If you wish to avoid this cost, consider making an overload like the method above, with the appropriate number of item-hash arguments
|
||||
public static int aggregate_hash(params int[] item_hashes) {
|
||||
int hash = _hash_seed;
|
||||
unchecked {
|
||||
foreach (int item_hash in item_hashes)
|
||||
hash = hash * _hash_multiplier + item_hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// WARNING: be sure to call this within in an unchecked scope
|
||||
static void _accumulate_hash(ref int aggregate_hash, ref int item_hash) =>
|
||||
aggregate_hash = aggregate_hash * _hash_multiplier + item_hash;
|
||||
|
||||
private class AnonymousEqualityComparer<T> : IEqualityComparer<T> {
|
||||
readonly Func<T, T, bool> _equals;
|
||||
readonly Func<T, int> _get_hash_code;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
|
||||
namespace mbts.utils {
|
||||
interface IObservableDemuxer<TKey, in TInput, out TOutput> : IObserver<TInput> {
|
||||
@@ -89,21 +89,17 @@ namespace mbts.utils {
|
||||
_group_key = group_key;
|
||||
}
|
||||
|
||||
public void OnCompleted() { }
|
||||
public void OnError(Exception error) { }
|
||||
public IObservable<ITableUpdateTransform<T, TKey>> subscribe(TGroupKey group_key) =>
|
||||
Observable.Create<ITableUpdateTransform<T, TKey>>(o => {
|
||||
var data = _map.GetOrAdd(group_key, gk => new Data());
|
||||
lock (data) {
|
||||
var current = data.current;
|
||||
if (current.state.Count > 0) {
|
||||
var state_clone = current.state.ToDictionary();
|
||||
o.OnNext(new TableUpdateTransform<T, TKey>(state_clone, state_clone.Values.ToList(), new List<T>(0)));
|
||||
}
|
||||
using (data.read()) {
|
||||
o.OnNext(data.current.as_snapshot());
|
||||
return data.subject.Subscribe(o);
|
||||
}
|
||||
return data.subject.Subscribe(o);
|
||||
});
|
||||
|
||||
public void OnCompleted() { }
|
||||
public void OnError(Exception error) { }
|
||||
public void OnNext(TableUpdate<T, TKey> table) {
|
||||
var changes = new Dictionary<TGroupKey, ChangeSet>();
|
||||
foreach (T value in table.added)
|
||||
@@ -116,16 +112,29 @@ namespace mbts.utils {
|
||||
}
|
||||
|
||||
Data _update(Data data, ChangeSet changes) {
|
||||
lock (data) {
|
||||
data.current = data.current.update(changes.removed, changes.added, _key);
|
||||
data.subject.OnNext(data.current);
|
||||
}
|
||||
using (data.write())
|
||||
data.subject.OnNext(data.current = data.current.update(changes.removed, changes.added, _key));
|
||||
return data;
|
||||
}
|
||||
|
||||
class Data {
|
||||
int _n_read_ops, _n_write_ops;
|
||||
public readonly Subject<TableUpdateTransform<T, TKey>> subject = new Subject<TableUpdateTransform<T, TKey>>();
|
||||
public TableUpdateTransform<T, TKey> current = new TableUpdateTransform<T, TKey>();
|
||||
|
||||
public IDisposable read() {
|
||||
while (_n_write_ops > 0)
|
||||
Thread.Yield();
|
||||
Interlocked.Increment(ref _n_read_ops);
|
||||
return Disposable.Create(() => Interlocked.Decrement(ref _n_read_ops));
|
||||
}
|
||||
|
||||
public IDisposable write() {
|
||||
while (_n_read_ops > 0)
|
||||
Thread.Yield();
|
||||
Interlocked.Increment(ref _n_write_ops);
|
||||
return Disposable.Create(() => Interlocked.Decrement(ref _n_write_ops));
|
||||
}
|
||||
}
|
||||
|
||||
// [John]I prefer this class over anonymous tuples bc it is easy to mix up Item1/Item2
|
||||
|
||||
@@ -7,12 +7,30 @@ using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace mbts.utils {
|
||||
interface IIncrementalUpdate<T, TKey> where TKey : IEquatable<TKey> {
|
||||
IReadOnlyDictionary<TKey, T> added { get; }
|
||||
IReadOnlyDictionary<TKey, T> removed { get; }
|
||||
}
|
||||
|
||||
interface ITableUpdateTransform<T, TKey> where TKey : IEquatable<TKey> {
|
||||
IReadOnlyDictionary<TKey, T> state { get; }
|
||||
IReadOnlyList<T> added { get; }
|
||||
IReadOnlyList<T> removed { get; }
|
||||
}
|
||||
|
||||
class IncrementalUpdate<T, TKey> : IIncrementalUpdate<T, TKey> where TKey : IEquatable<TKey> {
|
||||
public IncrementalUpdate()
|
||||
: this(new Dictionary<TKey, T>(0), new Dictionary<TKey, T>(0)) { }
|
||||
|
||||
public IncrementalUpdate(IReadOnlyDictionary<TKey, T> added, IReadOnlyDictionary<TKey, T> removed) {
|
||||
this.added = added;
|
||||
this.removed = removed;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<TKey, T> added { get; }
|
||||
public IReadOnlyDictionary<TKey, T> removed { get; }
|
||||
}
|
||||
|
||||
class TableUpdateTransform<T, TKey> : ITableUpdateTransform<T, TKey> where TKey : IEquatable<TKey> {
|
||||
readonly Dictionary<TKey, T> _state;
|
||||
readonly IReadOnlyList<T> _added, _removed;
|
||||
@@ -44,6 +62,18 @@ namespace mbts.utils {
|
||||
table._state.Add(key(item), item);
|
||||
return table;
|
||||
}
|
||||
|
||||
public TableUpdateTransform<T, TKey> update(IIncrementalUpdate<T, TKey> update, Func<T, TKey> key) {
|
||||
var table = new TableUpdateTransform<T, TKey>(new Dictionary<TKey, T>(_state), added, removed);
|
||||
foreach (var k in update.removed.Keys)
|
||||
table._state.Remove(k);
|
||||
foreach (var kv in update.added)
|
||||
table._state.Add(kv.Key, kv.Value);
|
||||
return table;
|
||||
}
|
||||
|
||||
internal TableUpdateTransform<T, TKey> as_snapshot() =>
|
||||
new TableUpdateTransform<T, TKey>(_state, _state.Values.ToList(), new T[0]);
|
||||
}
|
||||
|
||||
[Browsable(false)]
|
||||
@@ -58,6 +88,15 @@ namespace mbts.utils {
|
||||
.Select(u => new FilteredTableUpdateTransform<T, TKey>(u, predicate))
|
||||
.Where(u => u.added.Count > 0 || u.removed.Count > 0);
|
||||
|
||||
/// <summary>
|
||||
/// Makes a 1:1 conversion to an IncrementalUpdate sequence of TResult (TKey remains the same).
|
||||
/// </summary>
|
||||
public static IObservable<IIncrementalUpdate<TResult, TKey>> convert<T, TKey, TResult>(this IObservable<IIncrementalUpdate<T, TKey>> source, Func<T, TResult> selector)
|
||||
where TKey : IEquatable<TKey> =>
|
||||
source.Select(update => new IncrementalUpdate<TResult, TKey>(
|
||||
update.added.ToDictionary(kv => kv.Key, kv => selector(kv.Value)),
|
||||
update.removed.ToDictionary(kv => kv.Key, kv => selector(kv.Value))));
|
||||
|
||||
/// <summary>
|
||||
/// Converts sequence to show incremental changes with respect to the [TResult] value returned by result_selector.
|
||||
/// </summary>
|
||||
@@ -69,6 +108,33 @@ namespace mbts.utils {
|
||||
source.Scan(new PivotTableUpdateTransform<T, TKey, TResult, TResultKey>(selector, result_key, key),
|
||||
(pivot, table) => pivot.update(table));
|
||||
|
||||
/// <summary>
|
||||
/// Converts sequence to show incremental changes with respect to the [TResult] value returned by result_selector.
|
||||
/// </summary>
|
||||
public static IObservable<IIncrementalUpdate<TResult, TResultKey>> with_respect_to<T, TKey, TResult, TResultKey>(
|
||||
this IObservable<IIncrementalUpdate<T, TKey>> source, Func<T, TResult> selector, Func<TResult, TResultKey> result_key, Func<T, TKey> key)
|
||||
where TKey : IEquatable<TKey>
|
||||
where TResultKey : IEquatable<TResultKey> =>
|
||||
//
|
||||
source.Scan(new PivotIncrementalUpdate<T, TKey, TResult, TResultKey>(selector, result_key, key),
|
||||
(pivot, table) => pivot.update(table));
|
||||
|
||||
/// <summary>
|
||||
/// Uses a Scan operator to accumulate state in the resulting ITableUpdateTransform sequence.
|
||||
/// </summary>
|
||||
public static IObservable<ITableUpdateTransform<T, TKey>> table<T, TKey>(this IObservable<IIncrementalUpdate<T, TKey>> source, Func<T, TKey> key) where TKey : IEquatable<TKey> =>
|
||||
source.Scan(
|
||||
new TableUpdateTransform<T, TKey>(),
|
||||
(table, update) => table.update(update, key));
|
||||
|
||||
/// <summary>
|
||||
/// Filters the incremental added/removed lists by a predicate. Observers are only notified when the filtered added/removed lists have elements.
|
||||
/// </summary>
|
||||
public static IObservable<IIncrementalUpdate<T, TKey>> filter<T, TKey>(this IObservable<IIncrementalUpdate<T, TKey>> source, Func<T, bool> predicate)
|
||||
where TKey : IEquatable<TKey> => source
|
||||
.Select(u => new IncrementalUpdate<T, TKey>(u.added.Where(kv => predicate(kv.Value)).ToDictionary(), u.removed.Where(kv => predicate(kv.Value)).ToDictionary()))
|
||||
.Where(u => u.added.Count > 0 || u.removed.Count > 0);
|
||||
|
||||
class FilteredTableUpdateTransform<T, TKey> : ITableUpdateTransform<T, TKey> where TKey : IEquatable<TKey> {
|
||||
internal FilteredTableUpdateTransform(ITableUpdateTransform<T, TKey> source, Func<T, bool> predicate) {
|
||||
state = source.state;
|
||||
@@ -81,6 +147,57 @@ namespace mbts.utils {
|
||||
public IReadOnlyList<T> removed { get; }
|
||||
}
|
||||
|
||||
class PivotIncrementalUpdate<T, TKey, TResult, TResultKey> : IIncrementalUpdate<TResult, TResultKey> where TKey : IEquatable<TKey> where TResultKey : IEquatable<TResultKey> {
|
||||
readonly Dictionary<TResultKey, TResult> _state;
|
||||
readonly SetMap<TResultKey, TKey> _key_map;
|
||||
readonly Func<T, TResult> _selector;
|
||||
readonly Func<TResult, TResultKey> _result_key;
|
||||
readonly Func<T, TKey> _key;
|
||||
|
||||
internal PivotIncrementalUpdate(Func<T, TResult> selector, Func<TResult, TResultKey> result_key, Func<T, TKey> key) :
|
||||
this(new Dictionary<TResultKey, TResult>(), new Dictionary<TResultKey, TResult>(0), new Dictionary<TResultKey, TResult>(0), new SetMap<TResultKey, TKey>(), selector, result_key, key) { }
|
||||
|
||||
PivotIncrementalUpdate(Dictionary<TResultKey, TResult> state, IReadOnlyDictionary<TResultKey, TResult> added, IReadOnlyDictionary<TResultKey, TResult> removed,
|
||||
SetMap<TResultKey, TKey> key_map, Func<T, TResult> selector, Func<TResult, TResultKey> result_key, Func<T, TKey> key) {
|
||||
//
|
||||
_state = state;
|
||||
this.added = added;
|
||||
this.removed = removed;
|
||||
_key_map = key_map;
|
||||
_selector = selector;
|
||||
_result_key = result_key;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<TResultKey, TResult> state => _state;
|
||||
public IReadOnlyDictionary<TResultKey, TResult> added { get; }
|
||||
public IReadOnlyDictionary<TResultKey, TResult> removed { get; }
|
||||
|
||||
public PivotIncrementalUpdate<T, TKey, TResult, TResultKey> update(IIncrementalUpdate<T, TKey> table) {
|
||||
Dictionary<TResultKey, TResult> removed = new Dictionary<TResultKey, TResult>(), added = new Dictionary<TResultKey, TResult>();
|
||||
var state_clone = new Dictionary<TResultKey, TResult>(_state);
|
||||
foreach (var kv in table.removed) {
|
||||
var result = _selector(kv.Value);
|
||||
var rkey = _result_key(result);
|
||||
if (_key_map.removes_key(rkey, kv.Key))
|
||||
removed.Add(rkey, result);
|
||||
}
|
||||
foreach (var kv in table.added) {
|
||||
var result = _selector(kv.Value);
|
||||
var rkey = _result_key(result);
|
||||
if (_key_map.adds_key(rkey, kv.Key)) {
|
||||
if (!removed.Remove(rkey))
|
||||
added.Add(rkey, result);
|
||||
}
|
||||
}
|
||||
foreach (var k in removed.Keys)
|
||||
state_clone.Remove(k);
|
||||
foreach (var kv in added)
|
||||
state_clone.Add(kv.Key, kv.Value);
|
||||
return new PivotIncrementalUpdate<T, TKey, TResult, TResultKey>(state_clone, added, removed, _key_map, _selector, _result_key, _key);
|
||||
}
|
||||
}
|
||||
|
||||
class PivotTableUpdateTransform<T, TKey, TResult, TResultKey> : ITableUpdateTransform<TResult, TResultKey> where TKey : IEquatable<TKey> where TResultKey : IEquatable<TResultKey> {
|
||||
readonly Dictionary<TResultKey, TResult> _state;
|
||||
readonly SetMap<TResultKey, TKey> _key_map;
|
||||
|
||||
@@ -471,7 +471,7 @@ namespace mbts.ui.shell
|
||||
protected Property<T> backing<T, TData>(string name, IObservable<TData> source, Func<TData, T> select, T default_value = default) {
|
||||
if (_observable_properties == null) _observable_properties = new List<IDisposable>();
|
||||
var prop = new ObservablePropertySink<T>(this, name, default_value);
|
||||
_observable_properties.Add(source.ObserveOnDispatcher().Subscribe(d => prop.set(select(d))));
|
||||
_observable_properties.Add(source.OnDispatcher().Subscribe(d => prop.set(select(d))));
|
||||
return prop;
|
||||
}
|
||||
|
||||
@@ -526,8 +526,8 @@ namespace mbts.ui.shell
|
||||
protected SelectorProperty<T> backing<T>(string name, IObservable<IEnumerable<T>> items, IObservable<InlineUserProperty<T>> desired_item, Func<T> select_default, Func<T, T, bool> items_match, IReadOnlyList<PropertyValidation<T>> validation_steps) {
|
||||
if (_observable_properties == null) _observable_properties = new List<IDisposable>();
|
||||
var prop = new SelectorProperty<T>(this, name, select_default, items_match, validation_steps);
|
||||
_observable_properties.Add(items.ObserveOnDispatcher().Subscribe(prop));
|
||||
_observable_properties.Add(desired_item.ObserveOnDispatcher().Subscribe(prop));
|
||||
_observable_properties.Add(items.OnDispatcher().Subscribe(prop));
|
||||
_observable_properties.Add(desired_item.OnDispatcher().Subscribe(prop));
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,141 +1,220 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.extensions;
|
||||
using bts.api;
|
||||
using mbts.spark;
|
||||
using mbts.ui.utility.text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using static mbts.config.Configuration;
|
||||
using FontFamily = System.Windows.Media.FontFamily;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
public class Badge: Control {
|
||||
readonly static double _dpi = get<bts.config.System>().dpi_ratio;
|
||||
public class Badge : Control {
|
||||
public const double LINE_SPACING = 1;
|
||||
// Storing the defaults here for readability
|
||||
// Warning: these properties are used to calculate the ladder's row_height. Please change the values below if you want to change properties that affect the layout.
|
||||
// ** - value is not the actual PropertyMetadata.DefaultValue
|
||||
public static readonly Thickness
|
||||
DefaultMargin = new Thickness(4, 2, 4, 2), // **
|
||||
DefaultPadding = new Thickness(1),
|
||||
DefaultBorderThickness = new Thickness(2);
|
||||
public static readonly double
|
||||
DefaultCornerRadius = 1,
|
||||
DefaultFontSize = 10,
|
||||
DefaultWidth = 63; // **
|
||||
public static readonly FontFamily DefaultFontFamily = new FontFamily("Consolas");
|
||||
|
||||
readonly GlyphContentRect _gcr = new GlyphContentRect();
|
||||
Brush _background, _foreground, _border_brush;
|
||||
|
||||
Typeface _face;
|
||||
FormattedText _center_text;
|
||||
FormattedText _top_text;
|
||||
FormattedText _bottom_text;
|
||||
static Badge() {
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(typeof(Badge)));
|
||||
WidthProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(15.0));
|
||||
HeightProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(10.0));
|
||||
FontSizeProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender /*| FrameworkPropertyMetadataOptions.AffectsMeasure*/, update_text));
|
||||
ForegroundProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(Brushes.White, update_text));
|
||||
BackgroundProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(Brushes.Transparent, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
BorderThicknessProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(new Thickness(1)));
|
||||
BorderBrushProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(Brushes.Black));
|
||||
FontFamilyProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(new FontFamily("Consolas"), FrameworkPropertyMetadataOptions.AffectsRender /*| FrameworkPropertyMetadataOptions.AffectsMeasure*/, update_text));
|
||||
|
||||
//TextProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, update_text));
|
||||
//TopTextProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, update_text));
|
||||
//BottomTextProperty.OverrideMetadata(typeof(Badge), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, update_text));
|
||||
// MinWidth
|
||||
WPFHelper.override_metadata<double, Badge>(
|
||||
MinWidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.min_width = v,
|
||||
0);
|
||||
// Width
|
||||
WPFHelper.override_metadata<double, Badge>(
|
||||
WidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.width = v,
|
||||
double.NaN);
|
||||
// Max Width
|
||||
WPFHelper.override_metadata<double, Badge>(
|
||||
MaxWidthProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.max_width = v,
|
||||
double.MaxValue);
|
||||
// Min Height
|
||||
WPFHelper.override_metadata<double, Badge>(
|
||||
MinHeightProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.min_height = v,
|
||||
0);
|
||||
// Height
|
||||
WPFHelper.override_metadata<double, Badge>(
|
||||
HeightProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.height = v,
|
||||
double.NaN);
|
||||
// Max Height
|
||||
WPFHelper.override_metadata<double, Badge>(
|
||||
MaxHeightProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.max_height = v,
|
||||
double.MaxValue);
|
||||
// FontSize
|
||||
WPFHelper.override_metadata<double, Badge>(
|
||||
FontSizeProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.font_size = v,
|
||||
DefaultFontSize);
|
||||
// Foreground
|
||||
WPFHelper.override_metadata<Brush, Badge>(
|
||||
ForegroundProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._foreground = v,
|
||||
Brushes.White);
|
||||
// Background
|
||||
WPFHelper.override_metadata<Brush, Badge>(
|
||||
BackgroundProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._background = v,
|
||||
Brushes.Transparent);
|
||||
// BorderThickness
|
||||
WPFHelper.override_metadata<Thickness, Badge>(
|
||||
BorderThicknessProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.border_thickness = v,
|
||||
DefaultBorderThickness);
|
||||
// BorderBrush
|
||||
WPFHelper.override_metadata<Brush, Badge>(
|
||||
BorderBrushProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._border_brush = v,
|
||||
Brushes.Black);
|
||||
// FontFamily
|
||||
WPFHelper.override_metadata<FontFamily, Badge>(
|
||||
FontFamilyProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.glyph_context = b._gcr.glyph_context.with(v),
|
||||
DefaultFontFamily);
|
||||
// HoriontalAlignment
|
||||
WPFHelper.override_metadata<HorizontalAlignment, Badge>(
|
||||
HorizontalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.horizontal_alignment = v,
|
||||
HorizontalAlignment.Stretch);
|
||||
// HoriontalContentAlignment
|
||||
WPFHelper.override_metadata<HorizontalAlignment, Badge>(
|
||||
HorizontalContentAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.horizontal_content_alignment = v,
|
||||
HorizontalAlignment.Center);
|
||||
// VerticalAlignment
|
||||
WPFHelper.override_metadata<VerticalAlignment, Badge>(
|
||||
VerticalAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.vertical_alignment = v,
|
||||
VerticalAlignment.Center);
|
||||
// VerticalContentAlignment
|
||||
WPFHelper.override_metadata<VerticalAlignment, Badge>(
|
||||
VerticalContentAlignmentProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsArrange |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(o, v) => o._gcr.vertical_content_alignment = v,
|
||||
VerticalAlignment.Center);
|
||||
// Padding
|
||||
WPFHelper.override_metadata<Thickness, Badge>(
|
||||
PaddingProperty,
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(p, v) => p._gcr.padding = v,
|
||||
DefaultPadding);
|
||||
}
|
||||
|
||||
public string Text {
|
||||
get { return (string)GetValue(TextProperty); }
|
||||
set { SetValue(TextProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
DependencyProperty.Register("Text", typeof(string), typeof(Badge), new PropertyMetadata(string.Empty,
|
||||
format_text((b,v) => b._center_text= v)));
|
||||
|
||||
|
||||
public string TopText {
|
||||
get { return (string)GetValue(TopTextProperty); }
|
||||
set { SetValue(TopTextProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty TopTextProperty =
|
||||
DependencyProperty.Register(nameof(TopText), typeof(string), typeof(Badge), new PropertyMetadata(string.Empty,
|
||||
format_text((b, v) => b._top_text = v)));
|
||||
|
||||
public string BottomText {
|
||||
get { return (string)GetValue(BottomTextProperty); }
|
||||
set { SetValue(BottomTextProperty, value); }
|
||||
public Badge() {
|
||||
_gcr.min_width = MinWidth;
|
||||
_gcr.width = Width;
|
||||
_gcr.max_width = MaxWidth;
|
||||
_gcr.min_height = MinHeight;
|
||||
_gcr.height = Height;
|
||||
_gcr.max_height = Height;
|
||||
_gcr.padding = Padding;
|
||||
_gcr.border_thickness = BorderThickness;
|
||||
_gcr.horizontal_alignment = HorizontalAlignment;
|
||||
_gcr.horizontal_content_alignment = HorizontalContentAlignment;
|
||||
_gcr.vertical_alignment = VerticalAlignment;
|
||||
_gcr.vertical_content_alignment = VerticalContentAlignment;
|
||||
_background = Background;
|
||||
_foreground = Foreground;
|
||||
_border_brush = BorderBrush;
|
||||
_gcr.font_size = FontSize;
|
||||
_gcr.text_lines = TextLines;
|
||||
_gcr.glyph_context = GlyphContext.create(FontFamily, FontWeight);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty BottomTextProperty =
|
||||
DependencyProperty.Register(nameof(BottomText), typeof(string), typeof(Badge), new PropertyMetadata(string.Empty,
|
||||
format_text((b,v)=>b._bottom_text=v)));
|
||||
public static readonly DependencyProperty TextLinesProperty =
|
||||
WPFHelper.create_dp<IReadOnlyList<string>, Badge>(
|
||||
nameof(TextLines),
|
||||
FrameworkPropertyMetadataOptions.AffectsMeasure |
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
(b, v) => b._gcr.text_lines = v,
|
||||
new string[0]);
|
||||
|
||||
public IReadOnlyList<string> TextLines {
|
||||
get => GetValue(TextLinesProperty) as IReadOnlyList<string>;
|
||||
set => SetValue(TextLinesProperty, value);
|
||||
}
|
||||
|
||||
public double CornerRadius {
|
||||
get { return (double)GetValue(CornerRadiusProperty); }
|
||||
set { SetValue(CornerRadiusProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty CornerRadiusProperty =
|
||||
DependencyProperty.Register("CornerRadius", typeof(double), typeof(Badge), new PropertyMetadata(3.0));
|
||||
WPFHelper.create_dp<double, Badge>(nameof(CornerRadius), DefaultCornerRadius);
|
||||
|
||||
static PropertyChangedCallback format_text(Action<Badge,FormattedText> f) {
|
||||
return (d, e) => {
|
||||
Badge b = (Badge)d;
|
||||
ensure_fontface(b);
|
||||
string text = e.NewValue as string;
|
||||
if (string.IsNullOrEmpty(text)) f(b, null);
|
||||
f(b, new FormattedText(text,
|
||||
System.Globalization.CultureInfo.CurrentCulture,
|
||||
FlowDirection.LeftToRight,
|
||||
b._face, b.FontSize, b.Foreground, _dpi));
|
||||
b.InvalidateVisual();
|
||||
};
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
_gcr.available_size = availableSize;
|
||||
return _gcr.layout_size;
|
||||
}
|
||||
static FormattedText format_text(Badge badge, DependencyProperty source) {
|
||||
FormattedText result = null;
|
||||
var value = badge.GetValue(source);
|
||||
format_text((b, v) => result = v)(badge, new DependencyPropertyChangedEventArgs(source, value, value));
|
||||
return result;
|
||||
}
|
||||
|
||||
static void update_text(DependencyObject d, DependencyPropertyChangedEventArgs args) {
|
||||
// Force formatted text regeneration when font/color changes
|
||||
Badge badge = (Badge)d;
|
||||
badge._face = null;
|
||||
ensure_fontface(badge);
|
||||
badge._center_text = format_text(badge, TextProperty);
|
||||
badge._top_text = format_text(badge, TopTextProperty);
|
||||
badge._bottom_text = format_text(badge, BottomTextProperty);
|
||||
}
|
||||
|
||||
private static void ensure_fontface(Badge b) {
|
||||
b._face = b._face ?? new Typeface(b.FontFamily, b.FontStyle, b.FontWeight, b.FontStretch);
|
||||
}
|
||||
|
||||
//protected override Size MeasureOverride(Size availableSize) {
|
||||
// Double calc_height = 0.0;
|
||||
// Double calc_width = 0.0;
|
||||
// Size size = new Size(0, 0);
|
||||
|
||||
// Size center_size = new Size(_center_text.Width, _center_text.Height);
|
||||
// Size top_size = new Size(_top_text.Width, _top_text.Height);
|
||||
// Size bottom_size = new Size(_bottom_text.Width, _bottom_text.Height);
|
||||
|
||||
// calc_width = Math.Max(center_size.Width, top_size.Width);
|
||||
// calc_width = Math.Max(calc_width, bottom_size.Width);
|
||||
|
||||
// calc_height += center_size.Height;
|
||||
// calc_height += top_size.Height;
|
||||
// calc_height += bottom_size.Height;
|
||||
|
||||
// size.Width = calc_width;
|
||||
// size.Height = calc_height;
|
||||
|
||||
// return size;
|
||||
|
||||
//}
|
||||
|
||||
protected override void OnRender(DrawingContext dc) {
|
||||
base.OnRender(dc);
|
||||
double border_thickness = BorderThickness.Top;
|
||||
var pen = new Pen(BorderBrush, border_thickness);
|
||||
dc.DrawRoundedRectangle(Background, pen, new Rect(0, 0 + border_thickness, ActualWidth, ActualHeight - 2 * border_thickness), CornerRadius, CornerRadius);
|
||||
if (_center_text != null)
|
||||
dc.DrawText(_center_text, new Point((ActualWidth-_center_text.Width)/2, (ActualHeight-_center_text.Height)/2));
|
||||
if (_top_text != null)
|
||||
dc.DrawText(_top_text, new Point(border_thickness, border_thickness));
|
||||
if (_bottom_text != null)
|
||||
dc.DrawText(_bottom_text, new Point(border_thickness, ActualHeight - _bottom_text.Height - border_thickness));
|
||||
dc.DrawRoundedRectangle(Background, pen, new Rect(0, border_thickness, _gcr.layout_size.Width, _gcr.layout_size.Height), CornerRadius, CornerRadius);
|
||||
var gr = _gcr.glyph_run;
|
||||
if (gr != null)
|
||||
dc.DrawGlyphRun(_foreground, gr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BadgeTextLinesConverter : IMultiValueConverter {
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (ObjectHelper.try_convert_args(values, out Side side, out uint quantity, out uint filled, out mbts.spark.LadderSettings.BadgeStyle style))
|
||||
return BadgeRender.format(side, quantity, filled, style);
|
||||
else return new string[0];
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ namespace mbts.ui.controls {
|
||||
Disposable.dispose(ref timer._trigger_sub);
|
||||
IObservable<object> newval = args.NewValue as IObservable<object>;
|
||||
if (newval != null) {
|
||||
timer._trigger_sub = newval.ObserveOnDispatcher().Subscribe(timer.start);
|
||||
timer._trigger_sub = newval.OnDispatcher().Subscribe(timer.start);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using mbts.spark.filters;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -60,13 +61,19 @@ namespace mbts.ui.controls {
|
||||
WPFHelper.create_dp<ListSortDirection?, ColumnBase>(
|
||||
nameof(SortDirection), (o, v) => o.OnSortDirectionChanged(v));
|
||||
|
||||
public ListSortDirection? SortDirection { get => GetValue(SortDirectionProperty) as ListSortDirection?; set => SetValue(SortDirectionProperty, value); }
|
||||
public ListSortDirection? SortDirection {
|
||||
get => GetValue(SortDirectionProperty) as ListSortDirection?;
|
||||
set => SetValue(SortDirectionProperty, value);
|
||||
}
|
||||
|
||||
public event Action<ColumnBase, ListSortDirection?> sort_direction_changed;
|
||||
|
||||
public string sort_member_path { get; set; }
|
||||
public double original_width { get; set; }
|
||||
public bool is_collapsed { get; set; }
|
||||
|
||||
protected virtual void OnSortDirectionChanged(ListSortDirection? new_value) { }
|
||||
protected virtual void OnSortDirectionChanged(ListSortDirection? new_value) =>
|
||||
sort_direction_changed?.Invoke(this, new_value);
|
||||
}
|
||||
|
||||
internal abstract class ColumnBase<TColKey> : ColumnBase, IKeyedColumn<TColKey> {
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace mbts.spark.filters {
|
||||
) => new DynamicItemsFilter<TRow, TValue, TState>(items, get_row_val, get_state, load_state, prop_name, equality_comparer, comparer);
|
||||
|
||||
public static IDataGridColumnPersistentFilter<TRow, TState> Create<TRow, TValue, TState>(
|
||||
IObservable<(IReadOnlyList<TValue> added, IReadOnlyList<TValue> removed)> items,
|
||||
IObservable<(IEnumerable<TValue> added, IEnumerable<TValue> removed)> items,
|
||||
Func<TRow, TValue> get_row_val,
|
||||
Func<ISet<TValue>, TState> get_state,
|
||||
Func<TState, IObservable<IEnumerable<TValue>>> load_state,
|
||||
@@ -115,14 +115,14 @@ namespace mbts.spark.filters {
|
||||
IComparer<TValue> comparer = null
|
||||
) : this(get_row_val, get_state, load_state, prop_name, equality_comparer, comparer) {
|
||||
//
|
||||
_items_sub = items.ObserveOnDispatcher().Subscribe(new_items => {
|
||||
_items_sub = items.OnDispatcher().Subscribe(new_items => {
|
||||
_items_set.SetItems(new_items.Concat(_selected_items_set));
|
||||
_unsorted_items.sync_with(_items_set);
|
||||
});
|
||||
}
|
||||
|
||||
public DynamicItemsFilter(
|
||||
IObservable<(IReadOnlyList<TValue> added, IReadOnlyList<TValue> removed)> items,
|
||||
IObservable<(IEnumerable<TValue> added, IEnumerable<TValue> removed)> items,
|
||||
Func<TRow, TValue> get_row_val,
|
||||
Func<ISet<TValue>, TState> get_state,
|
||||
Func<TState, IObservable<IEnumerable<TValue>>> load_state,
|
||||
@@ -131,7 +131,7 @@ namespace mbts.spark.filters {
|
||||
IComparer<TValue> comparer = null
|
||||
) : this(get_row_val, get_state, load_state, prop_name, equality_comparer, comparer) {
|
||||
//
|
||||
_items_sub = items.ObserveOnDispatcher().Subscribe(update => {
|
||||
_items_sub = items.OnDispatcher().Subscribe(update => {
|
||||
foreach (var v in update.added)
|
||||
_items_set.Add(v);
|
||||
foreach (var v in update.removed.Except(_selected_items_set))
|
||||
@@ -182,7 +182,7 @@ namespace mbts.spark.filters {
|
||||
|
||||
protected override void _load_state_internal(TState state) {
|
||||
Disposable.dispose(ref _desired_items_sub);
|
||||
_desired_items_sub = _load_state(state).ObserveOnDispatcher().Subscribe(_on_desired_item);
|
||||
_desired_items_sub = _load_state(state).OnDispatcher().Subscribe(_on_desired_item);
|
||||
}
|
||||
|
||||
protected override void _apply() {
|
||||
|
||||
@@ -54,8 +54,9 @@ namespace mbts.ui {
|
||||
public static TimeTextTuple FromTicks(long ticks) =>
|
||||
FromDateTime(new DateTime(ticks));
|
||||
|
||||
public static TimeTextTuple FromDateTime(DateTime date_time) =>
|
||||
new TimeTextTuple(DateTimeFormatInfo.CurrentInfo.AbbreviatedMonthNames[date_time.Month - 1], date_time.Day.ToString(), date_time.Year.ToString(), date_time.ToString("t"));
|
||||
public static TimeTextTuple FromDateTime(DateTime date_time) => FromDateTime(date_time, "t");
|
||||
public static TimeTextTuple FromDateTime(DateTime date_time, string time_format) =>
|
||||
new TimeTextTuple(DateTimeFormatInfo.CurrentInfo.AbbreviatedMonthNames[date_time.Month - 1], date_time.Day.ToString(), date_time.Year.ToString(), date_time.ToString(time_format));
|
||||
|
||||
public TimeTextTuple(string month_text, string day_text, string year_text, string time_text) {
|
||||
_month_text = month_text;
|
||||
@@ -107,14 +108,23 @@ namespace mbts.ui {
|
||||
|
||||
|
||||
static DateTime? _parse(string month_text, string day_text, string year_text, string time_text) {
|
||||
DateTime? t = _parse(time_text);
|
||||
TimeSpan? t = _parse_time(time_text);
|
||||
if (t.HasValue) {
|
||||
DateTime? d = _parse($"{month_text}/{day_text}/{year_text}");
|
||||
if (d.HasValue) return d.Value.Date.AddDays(t.Value.TimeOfDay.TotalDays);
|
||||
if (d.HasValue) return d.Value.Date.Add(t.Value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static TimeSpan? _parse_time(string text) {
|
||||
DateTime? dt = _parse(text);
|
||||
if (dt.HasValue) {
|
||||
return dt.Value.TimeOfDay;
|
||||
} else if (TimeSpan.TryParse(text, out TimeSpan result) && result < TimeSpan.FromDays(1)) {
|
||||
return result;
|
||||
} else return null;
|
||||
}
|
||||
|
||||
static DateTime? _parse(string text) {
|
||||
if (!string.IsNullOrEmpty(text) && DateTime.TryParse(text, out DateTime result))
|
||||
return result;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
ItemTemplate="{StaticResource EnumDescriptionTemplate}"
|
||||
SelectedItem="{Binding Mode, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
/>
|
||||
<Grid x:Name="PeriodRoot" Grid.Row="1" Margin="0,6,0,0" IsEnabled="False">
|
||||
<Grid x:Name="PeriodRoot" Grid.Row="1" Margin="24,6,0,0" IsEnabled="False">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="4"/>
|
||||
|
||||
@@ -73,8 +73,9 @@ namespace mbts.ui.controls {
|
||||
public enum TimeFilterMode {
|
||||
Anytime,
|
||||
Today,
|
||||
[Description("Period:")]
|
||||
Period,
|
||||
[Description("Glbx Open")]
|
||||
[Description("Since Open")]
|
||||
CMEOpen
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
<ResourceDictionary Source="/view/Controls/PriceEntryControl.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/Popup/PopupControl.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/Time/TimeSpanControl.xaml"/>
|
||||
<ResourceDictionary Source="/view/Controls/Order/QuantityMeasureControl.xaml"/>
|
||||
<ResourceDictionary Source="/Modules/Spreader/ExecutionStrategyEditor.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<Style TargetType="uag:OrderDetailsPopupControl">
|
||||
@@ -155,6 +156,186 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:OrderModificationTemplateSelector.TimeBoxOrderTemplate>
|
||||
<usp:OrderModificationTemplateSelector.TickOrderTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="1" VerticalAlignment="Center">Cross Thresh</TextBlock>
|
||||
<uag:QuantityMeasureControl Grid.Row="1" Grid.Column="2" QuantityMeasure="{Binding cross_threshold}"/>
|
||||
|
||||
<TextBlock Grid.Row="3" VerticalAlignment="Center">Cross Ticks</TextBlock>
|
||||
<c:TickUpDownUInt Grid.Row="3" Grid.Column="2" Value="{Binding cross_ticks}"/>
|
||||
<TextBlock Grid.Row="5" VerticalAlignment="Center">Max Chase Ticks</TextBlock>
|
||||
<c:TickUpDownUInt Grid.Row="5" Grid.Column="2" Value="{Binding max_chase_ticks}"/>
|
||||
<CheckBox Grid.Row="7" Grid.Column="2" VerticalAlignment="Center" Margin="0,0,0,0" IsChecked="{Binding detach_on_toward}">Detach on Toward</CheckBox>
|
||||
<TextBlock Grid.Row="9" VerticalAlignment="Center">Initial Tick Offset</TextBlock>
|
||||
<c:TickUpDownUInt Grid.Row="9" Grid.Column="2" Value="{Binding tick_offset_on_deploy}"/>
|
||||
<TextBlock Grid.Row="11">Child</TextBlock>
|
||||
<uag:OrderDetailsPopupControl
|
||||
Grid.Row="11" Grid.Column="2"
|
||||
IsOpen="{Binding child_order.editable_properties_open, FallbackValue=False}"
|
||||
ItemsSource="{Binding child_order_types}"
|
||||
SelectedItem="{Binding child_order}"
|
||||
HasEditableProperties="{Binding child_order.has_editable_properties, FallbackValue=False}"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:OrderModificationTemplateSelector.TickOrderTemplate>
|
||||
<usp:OrderModificationTemplateSelector.LeanOrderTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="1" VerticalAlignment="Center">Cross Thresh</TextBlock>
|
||||
<uag:QuantityMeasureControl Grid.Row="1" Grid.Column="2" QuantityMeasure="{Binding cross_threshold}"/>
|
||||
<TextBlock Grid.Row="3" VerticalAlignment="Center">Max Chase Ticks</TextBlock>
|
||||
<c:TickUpDownUInt Grid.Row="3" Grid.Column="2" Value="{Binding max_chase_ticks}"/>
|
||||
<TextBlock Grid.Row="5">Child</TextBlock>
|
||||
<uag:OrderDetailsPopupControl
|
||||
Grid.Row="5" Grid.Column="2"
|
||||
IsOpen="{Binding child_order.editable_properties_open, FallbackValue=False}"
|
||||
ItemsSource="{Binding child_order_types}"
|
||||
SelectedItem="{Binding child_order}"
|
||||
HasEditableProperties="{Binding child_order.has_editable_properties, FallbackValue=False}"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:OrderModificationTemplateSelector.LeanOrderTemplate>
|
||||
<usp:OrderModificationTemplateSelector.JoinOrderTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="1" VerticalAlignment="Center">Join Thresh</TextBlock>
|
||||
<uag:QuantityMeasureControl Grid.Row="1" Grid.Column="2" QuantityMeasure="{Binding join_threshold}"/>
|
||||
<CheckBox Grid.Row="3" Grid.Column="2" VerticalAlignment="Center" Margin="0,0,0,0" IsChecked="{Binding enable_hedge}">Enable Hedge</CheckBox>
|
||||
<TextBlock Grid.Row="5" VerticalAlignment="Center" IsEnabled="{Binding enable_hedge}" >Hedge Ticks</TextBlock>
|
||||
<c:TickUpDownInt Grid.Row="5" Grid.Column="2" IsEnabled="{Binding enable_hedge}" Value="{Binding hedge_ticks}"/>
|
||||
<TextBlock Grid.Row="7">Hedge</TextBlock>
|
||||
<uag:OrderDetailsPopupControl
|
||||
Grid.Row="7" Grid.Column="2"
|
||||
IsEnabled="{Binding enable_hedge}"
|
||||
IsOpen="{Binding hedge_order.editable_properties_open, FallbackValue=False}"
|
||||
ItemsSource="{Binding hedge_order_types}"
|
||||
SelectedItem="{Binding hedge_order}"
|
||||
HasEditableProperties="{Binding hedge_order.has_editable_properties, FallbackValue=False}"
|
||||
/>
|
||||
<TextBlock Grid.Row="9" VerticalAlignment="Center">Initial Tick Offset</TextBlock>
|
||||
<c:TickUpDownUInt Grid.Row="9" Grid.Column="2" Value="{Binding tick_offset_on_deploy}"/>
|
||||
<TextBlock Grid.Row="11">Child</TextBlock>
|
||||
<uag:OrderDetailsPopupControl
|
||||
Grid.Row="11" Grid.Column="2"
|
||||
IsOpen="{Binding child_order.editable_properties_open, FallbackValue=False}"
|
||||
ItemsSource="{Binding child_order_types}"
|
||||
SelectedItem="{Binding child_order}"
|
||||
HasEditableProperties="{Binding child_order.has_editable_properties, FallbackValue=False}"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:OrderModificationTemplateSelector.JoinOrderTemplate>
|
||||
<usp:OrderModificationTemplateSelector.TopOrderTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="1" VerticalAlignment="Center">Top Thresh</TextBlock>
|
||||
<uag:QuantityMeasureControl Grid.Row="1" Grid.Column="2" QuantityMeasure="{Binding top_threshold}"/>
|
||||
<CheckBox Grid.Row="3" Grid.Column="2" VerticalAlignment="Center" Margin="0,0,0,0" IsChecked="{Binding enable_hedge}">Enable Hedge</CheckBox>
|
||||
<TextBlock Grid.Row="5" VerticalAlignment="Center" IsEnabled="{Binding enable_hedge}" >Hedge Ticks</TextBlock>
|
||||
<c:TickUpDownInt Grid.Row="5" Grid.Column="2" IsEnabled="{Binding enable_hedge}" Value="{Binding hedge_ticks}"/>
|
||||
<TextBlock Grid.Row="7">Hedge</TextBlock>
|
||||
<uag:OrderDetailsPopupControl
|
||||
Grid.Row="7" Grid.Column="2"
|
||||
IsEnabled="{Binding enable_hedge}"
|
||||
IsOpen="{Binding hedge_order.editable_properties_open, FallbackValue=False}"
|
||||
ItemsSource="{Binding hedge_order_types}"
|
||||
SelectedItem="{Binding hedge_order}"
|
||||
HasEditableProperties="{Binding hedge_order.has_editable_properties, FallbackValue=False}"
|
||||
/>
|
||||
<TextBlock Grid.Row="9" VerticalAlignment="Center">Watch Levels</TextBlock>
|
||||
<c:TickUpDownUInt Grid.Row="9" Grid.Column="2" Value="{Binding watch_level_tick_offset}"/>
|
||||
<TextBlock Grid.Row="11">Child</TextBlock>
|
||||
<uag:OrderDetailsPopupControl
|
||||
Grid.Row="11" Grid.Column="2"
|
||||
IsOpen="{Binding child_order.editable_properties_open, FallbackValue=False}"
|
||||
ItemsSource="{Binding child_order_types}"
|
||||
SelectedItem="{Binding child_order}"
|
||||
HasEditableProperties="{Binding child_order.has_editable_properties, FallbackValue=False}"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:OrderModificationTemplateSelector.TopOrderTemplate>
|
||||
<usp:OrderModificationTemplateSelector.IcebergOrderTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
@@ -180,21 +361,31 @@
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:OrderModificationTemplateSelector.IcebergOrderTemplate>
|
||||
</usp:OrderModificationTemplateSelector.IcebergOrderTemplate>
|
||||
<usp:OrderModificationTemplateSelector.SpreaderTemplate>
|
||||
<DataTemplate>
|
||||
<usp:ExecutionStrategyEditor
|
||||
HorizontalAlignment="Stretch" Margin="6"
|
||||
DataContext="{Binding strategy_editor}"
|
||||
SyntheticName="{Binding name}"
|
||||
IsNew="{Binding is_new}"
|
||||
IsNewLegPopupOpen="{Binding is_new_leg_popup_open}"
|
||||
PriceCalc="{Binding price_calc}"
|
||||
ParameterRows="{Binding rows}"
|
||||
LegTuples="{Binding leg_tuples}"
|
||||
RemoveLegCommand="{Binding remove_leg}"
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
/>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<usp:ExecutionStrategyEditor
|
||||
Margin="6"
|
||||
HorizontalAlignment="Stretch"
|
||||
DataContext="{Binding strategy_editor}"
|
||||
SyntheticName="{Binding name}"
|
||||
TickFactors="{Binding tick_factors}"
|
||||
AvailableTickFactors="{Binding available_tick_factors_view}"
|
||||
IsNew="False"
|
||||
IsNewLegPopupOpen="{Binding is_new_leg_popup_open}"
|
||||
PriceCalc="{Binding price_calc}"
|
||||
ParameterRows="{Binding rows}"
|
||||
LegTuples="{Binding leg_tuples}"
|
||||
RemoveLegCommand="{Binding remove_leg}"
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
/>
|
||||
<c:DialogSkirt Grid.Row="1" Margin="-4" RightItems="{Binding skirt_items}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:OrderModificationTemplateSelector.SpreaderTemplate>
|
||||
</usp:OrderModificationTemplateSelector>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<c:Badge Background="{Binding Metadata, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource OrderTypeBrushConverter}}" Width="15" Height="10" HorizontalAlignment="Left" />
|
||||
<c:Badge Background="{Binding Metadata, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource OrderTypeBrushConverter}}" BorderThickness="1" Width="15" Height="10" HorizontalAlignment="Left" />
|
||||
<TextBlock Text="{Binding OrderType, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource OrderTypeDisplayTextConverter}}" Margin="20,0,0,0" HorizontalAlignment="Left" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -28,7 +28,14 @@
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TickDisplayTwoWayConverter}, Mode=TwoWay}"
|
||||
/>
|
||||
>
|
||||
<TextBox.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:TextBoxBehavior.SelectAllCommand}" Executed="{x:Static u:TextBoxBehavior.SelectAllCommandExecutedHandler}"/>
|
||||
</TextBox.CommandBindings>
|
||||
<TextBox.InputBindings>
|
||||
<MouseBinding MouseAction="LeftDoubleClick" Command="{x:Static u:TextBoxBehavior.SelectAllCommand}"/>
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
</c:TickUpDown>
|
||||
</ControlTemplate>
|
||||
<Style TargetType="usp:PriceEntryControl">
|
||||
|
||||
@@ -47,24 +47,30 @@ namespace mbts.ui.spark {
|
||||
#endregion
|
||||
|
||||
TextBox _text;
|
||||
IDisposable _text_input_hook;
|
||||
|
||||
public PriceEntryControl() {
|
||||
TickUpCommand = new DelegateCommand(_tick_up);
|
||||
TickDownCommand = new DelegateCommand(_tick_down);
|
||||
}
|
||||
|
||||
IDisposable _keyboard_focus_sub;
|
||||
bool _has_keyboard_focus;
|
||||
|
||||
public override void OnApplyTemplate() {
|
||||
Disposable.dispose(ref _keyboard_focus_sub);
|
||||
_text = this.assert_template_child<TextBox>("_text");
|
||||
Disposable.dispose(ref _text_input_hook);
|
||||
_text_input_hook = _text.SubscribePreviewTextInput((s, e) => e.Handled = !RegexFilters.PriceInput.IsMatch(_text.PeekText(e)));
|
||||
_keyboard_focus_sub = _text.SubscribeKeyboardFocus(val => _has_keyboard_focus = val);
|
||||
_update_text_binding();
|
||||
}
|
||||
|
||||
void _tick_up() {
|
||||
if (_has_keyboard_focus) {
|
||||
_text.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
|
||||
}
|
||||
double v = Value;
|
||||
if (double.IsNaN(v)) SetCurrentValue(ValueProperty, TickDefaultValue);
|
||||
else {
|
||||
if (double.IsNaN(v)) {
|
||||
SetCurrentValue(ValueProperty, TickDefaultValue);
|
||||
} else {
|
||||
var spec = ContractSpec;
|
||||
if (spec.HasValue) SetCurrentValue(ValueProperty, spec.Value.tickup(v));
|
||||
else SetCurrentValue(ValueProperty, TickDefaultValue);
|
||||
@@ -72,9 +78,13 @@ namespace mbts.ui.spark {
|
||||
}
|
||||
|
||||
void _tick_down() {
|
||||
if (_has_keyboard_focus) {
|
||||
_text.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
|
||||
}
|
||||
double v = Value;
|
||||
if (double.IsNaN(v)) SetCurrentValue(ValueProperty, TickDefaultValue);
|
||||
else {
|
||||
if (double.IsNaN(v)) {
|
||||
SetCurrentValue(ValueProperty, TickDefaultValue);
|
||||
} else {
|
||||
var spec = ContractSpec;
|
||||
if (spec.HasValue) SetCurrentValue(ValueProperty, spec.Value.tickdown(v));
|
||||
else SetCurrentValue(ValueProperty, TickDefaultValue);
|
||||
|
||||
@@ -23,38 +23,42 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
|
||||
protected override void OnPreviewKeyDown(KeyEventArgs e) {
|
||||
switch (Keyboard.Modifiers) {
|
||||
case ModifierKeys.None: {
|
||||
switch (e.Key) {
|
||||
case Key.Up:
|
||||
TickUpCommand.TryExecuteIfCan(TickUpParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Down:
|
||||
TickDownCommand.TryExecuteIfCan(TickDownParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
if (EnableArrowKeyTick) {
|
||||
switch (Keyboard.Modifiers) {
|
||||
case ModifierKeys.None: {
|
||||
switch (e.Key) {
|
||||
case Key.Up:
|
||||
TickUpCommand.TryExecuteIfCan(TickUpParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Down:
|
||||
TickDownCommand.TryExecuteIfCan(TickDownParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ModifierKeys.Alt: {
|
||||
// see: https://stackoverflow.com/questions/3099472/previewkeydown-is-not-seeing-alt-modifiers
|
||||
switch (e.SystemKey) {
|
||||
case Key.Up:
|
||||
TickUpCommand.TryExecuteIfCan(TickUpAltParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Down:
|
||||
TickDownCommand.TryExecuteIfCan(TickDownAltParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
break;
|
||||
case ModifierKeys.Alt: {
|
||||
// see: https://stackoverflow.com/questions/3099472/previewkeydown-is-not-seeing-alt-modifiers
|
||||
switch (e.SystemKey) {
|
||||
case Key.Up:
|
||||
TickUpCommand.TryExecuteIfCan(TickUpAltParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Down:
|
||||
TickDownCommand.TryExecuteIfCan(TickDownAltParameter);
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
base.OnPreviewKeyDown(e);
|
||||
}
|
||||
|
||||
public bool EnableArrowKeyTick { get; set; } = true;
|
||||
|
||||
#region TickCommand
|
||||
public ICommand TickCommand {
|
||||
get { return (ICommand)GetValue(TickCommandProperty); }
|
||||
|
||||
@@ -31,8 +31,14 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<c:TickUpDown x:Name="PART_time_tick" Grid.Column="0" MinWidth="85" HorizontalAlignment="Stretch">
|
||||
<TextBox x:Name="PART_time_text" Height="20" u:TextBoxBehavior.EnableArrowKeyTraverse="True" u:Behavior.DisablePaste="True"/>
|
||||
<c:TickUpDown x:Name="PART_time_tick" Grid.Column="0" MinWidth="85" HorizontalAlignment="Stretch" EnableArrowKeyTick="False">
|
||||
<TextBox x:Name="PART_time_text" Height="20" u:TextBoxBehavior.EnableArrowKeyTraverse="True" u:Behavior.DisablePaste="True">
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static c:DateTimePicker.TickTimeComponentCommand}" CommandParameter="{u:Int 1}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static c:DateTimePicker.TickTimeComponentCommand}" CommandParameter="{u:Int -1}"/>
|
||||
<u:CustomInputBinding Gesture="{x:Static u:InputGestures.LeftTripleClick}" Command="{x:Static u:TextBoxBehavior.SelectAllCommand}"/>
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
</c:TickUpDown>
|
||||
<c:TickUpDown x:Name="PART_month_tick" Grid.Column="2" Width="50">
|
||||
<TextBox x:Name="PART_month_text" Height="20" u:TextBoxBehavior.EnableArrowKeyTraverse="True" u:Behavior.DisablePaste="True"/>
|
||||
|
||||
@@ -3,6 +3,7 @@ using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
@@ -14,7 +15,9 @@ namespace mbts.ui.controls {
|
||||
/// Interaction logic for DateTimePicker.xaml
|
||||
/// </summary>
|
||||
public class DateTimePicker : Control {
|
||||
TimeTupleVM _vm;
|
||||
public static readonly RoutedCommand TickTimeComponentCommand = new RoutedCommand();
|
||||
|
||||
DTPController _controller;
|
||||
bool _suspend_notify_vm;
|
||||
|
||||
static DateTimePicker() =>
|
||||
@@ -24,8 +27,8 @@ namespace mbts.ui.controls {
|
||||
new FrameworkPropertyMetadata(default(TimeTextTuple), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnTimeTupleChanged));
|
||||
static void OnTimeTupleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
var dtp = (DateTimePicker)d;
|
||||
if (!dtp._suspend_notify_vm && dtp._vm != null)
|
||||
dtp._vm.result = (TimeTextTuple)e.NewValue;
|
||||
if (!dtp._suspend_notify_vm && dtp._controller != null)
|
||||
dtp._controller.result = (TimeTextTuple)e.NewValue;
|
||||
}
|
||||
public TimeTextTuple TimeTuple { get => (TimeTextTuple)GetValue(TimeTupleProperty); set => SetValue(TimeTupleProperty, value); }
|
||||
|
||||
@@ -41,59 +44,74 @@ namespace mbts.ui.controls {
|
||||
var time = this.assert_template_child<TickUpDown>("PART_time_tick");
|
||||
var time_text = this.assert_template_child<TextBox>("PART_time_text");
|
||||
|
||||
if (_vm != null) _vm.PropertyChanged -= _vm_PropertyChanged;
|
||||
_vm = new TimeTupleVM();
|
||||
_vm.result = TimeTuple;
|
||||
_vm.PropertyChanged += _vm_PropertyChanged;
|
||||
if (_controller != null) _controller.PropertyChanged -= _vm_PropertyChanged;
|
||||
_controller = new DTPController(time_text);
|
||||
_controller.result = TimeTuple;
|
||||
_controller.PropertyChanged += _vm_PropertyChanged;
|
||||
time.CommandBindings.Add(new CommandBinding(TickTimeComponentCommand, (s, e) => {
|
||||
_controller.tick_time_component((int)e.Parameter);
|
||||
}));
|
||||
time.CommandBindings.Add(new CommandBinding(TextBoxBehavior.SelectAllCommand, TextBoxBehavior.SelectAllCommandExecutedHandler));
|
||||
|
||||
const int delay = 500;
|
||||
time.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_time") { Source = _vm });
|
||||
time.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_time") { Source = _controller });
|
||||
time.TickUpParameter = 1;
|
||||
time.TickDownParameter = -1;
|
||||
time.TickUpAltParameter = 60;
|
||||
time.TickDownAltParameter = -60;
|
||||
time_text.SetBinding(TextBox.TextProperty, new Binding("time_text") { Source = _vm, Delay = delay, ValidatesOnExceptions = true });
|
||||
_hooks.Add(time_text.SubscribePreviewTextInput((s, e) =>
|
||||
e.Handled = !MatchAny(time_text.PeekText(e), RegexFilters.Time.StandardTime, RegexFilters.Time.MilitaryTime)
|
||||
));
|
||||
time_text.SetBinding(TextBox.TextProperty, new Binding("time_text") { Source = _controller, Delay = delay, ValidatesOnExceptions = true });
|
||||
_hooks.Add(time_text.SubscribePreviewTextInput((s, e) => {
|
||||
string preview = time_text.PeekText(e);
|
||||
if (RegexFilters.Time.StandardTimeOptionalSeconds.IsMatch(preview)) {
|
||||
_controller.use_military_time = false;
|
||||
} else if (RegexFilters.Time.MilitaryTimeOptionalSeconds.IsMatch(preview)) {
|
||||
_controller.use_military_time = true;
|
||||
} else {
|
||||
e.Handled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
month.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_month") { Source = _vm });
|
||||
month.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_month") { Source = _controller });
|
||||
month.TickUpParameter = 1;
|
||||
month.TickDownParameter = -1;
|
||||
month_text.SetBinding(TextBox.TextProperty, new Binding("month_text") { Source = _vm, Delay = delay });
|
||||
month_text.SetBinding(TextBox.TextProperty, new Binding("month_text") { Source = _controller, Delay = delay });
|
||||
_hooks.Add(month_text.SubscribePreviewTextInput((s, e) =>
|
||||
e.Handled = !RegexFilters.Time.MonthAbbrev.IsMatch(month_text.PeekText(e))
|
||||
));
|
||||
|
||||
day.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_day") { Source = _vm });
|
||||
day.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_day") { Source = _controller });
|
||||
day.TickUpParameter = 1;
|
||||
day.TickDownParameter = -1;
|
||||
day_text.SetBinding(TextBox.TextProperty, new Binding("day_text") { Source = _vm, Delay = delay });
|
||||
day_text.SetBinding(TextBox.TextProperty, new Binding("day_text") { Source = _controller, Delay = delay });
|
||||
_hooks.Add(day_text.SubscribePreviewTextInput((s, e) =>
|
||||
e.Handled = !RegexFilters.Time.DaysInMonth.IsMatch(day_text.PeekText(e))
|
||||
));
|
||||
|
||||
year.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_year") { Source = _vm });
|
||||
year.SetBinding(TickUpDown.TickCommandProperty, new Binding("tick_year") { Source = _controller });
|
||||
year.TickUpParameter = 1;
|
||||
year.TickDownParameter = -1;
|
||||
year_text.SetBinding(TextBox.TextProperty, new Binding("year_text") { Source = _vm, Delay = delay });
|
||||
year_text.SetBinding(TextBox.TextProperty, new Binding("year_text") { Source = _controller, Delay = delay });
|
||||
_hooks.Add(year_text.SubscribePreviewTextInput((s, e) =>
|
||||
e.Handled = !RegexFilters.Time.Year.IsMatch(year_text.PeekText(e))
|
||||
));
|
||||
}
|
||||
|
||||
void _vm_PropertyChanged(object sender, PropertyChangedEventArgs e) {
|
||||
if (e.PropertyName == nameof(TimeTupleVM.result)) {
|
||||
if (e.PropertyName == nameof(DTPController.result)) {
|
||||
_suspend_notify_vm = true;
|
||||
TimeTuple = _vm.result;
|
||||
TimeTuple = _controller.result;
|
||||
_suspend_notify_vm = false;
|
||||
}
|
||||
}
|
||||
|
||||
private class TimeTupleVM : ViewModelBase {
|
||||
// This is not exactly a view-model, bc it accesses the time TextBox directly so it can get/set the CaretIndex,
|
||||
// which is used to determine which time-componenet - hh:mm:ss AM/PM - should be incremented/decremented via the up/down arrow-keys.
|
||||
class DTPController : ViewModelBase {
|
||||
readonly TextBox _time_text_box;
|
||||
TimeTextTuple _result;
|
||||
|
||||
public TimeTupleVM() {
|
||||
public DTPController(TextBox time_text_box) {
|
||||
_time_text_box = time_text_box;
|
||||
tick_time = new DelegateCommand<int>(_tick_time);
|
||||
tick_month = new DelegateCommand<int>(_tick_month);
|
||||
tick_day = new DelegateCommand<int>(_tick_day);
|
||||
@@ -135,12 +153,52 @@ namespace mbts.ui.controls {
|
||||
RaisePropertyChanged(nameof(month_text), nameof(day_text), nameof(year_text), nameof(time_text));
|
||||
}
|
||||
}
|
||||
|
||||
public bool use_military_time { get; set; }
|
||||
public ICommand tick_time { get; }
|
||||
public ICommand tick_month { get; }
|
||||
public ICommand tick_day { get; }
|
||||
public ICommand tick_year { get; }
|
||||
string _time_format => use_military_time ? StringFormats.Time.MilitaryWithSeconds : StringFormats.Time.StandardWithSeconds;
|
||||
|
||||
public void tick_time_component(int increment) {
|
||||
_ensure_time_text_up_to_date();
|
||||
var r = _result.result;
|
||||
if (r.HasValue) {
|
||||
int index = _time_text_box.CaretIndex;
|
||||
using (detect_property_changes()) {
|
||||
DateTime dt;
|
||||
if (index <= 0) {
|
||||
dt = r.Value.AddHours(increment);
|
||||
} else {
|
||||
int segment = _time_text_box.Text.Take(index).Count(c => c == ':');
|
||||
switch (segment) {
|
||||
case 0: dt = r.Value.AddHours(increment); break;
|
||||
case 1: dt = r.Value.AddMinutes(increment); break;
|
||||
case 2:
|
||||
if (_time_text_box.Text.Take(index).Contains(' ')) {
|
||||
// toggle AM/PM
|
||||
dt = r.Value.AddHours(r.Value.Hour < 12 ? 12 : -12);
|
||||
} else {
|
||||
dt = r.Value.AddSeconds(increment);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
dt = r.Value.AddSeconds(increment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_result = TimeTextTuple.FromDateTime(dt, _time_format);
|
||||
}
|
||||
_time_text_box.CaretIndex = index;
|
||||
} else {
|
||||
using (detect_property_changes())
|
||||
_force_valid_inputs();
|
||||
}
|
||||
}
|
||||
|
||||
void _tick_time(int increment) {
|
||||
_ensure_time_text_up_to_date();
|
||||
using (detect_property_changes()) {
|
||||
var r = _result.result;
|
||||
if (r.HasValue) {
|
||||
@@ -187,6 +245,11 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _ensure_time_text_up_to_date() {
|
||||
if (_time_text_box.IsKeyboardFocused)
|
||||
_time_text_box.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
|
||||
}
|
||||
|
||||
void _force_valid_inputs() {
|
||||
var dt = DateTime.Today.Date;
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace mbts.ui.converters
|
||||
internal class EnumCharacterCasingConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
|
||||
if (value is null) return string.Empty;
|
||||
string s = value is Enum ? EnumHelper.get_display_name((Enum)value) : value.ToString();
|
||||
|
||||
CharacterCasing casing;
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace mbts.ui
|
||||
|
||||
private void _subscribe() {
|
||||
Disposable.dispose(ref _sub);
|
||||
_sub = _source.ObserveOnDispatcher().Subscribe(_on_data);
|
||||
_sub = _source.OnDispatcher().Subscribe(_on_data);
|
||||
}
|
||||
private void _on_data(T data) { Target.SetValue(_target_property, _last_value = data); }
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui {
|
||||
class CustomInputBinding : InputBinding { }
|
||||
|
||||
static class InputGestures {
|
||||
public static readonly InputGesture LeftTripleClick = new LeftTripleClickGesture();
|
||||
|
||||
class LeftTripleClickGesture : InputGesture {
|
||||
public override bool Matches(object targetElement, InputEventArgs inputEventArgs) =>
|
||||
inputEventArgs is MouseButtonEventArgs mouse_args &&
|
||||
mouse_args.ChangedButton == MouseButton.Left &&
|
||||
mouse_args.ClickCount == 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,14 @@ using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui {
|
||||
static class TextBoxBehavior {
|
||||
public static readonly RoutedCommand ReleaseCaptureCommand = new RoutedCommand();
|
||||
#region RoutedCommands/Handlers
|
||||
public static readonly RoutedCommand
|
||||
ReleaseCaptureCommand = new RoutedCommand(),
|
||||
SelectAllCommand = new RoutedCommand();
|
||||
|
||||
public static readonly ExecutedRoutedEventHandler SelectAllCommandExecutedHandler =
|
||||
new ExecutedRoutedEventHandler((s, e) => (e.OriginalSource as TextBox ?? e.Source as TextBox ?? s as TextBox)?.SelectAll());
|
||||
#endregion
|
||||
|
||||
#region SelectAllOnFocus
|
||||
|
||||
@@ -168,7 +175,7 @@ namespace mbts.ui {
|
||||
public static void SetInputFilter(DependencyObject d, TextBoxInputFilter value) => d.SetValue(InputFilterProperty, value);
|
||||
#endregion
|
||||
|
||||
#region
|
||||
#region IncrementDecrementOnMouseClick
|
||||
public static readonly DependencyProperty IncrementDecrementOnMouseClickProperty = DependencyProperty.RegisterAttached(
|
||||
"IncrementDecrementOnMouseClick", typeof(bool), typeof(TextBoxBehavior), new PropertyMetadata(false, IncrementDecrementOnMouseClickPropertyChanged)
|
||||
);
|
||||
|
||||
@@ -2,11 +2,28 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui {
|
||||
static class TextElementHelper {
|
||||
public static IDisposable SubscribeKeyboardFocus(this TextBox text_box, Action<bool> callback) {
|
||||
text_box.GotKeyboardFocus += on_got_keyboard_focus;
|
||||
text_box.LostKeyboardFocus += on_lost_keyboard_focus;
|
||||
return Disposable.Create(() => {
|
||||
text_box.GotKeyboardFocus -= on_got_keyboard_focus;
|
||||
text_box.LostKeyboardFocus -= on_lost_keyboard_focus;
|
||||
});
|
||||
void on_got_keyboard_focus(object sender, KeyboardFocusChangedEventArgs e) {
|
||||
if (e.NewFocus == text_box)
|
||||
callback(true);
|
||||
|
||||
}
|
||||
void on_lost_keyboard_focus(object sender, KeyboardFocusChangedEventArgs e) {
|
||||
if (e.OldFocus == text_box)
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
|
||||
namespace mbts.ui
|
||||
{
|
||||
public class TextElementHelper
|
||||
{
|
||||
public static void GetTargetProperty(DependencyObject d, out FrameworkElement text_element, out DependencyProperty text_property) {
|
||||
text_element = (FrameworkElement)d;
|
||||
if (text_element is TextBox) text_property = TextBox.TextProperty;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Window}">
|
||||
<Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Loaded="style_loaded" x:Name="window_border" u:WindowStyle.IsNonClientArea="true">
|
||||
<Border BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Loaded="style_loaded" x:Name="window_border" u:WindowStyle.IsNonClientArea="true">
|
||||
<Grid Margin="{TemplateBinding Margin}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20" />
|
||||
@@ -134,8 +134,8 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=WindowState, RelativeSource={RelativeSource self}}" Value="Normal">
|
||||
<Setter Property="Padding" Value="0,0,0,0" />
|
||||
<Setter Property="BorderThickness" Value="7,1,2.66,2.66" TargetName="window_border" />
|
||||
<Setter Property="Padding" Value="0,0,2,2" />
|
||||
<Setter Property="BorderThickness" Value="7,1,1,1" TargetName="window_border" />
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace mbts.ui.shell
|
||||
|
||||
public async Task<IEnumerable<string>> get_workspaces(User user) {
|
||||
if (!_cache.TryGetValue(user.id, out List<string> result)) {
|
||||
using (var svc = new UserUISettings(user.id, _persist)) {
|
||||
using (var svc = new UserUISettings(user.id, _persist, null)) {
|
||||
var workspaces = await svc.query<WorkspaceDefinition>(WorkspacesModel.WORKSPACE_NAMESPACE, ".*");
|
||||
result = workspaces.Select(w => w.key).ToList();
|
||||
_cache[user.id] = result;
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace mbts.ui.shell {
|
||||
_window_settings = settings;
|
||||
_window_factory = factory;
|
||||
_undo = undo;
|
||||
_dispose = bus.subscribe(SparkTags.ShowWindow).ObserveOnDispatcher().Subscribe(w => add(w).ignore());
|
||||
_dispose = bus.subscribe(SparkTags.ShowWindow).OnDispatcher().Subscribe(w => add(w).ignore());
|
||||
}
|
||||
|
||||
#region Window creation
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace mbts.spark
|
||||
{
|
||||
internal class ContractPickerModel : ViewModelBase {
|
||||
|
||||
private const string dot = "•";
|
||||
private const string dot = "•";
|
||||
|
||||
public ContractPickerModel(IBus bus, bool edit_mode, SecurityCriteria existing_criteria, SecurityCriteria world_view_filter = null) {
|
||||
_bus = bus;
|
||||
@@ -38,7 +38,7 @@ namespace mbts.spark
|
||||
_world_view_filter = world_view_filter;
|
||||
securities_view = CollectionViewSource.GetDefaultView(securities);
|
||||
(securities_view as ListCollectionView).CustomSort = new SecuritySorter();
|
||||
_subs.Add(_bus.subscribe(SecuritiesTag).ObserveOnDispatcher().Subscribe(on_securities_changed));
|
||||
_subs.Add(_bus.subscribe(SecuritiesTag).OnDispatcher().Subscribe(on_securities_changed));
|
||||
|
||||
add_criteria_command = new DelegateCommand(
|
||||
execute: _ => {
|
||||
@@ -197,10 +197,6 @@ namespace mbts.spark
|
||||
selected_securities = secs.Select(s => s.Id).ToList();
|
||||
ok_button_command.TryRaiseCanExecuteChanged();
|
||||
_compose_criteria_clause_name();
|
||||
|
||||
// _security_in_criteria is our fun to be returned to our callers
|
||||
// test on click -- verify our func works
|
||||
//var results = all_securities.Select(all => all.Value.security).Where(x => _security_in_criteria(x)).ToList();
|
||||
}
|
||||
|
||||
private void on_securities_changed(IReadOnlyDictionary<uint, SecurityMD> changed_securities) {
|
||||
@@ -212,10 +208,6 @@ namespace mbts.spark
|
||||
all_securities = changed_securities;
|
||||
}
|
||||
|
||||
securities.sync_with(all_securities
|
||||
.Select(entry => entry.Value.security)
|
||||
);
|
||||
|
||||
_update_exchanges();
|
||||
_update_security_types();
|
||||
_update_symbols();
|
||||
@@ -327,7 +319,6 @@ namespace mbts.spark
|
||||
.Select(s => s.Value.security.Symbol)
|
||||
.Distinct()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private void _update_securities() {
|
||||
|
||||
@@ -149,6 +149,15 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public static string abbreviate(this Side side) {
|
||||
switch (side) {
|
||||
case Side.Buy: return "B";
|
||||
case Side.Sell: return "S";
|
||||
case Side.Undefined:
|
||||
default: return "E";
|
||||
}
|
||||
}
|
||||
|
||||
public static string order_sub_type_display_name(OrderSubType sub_type, bool use_long_name) {
|
||||
switch(sub_type) {
|
||||
case OrderSubType.HedgingLeg:
|
||||
|
||||
@@ -170,6 +170,13 @@ namespace mbts.spark {
|
||||
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)
|
||||
{
|
||||
var eq = EqualityComparer.Create((double a, double b) => TickUtils.tick_equals(contract, a, b),
|
||||
x => MathUtils.RoundFractionalUnits(x, contract.LowTick));
|
||||
return eq;
|
||||
}
|
||||
|
||||
public static double round_to_tick(this ContractSpecificationImmutable spec, double value) {
|
||||
double intpart = (long)value;
|
||||
double floatpart = value % 1;
|
||||
|
||||
@@ -204,12 +204,12 @@ namespace mbts.spark {
|
||||
protected override async void OnMouseDown(MouseButtonEventArgs e) {
|
||||
Keyboard.ClearFocus();
|
||||
Focus();
|
||||
if (e.ChangedButton == MouseButton.Middle)
|
||||
var point = e.GetPosition(this);
|
||||
var column = _column_for_point(point);
|
||||
if (column.column_type == ColumnType.Price && e.ChangedButton == MouseButton.Middle)
|
||||
_scroll_to_center();
|
||||
else if (_settings != null) {
|
||||
var point = e.GetPosition(this);
|
||||
var price = price_for_point(point);
|
||||
var column = _column_for_point(point);
|
||||
Side? bs = null;
|
||||
if (column.column_type == ColumnType.Bid) bs = Side.Buy;
|
||||
if (column.column_type == ColumnType.Ask) bs = Side.Sell;
|
||||
@@ -340,7 +340,7 @@ namespace mbts.spark {
|
||||
_price_comparer = Comparer<double>.Create((x, y) => security.ContractSpec.tick_equals(x, y) ? 0 : y.CompareTo(x));
|
||||
_price_dirty = true;
|
||||
_vm.on_tick += _tick;
|
||||
_settings_sub = _vm.settings.ObserveOnDispatcher().Subscribe(_settings_updated);
|
||||
_settings_sub = _vm.settings.OnDispatcher().Subscribe(_settings_updated);
|
||||
_tick_sub = Disposable.Create(() => _vm.on_tick -= _tick);
|
||||
}
|
||||
}
|
||||
@@ -533,6 +533,7 @@ namespace mbts.spark {
|
||||
_visualHost.AddVisualChild(_mid_mkt_break_vis);
|
||||
_visualHost.AddVisualChild(_ask_mkt_break_vis);
|
||||
_visualHost.AddVisualChild(_bid_mkt_break_vis);
|
||||
_render_grid_background();
|
||||
_init_tactile_adorner();
|
||||
// this effectively sets the background of the "Last" column
|
||||
if (_scrollViewer != null) {
|
||||
|
||||
@@ -13,9 +13,10 @@ using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.spark {
|
||||
class LadderOrderVM : DisposableViewModelBase, IEquatable<LadderOrderVM> {
|
||||
class LadderOrderVM : DisposableViewModelBase {
|
||||
#region Colors
|
||||
static readonly Color _colorbg = Colors.White, _darkbg = Color.FromRgb(0xbf, 0xbf, 0xbf);
|
||||
static readonly Brush _color_bg_brush = _colorbg.rgb_color().rgb_brush();
|
||||
static Brush mkbrush(Color color, Color endcolor, double pct_complete) {
|
||||
GradientStopCollection stops = new GradientStopCollection();
|
||||
stops.Add(new GradientStop(color, 0.0));
|
||||
@@ -34,7 +35,8 @@ namespace mbts.spark {
|
||||
readonly Property<FontFamily> _font_family;
|
||||
readonly Property<FontWeight> _font_weight;
|
||||
readonly Property<double> _font_size;
|
||||
readonly Property<Brush> _foreground;
|
||||
readonly Property<Brush> _foreground, _background, _hoverbackground, _border;
|
||||
readonly Property<string> _summary, _description;
|
||||
IObservable<LadderSettings.BadgeStyle> _bs;
|
||||
LadderSettings.BadgeStyle _current_badge_style;
|
||||
int _n_active_price_change_ops = 0;
|
||||
@@ -56,25 +58,26 @@ namespace mbts.spark {
|
||||
_font_weight = backing(nameof(font_weight), parent.settings, get_font_weight, get_font_weight(fs));
|
||||
_font_size = backing(nameof(font_size), parent.settings, get_font_size, get_font_size(fs));
|
||||
_foreground = backing(nameof(foreground), parent.settings, get_foreground, get_foreground(fs));
|
||||
_background = backing(nameof(background), _color_bg_brush);
|
||||
_hoverbackground = backing(nameof(hoverbackground), _color_bg_brush);
|
||||
_border = backing(nameof(border), _color_bg_brush);
|
||||
_summary = backing(nameof(summary), string.Empty);
|
||||
_description = backing(nameof(description), string.Empty);
|
||||
FontFamily get_font_family(LadderSettings s) => new FontFamily(s.orders_column.font_family);
|
||||
FontWeight get_font_weight(LadderSettings s) => ui.utility.text.TextUtils.from_open_type_weight_safe(s.orders_column.font_weight);
|
||||
double get_font_size(LadderSettings s) => s.orders_column.font_size;
|
||||
Brush get_foreground(LadderSettings s) => s.orders_column.foreground.rgb_brush();
|
||||
_text = _bottom_line = _top_line = string.Empty;
|
||||
//
|
||||
add_order(o);
|
||||
_dispose = Disposable.Create(
|
||||
_bs.Subscribe( badge => {
|
||||
_bs.Subscribe(badge => {
|
||||
_current_badge_style = badge;
|
||||
_set_badge_text();
|
||||
})
|
||||
);
|
||||
|
||||
//
|
||||
add_order(o);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _init() {
|
||||
public void update() {
|
||||
// update brushes
|
||||
Color color;
|
||||
switch (side) {
|
||||
@@ -86,35 +89,33 @@ namespace mbts.spark {
|
||||
}
|
||||
var darker = BTSColor.darken2(color.R, color.G, color.B, 0.75);
|
||||
Color dark = new Color() { A = 255, R = darker.r, G = darker.g, B = darker.b };
|
||||
var completion = _parent_and_child_not_sharing_slot().Sum(order => OrderUtils.filled(order)) / _parent_and_child_not_sharing_slot().Sum(order => OrderUtils.quantity(order));
|
||||
background = mkbrush(color, _colorbg, completion);
|
||||
hoverbackground = mkbrush(dark, _darkbg, completion);
|
||||
//var completion = _parent_and_child_not_sharing_slot().Sum(order => OrderUtils.filled(order)) / _parent_and_child_not_sharing_slot().Sum(order => OrderUtils.quantity(order));
|
||||
background = _color_bg_brush; //mkbrush(color, _colorbg, 0);
|
||||
hoverbackground = mkbrush(dark, _darkbg, 0);
|
||||
border = new SolidColorBrush(color);
|
||||
filled = _parent_and_child_not_sharing_slot().Select(order => OrderUtils.filled(order)).Sum();
|
||||
working_quantity = _parent_and_child_not_sharing_slot().Select(order => order.working_quantity()).Sum();
|
||||
quantity = _parent_and_child_not_sharing_slot().Select(order => order.State.Quantity).Sum();
|
||||
|
||||
if (_orders.Count > 0)
|
||||
{
|
||||
summary = _orders.Values.Select(order => order.short_description(_parent.security.ContractSpec)).Aggregate((s1, s2) => $"{s1}{Environment.NewLine}{Environment.NewLine}{s2}");
|
||||
description = _orders.Values.Select(order => order.full_description(_parent.security)).Aggregate((s1, s2) => $"{s1}{Environment.NewLine}{Environment.NewLine}{s2}");
|
||||
}
|
||||
_set_badge_text();
|
||||
}
|
||||
|
||||
private void _set_badge_text() {
|
||||
if (_orders.Count > 0) {
|
||||
switch (_current_badge_style) {
|
||||
case LadderSettings.BadgeStyle.Alternate:
|
||||
//text = $"{_buy_sell_indicator}:{filled}
|
||||
top_line = $"{_buy_sell_indicator}:{filled}";
|
||||
bottom_line = $"W:{ working_quantity}";
|
||||
break;
|
||||
case LadderSettings.BadgeStyle.Normal:
|
||||
default:
|
||||
text = $"{filled}/{quantity}"; break;
|
||||
}
|
||||
}
|
||||
text_lines = BadgeRender.format(side, quantity, filled, _current_badge_style);
|
||||
RaisePropertyChanged(nameof(text_lines));
|
||||
}
|
||||
|
||||
public void add_order(OrderRecordImmutable new_order) {
|
||||
_orders[new_order.Key] = new_order;
|
||||
_init();
|
||||
}
|
||||
|
||||
public void remove_order(uint order_key) {
|
||||
_orders.Remove(order_key);
|
||||
}
|
||||
|
||||
//public void update(OrderRecordImmutable neworder) {
|
||||
@@ -133,61 +134,13 @@ namespace mbts.spark {
|
||||
if (distinct_sides.Count() > 1) {
|
||||
return Side.Undefined;
|
||||
}
|
||||
return distinct_sides.First();
|
||||
return distinct_sides.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
string _buy_sell_indicator {
|
||||
get {
|
||||
switch (side) {
|
||||
case Side.Buy: return "B";
|
||||
case Side.Sell: return "S";
|
||||
case Side.Undefined:
|
||||
default:
|
||||
return "E";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<OrderRecordImmutable> _parent_and_child_not_sharing_slot() => _orders.Values.Where(o => !_orders.ContainsKey(o.ParentKey));
|
||||
|
||||
private string _text;
|
||||
public string text {
|
||||
get { return _text; }
|
||||
private set {
|
||||
_text = value;
|
||||
if (value != string.Empty) {
|
||||
top_line = string.Empty;
|
||||
bottom_line = string.Empty;
|
||||
}
|
||||
RaisePropertyChanged(); }
|
||||
}
|
||||
|
||||
private string _top_line;
|
||||
public string top_line {
|
||||
get { return _top_line; }
|
||||
private set {
|
||||
_top_line = value;
|
||||
if (value != string.Empty) {
|
||||
text = string.Empty;
|
||||
}
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
private string _bottom_line;
|
||||
public string bottom_line {
|
||||
get { return _bottom_line; }
|
||||
private set {
|
||||
_bottom_line = value;
|
||||
if (value != string.Empty) {
|
||||
text = string.Empty;
|
||||
}
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string summary => _orders.Values.Select(order => order.short_description(_parent.security.ContractSpec)).Aggregate((s1, s2) => $"{s1}{Environment.NewLine}{Environment.NewLine}{s2}");
|
||||
public string description => _orders.Values.Select(order => order.full_description(_parent.security)).Aggregate((s1, s2) => $"{s1}{Environment.NewLine}{Environment.NewLine}{s2}");
|
||||
public IReadOnlyList<string> text_lines { get; private set; }
|
||||
public bool has_parent => _orders.Values.Any( order => order.has_parent());
|
||||
public bool has_phantom_price => !double.IsNaN(_phantom_price);
|
||||
|
||||
@@ -204,10 +157,11 @@ namespace mbts.spark {
|
||||
public FontWeight font_weight => _font_weight.Value;
|
||||
public double font_size => _font_size.Value;
|
||||
public Brush foreground => _foreground.Value;
|
||||
public Brush background { get; private set; }
|
||||
public Brush hoverbackground { get; private set; }
|
||||
public Brush header_brush => _orders.Values.FirstOrDefault().Def.Side == Side.Buy ? BTSColors.MarketData.bid : BTSColors.MarketData.ask;
|
||||
public Brush border { get; private set; }
|
||||
public Brush background { get => _background.Value; private set => _background.Value = value; }
|
||||
public Brush hoverbackground { get => _hoverbackground.Value; private set => _hoverbackground.Value = value; }
|
||||
public Brush border { get => _border.Value; private set => _border.Value = value; }
|
||||
public string summary { get => _summary.Value; private set => _summary.Value = value; }
|
||||
public string description { get => _description.Value; private set => _description.Value = value; }
|
||||
public IEnumerable<OrderRecordImmutable> orders => _orders.Values;
|
||||
|
||||
void _cancel_order() {
|
||||
@@ -239,40 +193,8 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override bool Equals(object obj) => this.safe_equals(obj as LadderOrderVM, _non_null_equals);
|
||||
public override int GetHashCode() {
|
||||
// can't use aggregate_hash since OrderRecordImmutable is not a reference type
|
||||
unchecked {
|
||||
int hash = 19;
|
||||
foreach (var o in _orders.Values) {
|
||||
hash = hash * 31 + o.GetHashCode();
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
public bool Equals(LadderOrderVM other) => this.safe_equals(other, _non_null_equals);
|
||||
static bool _non_null_equals(LadderOrderVM x, LadderOrderVM y) {
|
||||
// null checks already taken care of safe_equals
|
||||
// same object ?
|
||||
if (object.ReferenceEquals(x,y)) return true;
|
||||
|
||||
// if the Dicts have different count, we have different objects
|
||||
if (x._orders.Count != y._orders.Count) return false;
|
||||
|
||||
// are the keys the same ?
|
||||
foreach(uint kx in x._orders.Keys) {
|
||||
if (!y._orders.ContainsKey(kx))
|
||||
return false;
|
||||
}
|
||||
|
||||
// I don't think we have to look at the Dictionary values
|
||||
// since the keys originate from the OrderRecordImmutable.key
|
||||
//foreach (uint kx in x._orders.Keys) {
|
||||
// if (!x._orders[kx].Equals(y._orders[kx)
|
||||
// return false;
|
||||
//}
|
||||
return true;
|
||||
public override string ToString() {
|
||||
return $"Price: {this.price} Order Count: {this.orders.Count()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,8 +143,9 @@ namespace mbts.spark {
|
||||
return price_cell.panel;
|
||||
else if (_price_list != null && !double.IsNaN(_row_height)) {
|
||||
var panel = new StackPanel() {
|
||||
Margin = Badge.DefaultMargin,
|
||||
Orientation = Orientation.Horizontal,
|
||||
MaxWidth = _col_width,
|
||||
MaxWidth = _col_width
|
||||
//MaxHeight = _row_height,
|
||||
//ClipToBounds = true
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using bts.api.orderman;
|
||||
using mbts.ui;
|
||||
using mbts.utils;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Linq;
|
||||
@@ -124,8 +125,6 @@ namespace mbts.spark {
|
||||
last_column = last_column,
|
||||
calculator_buttons = new[] { 1, 5, 10, 25, 50, 100 },
|
||||
scroll_bar_visible = false,
|
||||
ticks_to_recenter = 10,
|
||||
seconds_to_recenter = 300,
|
||||
default_order_type = CustomOrderDefinition.CustomOneofCase.Limit,
|
||||
default_time_in_force = TimeInForce.Day,
|
||||
animate_recenter = true,
|
||||
@@ -163,10 +162,8 @@ namespace mbts.spark {
|
||||
public int[] calculator_buttons { get; set; }
|
||||
[ProtoMember(10)]
|
||||
public bool scroll_bar_visible { get; set; }
|
||||
[ProtoMember(11)]
|
||||
public int ticks_to_recenter { get; set; }
|
||||
[ProtoMember(12)]
|
||||
public int seconds_to_recenter { get; set; }
|
||||
// USED [ProtoMember(11)]
|
||||
// USED [ProtoMember(12)]
|
||||
[ProtoMember(13)]
|
||||
public CustomOrderDefinition.CustomOneofCase default_order_type { get; set; }
|
||||
[ProtoMember(14)]
|
||||
@@ -197,11 +194,8 @@ namespace mbts.spark {
|
||||
other.price_column.safe_equals(price_column) &&
|
||||
other.calculator_buttons.safe_equals(calculator_buttons, (a, b) => a.SequenceEqual(b)) &&
|
||||
other.scroll_bar_visible == scroll_bar_visible &&
|
||||
other.ticks_to_recenter == ticks_to_recenter &&
|
||||
other.seconds_to_recenter == seconds_to_recenter &&
|
||||
other.default_order_type == default_order_type &&
|
||||
other.default_time_in_force == default_time_in_force &&
|
||||
other.seconds_to_recenter == seconds_to_recenter &&
|
||||
other.size == size &&
|
||||
other.animate_recenter == animate_recenter &&
|
||||
other.badge_style == badge_style &&
|
||||
@@ -213,26 +207,24 @@ namespace mbts.spark {
|
||||
public override bool Equals(object obj) => Equals(obj as LadderSettings);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
row_height.GetHashCode() +
|
||||
grid_line_color.GetHashCode() +
|
||||
orders_column.GetHashCode() +
|
||||
bid_column.GetHashCode() +
|
||||
ask_column.GetHashCode() +
|
||||
price_column.GetHashCode() +
|
||||
last_column.GetHashCode() +
|
||||
calculator_buttons.GetHashCode() +
|
||||
scroll_bar_visible.GetHashCode() +
|
||||
ticks_to_recenter.GetHashCode() +
|
||||
seconds_to_recenter.GetHashCode() +
|
||||
default_order_type.GetHashCode() +
|
||||
default_time_in_force.GetHashCode() +
|
||||
seconds_to_recenter.GetHashCode() +
|
||||
size.GetHashCode() +
|
||||
animate_recenter.GetHashCode() +
|
||||
badge_style.GetHashCode() +
|
||||
bid_break_line.GetHashCode() +
|
||||
ask_break_line.GetHashCode() +
|
||||
mid_break_line.GetHashCode() +
|
||||
alternate_order_size.GetHashCode();
|
||||
EqualityComparer.aggregate_hash(
|
||||
row_height.GetHashCode(),
|
||||
grid_line_color.GetHashCode(),
|
||||
orders_column.GetHashCode(),
|
||||
bid_column.GetHashCode(),
|
||||
ask_column.GetHashCode(),
|
||||
price_column.GetHashCode(),
|
||||
last_column.GetHashCode(),
|
||||
calculator_buttons.GetHashCode(),
|
||||
scroll_bar_visible.GetHashCode(),
|
||||
default_order_type.GetHashCode(),
|
||||
default_time_in_force.GetHashCode(),
|
||||
size.GetHashCode(),
|
||||
animate_recenter.GetHashCode(),
|
||||
badge_style.GetHashCode(),
|
||||
bid_break_line.GetHashCode(),
|
||||
ask_break_line.GetHashCode(),
|
||||
mid_break_line.GetHashCode(),
|
||||
alternate_order_size.GetHashCode());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using bts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using bts.api;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls.color_picker;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
@@ -14,6 +14,7 @@ namespace mbts.spark {
|
||||
class LadderSettingsVM : ViewModelBase, IWindowSettingsVM<LadderSettings> {
|
||||
readonly Property<LadderSettings.BadgeStyle> _badge_style;
|
||||
readonly Property<uint> _alternate_order_size;
|
||||
readonly DelegateProperty<double> _row_height;
|
||||
LadderSettings _current;
|
||||
LadderColumnSettingsVM
|
||||
_orders = new OrdersColumnSettingsVM(),
|
||||
@@ -30,6 +31,13 @@ namespace mbts.spark {
|
||||
_badge_style = backing<LadderSettings.BadgeStyle>(nameof(badge_style));
|
||||
_badge_style.Changed += _badge_style_Changed;
|
||||
_alternate_order_size = backing<uint>(nameof(alternate_order_size));
|
||||
_row_height = backing(nameof(row_height), () => LadderRender.measure_row_height(get_result()));
|
||||
_orders.font_size_changed += on_font_size_changed;
|
||||
_bid.font_size_changed += on_font_size_changed;
|
||||
_price.font_size_changed += on_font_size_changed;
|
||||
_ask.font_size_changed += on_font_size_changed;
|
||||
_last.font_size_changed += on_font_size_changed;
|
||||
void on_font_size_changed(double oldval, double newval) => _row_height.refresh();
|
||||
}
|
||||
|
||||
private void _badge_style_Changed(LadderSettings.BadgeStyle oldval, LadderSettings.BadgeStyle newval) {
|
||||
@@ -38,6 +46,7 @@ namespace mbts.spark {
|
||||
} else if (newval == LadderSettings.BadgeStyle.Normal && _orders.font_size == 7) {
|
||||
_orders.font_size = 10;
|
||||
}
|
||||
_row_height.refresh();
|
||||
}
|
||||
|
||||
public LadderSettings.BadgeStyle badge_style {
|
||||
@@ -50,6 +59,8 @@ namespace mbts.spark {
|
||||
set => _alternate_order_size.Value = value;
|
||||
}
|
||||
|
||||
public double row_height => _row_height.Value;
|
||||
|
||||
public IEnumerable<LadderColumnSettingsVM> column_items {
|
||||
get {
|
||||
yield return _orders;
|
||||
@@ -89,7 +100,6 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public LadderSettings get_result() {
|
||||
const double row_padding = 12;
|
||||
var clone = SparkUtils.clone_proto(_current);
|
||||
clone.badge_style = badge_style;
|
||||
clone.alternate_order_size = alternate_order_size;
|
||||
@@ -101,13 +111,7 @@ namespace mbts.spark {
|
||||
clone.bid_break_line = _bid_mkt_line.get_settings();
|
||||
clone.ask_break_line = _ask_mkt_line.get_settings();
|
||||
clone.mid_break_line = _mid_mkt_line.get_settings();
|
||||
clone.row_height = new[] {
|
||||
clone.orders_column,
|
||||
clone.bid_column,
|
||||
clone.price_column,
|
||||
clone.ask_column,
|
||||
clone.last_column
|
||||
}.Max(cs => cs.font_size) + row_padding;
|
||||
clone.row_height = LadderRender.measure_row_height(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -131,6 +135,7 @@ namespace mbts.spark {
|
||||
_background = backing(nameof(background), Colors.White);
|
||||
_font_display_name = backing(nameof(font_display_name),
|
||||
() => TextUtils.display_name(font_family, font_weight, font_size), nameof(font_family), nameof(font_weight), nameof(font_size));
|
||||
_font_size.Changed += (oldval, newval) => font_size_changed?.Invoke(oldval, newval);
|
||||
}
|
||||
|
||||
public string name { get; }
|
||||
@@ -145,6 +150,7 @@ namespace mbts.spark {
|
||||
public Color background { get => _background.Value; set => _background.Value = value; }
|
||||
public ObservableCollection<ColorPaletteVM> foreground_palettes { get; }
|
||||
public ObservableCollection<ColorPaletteVM> background_palettes { get; }
|
||||
public event Action<double, double> font_size_changed;
|
||||
|
||||
public virtual string sample_text => "999-56789.01234";
|
||||
|
||||
@@ -178,7 +184,9 @@ namespace mbts.spark {
|
||||
public class OrdersColumnSettingsVM : LadderColumnSettingsVM, ITemplateSelectorKey {
|
||||
public OrdersColumnSettingsVM() : base("Orders") { }
|
||||
|
||||
public override string sample_text => "12/50";
|
||||
public uint sample_quantity { get; } = 50;
|
||||
public uint sample_filled { get; } = 12;
|
||||
public Side sample_side { get; } = Side.Buy;
|
||||
public Brush badge_border { get; } = new SolidColorBrush(BTSColors.MarketData.ask);
|
||||
public Brush badge_background { get; } = Brushes.White;
|
||||
|
||||
@@ -213,4 +221,4 @@ namespace mbts.spark {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
TextElement.FontSize="{x:Static u:FontSizes.Small}"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<c:BadgeTextLinesConverter x:Key="BadgeTextLinesConverter"/>
|
||||
<u:KeyedTemplateSelector x:Key="ColumnSampleTemplateSelector">
|
||||
<u:KeyedTemplateSelector.Default>
|
||||
<DataTemplate>
|
||||
@@ -28,18 +29,23 @@
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static usp:LadderSettingsView.OrdersColumnSampleTemplateKey}">
|
||||
<DataTemplate>
|
||||
<c:Badge
|
||||
Width="50"
|
||||
Height="20"
|
||||
Background="{Binding badge_background}"
|
||||
BorderBrush="{Binding badge_border}"
|
||||
BorderThickness="3"
|
||||
FontFamily="{Binding font_family}"
|
||||
FontSize="{Binding font_size}"
|
||||
FontWeight="{Binding font_weight}"
|
||||
Foreground="{Binding foreground, Converter={StaticResource ColorToBrushConverter}}"
|
||||
Text="{Binding sample_text}"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
Width="{x:Static c:Badge.DefaultWidth}"
|
||||
>
|
||||
<c:Badge.TextLines>
|
||||
<MultiBinding Converter="{StaticResource BadgeTextLinesConverter}">
|
||||
<Binding Path="sample_side"/>
|
||||
<Binding Path="sample_quantity"/>
|
||||
<Binding Path="sample_filled"/>
|
||||
<Binding Path="DataContext.badge_style" RelativeSource="{RelativeSource AncestorType=usp:LadderSettingsView}"/>
|
||||
</MultiBinding>
|
||||
</c:Badge.TextLines>
|
||||
</c:Badge>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</u:KeyedTemplateSelector>
|
||||
@@ -82,7 +88,7 @@
|
||||
<ItemsControl Grid.Row="1" ItemsSource="{Binding column_items}" Margin="8,0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Height="27">
|
||||
<Grid Height="{Binding DataContext.row_height, Mode=OneWay, RelativeSource={RelativeSource AncestorType=usp:LadderSettingsView}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition SharedSizeGroup="c1"/>
|
||||
@@ -124,7 +130,7 @@
|
||||
<c:FontPicker SelectedFontSize="{Binding font_size}" Typeface="{Binding typeface}"/>
|
||||
</c:PopupControl>
|
||||
<Border Grid.Column="4" Background="{Binding background, Converter={StaticResource ColorToBrushConverter}}" BorderBrush="Black" BorderThickness="2,0,0,0" Padding="4,0">
|
||||
<ContentControl Content="{Binding}" ContentTemplateSelector="{StaticResource ColumnSampleTemplateSelector}"/>
|
||||
<ContentControl Content="{Binding}" ContentTemplateSelector="{StaticResource ColumnSampleTemplateSelector}" VerticalContentAlignment="Center"/>
|
||||
</Border>
|
||||
<Rectangle Grid.Column="1" Grid.ColumnSpan="5" Fill="DimGray" Height="1" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
|
||||
</Grid>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.shell;
|
||||
using bts.spark;
|
||||
using mbts.co2;
|
||||
using mbts.persist;
|
||||
@@ -22,14 +23,18 @@ namespace mbts.spark {
|
||||
readonly LadderModel _model;
|
||||
readonly Property<double> _high, _low, _change, _open, _settlement;
|
||||
readonly Property<ulong> _volume;
|
||||
readonly Property<bool> _revert;
|
||||
readonly Property<bool> _auto_clear;
|
||||
readonly Property<Brush> _bid_background, _bid_foreground, _ask_background, _ask_foreground;
|
||||
IWindowSettingsManager<LadderSettings> _settings_manager;
|
||||
ulong _misc_time = 0;
|
||||
IEqualityComparer<double> _equality;
|
||||
Dictionary<double, LadderOrderVM> _slot_price_vs_orders;
|
||||
|
||||
public LadderVM(IBus bus, LadderModel model, IWindowSettingsManager<LadderSettings> settings_manager) {
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
_equality = TickUtils.tick_equality_comparer(_model.security.ContractSpec);
|
||||
_slot_price_vs_orders = new Dictionary<double, LadderOrderVM>(_equality);
|
||||
_settings_manager = settings_manager;
|
||||
_high = backing(nameof(high), double.NaN);
|
||||
_low = backing(nameof(low), double.NaN);
|
||||
@@ -37,7 +42,7 @@ namespace mbts.spark {
|
||||
_open = backing(nameof(open), double.NaN);
|
||||
_settlement = backing(nameof(settlement), double.NaN);
|
||||
_volume = backing(nameof(volume), 0ul);
|
||||
_revert = backing(nameof(revert), false);
|
||||
_auto_clear = backing(nameof(auto_clear), false);
|
||||
_status = backing(nameof(status_message), "");
|
||||
_status_brush = backing(nameof(status_background), (Brush)BTSColors.UI.spark_menu_background);
|
||||
// brush props
|
||||
@@ -46,7 +51,7 @@ namespace mbts.spark {
|
||||
_ask_background = brush_prop(nameof(ask_background), s => s.ask_column.background.rgb_brush());
|
||||
_ask_foreground = brush_prop(nameof(ask_foreground), s => s.ask_column.foreground.rgb_brush());
|
||||
Property<Brush> brush_prop(string name, Func<LadderSettings, Brush> get_brush) =>
|
||||
backing(name, settings_manager.subscribe_current().ObserveOnDispatcher(), get_brush);
|
||||
backing(name, settings_manager.subscribe_current().OnDispatcher(), get_brush);
|
||||
//
|
||||
send_order_command = new DelegateCommand<(double price, Side? side, bool size_from_alternate)>(send_order);
|
||||
cancel_command = new AsyncCommand<LadderModel.CancelType>(_model.cancel_orders, true);
|
||||
@@ -62,46 +67,77 @@ namespace mbts.spark {
|
||||
return dialog;
|
||||
}, -500, -500);
|
||||
|
||||
var under_orders = new ObservableCollection<OrderRecordImmutable>();
|
||||
orders = new ObservableCollection<LadderOrderVM>();
|
||||
//new ObservableAutoWrapper<LadderOrderVM, OrderRecordImmutable>(under_orders, rec => new LadderOrderVM(rec, this, _bus, _settings_manager.subscribe_current().Select(s => s.badge_style)));
|
||||
|
||||
_dispose = Disposable.Create(
|
||||
settings_command,
|
||||
order_properties_vm = new OrderPropertiesEditorVM(model.order_gateway, Observable.Return(security), _settings_manager.subscribe_current().Select(s => new InlineOrderProperties(model.order_gateway, s))),
|
||||
size_calculator = new CalculatorLogic(0, _settings_manager.subscribe_current().Select(s => s.calculator_buttons)),
|
||||
bus.subscribe(SparkTags.LogActivity).ObserveOnDispatcher().Subscribe(status_update),
|
||||
// need to subscribe to all orders, and cache them, because parent orders don't necessarily have the same security
|
||||
// and OrderVM needs access to parent orders, if there are any
|
||||
model.order_gateway.working_orders.ObserveOnDispatcher().Subscribe(working => {
|
||||
under_orders.sync_with(working.Values.Where(o => o.Def.SecurityKey == security.Id && o.is_non_terminal()));
|
||||
bus.subscribe(SparkTags.LogActivity).OnDispatcher().Subscribe(status_update),
|
||||
model.order_gateway.working_orders.OnDispatcher().Subscribe(working => {
|
||||
// Here we manage N orders to 1 price slot in the ladder
|
||||
// there is likely a smarter way to do this than creating this anew each time
|
||||
// need to do something about stop and stop limits in here
|
||||
var slot_price_vs_orders = new Dictionary<double, LadderOrderVM>();
|
||||
foreach(var uo in under_orders) {
|
||||
var slot_price_target = uo.price();
|
||||
var order_type = uo.order_type();
|
||||
var filled = uo.filled();
|
||||
|
||||
Dictionary<double, HashSet<uint>> orders_used_in_this_update = new Dictionary<double, HashSet<uint>>(_equality);
|
||||
var ladders_orders = working.Values.Where(o => o.Def.SecurityKey == security.Id);
|
||||
foreach (var wo in ladders_orders) {
|
||||
var slot_price_target = wo.price();
|
||||
var order_type = wo.order_type();
|
||||
var filled = wo.filled();
|
||||
if (order_type == CustomOrderDefinition.CustomOneofCase.Stop) {
|
||||
// if the stop order has been submitted but hasn't been triggered
|
||||
// we draw the badge at the stop/trigger price, else the price price
|
||||
if (filled == 0) { slot_price_target = uo.stop_price(); }
|
||||
if (filled == 0) { slot_price_target = wo.stop_price(); }
|
||||
} else if (order_type == CustomOrderDefinition.CustomOneofCase.StopLimit) {
|
||||
// consistent behavior as Stop order above.
|
||||
if (filled == 0) { slot_price_target = uo.stop_limit_price(); }
|
||||
if (filled == 0) { slot_price_target = wo.stop_limit_price(); }
|
||||
}
|
||||
|
||||
slot_price_vs_orders.TryGetValue(slot_price_target, out LadderOrderVM slot_vm);
|
||||
if (slot_vm != null) {
|
||||
slot_vm.add_order(uo);
|
||||
if (_slot_price_vs_orders.TryGetValue(slot_price_target, out LadderOrderVM slot_vm)) {
|
||||
slot_vm.add_order(wo);
|
||||
} else {
|
||||
slot_vm = new LadderOrderVM(uo, slot_price_target, this, _bus, _settings_manager.subscribe_current().Select(s => s.badge_style));
|
||||
slot_vm = new LadderOrderVM(wo, slot_price_target, this, _bus, _settings_manager.subscribe_current().Select(s => s.badge_style));
|
||||
_slot_price_vs_orders[slot_price_target] = slot_vm;
|
||||
}
|
||||
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;
|
||||
|
||||
}
|
||||
orders.sync_with(slot_price_vs_orders.Values);
|
||||
orders_cache = working;
|
||||
|
||||
// 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.orders.Select(o => o.Key).ToList()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
orders.sync_with(_slot_price_vs_orders.Values);
|
||||
}),
|
||||
WorkLoop.Start($"Ladder '{security}'", _tick)
|
||||
);
|
||||
@@ -120,7 +156,7 @@ namespace mbts.spark {
|
||||
public ICommand status_click_command => SparkCommands.ShowActivityLog.ICommand(_bus);
|
||||
|
||||
public async void status_update(Activity a) {
|
||||
if (a.security == _model.security) {
|
||||
if (a.security == _model.security && a.category != ActivityLogCategory.modify) {
|
||||
_status.set(a.message);
|
||||
_status_brush.set(a.category.fill);
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
@@ -138,7 +174,7 @@ namespace mbts.spark {
|
||||
public double open => _open.Value;
|
||||
public double settlement => _settlement.Value;
|
||||
public ulong volume => _volume.Value;
|
||||
public bool revert { get => _revert.Value; set => _revert.set(value); }
|
||||
public bool auto_clear { get => _auto_clear.Value; set => _auto_clear.set(value); }
|
||||
public CalculatorLogic size_calculator { get; }
|
||||
public OrderPropertiesEditorVM order_properties_vm { get; }
|
||||
public ObservableCollection<LadderOrderVM> orders { get; }
|
||||
@@ -152,7 +188,6 @@ namespace mbts.spark {
|
||||
public Brush ask_foreground => _ask_foreground.Value;
|
||||
public DialogCommand settings_command { get; }
|
||||
internal LadderModel model => _model;
|
||||
internal IReadOnlyDictionary<uint, OrderRecordImmutable> orders_cache { get; private set; } = new Dictionary<uint, OrderRecordImmutable>();
|
||||
internal IObservable<LadderSettings> settings => _settings_manager.subscribe_current();
|
||||
|
||||
public LadderSettings get_save_state() {
|
||||
@@ -195,16 +230,8 @@ namespace mbts.spark {
|
||||
|
||||
uint size = args.size_from_alternate ? s.alternate_order_size : (uint)Math.Abs(size_calculator.value.Value);
|
||||
await _model.order_gateway.send_order(order, args.price, size);
|
||||
if (revert) {
|
||||
size_calculator.value = s.size;
|
||||
if (order_properties_vm.selected_time_in_force != s.default_time_in_force && order_properties_vm.time_in_forces.Contains(s.default_time_in_force))
|
||||
order_properties_vm.selected_time_in_force = s.default_time_in_force;
|
||||
foreach (var ot in order_properties_vm.order_types) {
|
||||
ot.load_from(_model.order_gateway.factory.custom_order(ot.order_type));
|
||||
if (ot.order_type == s.default_order_type)
|
||||
order_properties_vm.selected_order_type = ot;
|
||||
}
|
||||
}
|
||||
if (auto_clear)
|
||||
size_calculator.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,41 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="SettingsButton" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
|
||||
<Setter Property="Content">
|
||||
<Setter.Value>
|
||||
<TextBlock VerticalAlignment="Center">Settings</TextBlock>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
BorderBrush="{mbts:BTSColor UI.Button.normal_border}"
|
||||
Background="{mbts:BTSColor UI.Button.normal_background}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="True"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,12">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True"
|
||||
Name="button_content"
|
||||
/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{mbts:BTSColor UI.Button.mouseover_background}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{mbts:BTSColor UI.Button.mouseover_border}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<AdornerDecorator>
|
||||
@@ -359,7 +394,7 @@
|
||||
</ItemsControl>
|
||||
<Button Margin="2" HorizontalAlignment="Stretch" Command="{Binding size_calculator.clear}">Clear</Button>
|
||||
<!-- reset to defaults after order -->
|
||||
<CheckBox Margin="2" IsChecked="{Binding revert}">Auto Clear</CheckBox>
|
||||
<CheckBox Margin="2" IsChecked="{Binding auto_clear}">Auto Clear</CheckBox>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -371,16 +406,7 @@
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="Asks" Style="{StaticResource CancelSells}"/>
|
||||
<Button Command="{Binding cancel_command}" CommandParameter="All" Style="{StaticResource CancelAll}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Row="1" HorizontalContentAlignment="Stretch" VerticalAlignment="Bottom" Margin="5,12,5,16" Command="{Binding settings_command}" Padding="4,0" Style="{StaticResource LadderMenuButton}" ToolTip="Settings">
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Height="15" SnapsToDevicePixels="True" VerticalAlignment="Center" HorizontalAlignment="Left" Stretch="Uniform" Source="{x:Static mbts:Icons.ViewSettings}" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="6,2">Settings</TextBlock>
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Grid.Row="1" HorizontalContentAlignment="Stretch" Command="{Binding settings_command}" Style="{StaticResource SettingsButton}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Border Grid.Column="2" HorizontalAlignment="Stretch">
|
||||
@@ -422,15 +448,9 @@
|
||||
<Setter Property="FontFamily" Value="{Binding font_family}"/>
|
||||
<Setter Property="FontSize" Value="{Binding font_size}"/>
|
||||
<Setter Property="Foreground" Value="{Binding foreground}"/>
|
||||
<Setter Property="Text" Value="{Binding text}" />
|
||||
<Setter Property="TopText" Value="{Binding top_line}" />
|
||||
<Setter Property="BottomText" Value="{Binding bottom_line}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
<Setter Property="Width" Value="63" />
|
||||
<Setter Property="Height" Value="23" />
|
||||
<Setter Property="Margin" Value="4,0,0,0" />
|
||||
<Setter Property="TextLines" Value="{Binding text_lines}" />
|
||||
<Setter Property="Width" Value="{x:Static controls:Badge.DefaultWidth}"/>
|
||||
<Setter Property="ToolTip" Value="{Binding description}" />
|
||||
<Setter Property="CornerRadius" Value="1" />
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="ContextMenu">
|
||||
<Setter.Value>
|
||||
@@ -481,4 +501,4 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
</AdornerDecorator>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
@@ -1,20 +1,61 @@
|
||||
using bts.api;
|
||||
using mbts.co2;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.utility.text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.spark {
|
||||
static class BadgeRender {
|
||||
// IMPORTANT: the return values for this method MUST BE IN SYNC with the "format" method, else "measure_height" and "LadderRender.measure_row_height" will be broken.
|
||||
static int _line_count_for_style(LadderSettings.BadgeStyle badge_style) => badge_style == LadderSettings.BadgeStyle.Alternate ? 2 : 1;
|
||||
// IMPORTANT: at the risk of being redundant, see above comment.
|
||||
public static IReadOnlyList<string> format(Side side, uint quantity, uint filled, LadderSettings.BadgeStyle style) {
|
||||
switch (style) {
|
||||
case LadderSettings.BadgeStyle.Alternate:
|
||||
return new[] {
|
||||
$"{side.abbreviate()}:{filled}",
|
||||
$"W:{quantity - filled}"
|
||||
};
|
||||
default: return new[] { $"{filled}/{quantity}" };
|
||||
}
|
||||
}
|
||||
|
||||
public static double measure_height(LadderSettings.BadgeStyle badge_style, Thickness border_thickness, Thickness padding, double font_size) =>
|
||||
measure_height(_line_count_for_style(badge_style), border_thickness, padding, font_size);
|
||||
|
||||
public static double measure_height(int line_count, Thickness border_thickness, Thickness padding, double font_size) =>
|
||||
WPFHelper.control_base_height(border_thickness, padding) + font_size * line_count + Badge.LINE_SPACING * (line_count - 1);
|
||||
}
|
||||
|
||||
static class LadderRender {
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
public static double measure_row_height(LadderSettings settings) =>
|
||||
Math.Max(new[] {
|
||||
settings.bid_column.font_size,
|
||||
settings.price_column.font_size,
|
||||
settings.ask_column.font_size,
|
||||
settings.last_column.font_size
|
||||
}.Max() + row_padding.Top + row_padding.Bottom,
|
||||
BadgeRender.measure_height(
|
||||
settings.badge_style, Badge.DefaultBorderThickness, Badge.DefaultPadding, settings.orders_column.font_size
|
||||
) + Badge.DefaultMargin.Top + Badge.DefaultMargin.Bottom);
|
||||
|
||||
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);
|
||||
glyph_context.get_glyph_run(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
|
||||
|
||||
@@ -17,16 +17,17 @@ namespace mbts.spark {
|
||||
};
|
||||
SecurityImmutable? _security;
|
||||
MDConsumerHandle _handle;
|
||||
LadderSettings _settings;
|
||||
DateTime _last_recenter = DateTime.MaxValue;
|
||||
double _last_mid = 0.0;
|
||||
IDisposable _dispose;
|
||||
readonly TimeSpan _seconds_to_center;
|
||||
|
||||
public RecenterCountdown() {
|
||||
DataContextChanged += RecenterCountdown_DataContextChanged;
|
||||
Unloaded += (o,e) => on_unload(false);
|
||||
_timer.Tick += _timer_Tick;
|
||||
_timer.Start();
|
||||
_seconds_to_center = TimeSpan.FromSeconds(config.Configuration.get<bts.config.LadderKnobs>().seconds_to_recenter);
|
||||
}
|
||||
|
||||
static (double,double) tick_range(double price, int tick_count, ContractSpecificationImmutable spec) {
|
||||
@@ -34,11 +35,10 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
private void _timer_Tick(object sender, EventArgs e) {
|
||||
if (_settings != null && _security != null) {
|
||||
if (_security != null) {
|
||||
var mid = _handle.mid();
|
||||
(double low, double high) = tick_range(_last_mid, _settings.ticks_to_recenter, _security.Value.ContractSpec);
|
||||
if (//mid < low || mid > high ||
|
||||
(_last_recenter + TimeSpan.FromSeconds(_settings.seconds_to_recenter) < DateTime.UtcNow)) {
|
||||
(_last_recenter + _seconds_to_center < DateTime.UtcNow)) {
|
||||
trigger_recenter(mid);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,6 @@ namespace mbts.spark {
|
||||
vm.OnDisposed += on_unload;
|
||||
_dispose =
|
||||
Disposable.Create(
|
||||
vm.settings.Subscribe(s => _settings = s),
|
||||
Disposable.Create(() => vm.OnDisposed -= on_unload));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
using bts.api;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark
|
||||
{
|
||||
namespace mbts.spark {
|
||||
[Export]
|
||||
class PAndLModel : IDisposable {
|
||||
readonly IBus _bus;
|
||||
@@ -23,19 +18,8 @@ namespace mbts.spark
|
||||
//_account_service = account_service;
|
||||
}
|
||||
|
||||
public IObservable<List<PnLData>> on_pnl_data =>
|
||||
// combine w/ Securities to get SecurityMD
|
||||
_order_model.on_fill_tuples.CombineLatest(_bus.subscribe(SparkTags.SecuritiesTag), (fills, securities) => {
|
||||
List<PnLData> pnl_data = new List<PnLData>(fills.state.Count);
|
||||
foreach (var ft in fills.state.Values) {
|
||||
if (ft.security.HasValue && securities.TryGetValue(ft.security.Value.Id, out SecurityMD sec_md)) {
|
||||
pnl_data.Add(new PnLData(ft.fill, sec_md, ft.order));
|
||||
} else {
|
||||
pnl_data.Add(new PnLData(ft.fill, null, ft.order));
|
||||
}
|
||||
}
|
||||
return pnl_data;
|
||||
});
|
||||
public IObservable<ITableUpdateTransform<PnLData, FillTupleKey>> on_pnl_data =>
|
||||
_order_model.on_fill_tuples.convert(ft => new PnLData(ft)).table(pd => pd.key);
|
||||
|
||||
//public async Task<Account[]> get_accounts() {
|
||||
// var request = await _account_service.get_accounts_async(new GetAccountsRequest());
|
||||
@@ -45,12 +29,8 @@ namespace mbts.spark
|
||||
// }
|
||||
// return new Account[] { };
|
||||
//}
|
||||
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ namespace mbts.spark {
|
||||
_model = model;
|
||||
var pnl = model.on_pnl_data
|
||||
.Sample(_POSITION_UPDATE_PERIOD)
|
||||
.ObserveOnDispatcher()
|
||||
.OnDispatcher()
|
||||
.Subscribe(p => {
|
||||
if (p != null) {
|
||||
PnLCalculator calc = new PnLCalculator(p);
|
||||
PnLCalculator calc = new PnLCalculator(p.state.Values);
|
||||
var summary_position_rows = calc.get_summary_position();
|
||||
_pnl_data.sync_with(summary_position_rows);
|
||||
_last_pnl_update_time = DateTime.Now;
|
||||
|
||||
@@ -76,9 +76,9 @@ namespace mbts.spark {
|
||||
|
||||
|
||||
class PnLCalculator {
|
||||
readonly List<PnLData> _data;
|
||||
readonly IEnumerable<PnLData> _data;
|
||||
|
||||
public PnLCalculator(List<PnLData> data) {
|
||||
public PnLCalculator(IEnumerable<PnLData> data) {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using mbts.co2;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
|
||||
@@ -23,16 +18,16 @@ namespace mbts.spark {
|
||||
|
||||
class PnLData {
|
||||
|
||||
public readonly FillImmutable fill;
|
||||
public readonly SecurityMD? sec_md;
|
||||
public readonly OrderRecordImmutable? order;
|
||||
readonly FillTuple _fill_tuple;
|
||||
|
||||
public PnLData(FillImmutable fill, SecurityMD? sec_md, OrderRecordImmutable? order) {
|
||||
this.fill = fill;
|
||||
this.sec_md = sec_md;
|
||||
this.order = order;
|
||||
}
|
||||
public PnLData(FillTuple fill_tuple) =>
|
||||
_fill_tuple = fill_tuple;
|
||||
|
||||
public FillTupleKey key => _fill_tuple.key;
|
||||
public FillImmutable fill => _fill_tuple.fill;
|
||||
public SecurityMD? sec_md => _fill_tuple.security_md;
|
||||
public OrderRecordImmutable? order => _fill_tuple.order;
|
||||
//
|
||||
public string account => order?.Def.Account;
|
||||
public Venue? exchange => sec_md?.security.Exchange;
|
||||
public string symbol => sec_md?.security.Symbol;
|
||||
@@ -43,7 +38,5 @@ namespace mbts.spark {
|
||||
public override string ToString() {
|
||||
return $"Fill: ( {account} {exchange} {security.ToString()} buy: {buy_quantity} sell: {sell_quantity} ) Security: {security.ToString()}";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,92 +5,153 @@ using System.ComponentModel;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class CVOrderBookRowVM {
|
||||
public interface IOrderBookCell : IComparable, INotifyPropertyChanged {
|
||||
public interface IOrderBookCell : IComparable {
|
||||
string text { get; }
|
||||
}
|
||||
|
||||
public interface IOrderBookCell<T> : IOrderBookCell {
|
||||
T value { get; }
|
||||
IOrderBookCellStore<T> store { get; }
|
||||
}
|
||||
|
||||
public interface IOrderBookCellStore {
|
||||
CVOrderBookRowVM row { get; }
|
||||
ICVOrderBookColumn column { get; }
|
||||
string prop_name { get; }
|
||||
IOrderBookCell current { get; }
|
||||
//
|
||||
IOrderBookCellStore select_other(CVOrderBookRowVM row);
|
||||
bool refresh();
|
||||
void regenerate_cell();
|
||||
}
|
||||
|
||||
public interface IOrderBookCellStore<T> : IOrderBookCellStore, IComparer<T>, IEqualityComparer<T> {
|
||||
new IOrderBookCell<T> current { get; }
|
||||
new IOrderBookCellStore<T> select_other(CVOrderBookRowVM row);
|
||||
//
|
||||
T get_value(CVOrderBookRowVM row);
|
||||
string format(T value);
|
||||
}
|
||||
|
||||
public static class CellFactory {
|
||||
public static OrderBookCell<T> create<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, Func<CVOrderBookRowVM, T> get_value, Func<CVOrderBookRowVM, OrderBookCell<T>> get_cell, Func<T, string> format = null) =>
|
||||
new OrderBookCell<T>(row, column, get_value, get_cell, format, new Comparison<T>(Comparer<T>.Default.Compare), EqualityComparer<T>.Default);
|
||||
public static IOrderBookCellStore<T> create<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, string prop_name, Func<CVOrderBookRowVM, T> get_value, Func<CVOrderBookRowVM, IOrderBookCellStore<T>> select_other, Func<T, string> format = null) =>
|
||||
new OrderBookCellStore<T>(row, column, prop_name, get_value, select_other, format, new Comparison<T>(Comparer<T>.Default.Compare), EqualityComparer<T>.Default);
|
||||
|
||||
public static OrderBookCell<T> create<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, Func<CVOrderBookRowVM, T> get_value, Func<CVOrderBookRowVM, OrderBookCell<T>> get_cell, Comparison<T> comparison, Func<T, string> format = null) =>
|
||||
new OrderBookCell<T>(row, column, get_value, get_cell, format, comparison, EqualityComparer<T>.Default);
|
||||
public static IOrderBookCellStore<T> create<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, string prop_name, Func<CVOrderBookRowVM, T> get_value, Func<CVOrderBookRowVM, IOrderBookCellStore<T>> select_other, Comparison<T> comparison, Func<T, string> format = null) =>
|
||||
new OrderBookCellStore<T>(row, column, prop_name, get_value, select_other, format, comparison, EqualityComparer<T>.Default);
|
||||
|
||||
public static OrderBookCell<T?> create_nullable<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, Func<CVOrderBookRowVM, T?> get_value, Func<CVOrderBookRowVM, OrderBookCell<T?>> get_cell, Func<T?, string> format = null)
|
||||
where T : struct => new OrderBookCell<T?>(row, column, get_value, get_cell, format, CompareUtils.nullable_comparison<T>(Comparer<T>.Default.Compare), EqualityComparer.CreateNullable<T>(EqualityComparer<T>.Default.Equals));
|
||||
public static IOrderBookCellStore<T?> create_nullable<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, string prop_name, Func<CVOrderBookRowVM, T?> get_value, Func<CVOrderBookRowVM, IOrderBookCellStore<T?>> select_other, Func<T?, string> format = null)
|
||||
where T : struct => new OrderBookCellStore<T?>(row, column, prop_name, get_value, select_other, format, CompareUtils.nullable_comparison<T>(Comparer<T>.Default.Compare), EqualityComparer.CreateNullable<T>(EqualityComparer<T>.Default.Equals));
|
||||
|
||||
public static OrderBookCell<T?> create_nullable<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, Func<CVOrderBookRowVM, T?> get_value, Func<CVOrderBookRowVM, OrderBookCell<T?>> get_cell, Func<T, T, int> compare, Func<T?, string> format = null)
|
||||
where T : struct => new OrderBookCell<T?>(row, column, get_value, get_cell, format, CompareUtils.nullable_comparison(compare), EqualityComparer.CreateNullable<T>(EqualityComparer<T>.Default.Equals));
|
||||
}
|
||||
public static IOrderBookCellStore<T?> create_nullable<T>(CVOrderBookRowVM row, ICVOrderBookColumn column, string prop_name, Func<CVOrderBookRowVM, T?> get_value, Func<CVOrderBookRowVM, IOrderBookCellStore<T?>> select_other, Func<T, T, int> compare, Func<T?, string> format = null)
|
||||
where T : struct =>
|
||||
new OrderBookCellStore<T?>(row, column, prop_name, get_value, select_other, format, CompareUtils.nullable_comparison(compare), EqualityComparer.CreateNullable<T>(EqualityComparer<T>.Default.Equals));
|
||||
|
||||
public class OrderBookCell<T> : IComparable<OrderBookCell<T>>, IOrderBookCell {
|
||||
readonly CVOrderBookRowVM _row;
|
||||
readonly ICVOrderBookColumn _column;
|
||||
readonly Func<CVOrderBookRowVM, T> _get_value;
|
||||
readonly Func<CVOrderBookRowVM, OrderBookCell<T>> _get_cell;
|
||||
readonly Func<T, string> _format;
|
||||
readonly Comparison<T> _comparison;
|
||||
readonly IEqualityComparer<T> _eq_comparer;
|
||||
T _value;
|
||||
string _text;
|
||||
class OrderBookCellStore<T> : IOrderBookCellStore<T> {
|
||||
readonly CVOrderBookRowVM _row;
|
||||
readonly ICVOrderBookColumn _column;
|
||||
readonly string _prop_name;
|
||||
readonly Func<CVOrderBookRowVM, T> _get_value;
|
||||
readonly Func<CVOrderBookRowVM, IOrderBookCellStore<T>> _select_other;
|
||||
readonly Func<T, string> _format;
|
||||
readonly Comparison<T> _comparison;
|
||||
readonly IEqualityComparer<T> _eq_comparer;
|
||||
OrderBookCell<T> _current;
|
||||
|
||||
public OrderBookCell(CVOrderBookRowVM row, ICVOrderBookColumn column, Func<CVOrderBookRowVM, T> get_value, Func<CVOrderBookRowVM, OrderBookCell<T>> get_cell, Func<T, string> format, Comparison<T> comparison, IEqualityComparer<T> eq_comparer) {
|
||||
_row = row;
|
||||
_column = column;
|
||||
_get_value = get_value;
|
||||
_get_cell = get_cell;
|
||||
_format = format ?? new Func<T, string>(v => v?.ToString() ?? string.Empty);
|
||||
_comparison = comparison;
|
||||
_eq_comparer = eq_comparer;
|
||||
_value = _get_value(_row);
|
||||
}
|
||||
public OrderBookCellStore(
|
||||
CVOrderBookRowVM row,
|
||||
ICVOrderBookColumn column,
|
||||
string prop_name,
|
||||
Func<CVOrderBookRowVM, T> get_value,
|
||||
Func<CVOrderBookRowVM, IOrderBookCellStore<T>> select_other,
|
||||
Func<T, string> format,
|
||||
Comparison<T> comparison,
|
||||
IEqualityComparer<T> eq_comparer) {
|
||||
//
|
||||
_row = row;
|
||||
_column = column;
|
||||
_prop_name = prop_name;
|
||||
_get_value = get_value;
|
||||
_select_other = select_other;
|
||||
_format = format ?? new Func<T, string>(v => v?.ToString() ?? string.Empty);
|
||||
_comparison = comparison;
|
||||
_eq_comparer = eq_comparer;
|
||||
_current = new OrderBookCell<T>(this, _get_value(_row));
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public CVOrderBookRowVM row => _row;
|
||||
public ICVOrderBookColumn column => _column;
|
||||
public string prop_name => _prop_name;
|
||||
public IOrderBookCell<T> current => _current;
|
||||
IOrderBookCell IOrderBookCellStore.current => _current;
|
||||
|
||||
public T value => _value;
|
||||
public string text => _text ?? (_text = _format(_value));
|
||||
public int Compare(T x, T y) => _comparison(x, y);
|
||||
public bool Equals(T x, T y) => _eq_comparer.Equals(x, y);
|
||||
public string format(T value) => _format(value);
|
||||
public int GetHashCode(T obj) => _eq_comparer.GetHashCode(obj);
|
||||
public T get_value(CVOrderBookRowVM row) => _get_value(row);
|
||||
public IOrderBookCellStore<T> select_other(CVOrderBookRowVM row) => _select_other(row);
|
||||
IOrderBookCellStore IOrderBookCellStore.select_other(CVOrderBookRowVM row) => select_other(row);
|
||||
|
||||
public bool refresh() {
|
||||
var v = _get_value(_row);
|
||||
if (!_eq_comparer.Equals(v, _value)) {
|
||||
_value = v;
|
||||
_text = null;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(value)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(text)));
|
||||
return true;
|
||||
public bool refresh() {
|
||||
T v = _get_value(_row);
|
||||
if (!_eq_comparer.Equals(v, _current.value)) {
|
||||
_current = new OrderBookCell<T>(this, v);
|
||||
_row.RaisePropertyChanged(_prop_name);
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
public void regenerate_cell() {
|
||||
_current = new OrderBookCell<T>(this, _current.value);
|
||||
_row.RaisePropertyChanged(_prop_name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int CompareTo(OrderBookCell<T> other) {
|
||||
if (other == null)
|
||||
return 1;
|
||||
var lin = lineage(_row);
|
||||
var olin = lineage(other._row);
|
||||
int c = 0;
|
||||
using (var en = lin.GetEnumerator())
|
||||
using (var oen = olin.GetEnumerator()) {
|
||||
while (en.MoveNext() && oen.MoveNext()) {
|
||||
if (en.Current.order_key.value != oen.Current.order_key.value) {
|
||||
c = _comparison(_get_cell(en.Current).value, _get_cell(oen.Current).value);
|
||||
if (c != 0)
|
||||
return c;
|
||||
else {
|
||||
c = en.Current.order_key.value.CompareTo(oen.Current.order_key.value);
|
||||
return _column.SortDirection.HasValue && _column.SortDirection.Value == ListSortDirection.Ascending ? c : -c;
|
||||
class OrderBookCell<T> : IOrderBookCell<T> {
|
||||
string _text;
|
||||
|
||||
public OrderBookCell(OrderBookCellStore<T> store, T value) {
|
||||
this.store = store;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public string text => _text ?? (_text = store.format(value));
|
||||
public T value { get; }
|
||||
public IOrderBookCellStore<T> store { get; }
|
||||
|
||||
public int CompareTo(OrderBookCell<T> other) {
|
||||
if (other == null)
|
||||
return 1;
|
||||
var lin = lineage(store.row);
|
||||
var olin = lineage(other.store.row);
|
||||
int c = 0;
|
||||
using (var en = lin.GetEnumerator())
|
||||
using (var oen = olin.GetEnumerator()) {
|
||||
while (en.MoveNext() && oen.MoveNext()) {
|
||||
if (en.Current.order_key.value != oen.Current.order_key.value) {
|
||||
c = store.Compare(
|
||||
store.select_other(en.Current).current.value,
|
||||
store.select_other(oen.Current).current.value);
|
||||
if (c != 0)
|
||||
return c;
|
||||
else {
|
||||
c = en.Current.order_key.value.CompareTo(
|
||||
oen.Current.order_key.value);
|
||||
return store.column.SortDirection.HasValue && store.column.SortDirection.Value == ListSortDirection.Ascending ? c : -c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return store.column.SortDirection.HasValue && store.column.SortDirection.Value == ListSortDirection.Ascending ?
|
||||
lin.Count.CompareTo(olin.Count) :
|
||||
olin.Count.CompareTo(lin.Count);
|
||||
}
|
||||
return _column.SortDirection.HasValue && _column.SortDirection.Value == ListSortDirection.Ascending ?
|
||||
lin.Count.CompareTo(olin.Count) : olin.Count.CompareTo(lin.Count);
|
||||
|
||||
public int CompareTo(object obj) => CompareTo(obj as OrderBookCell<T>);
|
||||
|
||||
public override string ToString() => text;
|
||||
}
|
||||
|
||||
public int CompareTo(object obj) =>
|
||||
CompareTo(obj as OrderBookCell<T>);
|
||||
|
||||
public override string ToString() => text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,14 +34,15 @@ namespace mbts.spark {
|
||||
// TODO: Maybe binding to the SortDirection would be less smelly?
|
||||
// - initially, I tried to bind to the SortDirection of each column, unfortunately, the binding target was not updated before the ICollectionView sorting took place.
|
||||
// I decided it was not worth changing the DIYGrid.Sorting function to force the SortDirection binding targets to update immediately, before the ICollectionView sorting.
|
||||
interface ICVOrderBookColumn {
|
||||
interface ICVOrderBookColumn : IKeyedColumn<Columns> {
|
||||
string sort_member_path { get; }
|
||||
ListSortDirection? SortDirection { get; }
|
||||
IDataGridColumnFilter<CVOrderBookRowVM> filter { get; }
|
||||
}
|
||||
|
||||
partial class CVOrderBookVM {
|
||||
public class CVOrderBookColumnCollection : ObservableCollection<ColumnBase>, IDisposable {
|
||||
public CVOrderBookColumnCollection(IBus bus, OrdersModel model, OrderBookStartupData startup_data, IObservable<(IReadOnlyList<SecurityImmutable> added, IReadOnlyList<SecurityImmutable> removed)> order_securities) {
|
||||
public CVOrderBookColumnCollection(IBus bus, OrdersModel model, OrderBookStartupData startup_data, IObservable<(IEnumerable<SecurityImmutable> added, IEnumerable<SecurityImmutable> removed)> order_securities) {
|
||||
// security
|
||||
Add(security_column = new SecurityColumn(this,
|
||||
DiscreteValuesFilterVM.Create<CVOrderBookRowVM, SecurityImmutable, SecurityCriteria>(
|
||||
@@ -200,7 +201,7 @@ namespace mbts.spark {
|
||||
DataGridFilterKnownTemplates.Regex));
|
||||
|
||||
string value_path(string cell_name) =>
|
||||
string.Join(".", cell_name, nameof(CVOrderBookRowVM.OrderBookCell<uint>.value)); //uint is an arbitrary 'T', just want to get compile check on 'value' path.
|
||||
string.Join(".", cell_name, nameof(CVOrderBookRowVM.IOrderBookCell<uint>.value)); //uint is an arbitrary 'T', just want to get compile check on 'value' path.
|
||||
|
||||
if (startup_data?.filters is FilterStates fs) {
|
||||
security_column.filter.load_state(fs.security_criteria);
|
||||
@@ -317,7 +318,10 @@ namespace mbts.spark {
|
||||
string IFilterableColumn.filter_template => _filter_template;
|
||||
ListSortDirection? ICVOrderBookColumn.SortDirection => _cached_sort_direction;
|
||||
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) => _cached_sort_direction = new_value;
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) {
|
||||
_cached_sort_direction = new_value;
|
||||
base.OnSortDirectionChanged(new_value);
|
||||
}
|
||||
}
|
||||
|
||||
public class SimpleTextColumn<TFilterState> : OrderbookColumnBase<TFilterState>, IRenderColumn<CVOrderBookRowVM> {
|
||||
@@ -414,7 +418,7 @@ namespace mbts.spark {
|
||||
public class OrderBookOrderTypeColumn : OrderTypeColumn<Columns>, ICVOrderBookColumn {
|
||||
ListSortDirection? _cached_sort_direction;
|
||||
public OrderBookOrderTypeColumn(IDataGridColumnPersistentFilter<CVOrderBookRowVM, OrderType[]> filter)
|
||||
: base(Columns.OrderType, new Binding($"{nameof(CVOrderBookRowVM.order_type)}.{nameof(CVOrderBookRowVM.OrderBookCell<OrderType>.value)}"), new Binding(nameof(CVOrderBookRowVM.order_metadata)), filter) {
|
||||
: base(Columns.OrderType, new Binding($"{nameof(CVOrderBookRowVM.order_type)}.{nameof(CVOrderBookRowVM.IOrderBookCell<OrderType>.value)}"), new Binding(nameof(CVOrderBookRowVM.order_metadata)), filter) {
|
||||
this.filter = filter;
|
||||
sort_member_path = nameof(CVOrderBookRowVM.order_type);
|
||||
font_weight_binding = RenderUtils.get_font_weight_binding();
|
||||
@@ -426,14 +430,17 @@ namespace mbts.spark {
|
||||
IDataGridColumnFilter<CVOrderBookRowVM> ICVOrderBookColumn.filter => filter;
|
||||
ListSortDirection? ICVOrderBookColumn.SortDirection => _cached_sort_direction;
|
||||
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) => _cached_sort_direction = new_value;
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) {
|
||||
_cached_sort_direction = new_value;
|
||||
base.OnSortDirectionChanged(new_value);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderBookLastMessageColumn : TextColumn<Columns>, ICVOrderBookColumn {
|
||||
ListSortDirection? _cached_sort_direction;
|
||||
|
||||
public OrderBookLastMessageColumn(IDataGridColumnPersistentFilter<CVOrderBookRowVM, string> filter)
|
||||
: base(Columns.LastMessage, new Binding(nameof(CVOrderBookRowVM.last_msg)), filter, DataGridFilterKnownTemplates.Regex) {
|
||||
: base(Columns.LastMessage, new Binding($"{nameof(CVOrderBookRowVM.last_msg)}.{nameof(CVOrderBookRowVM.IOrderBookCell<string>.value)}"), filter, DataGridFilterKnownTemplates.Regex) {
|
||||
this.filter = filter;
|
||||
Header = "Last Message";
|
||||
Width = 80;
|
||||
@@ -446,7 +453,10 @@ namespace mbts.spark {
|
||||
IDataGridColumnFilter<CVOrderBookRowVM> ICVOrderBookColumn.filter => filter;
|
||||
ListSortDirection? ICVOrderBookColumn.SortDirection => _cached_sort_direction;
|
||||
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) => _cached_sort_direction = new_value;
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) {
|
||||
_cached_sort_direction = new_value;
|
||||
base.OnSortDirectionChanged(new_value);
|
||||
}
|
||||
}
|
||||
|
||||
public class SecurityColumn : FrameworkElementColumnBase<Columns>, ICVOrderBookColumn, IFilterableColumn {
|
||||
@@ -480,7 +490,7 @@ namespace mbts.spark {
|
||||
toggle_button.SetResourceReference(ToggleButton.StyleProperty, ExpandCollapseToggleStyleKey);
|
||||
//
|
||||
var security_view = new TextBlock();
|
||||
security_view.SetBinding(TextBlock.TextProperty, nameof(CVOrderBookRowVM.security));
|
||||
security_view.SetBinding(TextBlock.TextProperty, $"{nameof(CVOrderBookRowVM.security)}.{nameof(CVOrderBookRowVM.IOrderBookCell<SecurityImmutable?>.text)}");
|
||||
security_view.SetBinding(TextBlock.FontWeightProperty, _font_weight_binding);
|
||||
//
|
||||
var grid = new Grid();
|
||||
@@ -492,7 +502,10 @@ namespace mbts.spark {
|
||||
return grid;
|
||||
}
|
||||
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) => _cached_sort_direction = new_value;
|
||||
protected override void OnSortDirectionChanged(ListSortDirection? new_value) {
|
||||
_cached_sort_direction = new_value;
|
||||
base.OnSortDirectionChanged(new_value);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -5,23 +5,47 @@ using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Input;
|
||||
using static mbts.spark.OrderBookView;
|
||||
using OrderStatus = bts.api.orderman.OrderStatus;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class CVOrderBookRowVM : ViewModelBase, IEquatable<CVOrderBookRowVM>, ui.controls.DIYGrid.IRowVM, ui.controls.DIYGrid.IActionableRowVM {
|
||||
partial class CVOrderBookRowVM : DisposableViewModelBase, IEquatable<CVOrderBookRowVM>, ui.controls.DIYGrid.IRowVM, ui.controls.DIYGrid.IActionableRowVM {
|
||||
readonly CVOrderBookVM _book_vm;
|
||||
readonly OrdersModel _model;
|
||||
readonly Property<bool> _visible, _expanded;
|
||||
readonly DelegateProperty<bool> _passes_filter;
|
||||
readonly HashSet<uint> _child_orders_pass_filter = new HashSet<uint>();
|
||||
readonly Property<bool> _visible, _expanded, _passes_filter;
|
||||
readonly DelegateProperty<int> _depth_level;
|
||||
readonly HashSet<CVOrderBookRowVM> _child_rows = new HashSet<CVOrderBookRowVM>();
|
||||
readonly ObservantCommand _show_order_modify_cmd;
|
||||
OrderRecordImmutable _order;
|
||||
SecurityImmutable? _security;
|
||||
SecurityImmutable? _security;
|
||||
CVOrderBookRowVM _parent;
|
||||
bool _row_passes_filter;
|
||||
CVOrderBookVM.BranchFilterSource _branch_filter_source;
|
||||
IDisposable _branch_filter_sub;
|
||||
// Core
|
||||
IOrderBookCellStore<uint> _order_key_store;
|
||||
IOrderBookCellStore<uint> _parent_order_key_store;
|
||||
IOrderBookCellStore<uint> _security_key_store;
|
||||
IOrderBookCellStore<SecurityImmutable?> _security_store;
|
||||
IOrderBookCellStore<Venue> _exchange_store;
|
||||
// Def
|
||||
IOrderBookCellStore<CustomOrderDefinition.CustomOneofCase> _order_type_store;
|
||||
IOrderBookCellStore<Side> _side_store;
|
||||
IOrderBookCellStore<string> _user_store;
|
||||
IOrderBookCellStore<string> _account_store;
|
||||
IOrderBookCellStore<TimeInForce> _time_in_force_store;
|
||||
IOrderBookCellStore<DateTime> _time_created_store;
|
||||
// State
|
||||
IOrderBookCellStore<DateTime> _last_change_store { get; set; }
|
||||
IOrderBookCellStore<OrderStatus> _status_store;
|
||||
IOrderBookCellStore<double> _price_store;
|
||||
IOrderBookCellStore<uint> _quantity_store;
|
||||
IOrderBookCellStore<uint> _filled_quantity_store;
|
||||
IOrderBookCellStore<double> _last_price_store;
|
||||
IOrderBookCellStore<double> _avg_price_store;
|
||||
IOrderBookCellStore<string> _last_msg_store;
|
||||
IOrderBookCellStore<OrderSubType> _order_sub_type_store;
|
||||
|
||||
public CVOrderBookRowVM(IBus bus, CVOrderBookVM source_vm, OrdersModel model, OrderRecordImmutable record, SecurityImmutable? s) {
|
||||
_book_vm = source_vm;
|
||||
@@ -29,26 +53,26 @@ namespace mbts.spark {
|
||||
_order = record;
|
||||
_security = s;
|
||||
// cells
|
||||
security = CellFactory.create_nullable(this, _book_vm.columns.security_column, r => r._security, r => r.security, v => v.HasValue ? v.Value.ToString() : "unknown security");
|
||||
security_key = CellFactory.create(this, _book_vm.columns.security_key_column, r => r._order.security_key(), r => r.security_key);
|
||||
parent_order_key = CellFactory.create(this, _book_vm.columns.parent_key_column, r => r._order.ParentKey, r => r.parent_order_key);
|
||||
order_key = CellFactory.create(this, _book_vm.columns.order_key_column, r => r._order.Key, r => r.order_key);
|
||||
time_created = CellFactory.create(this, _book_vm.columns.time_created_column, r => r._order.created(), r => r.time_created, format_date);
|
||||
user = CellFactory.create(this, _book_vm.columns.user_column, r => _order.user(), r => r.user);
|
||||
account = CellFactory.create(this, _book_vm.columns.account_column, r => r._order.account(), r => r.account);
|
||||
exchange = CellFactory.create(this, _book_vm.columns.exchange_column, r => r._security.HasValue ? _security.Value.Exchange : Venue.Unset, r => r.exchange);
|
||||
time_in_force = CellFactory.create(this, _book_vm.columns.time_in_force_column, r => r._order.time_in_force(), r => r.time_in_force);
|
||||
order_type = CellFactory.create(this, _book_vm.columns.order_type_column, r => r._order.order_type(), r => r.order_type);
|
||||
order_sub_type = CellFactory.create(this, _book_vm.columns.sub_type_column, r => r._order.sub_type(), r => r.order_sub_type, format_sub_type);
|
||||
side = CellFactory.create(this, _book_vm.columns.side_column, r => r._order.side(), r => r.side);
|
||||
last_change = CellFactory.create(this, _book_vm.columns.last_change_column, r => _order.last_change(), r => r.last_change, format_date);
|
||||
price = CellFactory.create(this, _book_vm.columns.price_column, r => _order.price(), r => r.price, format_price);
|
||||
quantity = CellFactory.create(this, _book_vm.columns.qty_column, r => _order.quantity(), r => r.quantity);
|
||||
filled_quantity = CellFactory.create(this, _book_vm.columns.filled_qty_column, r => _order.filled(), r => r.filled_quantity);
|
||||
avg_price = CellFactory.create(this, _book_vm.columns.avg_price_column, r => _order.avg_price(), r => r.avg_price, format_price);
|
||||
last_price = CellFactory.create(this, _book_vm.columns.last_price_column, r => _order.last_price(), r => r.last_price, format_price);
|
||||
status = CellFactory.create(this, _book_vm.columns.status_column, r => _order.status(), r => r.status);
|
||||
last_msg = CellFactory.create(this, _book_vm.columns.last_message_column, r => _order.last_msg(), r => r.last_msg);
|
||||
_security_store = CellFactory.create_nullable(this, _book_vm.columns.security_column, nameof(security), r => r._security, r => r._security_store, v => v.HasValue ? v.Value.ToString() : "unknown security");
|
||||
_security_key_store = CellFactory.create(this, _book_vm.columns.security_key_column, nameof(security_key), r => r._order.security_key(), r => r._security_key_store);
|
||||
_parent_order_key_store = CellFactory.create(this, _book_vm.columns.parent_key_column, nameof(parent_order_key), r => r._order.ParentKey, r => r._parent_order_key_store);
|
||||
_order_key_store = CellFactory.create(this, _book_vm.columns.order_key_column, nameof(order_key), r => r._order.Key, r => r._order_key_store);
|
||||
_time_created_store = CellFactory.create(this, _book_vm.columns.time_created_column, nameof(time_created), r => r._order.created(), r => r._time_created_store, format_date);
|
||||
_user_store = CellFactory.create(this, _book_vm.columns.user_column, nameof(user), r => _order.user(), r => r._user_store);
|
||||
_account_store = CellFactory.create(this, _book_vm.columns.account_column, nameof(account), r => r._order.account(), r => r._account_store);
|
||||
_exchange_store = CellFactory.create(this, _book_vm.columns.exchange_column, nameof(exchange), r => r._security.HasValue ? _security.Value.Exchange : Venue.Unset, r => r._exchange_store);
|
||||
_time_in_force_store = CellFactory.create(this, _book_vm.columns.time_in_force_column, nameof(time_in_force), r => r._order.time_in_force(), r => r._time_in_force_store);
|
||||
_order_type_store = CellFactory.create(this, _book_vm.columns.order_type_column, nameof(order_type), r => r._order.order_type(), r => r._order_type_store);
|
||||
_order_sub_type_store = CellFactory.create(this, _book_vm.columns.sub_type_column, nameof(order_sub_type), r => r._order.sub_type(), r => r._order_sub_type_store, format_sub_type);
|
||||
_side_store = CellFactory.create(this, _book_vm.columns.side_column, nameof(side), r => r._order.side(), r => r._side_store);
|
||||
_last_change_store = CellFactory.create(this, _book_vm.columns.last_change_column, nameof(last_change), r => _order.last_change(), r => r._last_change_store, format_date);
|
||||
_price_store = CellFactory.create(this, _book_vm.columns.price_column, nameof(price), r => _order.price(), r => r._price_store, format_price);
|
||||
_quantity_store = CellFactory.create(this, _book_vm.columns.qty_column, nameof(quantity), r => _order.quantity(), r => r._quantity_store);
|
||||
_filled_quantity_store = CellFactory.create(this, _book_vm.columns.filled_qty_column, nameof(filled_quantity), r => _order.filled(), r => r._filled_quantity_store);
|
||||
_avg_price_store = CellFactory.create(this, _book_vm.columns.avg_price_column, nameof(avg_price), r => _order.avg_price(), r => r._avg_price_store, format_price);
|
||||
_last_price_store = CellFactory.create(this, _book_vm.columns.last_price_column, nameof(last_price), r => _order.last_price(), r => r._last_price_store, format_price);
|
||||
_status_store = CellFactory.create(this, _book_vm.columns.status_column, nameof(status), r => _order.status(), r => r._status_store);
|
||||
_last_msg_store = CellFactory.create(this, _book_vm.columns.last_message_column, nameof(last_msg), r => _order.last_msg(), r => r._last_msg_store);
|
||||
//
|
||||
string format_price(double v) => _security.HasValue ? _security.Value.ContractSpec.format(v) : v.ToString();
|
||||
string format_date(DateTime v) => $"{v:M/d} {v:h:mm:ss.fff tt}";
|
||||
@@ -60,61 +84,73 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
//
|
||||
_visible = backing(nameof(is_visible), !_order.has_parent());
|
||||
_visible = backing(nameof(is_visible), true);
|
||||
_expanded = backing(nameof(is_expanded), true);
|
||||
_row_passes_filter = _book_vm.columns.filter(this);
|
||||
_passes_filter = backing(nameof(passes_filter), () => {
|
||||
if (_parent != null)
|
||||
return _parent.passes_filter;
|
||||
else return _row_passes_filter || _descendents(this).Any(r => r._row_passes_filter);
|
||||
_depth_level = backing(nameof(depth_level), () => {
|
||||
int i = 0;
|
||||
var curr = this;
|
||||
while (curr._parent != null) {
|
||||
i++;
|
||||
curr = curr._parent;
|
||||
}
|
||||
return i;
|
||||
});
|
||||
toggle_command = new DelegateCommand(() => is_expanded = !is_expanded);
|
||||
_show_order_modify_cmd = new ObservantCommand(() => bus.post(SparkTags.ShowModifyOrderTag, _order.Key));
|
||||
_show_order_modify_cmd.OnNext(status.value == OrderStatus.Working);
|
||||
row_passes_filter = _book_vm.columns.filter(this);
|
||||
_passes_filter = backing(nameof(passes_filter),
|
||||
Observable.FromEvent<Action<IObservable<bool>>, IObservable<bool>>(
|
||||
cb => _branch_filter_source_changed += cb, cb => _branch_filter_source_changed -= cb)
|
||||
.SelectMany(o => o));
|
||||
}
|
||||
|
||||
|
||||
public event Action invalidated;
|
||||
event Action<IObservable<bool>> _branch_filter_source_changed;
|
||||
|
||||
public OrderRecordImmutable order => _order;
|
||||
// Core
|
||||
public OrderBookCell<uint> order_key { get; }
|
||||
public OrderBookCell<uint> parent_order_key { get; }
|
||||
public OrderBookCell<uint> security_key { get; }
|
||||
public OrderBookCell<SecurityImmutable?> security { get; }
|
||||
public OrderBookCell<Venue> exchange { get; }
|
||||
// Def
|
||||
public OrderBookCell<CustomOrderDefinition.CustomOneofCase> order_type { get; }
|
||||
public OrderBookCell<Side> side { get; }
|
||||
public OrderBookCell<string> user { get; }
|
||||
public OrderBookCell<string> account { get; }
|
||||
public OrderBookCell<TimeInForce> time_in_force { get; }
|
||||
public OrderBookCell<DateTime> time_created { get; }
|
||||
// State
|
||||
public OrderBookCell<DateTime> last_change { get; }
|
||||
public OrderBookCell<OrderStatus> status { get; }
|
||||
public OrderBookCell<double> price { get; }
|
||||
public OrderBookCell<uint> quantity { get; }
|
||||
public OrderBookCell<uint> filled_quantity { get; }
|
||||
public OrderBookCell<double> last_price { get; }
|
||||
public OrderBookCell<double> avg_price { get; }
|
||||
public OrderBookCell<string> last_msg { get; }
|
||||
public OrderBookCell<OrderSubType> order_sub_type { get; }
|
||||
public OrderMetadata order_metadata => _model.factory.get_metadata(order_type.value);
|
||||
public CVOrderBookRowVM parent => _parent;
|
||||
// Core
|
||||
public IOrderBookCell<uint> order_key => _order_key_store.current;
|
||||
public IOrderBookCell<uint> parent_order_key => _parent_order_key_store.current;
|
||||
public IOrderBookCell<uint> security_key => _security_key_store.current;
|
||||
public IOrderBookCell<SecurityImmutable?> security => _security_store.current;
|
||||
public IOrderBookCell<Venue> exchange => _exchange_store.current;
|
||||
// Def
|
||||
public IOrderBookCell<CustomOrderDefinition.CustomOneofCase> order_type => _order_type_store.current;
|
||||
public IOrderBookCell<Side> side => _side_store.current;
|
||||
public IOrderBookCell<string> user => _user_store.current;
|
||||
public IOrderBookCell<string> account => _account_store.current;
|
||||
public IOrderBookCell<TimeInForce> time_in_force => _time_in_force_store.current;
|
||||
public IOrderBookCell<DateTime> time_created => _time_created_store.current;
|
||||
// State
|
||||
public IOrderBookCell<DateTime> last_change => _last_change_store.current;
|
||||
public IOrderBookCell<OrderStatus> status => _status_store.current;
|
||||
public IOrderBookCell<double> price => _price_store.current;
|
||||
public IOrderBookCell<uint> quantity => _quantity_store.current;
|
||||
public IOrderBookCell<uint> filled_quantity => _filled_quantity_store.current;
|
||||
public IOrderBookCell<double> last_price => _last_price_store.current;
|
||||
public IOrderBookCell<double> avg_price => _avg_price_store.current;
|
||||
public IOrderBookCell<string> last_msg => _last_msg_store.current;
|
||||
public IOrderBookCell<OrderSubType> order_sub_type => _order_sub_type_store.current;
|
||||
// Commands
|
||||
public ICommand toggle_command { get; }
|
||||
ICommand ui.controls.DIYGrid.IActionableRowVM.default_command => _show_order_modify_cmd;
|
||||
// Accessories
|
||||
public CVOrderBookRowVM parent_row => _parent;
|
||||
public bool is_parent => _child_rows.Count > 0;
|
||||
public bool is_child => _order.has_parent();
|
||||
public IReadOnlyCollection<CVOrderBookRowVM> child_orders => _child_rows;
|
||||
public CVOrderBookVM.BranchFilterSource branch_filter_source => _branch_filter_source;
|
||||
public bool passes_filter => _passes_filter.Value;
|
||||
public bool row_passes_filter { get; private set; }
|
||||
public bool is_visible {
|
||||
get => _visible.Value;
|
||||
set {
|
||||
_visible.set(value);
|
||||
if (is_expanded) {
|
||||
foreach (var child_row in _child_rows)
|
||||
child_row._visible.set(value);
|
||||
child_row.is_visible = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,165 +162,34 @@ namespace mbts.spark {
|
||||
child_row.is_visible = value;
|
||||
}
|
||||
}
|
||||
public int depth_level {
|
||||
get {
|
||||
int i = 0;
|
||||
var curr = this;
|
||||
while (curr._parent != null) {
|
||||
i++;
|
||||
curr = curr._parent;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
public bool passes_filter => _passes_filter.Value;
|
||||
public bool row_passes_filter {
|
||||
get => _row_passes_filter;
|
||||
set {
|
||||
if (_row_passes_filter != value) {
|
||||
_row_passes_filter = value;
|
||||
if (value) {
|
||||
if (!passes_filter) {
|
||||
var root = root_node(this);
|
||||
root._passes_filter.refresh();
|
||||
foreach (var r in _descendents(root))
|
||||
r._passes_filter.refresh();
|
||||
}
|
||||
} else if (passes_filter) {
|
||||
var root = root_node(this);
|
||||
if (!root._row_passes_filter && !_descendents(root).AtLeast(2, r => r._row_passes_filter )) {
|
||||
root._passes_filter.refresh();
|
||||
foreach (var r in _descendents(root))
|
||||
r._passes_filter.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public int depth_level => _depth_level.Value;
|
||||
|
||||
// '_dynamic_render_cells' are presented by 'RenderColumns,' and may change value during the lifetime of this row
|
||||
// Note: by instantiating all these cells inside a CellPropertyGroup, we can update all of them with a single call.
|
||||
// this reduces the risk of accidentally failing to update one of the cells, esp if we make changes to them down the road.
|
||||
IEnumerable<IOrderBookCell> _dynamic_render_cells {
|
||||
IEnumerable<IOrderBookCellStore> _dynamic_render_cells {
|
||||
get {
|
||||
yield return last_change;
|
||||
yield return price;
|
||||
yield return quantity;
|
||||
yield return filled_quantity;
|
||||
yield return avg_price;
|
||||
yield return last_price;
|
||||
yield return status;
|
||||
yield return last_msg;
|
||||
yield return _last_change_store;
|
||||
yield return _price_store;
|
||||
yield return _quantity_store;
|
||||
yield return _filled_quantity_store;
|
||||
yield return _avg_price_store;
|
||||
yield return _last_price_store;
|
||||
yield return _status_store;
|
||||
yield return _last_msg_store;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<IOrderBookCell> _security_dependent_cells {
|
||||
IEnumerable<IOrderBookCellStore> _security_dependent_cells {
|
||||
get {
|
||||
yield return security;
|
||||
yield return exchange;
|
||||
yield return price;
|
||||
yield return avg_price;
|
||||
yield return last_price;
|
||||
yield return _security_store;
|
||||
yield return _exchange_store;
|
||||
yield return _price_store;
|
||||
yield return _avg_price_store;
|
||||
yield return _last_price_store;
|
||||
}
|
||||
}
|
||||
|
||||
public void add_child_order(CVOrderBookRowVM child_row) {
|
||||
// we don't need to raise this every single time
|
||||
if (_child_rows.Add(child_row) && _child_rows.Count == 1)
|
||||
RaisePropertyChanged("is_parent");
|
||||
child_row._parent = this;
|
||||
child_row.RaisePropertyChanged(nameof(depth_level));
|
||||
var root = root_node(this);
|
||||
if (child_row.passes_filter) {
|
||||
if (child_row._row_passes_filter)
|
||||
_child_orders_pass_filter.Add(child_row._order.Key);
|
||||
// if this row isn't currently passing the filter, update it and the rest of the branch
|
||||
if (!passes_filter) {
|
||||
_passes_filter.set(true);
|
||||
// child_row and its descendents already know they passed failter
|
||||
foreach (var row in _descendents_except_node(root, child_row))
|
||||
row._passes_filter.set(true);
|
||||
}
|
||||
}
|
||||
// recursively updates 'live_sorting_props,' and 'is_visible' for child_row and descendents
|
||||
child_row.on_parent_updated();
|
||||
if (child_row.row_passes_filter && !root.passes_filter) {
|
||||
root._passes_filter.Value = true;
|
||||
foreach (var d in _descendents(this))
|
||||
d._passes_filter.Value = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void remove_child_order(CVOrderBookRowVM child_row) {
|
||||
if (_child_rows.Remove(child_row) && _child_rows.Count == 0)
|
||||
RaisePropertyChanged(nameof(is_parent));
|
||||
if (child_row._row_passes_filter) {
|
||||
_child_orders_pass_filter.Remove(child_row._order.Key);
|
||||
_passes_filter.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises PropertyChanged events for all live sorting properties, then calls parent_updated() on all descendents.
|
||||
/// <para>Note: call this method when parent order is added, removed, or changed.</para>
|
||||
/// </summary>
|
||||
public void on_parent_updated() {
|
||||
foreach (var p in _book_vm.live_sorting_props)
|
||||
RaisePropertyChanged(p);
|
||||
is_visible = _parent == null || _parent.is_visible && _parent.is_expanded;
|
||||
foreach (var child_row in _child_rows)
|
||||
child_row.on_parent_updated();
|
||||
}
|
||||
|
||||
public void update_order(OrderRecordImmutable order) {
|
||||
_order = order;
|
||||
// use Count(), as opposed to Any(), to force it to iterate through all cells
|
||||
if (_dynamic_render_cells.Count(c => c.refresh()) > 0)
|
||||
invalidated?.Invoke();
|
||||
_show_order_modify_cmd.OnNext(status.value == OrderStatus.Working);
|
||||
_on_update();
|
||||
}
|
||||
|
||||
public void update_security(SecurityImmutable security) {
|
||||
_security = security;
|
||||
// use Count(), as opposed to Any(), to force it to iterate through all cells
|
||||
if (_security_dependent_cells.Count(c => c.refresh()) > 0)
|
||||
invalidated?.Invoke();
|
||||
_on_update();
|
||||
}
|
||||
|
||||
void _on_update() {
|
||||
foreach (var p in _book_vm.live_sorting_props)
|
||||
RaisePropertyChanged(p);
|
||||
foreach (var child_row in _child_rows)
|
||||
child_row.on_parent_updated();
|
||||
_row_passes_filter = _book_vm.columns.filter(this);
|
||||
row_passes_filter = _book_vm.columns.filter(this);
|
||||
}
|
||||
|
||||
public void refresh_row_passes_filter() =>
|
||||
_row_passes_filter = _book_vm.columns.filter(this);
|
||||
|
||||
public void refresh_branch_passes_filter() {
|
||||
_passes_filter.refresh();
|
||||
foreach (var r in _descendents(this))
|
||||
r._passes_filter.refresh();
|
||||
}
|
||||
|
||||
public void on_filter_changed() =>
|
||||
_passes_filter.refresh();
|
||||
|
||||
public bool Equals(CVOrderBookRowVM other) => other != null && other.order_key == order_key;
|
||||
public override bool Equals(object obj) => Equals(obj as CVOrderBookRowVM);
|
||||
public override int GetHashCode() => (int)_order.Key;
|
||||
public override string ToString() => $"{order_key} {last_change} {parent_order_key}";
|
||||
|
||||
public static CVOrderBookRowVM root_node(CVOrderBookRowVM node) {
|
||||
while (node._parent != null)
|
||||
node = node._parent;
|
||||
return node;
|
||||
}
|
||||
|
||||
public static Stack<CVOrderBookRowVM> lineage(CVOrderBookRowVM node) {
|
||||
var rv = new Stack<CVOrderBookRowVM>();
|
||||
rv.Push(node);
|
||||
@@ -293,16 +198,73 @@ namespace mbts.spark {
|
||||
return rv;
|
||||
}
|
||||
|
||||
static IEnumerable<CVOrderBookRowVM> _descendents_except_node(CVOrderBookRowVM root, CVOrderBookRowVM except) {
|
||||
foreach (var child in root._child_rows) {
|
||||
if (child == except)
|
||||
continue;
|
||||
yield return child;
|
||||
foreach (var grand_child in _descendents(child))
|
||||
yield return grand_child;
|
||||
public void join_branch_filter_source(CVOrderBookVM.BranchFilterSource new_branch_filter_source) {
|
||||
_branch_filter_source = new_branch_filter_source;
|
||||
_branch_filter_source.set_row_passes_filter(_order.Key, row_passes_filter);
|
||||
_branch_filter_source_changed?.Invoke(_branch_filter_source);
|
||||
foreach (var child_row in _child_rows)
|
||||
child_row.join_branch_filter_source(_branch_filter_source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the child-row to this row's branch-filter-source, and recursively refreshes sort/filter properties for the child and it's descendents.
|
||||
/// </summary>
|
||||
public void add_child_order(CVOrderBookRowVM child_row) {
|
||||
if (_child_rows.Add(child_row) && _child_rows.Count == 1)
|
||||
RaisePropertyChanged(nameof(is_parent));
|
||||
child_row._parent = this;
|
||||
// child orders that arrived in an earlier update use a temporary branch_filter_source
|
||||
// need to tell _book_vm to remove the now-stale branch_filter_source.
|
||||
if (child_row._branch_filter_source != null)
|
||||
_book_vm.remove_branch_filter_root(child_row._order.Key);
|
||||
// recursively updates 'live_sorting_props,' and 'is_visible' for child_row and descendents
|
||||
child_row._on_lineage_changed();
|
||||
}
|
||||
|
||||
public void remove_child_order(CVOrderBookRowVM child_row) {
|
||||
if (_child_rows.Remove(child_row) && _child_rows.Count == 0)
|
||||
RaisePropertyChanged(nameof(is_parent));
|
||||
}
|
||||
|
||||
public void remove_parent(CVOrderBookVM.BranchFilterSource new_branch_filter_source) {
|
||||
_parent = null;
|
||||
join_branch_filter_source(new_branch_filter_source);
|
||||
refresh_row_passes_filter();
|
||||
foreach (var p in _book_vm.live_sorting_props)
|
||||
RaisePropertyChanged(p);
|
||||
_depth_level.refresh();
|
||||
foreach (CVOrderBookRowVM child_row in _child_rows)
|
||||
child_row._on_lineage_changed();
|
||||
}
|
||||
|
||||
public void update_order(OrderRecordImmutable order) {
|
||||
Logger.assert(order.ParentKey == _order.ParentKey, "Parent Keys can change?");
|
||||
Logger.assert(order.Key == _order.Key, "Keys can change?");
|
||||
_order = order;
|
||||
_update_partial(_dynamic_render_cells);
|
||||
}
|
||||
|
||||
public void update_security(SecurityImmutable security) {
|
||||
_security = security;
|
||||
_update_partial(_security_dependent_cells);
|
||||
}
|
||||
|
||||
public void refresh_row_passes_filter() {
|
||||
row_passes_filter = _book_vm.columns.filter(this);
|
||||
if (_branch_filter_source != null) {
|
||||
_branch_filter_source.set_row_passes_filter(_order.Key, row_passes_filter);
|
||||
} else {
|
||||
_passes_filter.set(row_passes_filter);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(CVOrderBookRowVM other) => other != null && other._order.Key == _order.Key;
|
||||
public override bool Equals(object obj) => Equals(obj as CVOrderBookRowVM);
|
||||
public override int GetHashCode() => (int)_order.Key;
|
||||
public override string ToString() => $"{order_key} {last_change} {parent_order_key}";
|
||||
|
||||
protected override void Dispose(bool disposing) => Disposable.dispose(ref _branch_filter_sub);
|
||||
|
||||
static IEnumerable<CVOrderBookRowVM> _descendents(CVOrderBookRowVM root) {
|
||||
foreach (var child in root.child_orders) {
|
||||
yield return child;
|
||||
@@ -310,5 +272,66 @@ namespace mbts.spark {
|
||||
yield return grand_child;
|
||||
}
|
||||
}
|
||||
|
||||
static IOrderBookCellStore _cell_store_from_column(CVOrderBookRowVM row, Columns column) {
|
||||
switch (column) {
|
||||
case Columns.OrderKey: return row._order_key_store;
|
||||
case Columns.ParentKey: return row._parent_order_key_store;
|
||||
case Columns.SecurityKey: return row._security_key_store;
|
||||
case Columns.Security: return row._security_store;
|
||||
case Columns.Exchange: return row._exchange_store;
|
||||
case Columns.OrderType: return row._order_type_store;
|
||||
case Columns.Side: return row._side_store;
|
||||
case Columns.User: return row._user_store;
|
||||
case Columns.Account: return row._account_store;
|
||||
case Columns.TimeInForce: return row._time_in_force_store;
|
||||
case Columns.Time: return row._time_created_store;
|
||||
case Columns.LastChange: return row._last_change_store;
|
||||
case Columns.Status: return row._status_store;
|
||||
case Columns.Price: return row._price_store;
|
||||
case Columns.Size: return row._quantity_store;
|
||||
case Columns.Filled: return row._filled_quantity_store;
|
||||
case Columns.LastPrice: return row._last_price_store;
|
||||
case Columns.AvgFilled: return row._avg_price_store;
|
||||
case Columns.LastMessage: return row._last_msg_store;
|
||||
case Columns.SubType: return row._order_sub_type_store;
|
||||
default: throw new ArgumentException(nameof(column));
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this method assumes that lineage exists (i.e. _parent is not null).
|
||||
void _on_lineage_changed() {
|
||||
_depth_level.refresh();
|
||||
// parent 'branch_filter_source' can be null:
|
||||
// if the parent is itself an orphan - meaning this is the middle of an update -
|
||||
// the final branch-filter-root will be determined after all new orders have been added
|
||||
if (_parent._branch_filter_source != null)
|
||||
join_branch_filter_source(_parent._branch_filter_source);
|
||||
_visible.set(_parent.is_visible && _parent.is_expanded);
|
||||
// refresh the sort by calling regenerate_cell() for each live-sorting-columns corresponding cell-store
|
||||
foreach (Columns column in _book_vm.live_sorting_columns)
|
||||
_cell_store_from_column(this, column).regenerate_cell();
|
||||
// finally, repeat for each child-row
|
||||
foreach (var child_row in _child_rows)
|
||||
child_row._on_lineage_changed();
|
||||
}
|
||||
|
||||
void _update_partial(IEnumerable<IOrderBookCellStore> target_cell_stores) {
|
||||
bool invalidate = false;
|
||||
foreach (var cell_store in target_cell_stores) {
|
||||
if (cell_store.refresh()) {
|
||||
invalidate = true;
|
||||
if (_book_vm.live_sorting_columns.Contains(cell_store.column.key)) {
|
||||
// refresh the sort by calling regenerate_cell() for each descendent-row
|
||||
foreach (CVOrderBookRowVM dr in _descendents(this))
|
||||
cell_store.select_other(dr).regenerate_cell();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (invalidate)
|
||||
invalidated?.Invoke();
|
||||
_show_order_modify_cmd.OnNext(status.value == OrderStatus.Working);
|
||||
refresh_row_passes_filter();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
@@ -16,6 +17,8 @@ using System.Reactive.Subjects;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using static bts.shell.OrderBookWindowFactory;
|
||||
using static mbts.spark.OrderBookView;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class CVOrderBookVM : DisposableViewModelBase, IHandleRowSelection, IOrderBookVM {
|
||||
@@ -23,29 +26,27 @@ namespace mbts.spark {
|
||||
// each row accesses the public 'live_sorting_props' when it, or an ancestor order, is updated,
|
||||
// copying the collection locally avoids constantly casting rows_view to ICollectionViewLiveShaping.
|
||||
readonly ObservableCollection<string> _live_sorting_props;
|
||||
readonly LiveSortingColumKeys _live_sorting_columns;
|
||||
readonly Property<CVOrderBookRowVM> _current_row;
|
||||
readonly Subject<(IReadOnlyList<SecurityImmutable> added, IReadOnlyList<SecurityImmutable> removed)> _order_securities_subject =
|
||||
new Subject<(IReadOnlyList<SecurityImmutable> added, IReadOnlyList<SecurityImmutable> removed)>();
|
||||
readonly Subject<(IEnumerable<SecurityImmutable> added, IEnumerable<SecurityImmutable> removed)> _order_securities_subject =
|
||||
new Subject<(IEnumerable<SecurityImmutable> added, IEnumerable<SecurityImmutable> removed)>();
|
||||
|
||||
IReadOnlyDictionary<uint, OrderRecordImmutable> _orders_cache = new Dictionary<uint, OrderRecordImmutable>();
|
||||
Dictionary<uint, CVOrderBookRowVM> _order_vms_cache = new Dictionary<uint, CVOrderBookRowVM>();
|
||||
ObservableCollection<CVOrderBookRowVM> _order_vms = new ObservableCollection<CVOrderBookRowVM>();
|
||||
Dictionary<uint, CVOrderBookRowVM> _order_rows_cache = new Dictionary<uint, CVOrderBookRowVM>();
|
||||
Dictionary<uint, BranchFilterSource> _branch_filter_roots = new Dictionary<uint, BranchFilterSource>();
|
||||
ObservableCollection<CVOrderBookRowVM> _order_rows = new ObservableCollection<CVOrderBookRowVM>();
|
||||
ObservableCollection<CVOrderBookRowVM> _selected_rows = new ObservableCollection<CVOrderBookRowVM>();
|
||||
SetMap<uint, uint> _parent_to_children = new SetMap<uint, uint>();
|
||||
SetMap<uint, uint> _security_orders = new SetMap<uint, uint>();
|
||||
|
||||
public CVOrderBookVM(IBus bus, OrdersModel model, OrderBookStartupData state) {
|
||||
columns = new CVOrderBookColumnCollection(bus, model, state, _order_securities_subject);
|
||||
rows_view = CollectionViewSource.GetDefaultView(_order_vms);
|
||||
foreach (ColumnBase col in columns)
|
||||
col.sort_direction_changed += on_column_sort_direction_changed;
|
||||
rows_view = CollectionViewSource.GetDefaultView(_order_rows);
|
||||
rows_view.Filter = obj => obj is CVOrderBookRowVM vm && vm.is_visible && vm.passes_filter;
|
||||
columns.filter_changed += () => {
|
||||
var root_nodes = new HashSet<CVOrderBookRowVM>();
|
||||
foreach (var vm in _order_vms) {
|
||||
foreach (var vm in _order_rows)
|
||||
vm.refresh_row_passes_filter();
|
||||
root_nodes.Add(CVOrderBookRowVM.root_node(vm));
|
||||
}
|
||||
foreach (var vm in root_nodes)
|
||||
vm.refresh_branch_passes_filter();
|
||||
};
|
||||
var cvls = (ICollectionViewLiveShaping)rows_view;
|
||||
_live_sorting_props = cvls.LiveSortingProperties;
|
||||
@@ -63,98 +64,129 @@ namespace mbts.spark {
|
||||
rows_view.SortDescriptions.Add(new SortDescription(nameof(CVOrderBookRowVM.time_created), ListSortDirection.Descending));
|
||||
_live_sorting_props.Add(nameof(CVOrderBookRowVM.time_created));
|
||||
}
|
||||
_live_sorting_columns = new LiveSortingColumKeys(_live_sorting_props);
|
||||
|
||||
SetMap<uint, uint> missing_security_orders = new SetMap<uint, uint>();
|
||||
IReadOnlyDictionary<uint, SecurityMD> security_dict = new Dictionary<uint, SecurityMD>();
|
||||
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).ObserveOnDispatcher().Subscribe(on_securities),
|
||||
model.on_orders.ObserveOnDispatcher().Subscribe(on_orders),
|
||||
bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(on_securities),
|
||||
model.on_orders.OnDispatcher().Subscribe(on_orders),
|
||||
columns);
|
||||
|
||||
void on_orders(TableUpdate<OrderRecordImmutable, uint> table) {
|
||||
// check sum (seems like there is a bug in orders api, see repro steps below)
|
||||
// 1) with an CVOrderBook open, enter a few 500+ lot iceberg order with < 20 show_quantity
|
||||
// 2) wait until the orders are filled, or until fills inexplicably stop coming in
|
||||
// 3) open another CVOrderBook, all the orders in the new book will have 'Status = inactive'
|
||||
// 4) open the log file and scroll to the bottom to find a log-message that came from the code below
|
||||
IEnumerable<OrderRecordImmutable> added, removed;
|
||||
if (_orders_cache.Count != (table.state.Count + table.removed.Count - table.added.Count)) {
|
||||
Logger.error(bts.log.Subsystem.Ui, $"CVOrderBook detected non-incremental changes in orders api, will delete all rows and (re)add everything in api state: " +
|
||||
$"orderbook count before update({_orders_cache.Count}), update.state count({table.state.Count}), update.added count({table.added.Count}), update.removed count({table.removed.Count}).");
|
||||
_order_vms.Clear();
|
||||
// clear everything (most likely the server restarted)
|
||||
if (table.removed.Count == _order_rows.Count && table.added.Count == 0) {
|
||||
_selected_rows.Clear();
|
||||
_order_rows.DisposeAll();
|
||||
_order_rows.Clear();
|
||||
_order_rows_cache.Clear();
|
||||
_branch_filter_roots.Clear();
|
||||
_parent_to_children.clear();
|
||||
added = table.state.Values;
|
||||
removed = Enumerable.Empty<OrderRecordImmutable>();
|
||||
} else {
|
||||
added = table.added;
|
||||
removed = table.removed;
|
||||
_security_orders.clear();
|
||||
missing_security_orders.clear();
|
||||
return;
|
||||
}
|
||||
_orders_cache = table.state;
|
||||
var changed_keys = new HashSet<uint>();
|
||||
var added_securities = new List<SecurityImmutable>(1);
|
||||
var removed_securities = new List<SecurityImmutable>(1);
|
||||
foreach (var o in removed) {
|
||||
|
||||
foreach (var o in table.removed) {
|
||||
// if state has it, then it wasn't actually removed, just changed
|
||||
if (table.state.TryGetValue(o.Key, out OrderRecordImmutable changed_order)) {
|
||||
changed_keys.Add(o.Key);
|
||||
_order_vms_cache[o.Key].update_order(changed_order);
|
||||
} else if (_order_vms_cache.TryPopValue(o.Key, out CVOrderBookRowVM vm)) {
|
||||
_order_rows_cache[o.Key].update_order(changed_order);
|
||||
} else if (_order_rows_cache.TryPopValue(o.Key, out CVOrderBookRowVM row)) {
|
||||
row.Dispose();
|
||||
_order_rows.Remove(row);
|
||||
if (_security_orders.removes_key(o.Def.SecurityKey, o.Key)) {
|
||||
if (vm.security.value.HasValue)
|
||||
removed_securities.Add(vm.security.value.Value);
|
||||
if (row.security.value.HasValue)
|
||||
removed_securities.Add(row.security.value.Value);
|
||||
}
|
||||
_order_vms.Remove(vm);
|
||||
if (o.has_parent()) {
|
||||
_parent_to_children.remove(o.ParentKey, o.Key);
|
||||
vm.parent_row?.remove_child_order(vm);
|
||||
if (row.parent != null) {
|
||||
row.parent.remove_child_order(row);
|
||||
} else {
|
||||
_branch_filter_roots.Remove(o.Key);
|
||||
}
|
||||
} else {
|
||||
_branch_filter_roots.Remove(o.Key);
|
||||
}
|
||||
if (_parent_to_children.try_pop(o.Key, out ISet<uint> children)) {
|
||||
foreach (var k in children)
|
||||
_order_vms_cache[k].on_parent_updated();
|
||||
// the child_rows will most-likely be removed in this update, but we can't be sure,
|
||||
// until that happens they must serve as tempororay branch-filter-roots
|
||||
foreach (var child_row in row.child_orders) {
|
||||
var bfs = new BranchFilterSource();
|
||||
child_row.remove_parent(bfs);
|
||||
_branch_filter_roots.Add(child_row.order.Key, bfs);
|
||||
}
|
||||
missing_security_orders.remove(o.Key);
|
||||
}
|
||||
}
|
||||
var orphans = new List<CVOrderBookRowVM>();
|
||||
foreach (var o in added) {
|
||||
var orphans = new Dictionary<uint, CVOrderBookRowVM>();
|
||||
foreach (var o in table.added) {
|
||||
if (changed_keys.Contains(o.Key))
|
||||
continue;
|
||||
var vm = new CVOrderBookRowVM(bus, this, model, o, security_dict.TryGetValue(o.Def.SecurityKey, out SecurityMD smd) ? new SecurityImmutable?(smd.security) : null);
|
||||
if (!vm.security.value.HasValue)
|
||||
SecurityImmutable? security;
|
||||
if (security_dict.TryGetValue(o.Def.SecurityKey, out SecurityMD smd)) {
|
||||
security = smd.security;
|
||||
if (_security_orders.adds_key(o.Def.SecurityKey, o.Key))
|
||||
added_securities.Add(security.Value);
|
||||
} else {
|
||||
missing_security_orders.add(o.Def.SecurityKey, o.Key);
|
||||
if (_security_orders.adds_key(o.Def.SecurityKey, o.Key)) {
|
||||
if (vm.security.value.HasValue)
|
||||
added_securities.Add(vm.security.value.Value);
|
||||
security = null;
|
||||
_security_orders.add(o.Def.SecurityKey, o.Key);
|
||||
}
|
||||
_order_vms_cache.Add(o.Key, vm);
|
||||
if (_parent_to_children.try_lookup(o.Key, out ISet<uint> children))
|
||||
foreach (var k in children) {
|
||||
// ** if children arrived in earlier update, this will notify them
|
||||
// that their parent has arrived, and cause them to be properly sorted **
|
||||
vm.add_child_order(_order_vms_cache[k]);
|
||||
}
|
||||
if (o.has_parent()) {
|
||||
CVOrderBookRowVM row = new CVOrderBookRowVM(bus, this, model, o, security);
|
||||
if (!o.has_parent()) {
|
||||
// if it doesn't have a parent, it must be a root; create a new BranchFilterSource
|
||||
var bfs = new BranchFilterSource();
|
||||
_branch_filter_roots.Add(o.Key, bfs);
|
||||
row.join_branch_filter_source(bfs);
|
||||
} else {
|
||||
_parent_to_children.add(o.ParentKey, o.Key);
|
||||
if (_order_vms_cache.TryGetValue(o.ParentKey, out CVOrderBookRowVM pvm)) {
|
||||
pvm.add_child_order(vm);
|
||||
if (_order_rows_cache.TryGetValue(o.ParentKey, out CVOrderBookRowVM pvm)) {
|
||||
pvm.add_child_order(row);
|
||||
} else {
|
||||
// look for orphans' parents again after all new orders have been added to _order_vm_cache
|
||||
orphans.Add(vm);
|
||||
// (**) look for orphans' parents again after all new orders have been added to _order_vm_cache
|
||||
// don't create a temporary BranchFilterSource root just yet. (**)
|
||||
orphans.Add(o.Key, row);
|
||||
_order_rows_cache.Add(o.Key, row);
|
||||
try_add_children(row);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_order_vms.Add(vm);
|
||||
_order_rows_cache.Add(o.Key, row);
|
||||
_order_rows.Add(row);
|
||||
try_add_children(row);
|
||||
}
|
||||
// ** add the orphans now, most likely their parent was added after them in the iteration
|
||||
// if not, they will be notified when their parent arrives, and be properly sorted then. **
|
||||
foreach (var vm in orphans) {
|
||||
if (_order_vms_cache.TryGetValue(vm.parent_order_key.value, out CVOrderBookRowVM p))
|
||||
p.add_child_order(vm);
|
||||
_order_vms.Add(vm);
|
||||
// (**) since all new rows have been added by now, any remaining orphans will be assigned their own branch-filter-roots
|
||||
// (usually, rows are tempororily added to orphans bc their parent is just behind them in the list of added orders) (**)
|
||||
foreach (CVOrderBookRowVM row in orphans.Values) {
|
||||
var bfs = new BranchFilterSource();
|
||||
row.join_branch_filter_source(bfs);
|
||||
_branch_filter_roots.Add(row.order.Key, bfs);
|
||||
_order_rows.Add(row);
|
||||
}
|
||||
if (added_securities.Count > 0 || removed_securities.Count > 0)
|
||||
_order_securities_subject.OnNext((added_securities, removed_securities));
|
||||
|
||||
void try_add_children(CVOrderBookRowVM row) {
|
||||
if (_parent_to_children.try_lookup(row.order.Key, out ISet<uint> children))
|
||||
foreach (uint child_key in children) {
|
||||
CVOrderBookRowVM child_vm = _order_rows_cache[child_key];
|
||||
// (**) if children arrived in an earlier update, this will notify them that their parent has arrived (**)
|
||||
// - their tempororary branch-filter-roots will be removed, and they will join that of their parent's
|
||||
// - for the live-filter-properties, their corresponding cells will be regenerated and they will be re-inserted beneath their new parent.
|
||||
row.add_child_order(child_vm);
|
||||
if (orphans.Remove(child_key))
|
||||
_order_rows.Add(child_vm);
|
||||
else {
|
||||
_order_rows.Remove(child_vm);
|
||||
_order_rows.Add(child_vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// I assume we don't need to worry about orders "losing" or "changing" their security
|
||||
@@ -167,7 +199,7 @@ namespace mbts.spark {
|
||||
missing_security_orders.remove(missing_sec_orders.Key);
|
||||
added_securities.Add(smd.security);
|
||||
foreach (var order_key in missing_sec_orders) {
|
||||
var vm = _order_vms_cache[order_key];
|
||||
var vm = _order_rows_cache[order_key];
|
||||
vm.update_security(smd.security);
|
||||
}
|
||||
}
|
||||
@@ -175,30 +207,32 @@ namespace mbts.spark {
|
||||
if (added_securities.Count > 0)
|
||||
_order_securities_subject.OnNext((added_securities, new List<SecurityImmutable>(0)));
|
||||
}
|
||||
|
||||
// need to add sort_member_path to ICollectionView.LiveSortingProperties
|
||||
// since child-orders' sort position must always follow their parent,
|
||||
// if their parent is added after the child, the sort order must be updated.
|
||||
void on_column_sort_direction_changed(ColumnBase column, ListSortDirection? sort_direction) {
|
||||
if (column.SortDirection.HasValue) {
|
||||
if (!_live_sorting_props.Contains(column.sort_member_path))
|
||||
_live_sorting_props.Add(column.sort_member_path);
|
||||
} else {
|
||||
_live_sorting_props.Remove(column.sort_member_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<OrderRecordImmutable?> selected_order_changed;
|
||||
|
||||
public CVOrderBookColumnCollection columns { get; }
|
||||
public IReadOnlyList<string> live_sorting_props => _live_sorting_props;
|
||||
public ISet<Columns> live_sorting_columns => _live_sorting_columns;
|
||||
public ICollectionView rows_view { get; }
|
||||
// this is a misnomer, but i want to support both kinds of OrderBookVMs, and OrderBookView currently binds to 'visible_orders,'
|
||||
public ICollectionView visible_orders => rows_view;
|
||||
public CVOrderBookRowVM current_row { get => _current_row.Value; set => _current_row.Value = value ?? _selected_rows.FirstOrDefault(); }
|
||||
public OrderRecordImmutable? current_order => current_row?.order;
|
||||
public IEnumerable<OrderRecordImmutable> selected_orders => _selected_rows.Select(r => r.order);
|
||||
|
||||
// need to add sort_member_path to ICollectionView.LiveSortingProperties
|
||||
// since child-orders' sort position must always follow their parent,
|
||||
// if their parent is added after the child, the sort order must be updated.
|
||||
public void on_sorting(DIYGridSortingEventArgs e) {
|
||||
if (e.Column.SortDirection.HasValue) {
|
||||
if (!_live_sorting_props.Contains(e.Column.sort_member_path))
|
||||
_live_sorting_props.Add(e.Column.sort_member_path);
|
||||
} else {
|
||||
_live_sorting_props.Remove(e.Column.sort_member_path);
|
||||
}
|
||||
}
|
||||
void IOrderBookVM.on_sorting(DIYGridSortingEventArgs e) { }
|
||||
|
||||
public void row_selection_changed(SelectionChangedEventArgs e) {
|
||||
RowSelectionHelper.apply_changes(_selected_rows, e);
|
||||
@@ -207,5 +241,89 @@ namespace mbts.spark {
|
||||
|
||||
public List<SortingCriteria> get_sorting_states() => rows_view.GetSortingCriteria();
|
||||
public FilterStates get_filter_states() => columns.get_filter_states();
|
||||
|
||||
internal void remove_branch_filter_root(uint key) =>
|
||||
_branch_filter_roots.Remove(key);
|
||||
|
||||
internal class BranchFilterSource : IObservable<bool> {
|
||||
readonly HashSet<uint> _orders_pass_filter = new HashSet<uint>();
|
||||
event Action<bool> _passes_filter_changed;
|
||||
|
||||
public void set_row_passes_filter(uint key, bool value) {
|
||||
if (value) {
|
||||
if (_orders_pass_filter.Add(key) && _orders_pass_filter.Count == 1)
|
||||
_passes_filter_changed?.Invoke(value);
|
||||
} else if (_orders_pass_filter.Remove(key) && _orders_pass_filter.Count == 0) {
|
||||
_passes_filter_changed?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Subscribe(IObserver<bool> observer) {
|
||||
_passes_filter_changed += observer.OnNext;
|
||||
observer.OnNext(_orders_pass_filter.Count > 0);
|
||||
return Disposable.Create(() => _passes_filter_changed -= observer.OnNext);
|
||||
}
|
||||
}
|
||||
|
||||
class LiveSortingColumKeys : HashSet<Columns> {
|
||||
public LiveSortingColumKeys(ObservableCollection<string> live_sorting_props) {
|
||||
_add_items(live_sorting_props);
|
||||
live_sorting_props.CollectionChanged += (s, e) => {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add: _add_items(e.NewItems); break;
|
||||
case NotifyCollectionChangedAction.Remove: _remove_items(e.OldItems); break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
_remove_items(e.OldItems);
|
||||
_add_items(e.NewItems);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
Clear();
|
||||
_add_items(live_sorting_props);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static bool try_parse(string prop_name, out Columns result) {
|
||||
switch (prop_name) {
|
||||
case nameof(CVOrderBookRowVM.security): result = Columns.Security; break;
|
||||
case nameof(CVOrderBookRowVM.time_created): result = Columns.Time; break;
|
||||
case nameof(CVOrderBookRowVM.last_change): result = Columns.LastChange; break;
|
||||
case nameof(CVOrderBookRowVM.user): result = Columns.User; break;
|
||||
case nameof(CVOrderBookRowVM.account): result = Columns.Account; break;
|
||||
case nameof(CVOrderBookRowVM.exchange): result = Columns.Exchange; break;
|
||||
case nameof(CVOrderBookRowVM.time_in_force): result = Columns.TimeInForce; break;
|
||||
case nameof(CVOrderBookRowVM.order_type): result = Columns.OrderType; break;
|
||||
case nameof(CVOrderBookRowVM.order_sub_type): result = Columns.SubType; break;
|
||||
case nameof(CVOrderBookRowVM.side): result = Columns.Side; break;
|
||||
case nameof(CVOrderBookRowVM.quantity): result = Columns.Size; break;
|
||||
case nameof(CVOrderBookRowVM.filled_quantity): result = Columns.Filled; break;
|
||||
case nameof(CVOrderBookRowVM.price): result = Columns.Price; break;
|
||||
case nameof(CVOrderBookRowVM.avg_price): result = Columns.AvgFilled; break;
|
||||
case nameof(CVOrderBookRowVM.last_price): result = Columns.LastPrice; break;
|
||||
case nameof(CVOrderBookRowVM.status): result = Columns.Status; break;
|
||||
case nameof(CVOrderBookRowVM.last_msg): result = Columns.LastMessage; break;
|
||||
case nameof(CVOrderBookRowVM.security_key): result = Columns.SecurityKey; break;
|
||||
case nameof(CVOrderBookRowVM.order_key): result = Columns.OrderKey; break;
|
||||
case nameof(CVOrderBookRowVM.parent_order_key): result = Columns.ParentKey; break;
|
||||
default: result = default; return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _add_items(IList prop_names) {
|
||||
foreach (string pn in prop_names) {
|
||||
if (try_parse(pn, out Columns c))
|
||||
Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
void _remove_items(IList prop_names) {
|
||||
foreach (string pn in prop_names) {
|
||||
if (try_parse(pn, out Columns c))
|
||||
Remove(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,7 @@ namespace mbts.spark {
|
||||
}
|
||||
return orders_and_securities;
|
||||
})
|
||||
.ObserveOnDispatcher()
|
||||
.OnDispatcher()
|
||||
.Subscribe(order_security_pairs => {
|
||||
using (_visible_orders.defer_refresh())
|
||||
{
|
||||
|
||||
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user