Compare commits

...
94 Commits
Author SHA1 Message Date
Kevin Darby 25805cf675 Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2020-01-08 17:22:14 -05:00
Kevin Darby a565dcbab7 fix tests 2020-01-06 14:30:04 -05:00
Kevin Darby 4e85218d02 update dev keycodes to 3/31/2020 2020-01-02 10:12:18 -05:00
Taha Afzal f6784f1196 Merged in SP-253-spread-trading-needs-future-posit (pull request #386)
pos neutral spreads's fuures are not checked for pos limits
2019-12-20 23:38:00 +00:00
Taha Afzal 7f05a2c2af pos neutral spreads's fuures are not checked for pos limits 2019-12-20 16:35:29 -05:00
John Colvin c3d2101dcd infinity ticks be gone 2019-12-20 14:12:50 -06:00
John Colvin 8855ea44b0 remove ugly border around expander buttons in grid settings dialog 2019-12-20 13:19:36 -06:00
Kevin Darby 51f70ad5ed Merge branch 'prod' of bitbucket.org:bluetradesys/spark 2019-12-20 10:12:35 -05:00
Kevin Darby 5aa3f1a684 downgrade log 2019-12-19 16:06:26 -05:00
Pedro Pinto 5cd3a710bd Skipped lines that do no match a FIX prefix in FIX logs.
This is needed at the ICE where some messages can have new lines. We
use the new line separator to separate FIX messages so the parser was
getting confused when it saw multi-line messages.
2019-12-19 14:04:34 -05:00
John Colvin 6d8b4636ab fix synthetic import qty & side 2019-12-19 11:11:07 -06:00
Jake Norbut 64a75c4b48 updating release notes 2019-12-19 10:22:42 -06:00
Pedro Pinto 143b6ce5d8 Merged in multi-line-FIX-messages (pull request #385)
Skipped lines that do no match a FIX prefix in FIX logs.

Approved-by: Kevin Darby <kevin.darby@bluetradesys.com>
2019-12-19 15:29:06 +00:00
John Colvin aa5c0a5613 added create/clone synthetics test 2019-12-18 16:21:26 -06:00
Pedro Pinto d5fc0c5a56 Skipped lines that do no match a FIX prefix in FIX logs.
This is needed at the ICE where some messages can have new lines. We
use the new line separator to separate FIX messages so the parser was
getting confused when it saw multi-line messages.
2019-12-18 17:16:12 -05:00
Kevin Darby 7c4daa0f4b we are trying to log in for a deleted user 2019-12-18 10:32:26 -05:00
Taha Afzal ab430e4afb Merged in SP-272_IB_slowness (pull request #384)
Reports transition to end state right away (vs on a timer)
2019-12-18 15:24:14 +00:00
Taha Afzal 29f6ea6d48 Merged in SP-273-fills-highlighting (pull request #383)
SP-273 fills highlighting
2019-12-18 15:20:41 +00:00
Taha Afzal e46372d08e Merged in SP-271-synthetic-cloning (pull request #382)
SP-271 synthetic cloning
2019-12-18 15:18:48 +00:00
Taha Afzal 1d2faeba3e Merged in SP-270-piq-bug (pull request #381)
SP-270 Order Book - PIQ Bug (fix OrderSecurityAlgo.Equals impl)
2019-12-18 15:16:07 +00:00
Taha Afzal 23cffc0630 Merged synthetic-importer into master 2019-12-18 10:03:45 -05:00
Jake Norbut 90b597fc70 Merged in SP-267_ladder_message (pull request #380)
SP-267 ladder status message visibility
2019-12-18 14:57:34 +00:00
Kevin Darby 5ea2a8d6b6 build 2019-12-18 09:36:53 -05:00
Kevin Darby 6e64f7c6df ice login 2019-12-18 09:28:02 -05:00
Eric Harding 8a210f39b6 Merge branch 'master' of bitbucket.org:bluetradesys/spark into prod 2019-12-17 15:36:40 -05:00
Kevin Darby 2aacd141ad only log in with ice users that have the authorized user set 2019-12-17 15:34:46 -05:00
Taha Afzal 5c01e6a99a Reports transition to end state right away (vs on a timer) 2019-12-16 13:46:26 -05:00
John Colvin 7ccd9134c4 SyntheticSecurityParser trims whitespace from leg type text 2019-12-10 18:11:26 -06:00
John Colvin 54d82e1773 DeleteSyntheticDaemon - improve logging, support batch delete 2019-12-10 18:01:44 -06:00
John Colvin 9483962e7b DiscretePropertyChangeDetector uses generic/strong types rather than boxing to object 2019-12-04 13:27:44 -06:00
John Colvin f94c5bc542 SP-273 Fills are unselected when order tree changes 2019-12-04 13:24:30 -06:00
John Colvin ce985f4b44 FillsVM state doesn't need to be observable (every usage is equiv to Observable.Return) 2019-12-03 14:02:26 -06:00
John Colvin 032f2253a7 ExecutionStrategyEditor 'Remove Leg' tooltip 2019-12-03 12:49:42 -06:00
John Colvin 4b4eb66ef0 SP-271 Synthetic cloning 2019-12-03 12:49:42 -06:00
John Colvin a71914d566 TickFactorsPicker - disable auto complete behavior 2019-12-02 16:16:41 -06:00
John Colvin bd00183f7a SP-270 Order Book - PIQ Bug (fix OrderSecurityAlgo.Equals impl) 2019-12-02 15:24:10 -06:00
Jake Norbut 2674e526be formatting 2019-12-02 13:11:44 -06:00
Jake Norbut 4ea2c08cc3 hooking up message Visibility to new vm property 2019-12-02 13:06:30 -06:00
Jake Norbut 99432ad7ac adding status message visibility property to ladder vm 2019-12-02 13:05:43 -06:00
Jake Norbut 8c210bff34 adding status message visibility to setting vm 2019-12-02 13:04:57 -06:00
Jake Norbut e566a94380 adding status_message_visible to settings 2019-12-02 13:04:15 -06:00
John Colvin 07da9fd296 Import Synthetics manual test 2019-11-25 12:55:13 -06:00
John Colvin 436dbd217c SP-266 synthetic csv parser/import dialog 2019-11-25 12:55:13 -06:00
John Colvin 0937381a3f INotifyDataErrorInfo 'subscribe_has_errors' extension only notifies observers when HasErrors changes 2019-11-25 12:55:13 -06:00
John Colvin d42f6b6fff EnumHelper - generic add/remove_flags methods 2019-11-25 12:55:13 -06:00
John Colvin 03940b291b ArrayHelper.trim was not actually reassigning ref with trimmed result 2019-11-25 12:55:13 -06:00
John Colvin 2a72b03afc TickFactorsPicker - disable auto complete behavior 2019-11-25 12:55:13 -06:00
John Colvin 98caeb5410 MDDaemon - check that synthetic metadata.HasStrategy before adding to map 2019-11-25 12:55:13 -06:00
John Colvin 73cb56a661 DIYGridRowPresenter - don't perform drag selection when SelectionMode=Single 2019-11-25 12:55:13 -06:00
John Colvin 88932081b7 add WindowMaxSizeConverter to implement 'SizeToContent' max dimensions 2019-11-25 12:55:13 -06:00
Kevin Darby 1621ad03bf Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2019-10-13 14:09:45 -04:00
Kevin Darby 193f248f06 merge deleted some of my files 2019-10-07 17:40:01 -04:00
Kevin Darby 5212fb272e Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2019-10-07 17:27:12 -04:00
Kevin Darby 6772c77e01 rudimentary fix orders seem to work 2019-10-07 17:18:20 -04:00
Kevin Darby 95c589f594 hooking up to run this fix server 2019-09-30 15:00:30 -04:00
Kevin Darby 699c7c5bfe Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2019-09-30 12:02:13 -04:00
Kevin Darby 7de82b5685 Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2019-09-30 11:45:44 -04:00
Kevin Darby a8e9d0d7d8 ordersession and dropcopy session code complete for native orders, time to debug 2019-09-27 18:21:35 -04:00
Kevin Darby d078e73179 order session 2019-09-27 17:58:36 -04:00
Kevin Darby 4351d6e66a drop copy session code complete 2019-09-27 17:04:42 -04:00
Kevin Darby 9850d6bde0 rx-cpp requires concurrent use with prescription medicine 2019-09-27 14:29:22 -04:00
Kevin Darby d23a072ad6 rx 2019-09-26 18:27:41 -04:00
Kevin Darby 6ef0808f49 never thought I'd use rxcpp...it's truly terrible; I feel physical pain 2019-09-26 18:16:48 -04:00
Kevin Darby e1c2d8aaaf it builds, ship it 2019-09-26 12:33:42 -04:00
Kevin Darby 719fd03063 nothing builds 2019-09-26 11:45:08 -04:00
Kevin Darby d436dae7cd more fix 2019-09-24 18:22:09 -04:00
Kevin Darby f66004b465 fix port 2019-09-24 14:37:30 -04:00
Kevin Darby aa8a56787b fix fix fix 2019-09-23 18:16:05 -04:00
Kevin Darby 35a69396f1 Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2019-09-23 17:08:56 -04:00
Kevin Darby ddfb6d1105 update externals 2019-09-23 17:03:49 -04:00
Kevin Darby 005c0f9a5d c++ fix 2019-09-23 16:59:39 -04:00
Kevin Darby 57af89b628 Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2019-08-28 16:47:20 -04:00
Kevin Darby feab3cbb89 Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2019-01-22 16:10:16 -05:00
Kevin Darby 1f7c1be194 Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2018-12-28 13:51:59 -05:00
Kevin Darby f78f18b3b4 Merge branch 'master' of bitbucket.org:bluetradesys/spark into fix 2018-12-20 16:20:19 -05:00
Kevin Darby 6e7fa91718 ready for testing 2018-12-14 10:39:34 -05:00
Kevin Darby 8cac406547 fix error handling 2018-12-12 17:33:25 -05:00
Kevin Darby 0d83f28cbc fix lives 2018-12-12 14:26:03 -05:00
Kevin Darby 941f4bf837 Merge branch 'cleanup_mdclient' of bitbucket.org:bluetradesys/spark into fix 2018-12-11 23:05:35 -05:00
Kevin Darby 8bbe9cfd98 Merge branch 'master' of bitbucket.org:bluetradesys/spark into sdk_doc2 2018-12-06 12:56:02 -05:00
Kevin Darby ba633532eb hit a wall here with MDClient, going to refactor 2018-12-06 12:55:21 -05:00
Kevin Darby 7bf50415ca nearly ready for testing 2018-12-05 17:55:06 -05:00
Kevin Darby edbaa9c435 looks like we have dropcopy 2018-12-05 15:35:49 -05:00
Kevin Darby 7789f39c9d handle some messages 2018-12-04 18:20:23 -05:00
Kevin Darby 6e188b4f73 shaping up 2018-12-04 16:37:41 -05:00
Kevin Darby 33e8ff58c5 Merge branch 'master' of bitbucket.org:bluetradesys/spark into sdk_doc2 2018-12-04 09:42:30 -05:00
Kevin Darby 24ae03a9f8 access permissions api so fix session can report fills and orders 2018-12-03 18:19:20 -05:00
Kevin Darby 5fe0335e1a Merge branch 'master' of bitbucket.org:bluetradesys/spark into sdk_doc2 2018-12-03 17:02:34 -05:00
Kevin Darby 6cbf707094 Merge branch 'sdk_doc' of bitbucket.org:bluetradesys/spark into sdk_doc2 2018-12-03 16:06:58 -05:00
Kevin Darby 7e4ad55d9d established a session 2018-12-03 15:32:45 -05:00
Kevin Darby 35a0074014 bts fix44 adapter 2018-12-03 14:49:35 -05:00
Kevin Darby b8dd5c119d Merge branch 'sdk_doc' of bitbucket.org:bluetradesys/spark into sdk_doc2 2018-12-03 10:41:34 -05:00
Kevin Darby 8ada73aa64 Merge branch 'sdk_doc' of bitbucket.org:bluetradesys/spark into sdk_doc2 2018-11-30 18:37:39 -05:00
Kevin Darby a16ff30873 lets see how hard this is 2018-11-30 18:32:51 -05:00
182 changed files with 96179 additions and 546 deletions

No files matched your search

+22
View File
@@ -4,6 +4,28 @@ weight = 3
sort_by = "weight"
insert_anchor_links = "right"
+++
## 2.15.0
### User Visible Changes
- Added ability to hide Ladder status messages via Ladder Settings -> Left Panel tab -> Show/Hide groupbox -> Status Message
- Synthetic cloning now allows for any parameter to be changed, including tick size and leg contracts
- Added ability to import synthetics from a CSV file; see our manual for further details on this process
- Reduced Iceberg reloading latency
### Bug Fixes
- Fixed a bug where highlighted fills could lose their highlight status
- Fixed a bug where the (Mini) OrderBook PIQ could be displaying an old value
## 2.14.3
### Bug Fixes
- Fixed a bug regarding ICE logon messaging
## 2.14.2
### Bug Fixes
- Fixed a bug regarding ICE messaging
## 2.14.1
### Bug Fixes
- Fixed a bug regarding ICE logon authentication
## 2.14.0
### User Visible Changes
- All ladders can now be recentered by (shift or alt) + double clicking over the Price column in a ladder
+22
View File
@@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_test", "src\client\sp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_excel", "src\client\spark_excel\spark_excel.csproj", "{BE9F27C3-B583-47DF-95FA-1F8BBC89DCCB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_fix", "src\client\spark_fix\spark_fix.csproj", "{D8031C13-7BEA-430A-BB60-40927D15CFC3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -219,6 +221,26 @@ Global
{BE9F27C3-B583-47DF-95FA-1F8BBC89DCCB}.RelWithDebInfo|x64.Build.0 = Release|x64
{BE9F27C3-B583-47DF-95FA-1F8BBC89DCCB}.RelWithDebInfo|x86.ActiveCfg = Release|x64
{BE9F27C3-B583-47DF-95FA-1F8BBC89DCCB}.RelWithDebInfo|x86.Build.0 = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Debug|Any CPU.ActiveCfg = Debug|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Debug|x64.ActiveCfg = Debug|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Debug|x64.Build.0 = Debug|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Debug|x86.ActiveCfg = Debug|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.MinSizeRel|Any CPU.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.MinSizeRel|Any CPU.Build.0 = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.MinSizeRel|x64.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.MinSizeRel|x64.Build.0 = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.MinSizeRel|x86.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.MinSizeRel|x86.Build.0 = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Release|Any CPU.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Release|x64.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Release|x64.Build.0 = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.Release|x86.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.RelWithDebInfo|Any CPU.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.RelWithDebInfo|Any CPU.Build.0 = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.RelWithDebInfo|x64.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.RelWithDebInfo|x64.Build.0 = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.RelWithDebInfo|x86.ActiveCfg = Release|x64
{D8031C13-7BEA-430A-BB60-40927D15CFC3}.RelWithDebInfo|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -4,43 +4,112 @@ using mbts;
using mbts.spark;
using mbts.ui;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Windows;
namespace bts {
[Export, Export(typeof(IShellDaemon))]
class DeleteSyntheticDaemon : IShellDaemon, IDisposable {
// request_id is noted in the series of log messages that occur during a particular delete op.
static int _request_id = 1;
readonly OrdersModel _orders_model;
readonly ErrorReporter _error_reporter;
IReadOnlyDictionary<uint, ExecutionStrategyImmutable> _strategy_map =
DictionaryHelper.empty<uint, ExecutionStrategyImmutable>();
IDisposable _dispose;
[ImportingConstructor]
public DeleteSyntheticDaemon(IBus bus, OrdersModel orders_model, ErrorReporter error_reporter) {
_orders_model = orders_model;
_error_reporter = error_reporter;
_dispose = bus.subscribe(SparkTags.SyntheticDeleteTag)
.OnDispatcher()
.Subscribe(_delete_synthetic);
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SyntheticDeleteTag).Subscribe(_delete_synthetic),
orders_model.spreader.strategy.Subscribe(map => _strategy_map = map));
}
public void Dispose() =>
Disposable.dispose(ref _dispose);
async void _delete_synthetic(uint id) {
var (orders, orders_success) = await _orders_model.on_orders.FirstAsyncTask(SparkUtils.DefaultTimeOut);
async void _delete_synthetic(IImmutableSet<uint> keys) {
var log = new LoggerContext(Log.Ui, $"Synthetic Delete Req {unchecked(Interlocked.Increment(ref _request_id))} - ", null);
if (keys.is_null_or_empty()) {
log.warn("received empty delete request (no securities specified).");
return;
}
log.info($"received request to delete the following {keys.Count} securities: {string.Join(", ", keys)}");
var (orders, orders_success) = await _orders_model.security_order_groups.FirstAsyncTask(SparkUtils.DefaultTimeOut);
if (!orders_success) {
_error_reporter.report_error($"Unable to delete synthetic security ({id}): unable to determine whether or not security ({id}) has working orders (deletion is not permitted while there are working orders).");
Log.Ui.warn($"Ignoring request to delete synthetic security ({id}) because the orders lookup timed out (must confirm no working orders).");
} else if (orders.state.Values.Any(o => o.order.Def.SecurityKey == id && o.order.is_non_terminal())) {
Log.Ui.warn($"Ignoring request to delete synthetic security ({id}) because non-terminal-orders were detected.");
var msg = "Unable to delete synthetic securities: " +
$"timed out waiting for orders table (must verify that a synthetic security does not have any working orders before it can be deleted).";
_error_reporter.report_error(msg);
log.warn(msg);
} else {
ExecutionStrategyImmutable? strategy = await _orders_model.spreader.lookup_strategy_async(id);
if (strategy.HasValue) {
ExecutionStrategyImmutable deleted_strategy = strategy.Value.with(deleted: true);
await _orders_model.spreader.save_strategy(id, deleted_strategy);
var working_orders = new List<uint>();
var missing_strategies = new List<uint>();
var items = new List<(uint key, ExecutionStrategyImmutable strategy)>();
var temp_strats = _strategy_map;
foreach (var k in keys) {
if (orders.state.TryGetValue(k, out var sec_group) && sec_group.table.state.Values.Any(tup => tup.order.is_active())) {
working_orders.Add(k);
} else if (!temp_strats.TryGetValue(k, out var strat)) {
missing_strategies.Add(k);
} else {
items.Add((k, strat.with(deleted: true)));
}
}
var lines = new List<string>();
if (working_orders.Count > 0) {
lines.Add($"skipping {working_orders.Count} securities because they have working orders.");
}
if (missing_strategies.Count > 0) {
lines.Add($"skipping {missing_strategies.Count} securities because their execution strategies were not discovered.");
}
if (lines.Count > 0)
log.warn(string.Join("; ", lines));
var errors = await _orders_model.spreader.save_strategies(items);
int error_count = errors.Count(e => !string.IsNullOrEmpty(e));
log.info($"{items.Count - error_count}/{items.Count} attempted deletes were successful (skipped {working_orders.Count + missing_strategies.Count}).");
// display results to user
const string dialog_title = "Synthetic Deletion";
string display_msg;
MessageBoxImage icon = MessageBoxImage.None;
if (keys.Count > 1) {
if (error_count == 0 && working_orders.Count == 0 && missing_strategies.Count == 0) {
display_msg = $"Synthetic securities have been deleted.";
icon = MessageBoxImage.Information;
} else {
display_msg = $"{keys.Count - error_count - working_orders.Count - missing_strategies.Count}/{keys.Count} securities were successfully deleted.";
icon = MessageBoxImage.Error;
}
} else {
if (error_count == 0 && working_orders.Count == 0 && missing_strategies.Count == 0) {
display_msg = "Synthetic security has been deleted.";
icon = MessageBoxImage.Information;
} else {
string reason;
if (error_count > 0) {
reason = errors[0];
} else if (missing_strategies.Count > 0) {
reason = "did not find execution strategy.";
} else if (working_orders.Count > 0) {
reason = "working orders detected.";
} else {
// unexpected code path
reason = "unknown error.";
}
icon = MessageBoxImage.Error;
display_msg = $"Unable to delete synthetic security: " + reason;
}
}
BTSMessageBox.Show(display_msg, dialog_title, BTSMessageBoxButtons.OK, icon);
}
}
}
}
}
+44 -1
View File
@@ -1,12 +1,15 @@
using bts.api;
using bts.api.core;
using bts.support;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
@@ -164,5 +167,45 @@ namespace bts.spark {
void _report_error(string msg) =>
error_reporter.report_error($"Error {desc}: {msg}", AlgoStudioWindow.NAME, MessageBoxImage.Warning);
}
public static async Task<(TResult result, string error_msg)[]> safe_execute_batch<T, TResult>(
this ErrorReporter error_reporter,
IEnumerable<Task<T>> execute,
Func<T, (TResult result, string error_msg)> result_selector,
string desc,
string title) {
//
try {
var results = await Task.WhenAll(execute as Task<T>[] ?? ArrayHelper.clone_optimized(execute));
var result_values = new (TResult, string)[results.Length];
StringBuilder error_bd = null;
for (int i = 0; i < results.Length; i++) {
var r = results[i];
var (result, error) = result_selector(r);
if (!string.IsNullOrEmpty(error)) {
if (error_bd == null) {
error_bd = new StringBuilder();
} else {
error_bd.AppendLine();
}
error_bd.Append(error);
}
result_values[i] = (result, error);
}
if (error_bd != null) {
var error_msg = error_bd.ToString();
if (!string.IsNullOrEmpty(error_msg))
_report_error(error_msg);
}
return result_values;
}
catch (Exception ex) {
_report_error(ex.ToString());
}
return null;
//
void _report_error(string msg) =>
error_reporter.report_error($"Error {desc}: {msg}", title, MessageBoxImage.Warning);
}
}
}
}
+2 -2
View File
@@ -39,7 +39,7 @@ namespace bts {
_relay.set_token(_token.Value);
_dispose.Add(
_ss.Value.subscribei_metadata()
.Select(upd => upd.all.ToDictionary(i => i.SecurityKey, i => i.Strategy))
.Select(upd => upd.all.Where(i => i.HasStrategy).ToDictionary(i => i.SecurityKey, i => i.Strategy))
.Subscribe(t => {
var deleted_synthetic_keys = new HashSet<uint>(t.Where(kv => kv.Value.Deleted).Select(kv => kv.Key));
// add back 'undeleted' synthetics
@@ -64,4 +64,4 @@ namespace bts {
Task.Run(run);
}
}
}
}
+6 -1
View File
@@ -1,4 +1,5 @@
using bts;
using mbts.ui.dpi;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -55,7 +56,9 @@ namespace mbts.ui {
StarGeometryKey = nameof(StarGeometryKey),
ToolsGeometryKey = nameof(ToolsGeometryKey),
TrashGeometryKey = nameof(TrashGeometryKey),
UndoCircularGeometryKey = nameof(UndoCircularGeometryKey);
UndoCircularGeometryKey = nameof(UndoCircularGeometryKey),
WarningExclamationGeometryKey = nameof(WarningExclamationGeometryKey),
WarningGeometryKey = nameof(WarningGeometryKey);
public static Geometry PlayGeometry => Geometry.Parse(get_geometric_path(PlayGeometryKey).FirstOrDefault() ?? string.Empty);
@@ -190,6 +193,8 @@ namespace mbts.ui {
public static ImageSource Tetris { get { return load_resource_and_freeze("Tetris"); } }
public static ImageSource Tools => create_icon(ToolsGeometryKey);
public static ImageSource Trash => create_icon(TrashGeometryKey);
public static ImageSource Warning => create_icon(WarningGeometryKey, Brushes.IndianRed);
public static ImageSource WarningExclamation => create_icon(WarningExclamationGeometryKey, Brushes.IndianRed);
public static ImageSource UndoCircular => create_icon(UndoCircularGeometryKey);
public static ImageSource ViewSettings => load_resource_and_freeze("ViewSettings");
public static ImageSource Wrench { get { return load_resource_and_freeze("Wrench"); } }
+12
View File
@@ -363,6 +363,18 @@
c-84.15,0-153-68.85-153-153h-51c0,112.2,91.8,204,204,204s204-91.8,204-204S360.825,89.25,248.625,89.25z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.WarningExclamationGeometryKey}">
M15.998999,17.988002C16.551,17.988002 16.999,18.437009 16.999,18.990018 16.999,19.543027 16.551,19.992034 15.998999,19.992034 15.446999,19.992034 14.998999,19.543027 14.998999,18.990018 14.998999,18.437009 15.446999,17.988002 15.998999,17.988002z
M15.998999,8.0630169C16.552,8.0630169,16.999,8.5110159,16.999,9.0650149L16.999,16.167001C16.999,16.721 16.552,17.168998 15.998999,17.168998 15.446,17.168998 14.998999,16.721 14.998999,16.167001L14.998999,9.0650149C14.998999,8.5110159,15.446,8.0630169,15.998999,8.0630169z
</sys:String>
<sys:String x:Key="{x:Static mbts:Icons.WarningGeometryKey}">
M15.998999,17.988002C16.551,17.988002 16.999,18.437009 16.999,18.990018 16.999,19.543027 16.551,19.992034 15.998999,19.992034 15.446999,19.992034 14.998999,19.543027 14.998999,18.990018 14.998999,18.437009 15.446999,17.988002 15.998999,17.988002z
M15.998999,8.0630169C16.552,8.0630169,16.999,8.5110159,16.999,9.0650149L16.999,16.167001C16.999,16.721 16.552,17.168998 15.998999,17.168998 15.446,17.168998 14.998999,16.721 14.998999,16.167001L14.998999,9.0650149C14.998999,8.5110159,15.446,8.0630169,15.998999,8.0630169z
M15.999277,2.3337629L2.3830507,22.996989 29.615442,22.996989z
M15.999513,0C16.50052,-3.8678991E-07,17.001274,0.21826346,17.288297,0.65479128L31.74151,22.585975C32.057495,23.064982 32.084472,23.677966 31.812494,24.184989 31.541494,24.687009 31.02049,24.999999 30.450475,24.999999L1.5480773,24.999999C0.97903845,24.999999 0.45705819,24.687009 0.18703459,24.184989 -0.085979476,23.677966 -0.057963934,23.064982 0.25704309,22.585975L14.709219,0.65479128C14.997247,0.21826346,15.498506,-3.8678991E-07,15.999513,0z
</sys:String>
<!--
'y' for equilateral triangle of side length x:
(x/2)^2 + Y^2 = x^2
@@ -140,7 +140,7 @@
<DataTemplate>
<ComboBox SelectedItem="{Binding value}" ItemsSource="{u:EnumBindingSource {u:Nested sp:ConditionalQuoting.Types.PriceType}}" ItemTemplate="{StaticResource EnumDescriptionTemplate}"/>
</DataTemplate>
</usp:DataGridParameterCellTemplateConverter.ConditionalPriceTypeTemplate>
</usp:DataGridParameterCellTemplateConverter.ConditionalPriceTypeTemplate>
<usp:DataGridParameterCellTemplateConverter.QuantityMeasureTemplate>
<DataTemplate>
<Grid IsEnabled="{Binding enabled}">
@@ -333,13 +333,61 @@
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="ContractPickerButton" TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="ToolTip" Value="Change Security"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Bd"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Padding="{TemplateBinding Padding}"
>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="MouseStates">
<VisualState Name="Normal"/>
<VisualState Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="Bd" Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="#24FFFFFF"/>
</Storyboard>
</VisualState>
<VisualState Name="Pressed">
<Storyboard>
<ColorAnimation Storyboard.TargetName="Bd" Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="#40FFFFFF"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border.Background>
<SolidColorBrush/>
</Border.Background>
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<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"
ShowExpanderArrow="False"
PopupPlacement="Right"
Header="Add Leg"
Padding="6,0"
Background="{StaticResource Button.Static.BackgroundBrush}"
IsOpen="{Binding IsNewLegPopupOpen, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}, Mode=TwoWay}"
IsEnabled="{Binding CanUserAddNewLeg, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}}"
VerticalAlignment="Center"
>
<msp:ContractPickerView
CloseOnOK="False" CloseOnCancel="False"
@@ -348,7 +396,7 @@
</c:PopupControl>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.RemovableColumnHeaderTemplateKey}">
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.RemovableColumnHeaderTemplateKey}" DataType="{x:Type usp:LegSpreaderParamsTuple}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -356,13 +404,40 @@
</Grid.ColumnDefinitions>
<Button
Command="{Binding RemoveLegCommand, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}}"
CommandParameter="{Binding}" Style="{StaticResource RoundCloseButton}"
CommandParameter="{Binding}"
Style="{StaticResource RoundCloseButton}"
ToolTip="Remove Leg"
/>
<TextBlock Grid.Column="1" Margin="4,0,0,0" Text="{Binding Converter={StaticResource SecurityTextConverter}}"/>
<c:ContractPickerPopup
Grid.Column="1"
BorderThickness="0"
ButtonStyle="{StaticResource ContractPickerButton}"
CloseOnCancel="True"
CloseOnOK="True"
Header="{Binding security}"
IsEditMode="False"
IsLazy="True"
SecurityMap="{Binding ContractPickerModel.all_securities, RelativeSource={RelativeSource AncestorType=usp:ExecutionStrategyEditor}, Mode=OneWay}"
SelectedSecurity="{Binding security}"
ShowDialogSkirt="True"
>
<c:ContractPickerPopup.Background>
<SolidColorBrush/>
</c:ContractPickerPopup.Background>
<c:ContractPickerPopup.HeaderTemplate>
<DataTemplate>
<TextBlock Foreground="{x:Static u:BTSColors.header_text_brush}" Text="{Binding}"/>
</DataTemplate>
</c:ContractPickerPopup.HeaderTemplate>
</c:ContractPickerPopup>
</Grid>
</DataTemplate>
<DataTemplate x:Key="{x:Static usp:ExecutionStrategyEditor.NonRemovableColumnHeaderTemplateKey}">
<TextBlock Text="{Binding Converter={StaticResource SecurityTextConverter}}"/>
<DataTemplate x:Key="ReadOnlyColumnHeaderTemplate" DataType="{x:Type usp:LegSpreaderParamsTuple}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding security}"
/>
</DataTemplate>
<ControlTemplate x:Key="ExecutionStrategyEditorTemplate" TargetType="usp:ExecutionStrategyEditor">
<Border
@@ -560,6 +635,14 @@
</Setter>
</Style>
</DataGrid.Resources>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="5,0"/>
<Setter Property="MinHeight" Value="26"/>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.GroupStyle>
<GroupStyle ContainerStyle="{StaticResource GroupItemStyle}"/>
</DataGrid.GroupStyle>
@@ -227,8 +227,8 @@ namespace mbts.ui.spark {
}
DataGridColumn _column_factory(LegSpreaderParamsTuple l) {
var col = new DataGridColumnInternal(l.leg.Security, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter);
col.HeaderTemplate = (DataTemplate)FindResource(IsNew ? RemovableColumnHeaderTemplateKey : NonRemovableColumnHeaderTemplateKey);
var col = new DataGridColumnInternal(l, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter);
col.HeaderTemplate = TryFindResource(IsNew ? RemovableColumnHeaderTemplateKey : NonRemovableColumnHeaderTemplateKey) as DataTemplate;
DataGridEx.SetRightBorder(col, new DataGridExVerticalLine(false, true, Colors.DarkGray, 1));
return col;
}
@@ -237,10 +237,12 @@ namespace mbts.ui.spark {
private class DataGridColumnInternal : DataGridDynamicColumn {
readonly IValueConverter _cell_template_converter, _cell_editing_template_converter;
readonly LegSpreaderParamsTuple _leg_tup;
public DataGridColumnInternal(Security security, IValueConverter cell_template_converter, IValueConverter cell_editing_template_converter) {
Header = security;
SetValue(DataGridEx.CellKeyProperty, security);
public DataGridColumnInternal(LegSpreaderParamsTuple leg_tup, IValueConverter cell_template_converter, IValueConverter cell_editing_template_converter) {
_leg_tup = leg_tup;
Header = leg_tup;
SetValue(DataGridEx.CellKeyProperty, leg_tup);
_cell_template_converter = cell_template_converter;
_cell_editing_template_converter = cell_editing_template_converter;
}
@@ -248,8 +250,8 @@ namespace mbts.ui.spark {
// I decided this was an easy and simple enough solution to justify breaking the mvvm paradigm
// The alternative - within strict bounds of the paradigm - is to change the DataGridParameterCellTemplateConverter into an IMultiValueConverter, bound to data_type AND read_only
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) {
if (dataItem is SyntheticEditorVM.RowVM row_vm) {
if (row_vm.read_only || row_vm.get_cell(Header) is SyntheticEditorVM.ICell cell_vm && !cell_vm.enabled)
if (dataItem is RowVM row_vm) {
if (row_vm.read_only || row_vm.get_cell(_leg_tup) is ICell cell_vm && !cell_vm.enabled)
return base.GenerateElement(cell, dataItem);
}
return base.GenerateEditingElement(cell, dataItem);
@@ -352,4 +354,4 @@ namespace mbts.ui.spark {
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,25 @@
<UserControl
x:Class="bts.shell.spreader.ContractLegTypeView"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:spr="clr-namespace:bts.shell.spreader"
>
<ItemsControl ItemsSource="{Binding Controller.flags, RelativeSource={RelativeSource AncestorType=spr:ContractLegTypeView}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox
Margin="2"
IsChecked="{Binding is_selected}"
Content="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeShortDescConverter}}"
ToolTip="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeLongDescConverter}}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>
@@ -0,0 +1,104 @@
using bts.collections;
using mbts;
using mbts.ui;
using mbts.ui.converters;
using mbts.ui.shell;
using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace bts.shell.spreader {
static class EnumFlagItemsVM {
public static EnumFlagItemsVM<T> create<T>(IEnumerable<T> flags) where T : struct =>
new EnumFlagItemsVM<T>(flags);
}
class EnumFlagItemsVM<T> : ViewModelBase where T : struct {
readonly ObservableCollection<SelectableItemVM<T>> _flags = new ObservableCollection<SelectableItemVM<T>>();
bool _is_busy;
T _result;
public EnumFlagItemsVM()
: this(EnumHelper.get_values<T>()) { }
public EnumFlagItemsVM(IEnumerable<T> flags) {
this.flags = ObservableAutoWrapper.create_read_only_wrapper(_flags);
_flags.AddRange(flags.Select(f => new SelectableItemVM<T>(f)));
foreach (var vm in _flags) {
vm.is_selected_changed += on_is_selected_changed;
void on_is_selected_changed(bool value) {
if (!_is_busy) {
_is_busy = true;
result = value ?
EnumHelper.add_flags(result, vm.value) :
EnumHelper.remove_flags(result, vm.value);
_is_busy = false;
}
}
}
}
public event Action result_changed;
public IReadOnlyObservableCollection<SelectableItemVM<T>> flags { get; }
public T result {
get => _result;
set {
if (!EqualityComparer<T>.Default.Equals(_result, value)) {
_result = value;
if (!_is_busy) {
_is_busy = true;
for (int i = 0; i < flags.Count; i++)
flags[i].is_selected = EnumHelper.has_flags(value, flags[i].value);
_is_busy = false;
}
result_changed?.Invoke();
RaisePropertyChanged();
}
}
}
}
/// <summary>
/// Interaction logic for ContractLegTypeView.xaml
/// </summary>
partial class ContractLegTypeView : UserControl {
public static IValueConverter LegTypeShortDescConverter =>
ValueConverter.create<ContractLegType, string>(SyntheticSecurityParser.display_name);
public static IValueConverter LegTypeLongDescConverter =>
ValueConverter.create<ContractLegType, string>(lt =>
string.Join(" & ", EnumHelper.split_flags(lt).Select(f => EnumHelper.get_display_name(f))));
public ContractLegTypeView() {
InitializeComponent();
Controller.result = Value;
Controller.result_changed += _on_controller_result_changed;
}
public static readonly DependencyProperty ValueProperty =
WPFHelper.create_dp<ContractLegType, ContractLegTypeView>(
nameof(Value),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
(o, v) => {
o.Controller.result_changed -= o._on_controller_result_changed;
o.Controller.result = v;
o.Controller.result_changed += o._on_controller_result_changed;
});
public ContractLegType Value {
get => (ContractLegType)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public EnumFlagItemsVM<ContractLegType> Controller { get; } = EnumFlagItemsVM.create(EnumHelper.get_values_except(ContractLegType.None));
void _on_controller_result_changed() =>
SetCurrentValue(ValueProperty, Controller.result);
}
}
@@ -0,0 +1,47 @@
<Window
x:Class="bts.shell.spreader.SyntheticImportConflictsDialog"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:spr="clr-namespace:bts.shell.spreader"
u:WindowStyle.AllowResize="True"
u:WindowStyle.SizeToContent="WidthAndHeight"
u:WindowStyle.TitleContent="Import Synthetics Conflicts"
Style="{StaticResource BTSDialogTitleTemplate}"
>
<Window.MaxHeight>
<Binding Path="SizeToContent" RelativeSource="{RelativeSource Self}">
<Binding.Converter>
<cv:WindowMaxHeightConverter SizeToContentThreshold="{u:DPIDouble 700}"/>
</Binding.Converter>
</Binding>
</Window.MaxHeight>
<u:WindowStyle.TitleContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Image Height="12" Margin="5,2" Source="{x:Static u:Icons.Import}"/>
<TextBlock Margin="4,0,0,0" Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</u:WindowStyle.TitleContentTemplate>
<Window.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<spr:SyntheticImportConflictsView Margin="12,10,12,8"/>
<c:DialogSkirt Grid.Row="1">
<c:DialogSkirt.RightItems>
<x:Array Type="{x:Type Button}">
<Button Command="{Binding ok_cmd}">Continue</Button>
<Button Command="{Binding cancel_cmd}" IsCancel="True">Cancel</Button>
</x:Array>
</c:DialogSkirt.RightItems>
</c:DialogSkirt>
</Grid>
</Window.Content>
</Window>
@@ -0,0 +1,11 @@
using System.Windows;
namespace bts.shell.spreader {
/// <summary>
/// Interaction logic for SyntheticImportConflictsDialog.xaml
/// </summary>
partial class SyntheticImportConflictsDialog : Window {
public SyntheticImportConflictsDialog() =>
InitializeComponent();
}
}
@@ -0,0 +1,101 @@
using bts.api;
using bts.collections;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.user.settings;
using System.Collections.Generic;
using System.Linq;
namespace bts.shell.spreader {
readonly struct SyntheticComponent {
public readonly SyntheticContractComponent type;
public readonly int leg_index;
public SyntheticComponent(SyntheticContractComponent type, int leg_index) {
this.type = type;
this.leg_index = leg_index;
}
}
class SyntheticImportConflictsVM : ImportConflictsVM<(SyntheticImportRowVM synth, ContractLegVM leg), SyntheticImportRowVM, SyntheticContractComponent>, IRequestViewClose {
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
public SyntheticImportConflictsVM(int num_synthetics)
: base(num_synthetics, tup => tup.synth) { }
// misc items sources
public IReadOnlyDictionary<uint, SecurityMD> security_map {
get => _security_map;
set {
if (!ReferenceEquals(_security_map, value)) {
_security_map = value;
_resolve_securities();
RaisePropertyChanged();
}
}
}
void _resolve_securities() {
if (_security_map == null)
return;
var reverse_lookup = SyntheticSecurityParser.reverse_lookup(MappedDictionary.create(_security_map, v => v.security.ToString()));
foreach (var r in conflicts) {
if (r.component == SyntheticContractComponent.Security && r.has_error) {
var id = reverse_lookup(r.text);
if (id != 0 && _security_map.TryGetValue(id, out var smd))
r.value = smd.security;
}
}
}
}
class SyntheticImportConflictRowVM : ImportConflictRowVM<(SyntheticImportRowVM synth, ContractLegVM leg), SyntheticContractComponent>, ITemplateSelectorKey {
public SyntheticImportConflictRowVM(
SyntheticContractComponent component,
string text,
IReadOnlyCollection<(SyntheticImportRowVM synth, ContractLegVM leg)> affected_parts,
object error_value,
object value)
: base(component, text, affected_parts, error_value, value, affected_parts.DistinctOn(tup => tup.synth).Count()) { }
public override void on_conflicts_resolved() {
if (has_error)
return;
foreach (var (synth, leg) in affected_parts) {
switch (component) {
case SyntheticContractComponent.Name:
if (value is string str)
synth.name = str;
break;
case SyntheticContractComponent.TickSize:
if (value is double ts && ts.IsNonZero()) {
synth.tick_factors = TickUtils.get_regional_ticks(new ContractSpecificationImmutable(new ContractSpecification {
LowTick = ts,
HighTick = ts
})).low_tick;
} else if (value is TickFactors tf && tf.is_valid())
synth.tick_factors = tf;
break;
case SyntheticContractComponent.Security:
if (value is SecurityImmutable sec && sec.is_exchange_security())
leg.security = sec;
break;
case SyntheticContractComponent.PriceMultiplier:
if (value is double pm && pm.IsNonZero())
leg.price_multiplier = pm;
break;
case SyntheticContractComponent.Quantity:
if (value is double q && q.IsNonZero())
leg.quantity = q;
break;
case SyntheticContractComponent.LegType:
if (value is ContractLegType lt)
leg.leg_type = lt;
break;
}
}
}
}
}
@@ -0,0 +1,199 @@
<UserControl
x:Class="bts.shell.spreader.SyntheticImportConflictsView"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:spr="clr-namespace:bts.shell.spreader"
MinWidth="400"
>
<UserControl.Resources>
<Style x:Key="VerticallyCenteredText" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!--Value Cell Template Selector-->
<u:KeyedTemplateSelector x:Key="ValueCellTemplateSelector">
<u:KeyedTemplateSelector.Default>
<DataTemplate>
<TextBlock Text="{Binding value}" VerticalAlignment="Center"/>
</DataTemplate>
</u:KeyedTemplateSelector.Default>
<u:TemplateSelectorKeyValue Key="PriceMultiplier">
<DataTemplate>
<TextBlock Text="{Binding value, Converter={StaticResource DoubleTextConverter}}" VerticalAlignment="Center"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="Quantity">
<DataTemplate>
<TextBlock Text="{Binding value, Converter={StaticResource DoubleTextConverter}}" VerticalAlignment="Center"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="Security">
<DataTemplate>
<TextBlock Text="{Binding value, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="LegType">
<DataTemplate>
<TextBlock Text="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeShortDescConverter}}">
<TextBlock.ToolTip>
<TextBlock>
<Run Text="{Binding value, Converter={x:Static spr:ContractLegTypeView.LegTypeLongDescConverter}}"/>
<LineBreak/>
<Run Text="Q = Quote"/>
<LineBreak/>
<Run Text="H = Hedge"/>
<LineBreak/>
<Run Text="N = None"/>
</TextBlock>
</TextBlock.ToolTip>
</TextBlock>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</u:KeyedTemplateSelector>
<!--Value Cell Editing Template Selector-->
<u:KeyedTemplateSelector x:Key="ValueCellEditingTemplateSelector">
<u:TemplateSelectorKeyValue Key="Security">
<DataTemplate>
<c:ContractPickerPopup
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CloseOnCancel="True"
CloseOnOK="True"
Header="{Binding value, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
IsEditMode="False"
IsLazy="True"
SecurityMap="{Binding DataContext.security_map, RelativeSource={RelativeSource AncestorType=DataGrid}, Mode=OneWay}"
SelectedSecurity="{Binding value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ShowDialogSkirt="True"
/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="Name">
<DataTemplate>
<TextBox u:Behavior.FocusOnLoaded="True" Text="{Binding value}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="TickSize">
<DataTemplate>
<c:TickUpDownDouble Value="{Binding value, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="PriceMultiplier">
<DataTemplate>
<c:TickUpDownDouble Value="{Binding value}" DisplayConverter="{StaticResource DoubleTextConverter}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="Quantity">
<DataTemplate>
<c:TickUpDownDouble Value="{Binding value}" DisplayConverter="{StaticResource DoubleTextConverter}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
<u:TemplateSelectorKeyValue Key="LegType">
<DataTemplate>
<spr:ContractLegTypeView Value="{Binding value}"/>
</DataTemplate>
</u:TemplateSelectorKeyValue>
</u:KeyedTemplateSelector>
</UserControl.Resources>
<!--Content-->
<Grid x:Name="content_root">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image VerticalAlignment="Center" Margin="0,0,8,0">
<Image.Source>
<Binding Source="{x:Static MessageBoxImage.Exclamation}">
<Binding.Converter>
<cv:SystemIconConverter/>
</Binding.Converter>
</Binding>
</Image.Source>
</Image>
<Canvas x:Name="text_canvas" Grid.Column="1" HorizontalAlignment="Stretch"/>
<TextBlock Grid.Column="1" TextWrapping="Wrap" Width="{Binding ActualWidth, ElementName=text_canvas}">
<Run Text="{Binding num_successfully_parsed_rows, Mode=OneWay}"/>
<Run Text="/"/>
<Run Text="{Binding num_rows, Mode=OneWay}"/>
<Run Text=" synthetics were successfully converted."/>
<Run Text="Resolve conflicts below by selecting a value to assign each affected synthetic,"/>
<Run Text="or modify individual synthetics in the main window."/>
<LineBreak/>
<LineBreak/>
<Run Text="Click 'Continue' to proceed to the main window."/>
</TextBlock>
<DataGrid
x:Name="data_grid"
u:DataGridBehavior.SingleClickEdit="True"
Grid.Row="1"
Grid.ColumnSpan="2"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserSortColumns="True"
HeadersVisibility="All"
ItemsSource="{Binding conflicts_view}"
Margin="0,8,0,0"
MinRowHeight="17"
RowHeaderWidth="22"
SelectionMode="Single"
>
<DataGrid.RowValidationErrorTemplate>
<ControlTemplate>
<Grid Background="Green">
<Border x:Name="exclamation" HorizontalAlignment="Center">
<Image Height="12" VerticalAlignment="Center" Source="{x:Static u:Icons.WarningExclamation}"/>
</Border>
<ContentControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ToolTip="{Binding (Validation.Errors), RelativeSource={RelativeSource AncestorType=DataGridRow}}">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Grid Background="Transparent">
<Grid.ToolTip>
<ToolTip Content="{Binding ToolTip, RelativeSource={RelativeSource TemplatedParent}}">
<ToolTip.ContentTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}" DisplayMemberPath="ErrorContent"/>
</DataTemplate>
</ToolTip.ContentTemplate>
</ToolTip>
</Grid.ToolTip>
</Grid>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
</Grid>
</ControlTemplate>
</DataGrid.RowValidationErrorTemplate>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell" BasedOn="{StaticResource DataGridCellBaseStyle}">
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
</Style>
</DataGrid.CellStyle>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="ValidationErrorTemplate" Value="{Binding RowValidationErrorTemplate, RelativeSource={RelativeSource AncestorType=DataGrid}}"/>
<Setter Property="IsSelected" Value="{Binding is_selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource SelectedRowBackground}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Component" Binding="{Binding component, Converter={StaticResource EnumDescriptionConverter}}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
<DataGridTextColumn Header="Text" Binding="{Binding text}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
<DataGridTextColumn Header="Affected Synthetics" Binding="{Binding num_affected_rows}" ElementStyle="{StaticResource VerticallyCenteredText}" IsReadOnly="True"/>
<DataGridTemplateColumn Header="Value" CellTemplateSelector="{StaticResource ValueCellTemplateSelector}" CellEditingTemplateSelector="{StaticResource ValueCellEditingTemplateSelector}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
@@ -0,0 +1,20 @@
using System.Windows.Controls;
namespace bts.shell.spreader {
enum SyntheticContractComponent {
Name,
TickSize,
Security,
PriceMultiplier,
Quantity,
LegType
}
/// <summary>
/// Interaction logic for SyntheticImportConflictsView.xaml
/// </summary>
partial class SyntheticImportConflictsView : UserControl {
public SyntheticImportConflictsView() =>
InitializeComponent();
}
}
@@ -0,0 +1,45 @@
<Window
x:Class="bts.shell.spreader.SyntheticImportDialog"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:spr="clr-namespace:bts.shell.spreader"
u:WindowStyle.AllowResize="True"
u:WindowStyle.SizeToContent="WidthAndHeight"
u:WindowStyle.TitleContent="Import Synthetics"
Style="{StaticResource BTSDialogTitleTemplate}"
Content="{Binding}"
>
<Window.MaxHeight>
<Binding Path="SizeToContent" RelativeSource="{RelativeSource Self}">
<Binding.Converter>
<cv:WindowMaxHeightConverter SizeToContentThreshold="{u:DPIDouble 700}"/>
</Binding.Converter>
</Binding>
</Window.MaxHeight>
<u:WindowStyle.TitleContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Image Height="12" Margin="5,2" Source="{x:Static u:Icons.Import}"/>
<TextBlock Margin="4,0,0,0" Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</u:WindowStyle.TitleContentTemplate>
<Window.ContentTemplate>
<DataTemplate DataType="{x:Type spr:SyntheticImportVM}">
<AdornerDecorator>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<spr:SyntheticImportView/>
<c:DialogSkirtOKCancel Grid.Row="1" OKCommand="{Binding ok_cmd}" CancelCommand="{Binding cancel_cmd}"/>
</Grid>
</AdornerDecorator>
</DataTemplate>
</Window.ContentTemplate>
</Window>
@@ -0,0 +1,11 @@
using System.Windows;
namespace bts.shell.spreader {
/// <summary>
/// Interaction logic for SyntheticImportDialog.xaml
/// </summary>
partial class SyntheticImportDialog : Window {
public SyntheticImportDialog() =>
InitializeComponent();
}
}
@@ -0,0 +1,236 @@
using mbts;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.utility;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Data;
namespace bts.shell.spreader {
class SyntheticImportRowCollectionView : ListCollectionView {
ReadOnlyObservableCollection<object> _base_groups, _groups_wrapper;
ObservableAutoWrapper<object, object> _groups;
public SyntheticImportRowCollectionView(IList list)
: base(list) { }
public override bool CanGroup => true;
// Must return null if there are no GroupDescriptions (i.e., instead of an empty collection).
// If an empty collection is returned, the view assumes grouping is active and there are no items.
public override ReadOnlyObservableCollection<object> Groups {
get {
if ((GroupDescriptions?.Count ?? 0) == 0)
return null;
if (_groups_wrapper == null) {
_base_groups = base.Groups;
if (_base_groups == null)
return null;
_groups = new ObservableAutoWrapper<object, object>(_base_groups, obj => new SyntheticImportRowCollectionViewGroup((CollectionViewGroup)obj, this));
_groups_wrapper = new ReadOnlyObservableCollection<object>(_groups);
}
return _groups_wrapper;
}
}
public void set_all_expanded(bool is_expanded) {
foreach (var g in Groups.OfTypeSafe<SyntheticImportRowCollectionViewGroup>())
g.set_all_expanded(is_expanded);
}
}
class SyntheticImportRowCollectionViewGroup : CollectionViewGroup, INotifyDataErrorInfo, IRaiseNotifyPropertyChanged, IEquatable<SyntheticImportRowCollectionViewGroup> {
static readonly string _desc_delim = ", ";
readonly SyntheticImportRowCollectionView _root;
readonly CollectionViewGroup _from;
readonly Dictionary<object, InnerGroupSubscription> _inner_groups = new Dictionary<object, InnerGroupSubscription>();
readonly Dictionary<int, IDisposable> _rows = new Dictionary<int, IDisposable>();
string
_leg_type_desc,
_security_desc,
_quantity_desc,
_price_multiplier_desc;
IDisposable _dispose;
bool _is_expanded;
public SyntheticImportRowCollectionViewGroup(CollectionViewGroup from, SyntheticImportRowCollectionView root) : base(from.Name) {
_root = root;
_from = from;
((INotifyCollectionChanged)_from.Items).CollectionChanged += _from_items_CollectionChanged;
_from_items_CollectionChanged(_from.Items, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _from.Items, 0));
}
public event Action ProtectedItemCountChanged;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public override bool IsBottomLevel => _from.IsBottomLevel;
public bool is_expanded { get => _is_expanded; set => _set(ref _is_expanded, value); }
public int level => parent == null ? 1 : parent.level + 1;
public SyntheticImportRowCollectionViewGroup parent { get; private set; }
// leg type desc
public string leg_type_desc =>
_leg_type_desc ?? (_leg_type_desc = string.Join(_desc_delim, ProtectedItems.OfType<ContractLegVM>().Select(l => l.leg_type.display_name())));
// price multiplier desc
public string price_multiplier_desc =>
_price_multiplier_desc ?? (_price_multiplier_desc = string.Join(_desc_delim, ProtectedItems.OfType<ContractLegVM>().Select(l => l.price_multiplier)));
// quantity desc
public string quantity_desc =>
_quantity_desc ?? (_quantity_desc = string.Join(_desc_delim, ProtectedItems.OfType<ContractLegVM>().Select(l => l.quantity)));
// security desc
public string security_desc =>
_security_desc ?? (_security_desc = string.Join(_desc_delim, ProtectedItems.OfType<ContractLegVM>().Select(l => l.security?.ToString() ?? "?").ExceptNull()).if_null_or_empty("(No Legs)"));
new int ProtectedItemCount {
get => base.ProtectedItemCount;
set {
if (base.ProtectedItemCount != value) {
base.ProtectedItemCount = value;
ProtectedItemCountChanged?.Invoke();
}
}
}
public bool HasErrors => Items.OfType<ContractLegVM>().Any(l => l.HasErrors);
public IEnumerable GetErrors(string propertyName) =>
propertyName == nameof(Items) ?
Items.OfType<ContractLegVM>().SelectMany(l => l.AllErrors) :
Enumerable.Empty<string>();
public void set_all_expanded(bool is_expanded) {
this.is_expanded = is_expanded;
foreach (var g in Items.OfTypeSafe<SyntheticImportRowCollectionViewGroup>())
g.set_all_expanded(is_expanded);
}
public bool Equals(SyntheticImportRowCollectionViewGroup other) =>
other != null &&
level == other.level &&
Equals(Name, other.Name);
void IRaiseNotifyPropertyChanged.raise_notify_property_changed(string prop) =>
OnPropertyChanged(new PropertyChangedEventArgs(prop));
void _add_item(int index, object item) {
Log.assert(index >= 0, "Index is less than 0.");
if (item is CollectionViewGroup group) {
var inner_group = new SyntheticImportRowCollectionViewGroup(group, _root);
_inner_groups.Add(group.Name, new InnerGroupSubscription(this, inner_group));
ProtectedItems.Insert(index, inner_group);
} else {
var row = (ContractLegVM)item;
if (_rows.TryPopValue(row.id, out var sub))
sub.Dispose();
_rows[row.id] = _connect_row(row);
ProtectedItems.Insert(index, item);
}
}
void _remove_item(int index, object item) {
if (item is SyntheticImportRowCollectionViewGroup inner_group) {
if (!_inner_groups.TryPopValue(inner_group.Name, out var ig_sub))
throw new InvalidOperationException($"Inner groups cache does not have an entry for the specified name '{inner_group.Name}' (entire tree structure may be compromised).");
ig_sub.Dispose();
Disposable.dispose(ref inner_group._dispose);
}
ProtectedItems.RemoveAt(index);
}
void _from_items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
for (int i = 0; i < e.NewItems.Count; i++)
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
break;
case NotifyCollectionChangedAction.Remove:
for (int i = 0; i < e.OldItems.Count; i++)
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
break;
case NotifyCollectionChangedAction.Replace:
for (int i = 0; i < e.OldItems.Count; i++)
_remove_item(e.OldStartingIndex, ProtectedItems[e.OldStartingIndex]);
for (int i = 0; i < e.NewItems.Count; i++)
_add_item(e.NewStartingIndex + i, e.NewItems[i]);
break;
case NotifyCollectionChangedAction.Reset:
while (ProtectedItems.Count > 0)
_remove_item(0, ProtectedItems[0]);
for (int i = 0; i < _from.Items.Count; i++)
_add_item(i, _from.Items[i]);
break;
}
}
void _invalidate_props(params string[] props) {
for (int i = 0; i < props.Length; i++)
OnPropertyChanged(new PropertyChangedEventArgs(props[i]));
}
void _set<T>(ref T backing, T new_value, [CallerMemberName]string prop = "") =>
_set(ref backing, new_value, EqualityComparer<T>.Default, prop);
void _set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = "") {
if (!comparer.Equals(backing, new_value)) {
backing = new_value;
OnPropertyChanged(new PropertyChangedEventArgs(prop));
}
}
IDisposable _connect_row(ContractLegVM row) {
row.result_changed += on_result_changed;
row.ErrorsChanged += on_errors_changed;
return Disposable.Create(() => {
row.result_changed -= on_result_changed;
row.ErrorsChanged -= on_errors_changed;
});
void on_result_changed() {
_security_desc = null;
_price_multiplier_desc = null;
_quantity_desc = null;
_leg_type_desc = null;
_invalidate_props(nameof(security_desc), nameof(price_multiplier_desc), nameof(quantity_desc), nameof(leg_type_desc));
}
void on_errors_changed(object sender, DataErrorsChangedEventArgs e) =>
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Items)));
}
class InnerGroupSubscription : IDisposable {
public readonly SyntheticImportRowCollectionViewGroup _inner_group;
readonly SyntheticImportRowCollectionViewGroup _parent;
int _last_count;
public InnerGroupSubscription(SyntheticImportRowCollectionViewGroup parent, SyntheticImportRowCollectionViewGroup inner_group) {
_parent = parent;
_inner_group = inner_group;
_inner_group.parent = parent;
// init item count
_on_item_count_changed();
// listen to item count changes
_inner_group.ProtectedItemCountChanged += _on_item_count_changed;
}
public void Dispose() {
_inner_group.ProtectedItemCountChanged -= _on_item_count_changed;
_inner_group.parent = null;
// make sure the state is valid (if a bad state goes by undetected here, nefarious/difficult-to-track bugs could arise)
if (_last_count != _inner_group.ProtectedItemCount) {
Log.Ui.warn("Cached inner count is not equal to the inner group's ItemCount (entire tree structure may be compromised).");
}
_parent.ProtectedItemCount -= _last_count;
}
void _on_item_count_changed() {
// add the delta
_parent.ProtectedItemCount += (_inner_group.ProtectedItemCount - _last_count);
// reset the local cache
_last_count = _inner_group.ProtectedItemCount;
}
}
}
}
@@ -0,0 +1,253 @@
using bts.api;
using bts.collections;
using bts.spark;
using mbts;
using mbts.collections;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.utility;
using mbts.user.settings;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Data;
using System.Windows.Input;
using Disposable = mbts.Disposable;
namespace bts.shell.spreader {
class SyntheticImportVM : DisposableViewModelBase, IRequestViewClose {
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
public SyntheticImportVM() {
var rows_view = new SyntheticImportRowCollectionView(flat_legs);
rows_view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(ContractLegVM.owner)));
this.rows_view = rows_view;
ok_cmd = new DelegateCommand(() => import_request?.Invoke());
cancel_cmd = new DelegateCommand(() => RequestClose?.Invoke(this, new EventArgs()));
_dispose = ObservableAutoWrapper.connect_item_hooks(
rows,
r => {
flat_legs.AddRange(r.legs);
return Disposable.Create(() => flat_legs.removeRange(r.legs));
});
}
public event Action import_request;
public event EventHandler RequestClose;
public ObservableCollection<SyntheticImportRowVM> rows { get; } = new ObservableCollection<SyntheticImportRowVM>();
public ObservableCollection<ContractLegVM> flat_legs { get; } = new ObservableCollection<ContractLegVM>();
public IReadOnlyDictionary<uint, SecurityMD> security_map { get => _security_map; set => Set(ref _security_map, value); }
public ICollectionView rows_view { get; }
// commands
public ICommand ok_cmd { get; }
public ICommand cancel_cmd { get; }
}
class SyntheticImportRowVM : DisposableViewModelBase, IEquatable<SyntheticImportRowVM> {
readonly ReactiveThresholdMonitor<ContractLegVM> _leg_errors_mon = new ReactiveThresholdMonitor<ContractLegVM>(l => l.subscribe_has_errors(true));
readonly Func<string, bool> _get_name_exists;
SecurityImmutable? _security;
ExecutionStrategyImmutable? _strategy;
TickFactors _tick_factors;
string _name;
bool _is_selected;
public SyntheticImportRowVM(uint user_id, Func<string, bool> get_name_exists) {
this.user_id = user_id;
_get_name_exists = get_name_exists;
_validate();
add_leg_cmd = new DelegateCommand<SyntheticImportRowVM>(r => {
if (r != null) {
r.legs.Add(new ContractLegVM {
price_multiplier = 1,
quantity = 1,
owner = r
});
}
});
remove_leg_cmd = new DelegateCommand<ContractLegVM>(l => l?.owner?.legs?.Remove(l));
PropertyChanged += on_property_changed;
var disp = new DisposableCombo(
_leg_errors_mon.OnDispatcher().Subscribe(v => _validate()),
ObservableAutoWrapper.connect_item_hooks(legs,
l => {
l.result_changed += invalidate;
return new DisposableCombo(_leg_errors_mon.join(l), l) +
new Action(() => l.result_changed -= invalidate);
})) +
new Action(() => PropertyChanged -= on_property_changed);
void invalidate() {
_security = null;
_strategy = null;
_validate();
result_changed?.Invoke();
}
void on_property_changed(object sender, PropertyChangedEventArgs e) =>
invalidate();
}
public event Action result_changed;
public SecurityImmutable result =>
_security ?? (_security = new SecurityImmutable(to_security())).Value;
public ExecutionStrategyImmutable strategy =>
_strategy ?? (_strategy = new ExecutionStrategyImmutable(to_strategy())).Value;
public (SecurityImmutable security, ExecutionStrategyImmutable strategy) result_tuple =>
(result, strategy);
public ParsedSyntheticContract parsed_synth { get; set; }
public string name { get => _name; set => Set(ref _name, value); }
public uint user_id { get; }
public TickFactors tick_factors { get => _tick_factors; set => Set(ref _tick_factors, value); }
public ObservableCollection<ContractLegVM> legs { get; } = new ObservableCollection<ContractLegVM>();
public bool is_selected { get => _is_selected; set => Set(ref _is_selected, value); }
public ICommand add_leg_cmd { get; }
public ICommand remove_leg_cmd { get; }
public Security to_security() {
var sc = new SyntheticContract {
Name = name,
UserId = user_id
};
sc.Legs.AddRange(legs.Select(l => l.to_contract_leg()));
return new Security {
Synthetic = sc,
Symbol = sc.Legs.FirstOrDefault()?.Security?.Symbol ?? string.Empty,
Exchange = Venue.Bts,
ContractSpec = tick_factors.to_spec()
};
}
public ExecutionStrategy to_strategy() {
var strat = new ExecutionStrategy();
foreach (var l in legs) {
if (l.security.HasValue)
strat.LegParamsMap[l.security.Value.Id] = l.to_leg_params();
}
return strat;
}
void _validate() {
using (validation()) {
if (string.IsNullOrEmpty(name)) {
validation_assert(false, "Name is not specified.", nameof(name));
} else if (_get_name_exists(name)) {
validation_assert(false, "Name already exists.", nameof(name));
}
validation_assert(tick_factors != null && tick_factors.is_valid(), "Invalid tick", nameof(tick_factors));
validation_assert(legs.Count > 0 && !_leg_errors_mon.is_above_threshold, "Invalid legs.", nameof(legs));
}
}
public bool Equals(SyntheticImportRowVM other) =>
other != null &&
parsed_synth.unpersisted_id == other.parsed_synth.unpersisted_id;
public override bool Equals(object obj) =>
Equals(obj as SyntheticImportRowVM);
public override int GetHashCode() =>
parsed_synth.unpersisted_id;
}
class ContractLegVM : DisposableViewModelBase, IEquatable<ContractLegVM> {
ContractLegImmutable? _leg;
LegSpreaderParamsImmutable? _leg_params;
SecurityImmutable? _security;
double _price_multiplier, _quantity;
ContractLegType _leg_type;
bool _is_selected;
public ContractLegVM() {
PropertyChanged += on_property_changed;
_dispose = Disposable.Create(() => PropertyChanged -= on_property_changed);
_validate();
void on_property_changed(object sender, PropertyChangedEventArgs e) {
switch (e.PropertyName) {
case nameof(security):
case nameof(price_multiplier):
case nameof(quantity):
_leg = null;
break;
case nameof(leg_type):
_leg_params = null;
break;
default: return;
}
_validate();
result_changed?.Invoke();
}
}
public event Action result_changed;
public ParsedContractLeg parsed_leg { get; set; }
public SyntheticImportRowVM owner { get; set; }
public int owner_id => owner.parsed_synth.unpersisted_id;
public int id => parsed_leg.id;
public ContractLegImmutable leg =>
_leg ?? (_leg = new ContractLegImmutable(to_contract_leg())).Value;
public LegSpreaderParamsImmutable leg_params =>
_leg_params ?? (_leg_params = new LegSpreaderParamsImmutable(to_leg_params())).Value;
public (ContractLegImmutable leg, LegSpreaderParamsImmutable leg_params) result_tuple =>
(leg, leg_params);
public SecurityImmutable? security { get => _security; set => Set(ref _security, value); }
public double price_multiplier { get => _price_multiplier; set => Set(ref _price_multiplier, value); }
public double quantity { get => _quantity; set => Set(ref _quantity, value); }
public ContractLegType leg_type { get => _leg_type; set => Set(ref _leg_type, value); }
public bool is_selected { get => _is_selected; set => Set(ref _is_selected, value); }
public void load(ContractLegImmutable leg) {
security = leg.Security;
price_multiplier = leg.PriceMultiplier;
quantity = leg.Quantity;
}
public ContractLeg to_contract_leg() =>
new ContractLeg {
Security = security?.CloneToMutable(),
PriceMultiplier = price_multiplier,
Side = quantity < 0 ? Side.Sell : Side.Buy,
Quantity = Math.Abs(quantity)
};
public LegSpreaderParams to_leg_params() {
var lps = SyntheticDataHelper.Defaults.LegSpreaderParams;
lps.EnableHedging = (leg_type & ContractLegType.Hedge) != 0;
lps.IsActiveQuoting = (leg_type & ContractLegType.Quote) != 0;
return lps;
}
void _validate() {
using (validation()) {
validation_assert(security.is_exchange_security(), "Invalid security", nameof(security));
validation_assert(price_multiplier.IsNonZero(), "Invalid price multiplier", nameof(price_multiplier));
validation_assert(quantity.IsNonZero(), "Invalid quantity", nameof(quantity));
}
}
public bool Equals(ContractLegVM other) =>
other != null && id == other.id;
public override bool Equals(object obj) =>
Equals(obj as ContractLegVM);
public override int GetHashCode() =>
id;
}
}
@@ -0,0 +1,270 @@
<UserControl
x:Class="bts.shell.spreader.SyntheticImportView"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:spr="clr-namespace:bts.shell.spreader"
xmlns:pnl="clr-namespace:bts.shell.pnl"
MinWidth="400"
Content="{Binding}"
>
<UserControl.Resources>
<cv:GridLengthConverter x:Key="GridLengthPixelConverter" UnitType="Pixel"/>
<Style x:Key="GroupDesc" TargetType="TextBlock">
<Setter Property="Margin" Value="{x:Static spr:SyntheticImportView.CellPadding}"/>
<Setter Property="ToolTip" Value="{Binding Text, RelativeSource={RelativeSource Self}}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="#80000000"/>
<Setter Property="FontStyle" Value="Italic"/>
</Style>
<DataTemplate DataType="{x:Type spr:ContractLegVM}">
<c:ContractPickerPopup
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CloseOnCancel="True"
CloseOnOK="True"
Header="{Binding security}"
IsEditMode="False"
IsLazy="True"
SecurityMap="{Binding DataContext.security_map, RelativeSource={RelativeSource AncestorType=DataGrid}, Mode=OneWay}"
SelectedSecurity="{Binding security, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ShowDialogSkirt="True"
ToolTip="{Binding SelectedCriteria, RelativeSource={RelativeSource Self}, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}, Mode=OneWay}"
/>
</DataTemplate>
<DataTemplate DataType="{x:Type spr:SyntheticImportVM}">
<c:DIYGrid
Grid.IsSharedSizeScope="True"
Rows="{Binding rows_view}"
RowHeight="{x:Static spr:SyntheticImportView.RowHeight}"
RowsHorizontalScrollBarVisibility="Collapsed"
SelectionHighlight="#30000000"
SelectionMode="Single"
SupportGroupItems="True"
>
<c:DIYGrid.Columns>
<c:DIYGridColumnCollection>
<spr:SyntheticImportColumn key="{x:Static spr:SyntheticImportColumns.Name}" Width="106"/>
<spr:SyntheticImportColumn key="{x:Static spr:SyntheticImportColumns.Ticks}" Width="128"/>
<spr:SyntheticImportColumn key="{x:Static spr:SyntheticImportColumns.Security}" Header="Securities" Width="120"/>
<spr:SyntheticImportColumn key="{x:Static spr:SyntheticImportColumns.Quantity}" Header="Quantities" Width="70"/>
<spr:SyntheticImportColumn key="{x:Static spr:SyntheticImportColumns.PriceMultiplier}" Header="Price Multipliers" Width="80"/>
<spr:SyntheticImportColumn key="{x:Static spr:SyntheticImportColumns.LegType}" Header="Leg Types" Width="83"/>
</c:DIYGridColumnCollection>
</c:DIYGrid.Columns>
<c:DIYGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="GroupItem">
<Setter Property="HorizontalAlignment" Value="Left"/>
<EventSetter Event="MouseDown" Handler="_on_group_item_mouse_down"/>
<!--Note: Tag is bound to 'Items' so errors on the 'Items' prop will be included in Validation.Errors.-->
<Setter Property="Tag">
<Setter.Value>
<MultiBinding>
<MultiBinding.Converter>
<cv:MultiValueConverterPipe/>
</MultiBinding.Converter>
<Binding Path="Name.legs"/>
<Binding Path="Name.name"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupItem">
<Grid
c:DIYGrid.SyncWithColumnGeometry="True"
u:StateManager.VisualState="{Binding IsChecked, ElementName=expander, Converter={x:Static pnl:PNLView.ExpandedVisualStateConverter}}"
Background="{TemplateBinding Background}"
>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ExpansionStates">
<VisualState Name="Expanded"/>
<VisualState Name="Collapsed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="presenter" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="{Binding RowHeight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}, Converter={StaticResource GridLengthPixelConverter}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.ColumnSpan="99" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Fill="Gray" UseLayoutRounding="True"/>
<!--Expander Toggle-->
<c:ExpanderToggleButton
x:Name="expander"
Grid.ColumnSpan="99"
Validation.ValidationAdornerSiteFor="{Binding RelativeSource={RelativeSource TemplatedParent}}"
ArrowPadding="14,2,0,0"
Background="{u:Brush Transparent}"
BorderThickness="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Height="{Binding RowHeight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
IsChecked="{Binding is_expanded}"
>
<u:StateManager.VisualState>
<Binding Path="TemplatedParent.(Validation.Errors).Count" RelativeSource="{RelativeSource Self}">
<Binding.Converter>
<cv:ValueConverterPipe>
<cv:IntGreaterThanConverter Value="0"/>
<cv:BooleanToObjectConverter ValueForFalse="NoErrors" ValueForTrue="Errors"/>
</cv:ValueConverterPipe>
</Binding.Converter>
</Binding>
</u:StateManager.VisualState>
<u:StateManager.Groups>
<u:StateGroupsManager AttachToMembers="True">
<u:StateDefinitionCollection>
<u:MouseStateDefinition MouseSource="{Binding Group.Target, RelativeSource={RelativeSource Self}}"/>
</u:StateDefinitionCollection>
</u:StateGroupsManager>
</u:StateManager.Groups>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="MouseStates">
<VisualState Name="Normal"/>
<VisualState Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="expander" Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="#10000000"/>
</Storyboard>
</VisualState>
<VisualState Name="Pressed">
<Storyboard>
<ColorAnimation Storyboard.TargetName="expander" Storyboard.TargetProperty="Background.Color" BeginTime="0" Duration="0" To="#10000000"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Validation.ErrorTemplate>
<ControlTemplate TargetType="Control">
<Grid>
<AdornedElementPlaceholder x:Name="adorned_element_pl"/>
<Border x:Name="exclamation" HorizontalAlignment="Left">
<Image Height="12" Margin="6,0,0,0" VerticalAlignment="Center" Source="{x:Static u:Icons.WarningExclamation}"/>
</Border>
<ContentControl u:Behavior.MirrorLayoutFor="{Binding ElementName=exclamation}" ToolTip="{Binding AdornedElement.(Validation.ValidationAdornerSiteFor).(Validation.Errors), ElementName=adorned_element_pl}">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Grid Background="Transparent">
<Grid.ToolTip>
<ToolTip Content="{Binding ToolTip, RelativeSource={RelativeSource TemplatedParent}}">
<ToolTip.ContentTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}" DisplayMemberPath="ErrorContent"/>
</DataTemplate>
</ToolTip.ContentTemplate>
</ToolTip>
</Grid.ToolTip>
</Grid>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
</Grid>
</ControlTemplate>
</Validation.ErrorTemplate>
<Grid c:DIYGrid.SyncWithColumnGeometry="True" Margin="-22,0,0,0">
<c:EditableTextBlock
x:Name="PART_Name_EditableTextBlock"
Grid.Column="0"
Margin="22,0,0,0"
EndEditOnLostKeyboardFocus="True"
Foreground="{u:Brush Black}"
Text="{Binding Name.name}"
ToolTip="Click to Edit Name"
UseLayoutRounding="True"
VerticalAlignment="Center"
>
<c:EditableTextBlock.CommandBindings>
<CommandBinding Command="{x:Static c:EditableTextBlock.BeginEditCommand}" Executed="{x:Static c:EditableTextBlock.BeginEditExecuted}"/>
<CommandBinding Command="{x:Static c:EditableTextBlock.EndEditCommand}" Executed="{x:Static c:EditableTextBlock.EndEditExecuted}"/>
<CommandBinding Command="{x:Static c:EditableTextBlock.CancelEditCommand}" Executed="{x:Static c:EditableTextBlock.CancelEditExecuted}"/>
</c:EditableTextBlock.CommandBindings>
<c:EditableTextBlock.InputBindings>
<MouseBinding MouseAction="LeftClick" Command="{x:Static c:EditableTextBlock.BeginEditCommand}"/>
<KeyBinding Key="Return" Command="{x:Static c:EditableTextBlock.EndEditCommand}"/>
<KeyBinding Key="Esc" Command="{x:Static c:EditableTextBlock.CancelEditCommand}"/>
</c:EditableTextBlock.InputBindings>
</c:EditableTextBlock>
<c:TickFactorsPicker
c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.Ticks}"
Margin="{x:Static spr:SyntheticImportView.CellPadding}"
SelectedTickFactors="{Binding Name.tick_factors}"
UseLayoutRounding="True"
/>
<TextBlock c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.Security}" Text="{Binding security_desc}" Style="{StaticResource GroupDesc}"/>
<TextBlock c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.Quantity}" Text="{Binding quantity_desc}" Style="{StaticResource GroupDesc}"/>
<TextBlock c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.PriceMultiplier}" Text="{Binding price_multiplier_desc}" Style="{StaticResource GroupDesc}"/>
<TextBlock c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.LegType}" Text="{Binding leg_type_desc}" Style="{StaticResource GroupDesc}">
<TextBlock.ToolTip>
<TextBlock>
<Run Text="{Binding leg_type_desc, Mode=OneWay}"/>
<LineBreak/>
<Run Text="Q = Quote"/>
<LineBreak/>
<Run Text="H = Hedge"/>
<LineBreak/>
<Run Text="N = None"/>
</TextBlock>
</TextBlock.ToolTip>
</TextBlock>
</Grid>
</c:ExpanderToggleButton>
<ItemsPresenter x:Name="presenter" Grid.Row="1" Grid.ColumnSpan="99"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</c:DIYGrid.GroupStyle>
<c:DIYGrid.RowTemplate>
<DataTemplate DataType="{x:Type spr:ContractLegVM}">
<Grid c:DIYGrid.SyncWithColumnGeometry="True">
<c:ContractPickerPopup
c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.Security}"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="{x:Static spr:SyntheticImportView.CellPadding}"
CloseOnCancel="True"
CloseOnOK="True"
Header="{Binding security, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
IsEditMode="False"
IsLazy="True"
SecurityMap="{Binding DataContext.security_map, RelativeSource={RelativeSource AncestorType=c:DIYGrid}, Mode=OneWay}"
SelectedSecurity="{Binding security, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ShowDialogSkirt="True"
ToolTip="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}}"
>
</c:ContractPickerPopup>
<c:TickUpDownDouble
c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.Quantity}"
Margin="{x:Static spr:SyntheticImportView.CellPadding}"
Value="{Binding quantity}"
/>
<c:TickUpDownDouble
c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.PriceMultiplier}"
Margin="{x:Static spr:SyntheticImportView.CellPadding}"
Value="{Binding price_multiplier}"
/>
<spr:ContractLegTypeView
c:DIYGrid.ColumnKey="{x:Static spr:SyntheticImportColumns.LegType}"
Value="{Binding leg_type}"
/>
</Grid>
</DataTemplate>
</c:DIYGrid.RowTemplate>
</c:DIYGrid>
</DataTemplate>
</UserControl.Resources>
</UserControl>
@@ -0,0 +1,50 @@
using mbts.ui.controls;
using mbts.ui.dpi;
using mbts.ui.utility;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace bts.shell.spreader {
enum SyntheticImportColumns {
Name = 1,
Security,
Ticks,
PriceMultiplier,
Quantity,
LegType
}
/// <summary>
/// Interaction logic for SyntheticImportView.xaml
/// </summary>
partial class SyntheticImportView : UserControl {
public static double RowHeight =>
27.0.RoundLayoutValue();
public static Thickness CellPadding =>
new Thickness(6, 3, 6, 3).RoundToLayout();
public SyntheticImportView() =>
InitializeComponent();
void _on_group_item_mouse_down(object sender, MouseButtonEventArgs e) {
if (!e.Handled && e.ClickCount == 2 && sender is FrameworkElement el && el.DataContext is SyntheticImportRowCollectionViewGroup group) {
group.is_expanded = !group.is_expanded;
e.Handled = true;
}
}
}
class SyntheticImportColumn : DIYGrid.ColumnBase<SyntheticImportColumns> {
public SyntheticImportColumn() =>
is_visible = true;
protected override void on_key_initialized(SyntheticImportColumns key) {
if (Header == null)
Header = EnumHelper.get_display_name(key);
if (id == null)
id = key.ToString();
}
}
}
@@ -0,0 +1,194 @@
using bts;
using bts.api;
using bts.collections;
using bts.shell.spreader;
using mbts.ui;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace mbts.spark {
static class SyntheticImporter {
const string _import_title = "Import Synthetics";
static readonly TimeSpan _import_op_sec_map_timeout = TimeSpan.FromSeconds(30);
public static IDisposable import_from_file(string path, IBus bus, OrdersModel orders_model) {
bool is_disposed = false;
IDisposable dispose_op = null;
dispose_op = Disposable.Create(() => is_disposed = true);
impl();
return Disposable.Create(() => Disposable.dispose(ref dispose_op));
async void impl() {
IReadOnlyDictionary<uint, SecurityMD> sec_md_map = null;
while (true) {
await Task.WhenAny(init_sec_md_map(), Task.Delay(_import_op_sec_map_timeout));
if (is_disposed)
return;
if (sec_md_map != null)
break;
if (!retry_on_error($"Synthetic import op timed out waiting for security map after {_import_op_sec_map_timeout.TotalSeconds:0.00} seconds."))
return;
}
// The following conditions should break the while loop:
// 1) the import succeeds
// 2) the import fails, and the user does not click 'Yes' in the 'try again...' confirmation dialog
while (true) {
string error_msg = null;
try {
var raw_lines = File.ReadAllLines(path);
var used_names = new HashSet<string>(sec_md_map.Values.Where(smd => smd.security.ContractCase == Security.ContractOneofCase.Synthetic)
.Select(smd => smd.security.Synthetic.Name));
var parsed_synths = SyntheticSecurityParser.parse_rows(raw_lines, MappedDictionary.create(sec_md_map, smd => smd.security), used_names).ToList();
var main_vm = new SyntheticImportVM() {
security_map = sec_md_map
};
// add rows/conflicts
main_vm.rows.AddRange(parsed_synths.Select(ps => {
var sec = ps.result;
var r = new SyntheticImportRowVM(orders_model.admin_model.user.id, used_names.Contains) {
tick_factors = TickUtils.get_regional_ticks(sec.ContractSpec).low_tick,
name = sec.Synthetic.Name,
parsed_synth = ps
};
r.legs.AddRange(ps.legs.Zip(sec.Synthetic.Legs).Select(tup => {
var (pl, l) = tup;
var leg_vm = new ContractLegVM {
price_multiplier = l.PriceMultiplier,
quantity = l.Quantity,
parsed_leg = pl,
owner = r,
leg_type =
(pl.result_tuple.leg_params.EnableHedging ? ContractLegType.Hedge : ContractLegType.None) |
(pl.result_tuple.leg_params.IsActiveQuoting ? ContractLegType.Quote : ContractLegType.None)
};
if (l.Security != null)
leg_vm.security = new SecurityImmutable(l.Security);
return leg_vm;
}));
return r;
}));
var main_win = new SyntheticImportDialog { DataContext = main_vm };
ui.WindowStyle.SetSizeToContent(main_win, SizeToContent.WidthAndHeight);
main_vm.import_request += on_main_import_request;
main_win.move_to_mouse(-200, -75);
dispose_op = WindowUtilities.connect_vm(main_win, main_vm, main_vm);
main_win.Show();
// conflicts
var conflicts = SyntheticSecurityParser.get_conflicts(main_vm.rows);
if (conflicts.Values.Any(c => c.Count > 0)) {
var conflicts_vm = new SyntheticImportConflictsVM(parsed_synths.Count) {
security_map = sec_md_map
};
foreach (var kv in conflicts) {
var assign_del = SyntheticSecurityParser.assign_component(kv.Key);
foreach (var text_groups in kv.Value) {
var eval = get_error_value(kv.Key);
var row = new SyntheticImportConflictRowVM(kv.Key, text_groups.Key, text_groups.Value, eval, eval);
conflicts_vm.conflicts.Add(row);
conflicts_vm.committed += () => {
foreach (var (synth, leg_vm) in text_groups.Value) {
var leg_tup = leg_vm?.parsed_leg?.result_tuple;
assign_del(synth.parsed_synth.result, leg_tup?.leg, leg_tup?.leg_params, row.value);
}
row.on_conflicts_resolved();
};
}
}
var conflicts_win = new SyntheticImportConflictsDialog {
DataContext = conflicts_vm,
Owner = main_win,
Title = $"{_import_title} Conflicts",
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
dispose_op = new DisposableCombo(WindowUtilities.connect_vm(conflicts_win, conflicts_vm, conflicts_vm)) + dispose_op;
conflicts_win.ShowDialog();
}
object get_error_value(SyntheticContractComponent component) {
switch (component) {
case SyntheticContractComponent.PriceMultiplier:
case SyntheticContractComponent.Quantity: return double.NaN;
default: return null;
}
}
void on_main_import_request() {
if (!is_disposed)
import_synths(parsed_synths, main_vm.rows);
Disposable.dispose(ref dispose_op);
}
}
catch (Exception ex) {
error_msg = $"Error parsing synthetics from file '{path}': {ex}.";
}
if (error_msg == null) {
// Exit loop: the import succeeded.
break;
}
if (!retry_on_error(error_msg)) {
// Exit loop: the import failed, and the user did not click 'Yes' in the 'try again...' confirmation dialog.
break;
}
// Continue loop: the import failed, and the user wants to try again.
error_msg = null;
}
async void import_synths(IReadOnlyList<ParsedSyntheticContract> parsed_synths, IEnumerable<SyntheticImportRowVM> rows) {
(uint? key, string error_msg)[] new_ids = null;
string error_msg = null;
try {
new_ids = await orders_model.spreader.create_synthetics(
rows.Where(r => !r.HasErrors)
.Select(r => r.result_tuple));
}
catch (Exception ex) {
Log.Ui.error($"Failed to save all synthetics during import op (one or more may have been successfully saved): {ex}.");
error_msg = $"Import was unsuccessful: {ex}.";
}
if (new_ids == null)
error_msg = "An unknown error occurred (check the synthetic management window to see if any synthetics were successfully added).";
if (error_msg != null) {
BTSMessageBox.Show(error_msg, "Import Synthetics", BTSMessageBoxButtons.OK);
} else {
Log.assert(new_ids != null);
var failed = new_ids
.Where(r => !r.error_msg.is_null_or_empty())
.ToList();
int num_succeeded = new_ids.Length - failed.Count;
if (num_succeeded < parsed_synths.Count) {
if (!BTSMessageBox.Confirm(
$"{num_succeeded}/{parsed_synths.Count} synthetics were successfully imported.\n" +
"Would you like to keep the imports that were successful?",
_import_title)) {
bus.post(SparkTags.SyntheticDeleteTag,
ImmutableHashSet.CreateRange(new_ids.Where(r => r.key.HasValue).Select(r => r.key.Value)));
}
} else {
BTSMessageBox.Show(
$"All {num_succeeded} synthetics were successfully imported.",
_import_title,
BTSMessageBoxButtons.OK,
MessageBoxImage.Information);
}
}
}
async Task init_sec_md_map() =>
sec_md_map = await bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().FirstOrDefaultAsync();
bool retry_on_error(string error_msg) {
Log.Ui.warn(error_msg);
return BTSMessageBox.Confirm(error_msg + "\nTry again?", _import_title, MessageBoxImage.Exclamation);
}
}
}
}
}
@@ -0,0 +1,307 @@
using bts.api;
using bts.spark;
using bts.utils;
using mbts;
using mbts.ui.utility;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
namespace bts.shell.spreader {
[Flags]
enum ContractLegType {
None = 0,
Quote = 1,
Hedge = 2
}
class ParsedSyntheticContract : IDisposable {
[ThreadStatic]
static int _id_counter;
public readonly int unpersisted_id = unchecked(_id_counter++);
public readonly Security result = new Security {
Synthetic = new SyntheticContract(),
ContractSpec = new ContractSpecification(),
Exchange = Venue.Bts
};
readonly ReactiveThresholdMonitor<ParsedContractLeg> _legs_valid_mon =
new ReactiveThresholdMonitor<ParsedContractLeg>(l => l.subscribe_is_valid());
IDisposable _dispose;
public ParsedSyntheticContract() {
_dispose = Disposable.Create(
ObservableAutoWrapper.connect_item_hooks(legs, _legs_valid_mon.join),
_legs_valid_mon.Subscribe(_ => _is_dirty = true));
}
bool
_is_dirty,
_is_valid;
(string text, bool success)
_name,
_tick_size;
public (string text, bool success) name { get => _name; set => _set(ref _name, value); }
public (string text, bool success) tick_size { get => _tick_size; set => _set(ref _tick_size, value); }
public ObservableCollection<ParsedContractLeg> legs { get; } = new ObservableCollection<ParsedContractLeg>();
public bool is_valid {
get {
if (_is_dirty) {
_is_valid = tick_size.success && name.success && !_legs_valid_mon.is_above_threshold;
_is_dirty = false;
}
return _is_valid;
}
}
public ParsedContractLeg current_leg =>
legs.LastOrDefault() ?? add_new_leg();
public ParsedContractLeg add_new_leg() {
var leg = new ParsedContractLeg();
legs.Add(leg);
result.Synthetic.Legs.Add(leg.result);
return leg;
}
public void Dispose() =>
Disposable.dispose(ref _dispose);
public bool Equals(ParsedSyntheticContract other) => other != null && unpersisted_id == other.unpersisted_id;
public override bool Equals(object obj) => Equals(obj as ParsedSyntheticContract);
public override int GetHashCode() => unpersisted_id.GetHashCode();
void _set<T>(ref T backing, T value) {
backing = value;
_is_dirty = true;
}
}
class ParsedContractLeg {
[ThreadStatic]
static int _id_counter;
public readonly ContractLeg result = new ContractLeg { };
public readonly LegSpreaderParams leg_params = new LegSpreaderParams();
bool
_is_dirty,
_is_valid;
(string text, bool success)
_security,
_quantity,
_price_multiplier,
_leg_type;
public event Action invalidated;
public int id { get; } = unchecked(_id_counter++);
public (string text, bool success) security { get => _security; set => _set(ref _security, value); }
public (string text, bool success) quantity { get => _quantity; set => _set(ref _quantity, value); }
public (string text, bool success) price_multiplier { get => _price_multiplier; set => _set(ref _price_multiplier, value); }
public (string text, bool success) leg_type { get => _leg_type; set => _set(ref _leg_type, value); }
public (ContractLeg leg, LegSpreaderParams leg_params) result_tuple => (result, leg_params);
public bool is_valid {
get {
if (_is_dirty) {
if (_is_valid != (security.success && quantity.success && price_multiplier.success && leg_type.success)) {
_is_valid = !_is_valid;
invalidated?.Invoke();
}
_is_dirty = false;
}
return _is_valid;
}
}
public IObservable<bool> subscribe_is_valid() =>
Observable.Create<bool>(o => {
o.OnNext(is_valid);
invalidated += on_invalidated;
return Disposable.Create(() => invalidated -= on_invalidated);
void on_invalidated() =>
o.OnNext(is_valid);
});
void _set<T>(ref T backing, T value) {
backing = value;
_is_dirty = true;
}
}
static class SyntheticSecurityParser {
public const string file_extension = "csv";
public static string display_name(this ContractLegType leg_type) {
if (leg_type == ContractLegType.None)
return "N";
var flags = EnumHelper.split_flags(leg_type);
return string.Join("", flags.Select(f => f.ToString())
.Where(str => str.Length > 0)
.Select(str => str.Substring(0, 1)));
}
public static Action<Security, ContractLeg, LegSpreaderParams, object> assign_component(SyntheticContractComponent component) {
switch (component) {
case SyntheticContractComponent.Name: return (sec, leg, leg_params, val) => sec.Synthetic.Name = val?.ToString() ?? string.Empty;
case SyntheticContractComponent.TickSize: return (sec, leg, leg_params, val) => sec.ContractSpec.LowTick = sec.ContractSpec.HighTick = ConvertUtils.try_convert<double?>(val) ?? 0.0;
case SyntheticContractComponent.PriceMultiplier: return (sec, leg, leg_params, val) => leg.PriceMultiplier = ConvertUtils.try_convert<double?>(val) ?? double.NaN;
case SyntheticContractComponent.Quantity: return (sec, leg, leg_params, val) => leg.Quantity = ConvertUtils.try_convert<double?>(val) ?? double.NaN;
case SyntheticContractComponent.Security: return (sec, leg, leg_params, val) => leg.Security = val as Security ?? (val is SecurityImmutable si ? si.CloneToMutable() : null);
case SyntheticContractComponent.LegType: return (sec, leg, leg_params, val) => assign_leg_type(leg_params, ConvertUtils.try_convert(val, out ContractLegType lt) ? lt : ContractLegType.None);
default: throw new NotImplementedException();
}
}
/// Note: the returned dictionary will have entries for each <see cref="SyntheticContractComponent"/>, regardless of whether or not that component has any conflicts.
public static Dictionary<SyntheticContractComponent, Dictionary<string, List<(ParsedSyntheticContract synth, ParsedContractLeg leg)>>> get_conflicts(IEnumerable<ParsedSyntheticContract> parsed_fills) {
var missing_components = EnumHelper.get_values<SyntheticContractComponent>().ToDictionary(c => c, c => new Dictionary<string, List<(ParsedSyntheticContract synth, ParsedContractLeg leg)>>());
var mk_list = new Func<string, List<(ParsedSyntheticContract synth, ParsedContractLeg leg)>>(_ => new List<(ParsedSyntheticContract, ParsedContractLeg)>());
foreach (var psc in parsed_fills) {
if (!psc.name.success) {
add_missing(SyntheticContractComponent.Name, psc.name.text, psc);
}
if (!psc.tick_size.success) {
add_missing(SyntheticContractComponent.TickSize, psc.tick_size.text, psc);
}
foreach (var leg in psc.legs) {
if (!leg.price_multiplier.success)
add_missing(SyntheticContractComponent.PriceMultiplier, leg.price_multiplier.text, psc, leg);
if (!leg.quantity.success)
add_missing(SyntheticContractComponent.Quantity, leg.quantity.text, psc, leg);
if (!leg.security.success)
add_missing(SyntheticContractComponent.Security, leg.security.text, psc, leg);
if (!leg.leg_type.success)
add_missing(SyntheticContractComponent.LegType, leg.leg_type.text, psc, leg);
}
}
return missing_components;
void add_missing(SyntheticContractComponent component, string text, ParsedSyntheticContract result, ParsedContractLeg leg = null) =>
missing_components[component].GetOrAdd(text ?? string.Empty, mk_list).Add((result, leg));
}
public static Dictionary<SyntheticContractComponent, Dictionary<string, List<(SyntheticImportRowVM row, ContractLegVM leg)>>> get_conflicts(IEnumerable<SyntheticImportRowVM> import_rows) {
var missing_components = EnumHelper.get_values<SyntheticContractComponent>().ToDictionary(c => c, c => new Dictionary<string, List<(SyntheticImportRowVM row, ContractLegVM leg)>>());
var mk_list = new Func<string, List<(SyntheticImportRowVM row, ContractLegVM leg)>>(_ => new List<(SyntheticImportRowVM, ContractLegVM)>());
foreach (var ir in import_rows) {
var psc = ir.parsed_synth;
if (!psc.name.success) {
add_missing(SyntheticContractComponent.Name, psc.name.text, ir);
}
if (!psc.tick_size.success) {
add_missing(SyntheticContractComponent.TickSize, psc.tick_size.text, ir);
}
foreach (var leg in ir.legs) {
if (!leg.parsed_leg.price_multiplier.success)
add_missing(SyntheticContractComponent.PriceMultiplier, leg.parsed_leg.price_multiplier.text, ir, leg);
if (!leg.parsed_leg.quantity.success)
add_missing(SyntheticContractComponent.Quantity, leg.parsed_leg.quantity.text, ir, leg);
if (!leg.parsed_leg.security.success)
add_missing(SyntheticContractComponent.Security, leg.parsed_leg.security.text, ir, leg);
if (!leg.parsed_leg.leg_type.success)
add_missing(SyntheticContractComponent.LegType, leg.parsed_leg.leg_type.text, ir, leg);
}
}
return missing_components;
void add_missing(SyntheticContractComponent component, string text, SyntheticImportRowVM row, ContractLegVM leg = null) =>
missing_components[component].GetOrAdd(text ?? string.Empty, mk_list).Add((row, leg));
}
// This implementation prioritizes simplicity over speed.
// The security desc lookup could be better optimized, but the speed at which fills are imported is probably not a paramount concern.
public static IEnumerable<ParsedSyntheticContract> parse_rows(IEnumerable<string> rows, IReadOnlyDictionary<uint, SecurityImmutable> security_map, ISet<string> used_names) {
var sec_lookup = security_map.ToLookup(kv => kv.Value.ToString());
var name_col = Cells.name(used_names.Contains);
var sec_col = Cells.contract_security(lookup_security);
return RowParser.parse_rows(rows, columns(), Cells.delimiter);
Security lookup_security(string sec_text) =>
sec_lookup.Contains(sec_text) ? sec_lookup[sec_text].First().Value.CloneToMutable() : null;
IEnumerable<RowParser.AssignColumnValue<ParsedSyntheticContract>> columns() {
yield return name_col;
yield return Cells.tick_size;
// if an imported synth has > 255 legs, assume it is an error.
const int max_legs = 255;
int leg_count = 0;
do {
yield return sec_col;
yield return Cells.contract_quantity;
yield return Cells.contract_price_multiplier;
yield return Cells.leg_type;
} while (leg_count++ < max_legs);
}
}
public static Func<string, TKey> reverse_lookup<TKey>(IReadOnlyDictionary<TKey, string> map) {
var lookup = map.ToLookup(kv => kv.Value, kv => kv.Key);
return cell_text => lookup.Contains(cell_text) ? lookup[cell_text].First() : default;
}
public static void assign_leg_type(LegSpreaderParams leg_params, ContractLegType leg_type) {
leg_params.IsActiveQuoting = (leg_type & ContractLegType.Quote) != 0;
leg_params.EnableHedging = (leg_type & ContractLegType.Hedge) != 0;
}
static class Cells {
public const char delimiter = ',';
public static RowParser.AssignColumnValue<ParsedSyntheticContract> name(Func<string, bool> name_exists) =>
(ref ParsedSyntheticContract psc, string cell_text) => {
psc.name = (cell_text, !name_exists(cell_text));
psc.result.Synthetic.Name = cell_text;
};
public static void tick_size(ref ParsedSyntheticContract psc, string cell_text) {
psc.tick_size = (cell_text, double.TryParse(cell_text.Trim(), out var val));
if (psc.tick_size.success) {
psc.tick_size = (cell_text, val.IsNonZero());
psc.result.ContractSpec.LowTick = psc.result.ContractSpec.HighTick = val;
}
}
public static RowParser.AssignColumnValue<ParsedSyntheticContract> contract_security(Func<string, Security> reverse_security_lookup) =>
(ref ParsedSyntheticContract psc, string cell_text) => {
var leg = psc.add_new_leg();
leg.result.Security = reverse_security_lookup(cell_text.Trim());
leg.security = (cell_text, leg.result.Security != null);
};
public static void contract_quantity(ref ParsedSyntheticContract psc, string cell_text) {
psc.current_leg.quantity = (cell_text, double.TryParse(cell_text.Trim(), out var val) && val.IsNonZero());
psc.current_leg.result.Quantity = val;
}
public static void contract_price_multiplier(ref ParsedSyntheticContract psc, string cell_text) {
psc.current_leg.price_multiplier = (cell_text, double.TryParse(cell_text.Trim(), out var val) && val.IsNonZero());
psc.current_leg.result.PriceMultiplier = val;
}
public static void leg_type(ref ParsedSyntheticContract psc, string cell_text) {
psc.current_leg.leg_type = (cell_text, _parse_leg_type(cell_text.Trim(), out var leg_type));
assign_leg_type(psc.current_leg.leg_params, leg_type);
}
static bool _parse_leg_type(string cell_text, out ContractLegType result) {
result = ContractLegType.None;
if (cell_text.IsNullOrEmpty())
return false;
switch (char.ToUpper(cell_text[0])) {
case 'Q': result = ContractLegType.Quote; break;
case 'H': result = ContractLegType.Hedge; break;
case 'N': break;
default: return false;
}
return true;
}
}
}
}
@@ -1,17 +1,52 @@
using bts.api;
using bts.spark;
using mbts.ui.shell;
using System;
namespace mbts.ui.spark {
class LegSpreaderParamsTuple : ViewModelBase, IEquatable<LegSpreaderParamsTuple> {
[ThreadStatic]
static int _id_counter;
SecurityImmutable? _security;
namespace mbts.ui.spark
{
internal class LegSpreaderParamsTuple
{
public LegSpreaderParamsTuple(ContractLeg leg, LegSpreaderParams leg_params) {
id = unchecked(_id_counter++);
this.leg = leg;
this.leg_params = leg_params;
if (leg.Security != null)
_security = new SecurityImmutable(leg.Security);
}
public event Action security_changed;
public int id { get; }
/// <summary>
/// Important: do NOT modify security via <see cref="ContractLeg.Security"/>,
/// use <see cref="security"/> instead ([only]the latter raises the PropertyChanged event).
/// </summary>
public ContractLeg leg { get; }
public LegSpreaderParams leg_params { get; }
public SecurityImmutable? security {
get => _security;
set {
if (!_security.Equals(value)) {
_security = value;
leg.Security = value?.CloneToMutable();
security_changed?.Invoke();
RaisePropertyChanged();
}
}
}
public uint SecurityKey => leg.Security.Id;
public bool Equals(LegSpreaderParamsTuple other) =>
ReferenceEquals(this, other);
public override bool Equals(object obj) =>
Equals(obj as LegSpreaderParamsTuple);
public override int GetHashCode() =>
id;
}
}
}
@@ -4,6 +4,8 @@ using bts.api.core;
using bts.api.synthetic;
using bts.collections;
using bts.spark;
using bts.utils;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
@@ -82,6 +84,61 @@ namespace mbts.spark {
(mdc, success) => success ? mdc.Data[0].Strategy : (ExecutionStrategyImmutable?)null,
"lookup strategy");
public Task<(uint? key, string error_msg)[]> create_synthetics(IEnumerable<(SecurityImmutable security, ExecutionStrategyImmutable strategy)> synthetics) {
var tasks = synthetics.Select(tup => {
var (sec, strat) = tup;
if (sec.ContractCase != Security.ContractOneofCase.Synthetic)
return Task.FromException<(Result<CreateResult>, Result<UpdateResult>?)>(ExceptionBuilder.Argument.unexpected_case(sec.ContractCase, nameof(sec)));
if (sec.Synthetic.UserId != _user_id) {
var msec = sec.CloneToMutable();
msec.Synthetic.UserId = _user_id;
sec = new SecurityImmutable(msec);
}
return impl();
async Task<(Result<CreateResult> sec, Result<UpdateResult>? strat)> impl() {
var result = await _service.create_synthetic_async(sec);
Result<UpdateResult>? strategy;
if (result.success && result.value.Error == CreateResult.Types.Error.Ok) {
strategy = await _service.update_security_metadata_async(new Metadata {
SecurityKey = result.value.Key,
Strategy = tup.strategy.CloneToMutable()
});
} else {
strategy = null;
}
return (result, strategy);
}
});
return _error.safe_execute_batch(tasks,
tup => {
var (sec, strat) = tup;
// security
string sec_error_msg = null;
uint? key = null;
if (!sec.success) {
sec_error_msg = sec.error_message;
} else if (sec.value.Error != CreateResult.Types.Error.Ok) {
sec_error_msg = sec.value.Error.ToString();
} else {
key = sec.value.Key;
}
// strategy
string strat_error_msg = null;
if (!strat.HasValue) {
strat_error_msg = "Strategy was not successfully saved";
} else if (!strat.Value.success) {
strat_error_msg = strat.Value.error_message;
} else if (strat.Value.value.Error != UpdateResult.Types.Error.Ok) {
strat_error_msg = strat.Value.value.Error.ToString();
}
// merge error msgs (result should be null if both errors are empty)
var error_msg = string.Join(Environment.NewLine, new[] { sec_error_msg, strat_error_msg }.ExceptNull());
return (key, error_msg.NullIfEmpty());
},
"Synthetic",
"creating synthetics");
}
public Task<uint?> create_synthetic(SecurityImmutable synthetic) {
if (synthetic.ContractCase != Security.ContractOneofCase.Synthetic)
return null;
@@ -106,10 +163,32 @@ namespace mbts.spark {
(r, success) => success,
"saving strategy");
public async Task<string[]> save_strategies(IEnumerable<(uint key, ExecutionStrategyImmutable strategy)> items) {
// Copy everything to a new list, to guarentee that it won't be altered during the op.
// The performance impact should be insignificant, as this should be a rare call.
var item_list = new List<(uint key, ExecutionStrategyImmutable strategy, int index)>(items.Select((item, index) => (item.key, item.strategy, index)));
string[] errors = new string[item_list.Count];
await Task.WhenAll(item_list.Select(impl));
return errors;
async Task impl((uint key, ExecutionStrategyImmutable strategy, int index) tup) {
var (key, strategy, index) = tup;
try {
var result = await _service.update_security_metadata_async(new Metadata {
SecurityKey = key,
Strategy = strategy.CloneToMutable()
});
if (!result.success)
errors[index] = result.error_message;
} catch (Exception ex) {
errors[index] = ex.ToString();
}
}
}
public async Task<bool> verify_name_is_unique(string name) {
var (result, success) = await strategy_tuple.FirstAsyncTask(SparkUtils.DefaultTimeOut);
name = name.Trim().ToLower();
return success && !result.Values.Any(st => st.md.security.Synthetic.Name.Trim().ToLower() == name);
}
}
}
}
@@ -1,6 +1,7 @@
using bts;
using bts.api;
using bts.shell;
using bts.shell.spreader;
using bts.spark;
using mbts.persist;
using mbts.ui;
@@ -18,7 +19,10 @@ using static bts.shell.SyntheticManagementWindowFactory;
namespace mbts.spark {
class SyntheticManagementWindow : SparkWindow<StartupData>, ISharedWindow {
const string _window_type = "SyntheticManagement";
const string
_window_type = "SyntheticManagement",
_import_title = "Import Synthetics";
static readonly TimeSpan _import_op_sec_map_timeout = TimeSpan.FromSeconds(30);
SingleValuePump<SyntheticManagementDialogVM> _vm = new SingleValuePump<SyntheticManagementDialogVM>();
StartupData _state;
OrdersModel _orders_model;
@@ -27,7 +31,6 @@ namespace mbts.spark {
IReadOnlyList<SparkCommand> _commands;
readonly IWindowSettingsManager<GridSettings> _settings_manager;
public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
: base("Synthetic Management", bus, factory, SparkCommands.ShowSyntheticManagement.icon) {
_orders_model = orders_model;
@@ -90,7 +93,11 @@ namespace mbts.spark {
var dialog = new ShellSettingsDialog { Title = "Synthetic Management Grid Settings", DataContext = vm };
vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog);
return dialog;
}))
})),
new SparkCommandCommand(
"Import Synthetics...", Icons.Import, FileDialogCommand.open_file(import, extensions: SyntheticSecurityParser.file_extension)) {
show_in_toolbar = false
}
};
_subscriptions = Disposable.Create(
@@ -105,13 +112,16 @@ namespace mbts.spark {
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;
IImmutableSet<uint> select_security_id(RowModificationParams? rmp) => ImmutableHashSet.Create(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;
_vm.value = new SyntheticManagementDialogVM(_bus, _orders_model, _settings_manager, _state.sorting_criteria, _state.filters ?? new FilterStates());
if (_view == null) _view = new SyntheticManagementView { DataContext = _vm.value };
else _view.DataContext = _vm.value;
void import(string path) =>
SyntheticImporter.import_from_file(path, _bus, _orders_model);
}
public override void Dispose() {
@@ -145,4 +155,4 @@ namespace mbts.spark {
new RowStrategy(security, strategy);
}
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PostHedgeOppositeMarketVolumeBasedCrossCrossTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Cross Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_omv_cross(leg_tup) is PostHedgeOppositeMarketVolumeBasedForcedCross omv_cross)
omv_cross.CrossTicks = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.PostHedgeParams.OppositeMarketVolumeBasedForcedCross;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PostHedgeOppositeMarketVolumeBasedForcedCross.CrossTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PostHedgeOppositeMarketVolumeBasedForcedCross.CrossTicks);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PostHedgeOppositeMarketVolumeBasedCrossThresholdModel : RowModelBase<QuantityMeasure> {
IDictionary<uint, QuantityMeasure> _value_store = new Dictionary<uint, QuantityMeasure>();
IDictionary<int, QuantityMeasure> _value_store = new Dictionary<int, QuantityMeasure>();
public override string display_name { get; } = "Threshold";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, QuantityMeasure value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_omv_cross(leg_tup) is PostHedgeOppositeMarketVolumeBasedForcedCross omv_cross)
omv_cross.QuantityThreshold = value;
}
@@ -36,7 +36,7 @@ namespace mbts.spark {
leg_tup.leg_params.PostHedgeParams.OppositeMarketVolumeBasedForcedCross;
QuantityMeasure _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PostHedgeOppositeMarketVolumeBasedForcedCross.QuantityThreshold);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PostHedgeOppositeMarketVolumeBasedForcedCross.QuantityThreshold);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PostHedgePriceBasedCrossMarketTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Cross Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_price_based_cross(leg_tup) is PostHedgePriceBasedForcedCross price_based_cross)
price_based_cross.MarketCrossTicks = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.PostHedgeParams.PriceBasedCross;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PostHedgePriceBasedForcedCross.MarketCrossTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PostHedgePriceBasedForcedCross.MarketCrossTicks);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PostHedgePriceBasedCrossTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_price_based_cross(leg_tup) is PostHedgePriceBasedForcedCross price_based_cross)
price_based_cross.TicksThreshold = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.PostHedgeParams.PriceBasedCross;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PostHedgePriceBasedForcedCross.TicksThreshold);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PostHedgePriceBasedForcedCross.TicksThreshold);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PostHedgeTimeBasedCrossMarketTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Cross Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_time_based_cross(leg_tup) is PostHedgeTimeBasedForcedCross time_based_cross)
time_based_cross.MarketCrossTicks = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.PostHedgeParams.TimeBasedForcedCross;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PostHedgeTimeBasedForcedCross.MarketCrossTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PostHedgeTimeBasedForcedCross.MarketCrossTicks);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PostHedgeTimeBasedCrossTimeModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Time(ms)";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_time_based_cross(leg_tup) is PostHedgeTimeBasedForcedCross time_based_cross)
time_based_cross.TimeInMsThreshold = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.PostHedgeParams.TimeBasedForcedCross;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PostHedgeTimeBasedForcedCross.TimeInMsThreshold);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PostHedgeTimeBasedForcedCross.TimeInMsThreshold);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PreHedgePaylessThresholdModel : RowModelBase<QuantityMeasure> {
IDictionary<uint, QuantityMeasure> _value_store = new Dictionary<uint, QuantityMeasure>();
IDictionary<int, QuantityMeasure> _value_store = new Dictionary<int, QuantityMeasure>();
public override string display_name { get; } = "Threshold";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, QuantityMeasure value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_payless_ticks(leg_tup) is PreHedgeQuantityBasedPayLessTicks payless_ticks)
payless_ticks.QuantityThreshold = value;
}
@@ -36,7 +36,7 @@ namespace mbts.spark {
leg_tup.leg_params.PreHedgeParams.QuantityBasedPaylessTicks;
QuantityMeasure _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayLessTicks.QuantityThreshold);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayLessTicks.QuantityThreshold);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PreHedgePaylessTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Payless Ticks";
public override string help_text { get; } = string.Empty;
@@ -23,7 +23,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_payless_ticks(leg_tup) is PreHedgeQuantityBasedPayLessTicks payless_ticks)
payless_ticks.PaylessTicks = value;
}
@@ -35,7 +35,7 @@ namespace mbts.spark {
leg_tup.leg_params.PreHedgeParams.QuantityBasedPaylessTicks;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayLessTicks.PaylessTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayLessTicks.PaylessTicks);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PreHedgePayupThresholdModel : RowModelBase<QuantityMeasure> {
IDictionary<uint, QuantityMeasure> _value_store = new Dictionary<uint, QuantityMeasure>();
IDictionary<int, QuantityMeasure> _value_store = new Dictionary<int, QuantityMeasure>();
public override string display_name { get; } = "Threshold";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, QuantityMeasure value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_payup_ticks(leg_tup) is PreHedgeQuantityBasedPayupTicks payup_ticks)
payup_ticks.QuantityThreshold = value;
}
@@ -36,7 +36,7 @@ namespace mbts.spark {
leg_tup.leg_params.PreHedgeParams.QuantityBasedPayupTicks;
QuantityMeasure _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayUpTicks.QuantityThreshold);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayUpTicks.QuantityThreshold);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class PreHedgePayupTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Payup Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_payup_ticks(leg_tup) is PreHedgeQuantityBasedPayupTicks payup_ticks)
payup_ticks.PayupTicks = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.PreHedgeParams.QuantityBasedPayupTicks;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayUpTicks.PayupTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.PreHedgeQuantityBasedPayUpTicks.PayupTicks);
}
}
}
@@ -9,7 +9,7 @@ namespace mbts.spark {
{
class ConditionalQuotingPriceTypeModel : RowModelBase<PriceType>
{
IDictionary<uint, PriceType> _value_store = new Dictionary<uint, PriceType>();
IDictionary<int, PriceType> _value_store = new Dictionary<int, PriceType>();
public override string display_name { get; } = "Price Type";
public override string help_text { get; } = string.Empty;
@@ -26,7 +26,7 @@ namespace mbts.spark {
public override void set_value(LegSpreaderParamsTuple leg_tup, PriceType value)
{
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_value(leg_tup) is ConditionalQuoting cond)
cond.PriceType = value;
}
@@ -37,7 +37,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.ConditionalQuoting;
PriceType _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.ConditionalQuoting.PriceType);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.ConditionalQuoting.PriceType);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class ConditionalQuotingTicksModel : RowModelBase<int> {
IDictionary<uint, int> _value_store = new Dictionary<uint, int>();
IDictionary<int, int> _value_store = new Dictionary<int, int>();
public override string display_name { get; } = "Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, int value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_value(leg_tup) is ConditionalQuoting cond)
cond.NTicks = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.ConditionalQuoting;
int _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.ConditionalQuoting.NTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.ConditionalQuoting.NTicks);
}
}
}
@@ -6,7 +6,7 @@ using System.Windows;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteHiddenModel : RowModelBase<bool>, IHaveFontParams {
IDictionary<uint, bool> _value_store = new Dictionary<uint, bool>();
IDictionary<int, bool> _value_store = new Dictionary<int, bool>();
public override string display_name { get; } = "Hidden";
public override string help_text { get; } = "A hidden quote is not sent out until the market crosses the computed quoting price. When that happens, the quote is sent out as an IOC order. This continues until the synthetic spreader order is fully filled, or cancelled.";
@@ -21,7 +21,7 @@ namespace mbts.spark {
_get_throttle(leg_tup) != null;
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
leg_tup.leg_params.QuoteParams.QuoteHidden = value ? new QuoteHidden() : null;
}
@@ -32,7 +32,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.QuoteHidden;
bool _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => false);
_value_store.GetValueOrDefault(leg_tup.id, _ => false);
}
}
}
@@ -5,7 +5,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteInsideMarketDistanceTowardModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Toward Distance";
public override string help_text { get; } = string.Empty;
@@ -21,7 +21,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_inside_market_distance(leg_tup) is QuoteInsideMarketDistanceThrottle inside_market_distance)
inside_market_distance.OnTowardDistance = value;
}
@@ -33,7 +33,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey);
_value_store.GetValueOrDefault(leg_tup.id);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteInsideMarketThrottleMinMoveModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Min Move";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_inside_market_throttle(leg_tup) is QuoteInsideMarketDistanceWithTravel inside_market_throttle)
inside_market_throttle.Limit = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceWithTravel;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.QuoteInsideMarketDistanceWithTravel.Limit);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.QuoteInsideMarketDistanceWithTravel.Limit);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteInsideMarketThrottleTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_inside_market_throttle(leg_tup) is QuoteInsideMarketDistanceWithTravel inside_market_throttle)
inside_market_throttle.TicksFromInside = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceWithTravel;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.QuoteInsideMarketDistanceWithTravel.TicksFromInside);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.QuoteInsideMarketDistanceWithTravel.TicksFromInside);
}
}
}
@@ -9,7 +9,7 @@ namespace mbts.spark
{
class QuoteQueueHolderModel : RowModelBase<uint>, IHaveFontParams
{
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public QuoteQueueHolderModel()
{
}
@@ -27,7 +27,7 @@ namespace mbts.spark
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value)
{
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
leg_tup.leg_params.QuoteParams.NQueueHolderOrders = value ;
}
@@ -38,7 +38,7 @@ namespace mbts.spark
leg_tup.leg_params.QuoteParams;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => 0u);
_value_store.GetValueOrDefault(leg_tup.id, _ => 0u);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
internal class ShowQuantityQuantityModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name => "Quantity";
@@ -24,7 +24,7 @@ namespace mbts.spark {
_get_value(leg_tup) is NullableQuantity show_quantity ? show_quantity.Quantity : _value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_value(leg_tup) is NullableQuantity show_quantity) {
show_quantity.Quantity = value;
}
@@ -37,7 +37,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.ShowQuantity;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.ShowQuantity.Quantity);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.ShowQuantity.Quantity);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteThrottleInsideTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Inside Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_throttle(leg_tup) is QuoteMuteZone throttle)
throttle.InsideTicks = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.QuoteMuteZone;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.QuoteMuteZone.InsideTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.QuoteMuteZone.InsideTicks);
}
}
}
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace mbts.spark {
partial class SyntheticEditorVM {
class QuoteThrottleOutsideTicksModel : RowModelBase<uint> {
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
IDictionary<int, uint> _value_store = new Dictionary<int, uint>();
public override string display_name { get; } = "Outside Ticks";
public override string help_text { get; } = string.Empty;
@@ -22,7 +22,7 @@ namespace mbts.spark {
_value_from_store(leg_tup);
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
_value_store[leg_tup.SecurityKey] = value;
_value_store[leg_tup.id] = value;
if (_get_throttle(leg_tup) is QuoteMuteZone throttle)
throttle.OutsideTicks = value;
}
@@ -34,7 +34,7 @@ namespace mbts.spark {
leg_tup.leg_params.QuoteParams.QuoteMuteZone;
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
_value_store.GetValueOrDefault(leg_tup.SecurityKey, _ => SyntheticDataHelper.Defaults.QuoteMuteZone.OutsideTicks);
_value_store.GetValueOrDefault(leg_tup.id, _ => SyntheticDataHelper.Defaults.QuoteMuteZone.OutsideTicks);
}
}
}
@@ -42,8 +42,8 @@ namespace mbts.spark {
}
internal abstract class RowModelBase<T> : ViewModelBase, IRowModel {
readonly Dictionary<uint, ReplaySubject<bool>> _enabled_store = new Dictionary<uint, ReplaySubject<bool>>();
readonly Dictionary<uint, Subject<T>> _override_value_store = new Dictionary<uint, Subject<T>>();
readonly Dictionary<LegSpreaderParamsTuple, ReplaySubject<bool>> _enabled_store = new Dictionary<LegSpreaderParamsTuple, ReplaySubject<bool>>();
readonly Dictionary<LegSpreaderParamsTuple, Subject<T>> _override_value_store = new Dictionary<LegSpreaderParamsTuple, Subject<T>>();
protected RowModelBase() { }
@@ -59,22 +59,22 @@ namespace mbts.spark {
public virtual ICell create_cell(LegSpreaderParamsTuple leg_tup) => new Cell<T>(this, leg_tup);
public void set_enabled(LegSpreaderParamsTuple leg_tup, bool enabled) =>
_enabled_store.GetOrAdd(leg_tup.SecurityKey, sec => new ReplaySubject<bool>(1)).OnNext(enabled);
_enabled_store.GetOrAdd(leg_tup, sec => new ReplaySubject<bool>(1)).OnNext(enabled);
public void override_value(LegSpreaderParamsTuple leg_tup, T value) {
if (_override_value_store.TryGetValue(leg_tup.SecurityKey, out Subject<T> subject))
if (_override_value_store.TryGetValue(leg_tup, out Subject<T> subject))
subject.OnNext(value);
}
public IObservable<bool> observe_enabled(LegSpreaderParamsTuple leg_tup) =>
_enabled_store.GetOrAdd(leg_tup.leg.Security.Id, sec => {
_enabled_store.GetOrAdd(leg_tup, sec => {
var rs = new ReplaySubject<bool>(1);
rs.OnNext(_get_enabled(leg_tup));
return rs;
});
public IObservable<T> observe_override_value(LegSpreaderParamsTuple leg_tup) =>
_override_value_store.GetOrAdd(leg_tup.leg.Security.Id, sec => new Subject<T>());
_override_value_store.GetOrAdd(leg_tup, sec => new Subject<T>());
protected virtual bool _get_enabled(LegSpreaderParamsTuple leg_tup) => true;
#region IRowModel (explicit)
+17 -13
View File
@@ -1,6 +1,4 @@
using bts.api;
using mbts.collections;
using mbts.ui.controls;
using mbts.ui.controls;
using mbts.ui.spark;
using mbts.ui.utility;
using System;
@@ -18,8 +16,8 @@ namespace mbts.spark {
ParameterSection[] section_path { get; }
}
internal class RowVM : DataGridExRow<Security>, IParameterRowVM, IDisposable {
readonly ObservableKeyedCollection<KeyValuePair<uint, (ICell cell, LegSpreaderParamsTuple leg)>, uint> _cells = new ObservableKeyedCollection<KeyValuePair<uint, (ICell cell, LegSpreaderParamsTuple leg)>, uint>(kv => kv.Key);
internal class RowVM : DataGridExRow<LegSpreaderParamsTuple>, IParameterRowVM, IDisposable {
readonly ObservableCollection<(LegSpreaderParamsTuple leg, ICell cell)> _cells = new ObservableCollection<(LegSpreaderParamsTuple leg, ICell cell)>();
readonly Property<bool> _has_errors;
IDisposable _dispose;
@@ -34,15 +32,16 @@ namespace mbts.spark {
_has_errors = backing(nameof(has_errors), false);
_dispose = Disposable.Create(
ObservableAutoWrapper.connect(legs, _cells, lt => new KeyValuePair<uint, (ICell cell, LegSpreaderParamsTuple leg)>(lt.SecurityKey, (model.create_cell(lt), lt))),
ObservableAutoWrapper.connect_item_hooks(_cells, kv => {
kv.Value.cell.value_changed += raise_cell_value_changed;
ObservableAutoWrapper.connect(legs, _cells, lt => (lt, model.create_cell(lt))),
ObservableAutoWrapper.connect_item_hooks(_cells, tup => {
var (leg, cell) = tup;
cell.value_changed += raise_cell_value_changed;
return new CompositeDisposable {
Disposable.Create(() => kv.Value.cell.value_changed -= raise_cell_value_changed),
kv.Value.cell as IDisposable ?? Disposable.empty
Disposable.Create(() => cell.value_changed -= raise_cell_value_changed),
cell as IDisposable ?? Disposable.empty
};
void raise_cell_value_changed() =>
cell_value_changed?.Invoke(kv.Value.cell, kv.Value.leg);
cell_value_changed?.Invoke(cell, leg);
}, true));
}
@@ -56,8 +55,13 @@ namespace mbts.spark {
public bool read_only { get; }
public bool has_errors => _has_errors.Value;
public override object get_cell(Security col_key) =>
_cells.TryGetValue(col_key.Id, out KeyValuePair<uint, (ICell cell, LegSpreaderParamsTuple leg)> kv) ? kv.Value.cell : null;
public override object get_cell(LegSpreaderParamsTuple leg_tup) =>
// Note this is an O(n) op.
// If need ever arises to improve perf here,
// an id member could be added to the LegSpreaderParamsTuple,
// which could serve as a sorting key to enable Log(n) lookups.
leg_tup != null &&
_cells.TryGetFirst(t => t.leg == leg_tup, out var match) ? match.cell : null;
public ParameterSection this[string section_no] =>
int.TryParse(section_no, out int i) && i < section_path.Length ? section_path[i] : null;
@@ -88,7 +88,7 @@ namespace mbts.spark {
_close_new_leg_popup();
}
};
remove_leg = new DelegateCommand<Security>(s => leg_tuples.removeFirst(l => l.leg.Security.Id == s.Id));
remove_leg = new DelegateCommand<LegSpreaderParamsTuple>(l => leg_tuples.Remove(l));
commit = new AsyncCommand(commit_async, () => leg_count >= 2);
if (override_auto_set_behavior) {
@@ -112,6 +112,14 @@ namespace mbts.spark {
commit.RaiseCanExecuteChanged();
_validate_leg_count();
};
_resources.Add(ObservableAutoWrapper.connect_item_hooks(leg_tuples, l => {
l.security_changed += on_sec_changed;
return Disposable.Create(() => l.security_changed -= on_sec_changed);
void on_sec_changed() {
if (_set_name_automatically)
_deduce_name();
}
}));
async Task commit_async() {
RequestUpdate?.Invoke();
@@ -130,12 +138,7 @@ namespace mbts.spark {
if (valid) {
_synthetic.Synthetic.Legs.SetItems(leg_tuples.Select(l => l.leg));
_synthetic.Symbol = _deduce_symbol(_synthetic);
double tick_frac = _tick_factors.tick_unit / _tick_factors.tick_base;
_synthetic.ContractSpec.LowTick = tick_frac;
_synthetic.ContractSpec.HighTick = tick_frac;
_synthetic.ContractSpec.PriceBreak = double.MaxValue;
_synthetic.ContractSpec.MainFraction = (uint)MathUtils.factor_out(_tick_factors.tick_base, 10);
_synthetic.ContractSpec.PriceDisplayFormat = (uint)_tick_factors.display_digits();
_synthetic.ContractSpec = _tick_factors.to_spec();
uint? id = await model.create_synthetic(new SecurityImmutable(_synthetic));
if (id.HasValue && id != 0) {
if (await model.save_strategy(id.Value, get_strategy())) {
@@ -377,4 +380,4 @@ namespace mbts.spark {
else clear_error(nameof(leg_count));
}
}
}
}
@@ -38,10 +38,10 @@ namespace mbts.spark {
IWindowSettingsManager<GridSettings> _settings_manager;
int _total_row_count;
public FillsVM(IBus bus, OrdersModel model, IObservable<FillsSettings> state_obs, IWindowSettingsManager<GridSettings> settings_manager)
: this(bus, model, state_obs, _get_observable(bus, model, state_obs), settings_manager) { }
public FillsVM(IBus bus, OrdersModel model, FillsSettings settings, IWindowSettingsManager<GridSettings> settings_manager)
: this(bus, model, settings, _get_observable(bus, model, settings), settings_manager) { }
public FillsVM(IBus bus, OrdersModel model, IObservable<FillsSettings> state_obs, IObservable<TableUpdate<FillTuple, string>> fills_source, IWindowSettingsManager<GridSettings> settings_manager) {
public FillsVM(IBus bus, OrdersModel model, FillsSettings settings, IObservable<TableUpdate<FillTuple, string>> fills_source, IWindowSettingsManager<GridSettings> settings_manager) {
_model = model;
_settings_manager = settings_manager;
/// Columns
@@ -136,8 +136,6 @@ namespace mbts.spark {
// this will initialize the immutable properties that correspond to the col_settings_flags above (e.g. Bid/Ask font_color_override, etc.)
if (_settings_manager != null)
grid_settings_manager.load_from(_settings_manager.current_settings());
var filter_src = Observable.FromEvent<Action<Func<FillTuple, bool>>, Func<FillTuple, bool>>(cb => _rows_manager.filter_changed += cb, cb => _rows_manager.filter_changed -= cb);
var comp_src = Observable.FromEvent<Action<IComparer<FillTuple>>, IComparer<FillTuple>>(cb => _rows_manager.comparer_changed += cb, cb => _rows_manager.comparer_changed -= cb);
TableUpdate<FillTuple, string> last_unfiltered_fills = TableUpdate<FillTuple, string>.mk_empty(FillTuple.key_fn);
var fills_buffer = Observable.Create<TableUpdate<FillTuple, string>>(o => {
// this is what .Cache() does, but is a little cheaper, since we know subscriptions and callbacks occur on the same thread (i.e., on this Dispatcher).
@@ -145,29 +143,36 @@ namespace mbts.spark {
_unfiltered_fills_changed += o.OnNext;
return Disposable.Create(() => _unfiltered_fills_changed -= o.OnNext);
});
if (settings.grid != null && _settings_manager != null) {
// this means we are not using defaults,
// Note: settings_manager in turn notifies grid_settings_manager of this update
_settings_manager.instance_settings = settings.grid;
}
_rows_manager.on_settings(settings.filters ?? new FillsFilterStates {
fill_time = new TimeFilterTuple(
TimeFilterMode.CMEOpen,
TimeTextTuple.FromDateTime(DateTime.Today),
TimeTextTuple.FromDateTime(DateTime.Today.AddDays(1)))
},
settings.sorting_criterion);
// resources/subscriptions
_dispose = new CompositeDisposable(
ObservableAutoWrapper.connect(col_tups, columns, ct => ct.col),
ObservableAutoWrapper.connect_item_hooks(col_tups, ct => ct.filter as IDisposable),
RXCollection.connect(rows, fills_buffer.Filter(filter_src, FillTuple.key_fn), comp_src, ft => new Row(ft, model), r => r.fill_tuple),
RXCollection.connect(
rows,
fills_buffer.Filter(_rows_manager.observe_filter(), FillTuple.key_fn),
_rows_manager.observe_comparer(),
ft => new Row(ft, model),
r => r.fill_tuple,
Row.update,
r => r.is_selected,
(r, is_selected) => r.is_selected = is_selected),
fills_source.OnDispatcher().Subscribe(t => {
last_unfiltered_fills = t;
Set(ref _total_row_count, t.state.Count, nameof(total_row_count));
_unfiltered_fills_changed?.Invoke(t);
}),
state_obs.OnDispatcher().Subscribe(s => {
if (s.grid != null && _settings_manager != null) {
// this means we are not using defaults,
// Note: settings_manager in turn notifies grid_settings_manager of this update
_settings_manager.instance_settings = s.grid;
}
var fs = s.filters ?? new FillsFilterStates {
fill_time = new TimeFilterTuple(
TimeFilterMode.CMEOpen,
TimeTextTuple.FromDateTime(DateTime.Today),
TimeTextTuple.FromDateTime(DateTime.Today.AddDays(1)))
};
_rows_manager.on_settings(fs, s.sorting_criterion);
}),
model.subscribe_all_algos().SelectKeyValue(AlgoDataVM.from_data, d => AlgoID.from_algo_key(d.data.key)).OnDispatcher().Subscribe(root_algo_type_observer),
settings_manager == null ? Disposable.empty : settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager)
);
@@ -262,33 +267,40 @@ namespace mbts.spark {
}
public class Row : SecurityRow, IEquatable<Row>, IRowVM {
public readonly FillTuple fill_tuple;
public static readonly Func<Row, FillTuple, Row> update =
(r, ft) => {
using (_change_detector.detect_changes(r)) {
r._badge_fill = null;
r._root_algo_badge_color = null;
r._order_metadata = null;
r.fill_tuple = ft;
}
return r;
};
static readonly IPropertyChangedDetector<Row> _change_detector =
ObjectSnapshot.build_detector_except<Row>(nameof(fill_key), nameof(is_selected));
readonly OrdersModel _model;
readonly FillImmutable _fill;
readonly OrderRecordImmutable _order;
AlgoData? _root_algo => fill_tuple.root_algo;
AlgoDefinitionMetadata? _root_algo_meta => fill_tuple.root_algo_meta;
Brush _badge_fill;
Color? _root_algo_badge_color;
OrderMetadata _order_metadata;
bool _is_selected;
public Row(FillTuple fill_tuple, OrdersModel model) :
base(fill_tuple.security ?? SecurityDataHelper.Defaults.SecurityImmutable) {
this.fill_tuple = fill_tuple;
_fill = fill_tuple.fill;
_order = fill_tuple.order ?? OrderDataHelper.Defaults.OrderRecordImmutable;
_model = model;
account = fill_tuple.account;
}
public FillTuple fill_tuple { get; private set; }
public DateTime fill_time => _fill.Timestamp.to_local_time();
public TimeInForce time_in_force => _order.time_in_force();
public OrderType order_type => _order.order_type();
public string display_name => OrderUtils.display_name(_order, fill_tuple.algo);
public OrderMetadata order_metadata => _order_metadata ?? (_order_metadata = _model.factory.get_metadata(_order.Def.CustomDef.CustomCase));
public Brush badge_fill => _badge_fill ?? (_badge_fill = OrderUtils.badge_fill(fill_tuple.algo_meta, order_metadata));
public string account { get; private set; }
public string account => fill_tuple.account;
public string user => _order.user();
public Venue exchange => security.Exchange;
public Side side => _fill.Side;
@@ -298,6 +310,10 @@ namespace mbts.spark {
public FillStatus fill_status => _fill.FillStatus;
public string fill_key => _fill.FillKey;
public string order_key => _order.Key.ToString();
public bool is_selected {
get => _is_selected;
set => Set(ref _is_selected, value);
}
// Root Algo
public OrderType root_algo_order_type => _root_algo.HasValue ? _root_algo.Value.value.CustomCase : OrderType.None;
@@ -305,6 +321,11 @@ namespace mbts.spark {
public Color root_algo_badge_color => _root_algo_badge_color ?? (_root_algo_badge_color = OrderUtils.badge_color(_root_algo_meta, root_algo_order_metadata)).Value;
public string root_algo_display_name => fill_tuple.root_order_security?.display_name() ?? OrderType.None.display_name();
FillImmutable _fill => fill_tuple.fill;
OrderRecordImmutable _order => fill_tuple.order ?? OrderDataHelper.Defaults.OrderRecordImmutable;
AlgoData? _root_algo => fill_tuple.root_algo;
AlgoDefinitionMetadata? _root_algo_meta => fill_tuple.root_algo_meta;
public bool Equals(Row other) => other != null && other._fill.Equals(_fill) && other._order.Equals(_order);
public override bool Equals(object obj) => Equals(obj as Row);
public override int GetHashCode() => _fill.FillKey.GetHashCode() ^ _order.Key.GetHashCode();
@@ -313,4 +334,4 @@ namespace mbts.spark {
void IRowVM.raise_invalidated() => invalidated?.Invoke();
}
}
}
}
@@ -9,7 +9,6 @@
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:u="clr-namespace:mbts.ui"
xmlns:api="clr-namespace:bts.api;assembly=spark_api"
xmlns:bts="clr-namespace:mbts.ui.controls"
FocusManager.IsFocusScope="True"
Background="{StaticResource CommonBackground}"
>
@@ -49,10 +48,15 @@
CanExecute="{x:Static c:DIYGrid.DefaultRowActionCanExecute}"
/>
</c:DIYGrid.CommandBindings>
<c:DIYGrid.RowTemplate>
<DataTemplate>
<usp:FillsRowView Background="{Binding side, Converter={StaticResource SideToBrushConverter}}"/>
</DataTemplate>
<c:DIYGrid.RowStyle>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource DIYGridRowStyle}">
<Setter Property="IsSelected" Value="{Binding is_selected}"/>
</Style>
</c:DIYGrid.RowStyle>
<c:DIYGrid.RowTemplate>
<DataTemplate>
<usp:FillsRowView Background="{Binding side, Converter={StaticResource SideToBrushConverter}}"/>
</DataTemplate>
</c:DIYGrid.RowTemplate>
</c:DIYGrid>
<StatusBar Grid.Row="1" DockPanel.Dock="Bottom">
@@ -6,7 +6,6 @@ using bts.spark;
using mbts.persist;
using mbts.ui;
using mbts.ui.settings;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using mbts.utils;
@@ -26,7 +25,6 @@ namespace mbts.spark {
FillsView _view;
FillsSettings _state;
IReadOnlyList<SparkCommand> _commands;
IDisposable _subscriptions;
public FillsWindow(FillsWindowFactory factory, IBus bus, OrdersModel orders_model, FillsSettings state, WindowSettingsManager<GridSettings> settings_manager)
: base("Fills", bus, factory, SparkCommands.ShowFills().icon) {
@@ -36,8 +34,6 @@ namespace mbts.spark {
thread_id = config.Configuration.get<bts.config.MultiThreadedWindows>().fills ? "Fills" : null;
}
event Action<FillsSettings> _state_changed;
public override string window_type => window_type_name;
public override UIElement Content => _view;
public override IEnumerable<SparkCommand> Commands => _commands;
@@ -54,8 +50,7 @@ namespace mbts.spark {
public void thread_init() {
_init_guard.check_access();
Log.notnull(_state);
var state_src = Observable.FromEvent<Action<FillsSettings>, FillsSettings>(cb => _state_changed += cb, cb => _state_changed -= cb).StartWith(_state);
_vm = new FillsVM(_bus, _orders_model, state_src, _settings_manager);
_vm = new FillsVM(_bus, _orders_model, _state, _settings_manager);
_view = new FillsView { DataContext = _vm };
_commands = new List<SparkCommand> {
@@ -77,15 +72,6 @@ namespace mbts.spark {
return dialog;
}))
};
_subscriptions = Disposable.Create(() =>
_state_changed?.Invoke(
_state = new FillsSettings(_state.orders,
_vm.get_sorting_criterion().ToList(),
_vm.get_filters(),
_vm.grid_settings_manager.create()
)
)
);
}
public override void Dispose() {
@@ -93,4 +79,4 @@ namespace mbts.spark {
Commands.OfType<IDisposable>().DisposeAll();
}
}
}
}
@@ -284,6 +284,7 @@ namespace mbts.spark {
favorite_algos = new LadderFavoriteAlgosSettings(null, 2, null);
show_warn_on_cross = _default_show_warn_on_cross;
enforce_max_clip = _default_enforce_max_clip;
status_message_visible = true;
}
public static LadderSettings Default {
@@ -351,6 +352,7 @@ namespace mbts.spark {
mini_ob = new LadderMiniOrderBookSettings(bts.shell.orderbook.OrderBookStartupData.default_mini_grid.general),
max_clip = 0,
price_display_format = PriceDisplayFormat.Default,
status_message_visible = true,
};
rv.row_height = LadderRender.measure_row_height(rv);
return rv;
@@ -519,6 +521,9 @@ namespace mbts.spark {
[ProtoMember(60)]
public PriceDisplayFormat price_display_format { get; set; }
[ProtoMember(61), DefaultValue(true)]
public bool status_message_visible { get; set; }
public void sanitize() =>
sanitize(this);
@@ -623,7 +628,8 @@ namespace mbts.spark {
a.mini_ob == b.mini_ob &&
a.enforce_max_clip == b.enforce_max_clip &&
a.max_clip == b.max_clip &&
a.price_display_format == b.price_display_format;
a.price_display_format == b.price_display_format &&
a.status_message_visible == b.status_message_visible;
public bool Equals(LadderSettings other) =>
profile_equals(this, other) &&
@@ -79,7 +79,8 @@ namespace mbts.spark {
_clear_visibility = new OrderEntryPanelItemVisibilityVM("Clear"),
_cancel_buys_visibility = new OrderEntryPanelItemVisibilityVM("Cancel Buys"),
_cancel_sells_visibility = new OrderEntryPanelItemVisibilityVM("Cancel Sells"),
_cancel_all_visibility = new OrderEntryPanelItemVisibilityVM("Cancel All");
_cancel_all_visibility = new OrderEntryPanelItemVisibilityVM("Cancel All"),
_status_message_visibility = new OrderEntryPanelItemVisibilityVM("Status Message");
public LadderSettingsVM(OrdersModel model, SecurityImmutable security) {
_model = model;
@@ -253,6 +254,7 @@ namespace mbts.spark {
yield return _cancel_buys_visibility;
yield return _cancel_sells_visibility;
yield return _cancel_all_visibility;
yield return _status_message_visibility;
}
}
@@ -296,6 +298,7 @@ namespace mbts.spark {
yield return _cancel_buys_visibility;
yield return _cancel_sells_visibility;
yield return _cancel_all_visibility;
yield return _status_message_visibility;
// other
yield return size_display;
yield return favorite_algos_vm;
@@ -361,6 +364,7 @@ namespace mbts.spark {
_cancel_all_visibility.show = settings.cancel_all_visible;
price_format = settings.net_change;
price_display_format = settings.price_display_format;
_status_message_visibility.show = settings.status_message_visible;
// safety checks
show_warn_on_cross = settings.show_warn_on_cross;
enforce_max_clip = settings.enforce_max_clip;
@@ -438,6 +442,7 @@ namespace mbts.spark {
clone.zoom_slider_visible = _zoom_slider_visbility.show;
clone.net_change = price_format;
clone.price_display_format = price_display_format;
clone.status_message_visible = _status_message_visibility.show;
// safety checks
clone.show_warn_on_cross = show_warn_on_cross;
clone.enforce_max_clip = enforce_max_clip;
@@ -32,7 +32,7 @@ namespace mbts.spark {
readonly Property<bool> _auto_clear;
readonly PropertyAutoWrapper<LadderSettings, bool> _show_day, _show_high, _show_low, _show_open, _show_settle, _show_volume, _show_account,
_show_tif, _show_order_type, _show_size_preset, _show_auto_clear, _show_clear, _show_cancel_buys,
_show_cancel_sells, _show_cancel_all, _show_zoom;
_show_cancel_sells, _show_cancel_all, _show_zoom, _show_status_message;
readonly PropertyAutoWrapper<LadderSettings, Brush> _bid_background, _bid_foreground, _ask_background, _ask_foreground;
readonly HashSet<AlgoKey> _sticky_algos = new HashSet<AlgoKey>();
double
@@ -116,6 +116,7 @@ namespace mbts.spark {
(_show_cancel_sells = bool_prop(nameof(show_cancel_sells), s => s.cancel_sells_visible)).set,
(_show_cancel_all = bool_prop(nameof(show_cancel_all), s => s.cancel_all_visible)).set,
(_show_zoom = bool_prop(nameof(show_zoom), s => s.zoom_slider_visible)).set,
(_show_status_message = bool_prop(nameof(show_status_message), s => s.status_message_visible)).set,
s => {
if (
Set(ref _show_account_position, s.account_position_visible, nameof(show_account_position)) |
@@ -341,6 +342,7 @@ namespace mbts.spark {
public bool show_cancel_sells => _show_cancel_sells.view;
public bool show_cancel_all => _show_cancel_all.view;
public bool show_zoom => _show_zoom.view;
public bool show_status_message => _show_status_message.view;
// left panel scroll props
public double left_panel_view_port_height {
get => _left_panel_view_port_height;
@@ -627,8 +627,8 @@
message to have the topmost Z order
-->
<GridSplitter Grid.Column="0" Grid.RowSpan="2" Width="5" />
<!-- Ladder Status message -->
<Button x:Name="_status" Grid.ColumnSpan="4" Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Command="{Binding status_click_command}">
<!-- Ladder Status message -->
<Button x:Name="_status" Grid.ColumnSpan="4" Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Command="{Binding status_click_command}" Visibility="{Binding show_status_message, Converter={StaticResource BooleanToVisibilityConverter}}">
<Button.Template>
<ControlTemplate>
<TextBlock Text="{Binding status_message}" Background="{Binding status_background}" Padding="5,0" />
@@ -245,7 +245,7 @@ namespace mbts.spark {
.Subscribe(is_visible => {
if (is_visible) {
var init = _cached_fills_settings.to_startup_data();
fills_vm.value = new FillsVM(bus, model, Observable.Return(init), model.on_fill_tuples.Filter(mk_fills_slave_predicate_src(), ft => ft.fill.FillKey, _ => false), _fills_settings_manager);
fills_vm.value = new FillsVM(bus, model, init, model.on_fill_tuples.Filter(mk_fills_slave_predicate_src(), ft => ft.fill.FillKey, _ => false), _fills_settings_manager);
} else if (fills_vm.value != null) {
_cached_fills_settings = _get_fills_settings(fills_vm.value);
fills_vm.value = null;
@@ -383,7 +383,7 @@ namespace bts.shell.pnl {
// hide implicit cols
state.grid = _hide_cols(state.grid, anchors);
return new FillsVM(
_bus, _orders_model, Observable.Return(state), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
_bus, _orders_model, state, new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
}
public FillsVM mk_fills_vm(PNLRowCollectionViewGroup group, ColumnKeys key) {
@@ -393,7 +393,7 @@ namespace bts.shell.pnl {
// hide implicit cols
state.grid = _hide_cols(state.grid, anchors.WrapSubRange(0, group.level));
return new FillsVM(
_bus, _orders_model, Observable.Return(state), _visible_fills_source(), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
_bus, _orders_model, state, _visible_fills_source(), new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
}
public FillsVM mk_fills_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
@@ -419,7 +419,7 @@ namespace bts.shell.pnl {
break;
}
return new FillsVM(
_bus, _orders_model, Observable.Return(state), src, new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
_bus, _orders_model, state, src, new WindowSettingsManager<GridSettings>.DisconnectedImpl(state.grid));
}
public OrderBookVM mk_orderbook_vm(PNLHeaderRowDisplay header, ColumnKeys key) {
+2
View File
@@ -83,6 +83,7 @@ namespace bts.api.orderman {
public bool Equals(OrderSecurityAlgo other) =>
order == other.order &&
piq == other.piq &&
security == other.security &&
algo == other.algo;
@@ -92,6 +93,7 @@ namespace bts.api.orderman {
public override int GetHashCode() =>
EqualityComparer.auto_phase(
order.Key.GetHashCode(),
piq?.GetHashCode() ?? 0,
security.HasValue ? security.Value.Id.GetHashCode() : 0,
algo.GetHashCode());
+6
View File
@@ -148,6 +148,11 @@ namespace mbts.spark {
OrderSecurityRootPair.from_order_security,
OrderSecurityHelper.root_pair_key_fn
).Cache();
// security order groups
security_order_groups = on_orders
.SelectKeyValue(tup => new OrderSecurity(tup), OrderSecurityHelper.key_fn)
.GroupTable(OrderSecurityHelper.key_fn, tup => tup.order.Def.SecurityKey)
.Cache();
// working account orders
var working_account_order_groups = working_orders.GroupTable(t => t.order.Key, t => new PNLKey(t.order.account_id(), t.order.security_key()));
// security account pnl
@@ -278,6 +283,7 @@ namespace mbts.spark {
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
public IObservable<TableUpdate<(SecurityImmutable?, ManualFillImmutable), int>> on_manual_fills_securities { get; }
public IObservable<TableUpdate<ManualFillTuple, int>> on_manual_fill_tuples { get; }
public IObservable<TableUpdate<TableUpdateGrouping<OrderSecurity, uint, uint>, uint>> security_order_groups { get; }
public AdminModel admin_model { get; }
public MBOService mbo_service => _mbo_service;
-1
View File
@@ -9,7 +9,6 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release</Configurations>
<Platforms>x64</Platforms>
<RuntimeIdentifiers>win7-x64</RuntimeIdentifiers>
<PlatformTarget>AnyCPU</PlatformTarget>
<LanguageTargets>$(MSBuildExtensionsPath)\$(VisualStudioVersion)\Bin\Microsoft.CSharp.targets</LanguageTargets>
<ApplicationIcon>logo.ico</ApplicationIcon>
+1 -1
View File
@@ -79,7 +79,7 @@ namespace mbts.spark {
#endregion
public static ITag<uint> SyntheticDeleteTag { get; } = new SparkTag<uint>(nameof(SyntheticDeleteTag));
public static ITag<IImmutableSet<uint>> SyntheticDeleteTag { get; } = new SparkTag<IImmutableSet<uint>>(nameof(SyntheticDeleteTag));
public static ITag<Nothing> ShowGlobalAlertsTag { get; } = new SparkTag<Nothing>(nameof(ShowGlobalAlertsTag));
@@ -28,6 +28,9 @@ namespace mbts.ui.utility {
public static IReadOnlyObservableCollection<TResult> SelectObservable<T, TResult>(this ObservableCollection<T> source, Func<T, TResult> selector) =>
new SelectorWrapper<T, TResult>(source, source, selector);
public static IObservableCollection<TResult> SelectObservable<T, TResult>(this ObservableCollection<T> source, Func<T, TResult> selector, Func<TResult, T> reverse_selector) =>
new SelectorWrapper<T, TResult>(source, source, selector, reverse_selector);
public static IDisposable connect_sorted<T>(ObservableCollection<T> source, ObservableCollection<T> target, IComparer<T> comparer) {
target.sync_with(source.Ordered(comparer));
return source.Subscribe(on_source_collection_changed);
@@ -371,13 +374,15 @@ namespace mbts.ui.utility {
IEnumerator IEnumerable.GetEnumerator() => _source.GetEnumerator();
}
class SelectorWrapper<T, TResult> : IReadOnlyObservableCollection<TResult> {
readonly IReadOnlyList<T> _source_list;
class SelectorWrapper<T, TResult> : IObservableCollection<TResult>, IReadOnlyObservableCollection<TResult>, IList {
readonly IList<T> _source_list;
readonly Func<T, TResult> _selector;
readonly Func<TResult, T> _reverse_selector;
public SelectorWrapper(IReadOnlyList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, TResult> selector) {
public SelectorWrapper(IList<T> source_list, INotifyCollectionChanged source_notifier, Func<T, TResult> selector, Func<TResult, T> reverse_selector = null) {
_source_list = source_list;
_selector = selector;
_reverse_selector = reverse_selector;
source_notifier.CollectionChanged += on_collection_changed;
void on_collection_changed(object sender, NotifyCollectionChangedEventArgs e) {
var cc = CollectionChanged;
@@ -398,17 +403,91 @@ namespace mbts.ui.utility {
selector((T)obj);
}
public TResult this[int index] => _selector(_source_list[index]);
public event NotifyCollectionChangedEventHandler CollectionChanged;
public TResult this[int index] {
get => _selector(_source_list[index]);
set => _source_list[index] = _reverse_selector(value);
}
public int Count => _source_list.Count;
public bool IsReadOnly => _reverse_selector == null;
public bool IsFixedSize => false;
public object SyncRoot => null;
public bool IsSynchronized => false;
public event NotifyCollectionChangedEventHandler CollectionChanged;
object IList.this[int index] {
get => this[index];
set => this[index] = (TResult)value;
}
public void Add(TResult item) =>
_source_list.Add(_reverse_selector(item));
public void Clear() =>
_source_list.Clear();
public bool Contains(TResult item) =>
IndexOf(item) >= 0;
public void CopyTo(TResult[] array, int arrayIndex) {
int stop = _source_list.Count;
for (int i = 0; i < stop; i++)
array[arrayIndex + i] = _selector(_source_list[i]);
}
public IEnumerator<TResult> GetEnumerator() {
foreach (var v in _source_list)
yield return _selector(v);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int IndexOf(TResult item) {
int stop = _source_list.Count;
for (int i = 0; i < stop; i++) {
if (EqualityComparer<TResult>.Default.Equals(_selector(_source_list[i]), item))
return i;
}
return -1;
}
public void Insert(int index, TResult item) =>
_source_list.Insert(index, _reverse_selector(item));
public bool Remove(TResult item) {
int i = IndexOf(item);
if (i >= 0) {
_source_list.RemoveAt(i);
return true;
}
return false;
}
public void RemoveAt(int index) =>
_source_list.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() =>
GetEnumerator();
public int Add(object value) {
Add((TResult)value);
return Count - 1;
}
public bool Contains(object value) =>
Contains((TResult)value);
public int IndexOf(object value) =>
IndexOf((TResult)value);
public void Insert(int index, object value) =>
Insert(index, (TResult)value);
public void Remove(object value) =>
Remove((TResult)value);
public void CopyTo(Array array, int index) {
int stop = _source_list.Count;
for (int i = 0; i < stop; i++)
array.SetValue(_selector(_source_list[i]), index + i);
}
}
}
@@ -34,6 +34,13 @@ namespace mbts {
public static bool IsNaNOrInfinity(double d) => double.IsNaN(d) || double.IsInfinity(d);
public static bool IsNaNOrInfinityOr(double d, params double[] values) => IsNaNOrInfinity(d) || values.Contains(d);
public static bool IsOneOf(double d, params double[] values) => values.Contains(d);
public static bool IsNonZero(this double value) =>
value != 0 && !double.IsNaN(value) && !double.IsInfinity(value);
public static bool IsRealNonZero(this double value) =>
value > 0 && value != double.PositiveInfinity;
public static double CoerceNaN(double value) => CoerceNaN(value, 0.0);
public static double CoerceNaN(double value, double if_nan) => double.IsNaN(value) ? if_nan : value;
public static double CoerceNaNOrInfinity(double value) => CoerceNaNOrInfinity(value, 0.0);
@@ -57,4 +64,4 @@ namespace mbts {
public static double? nan_nullable(double value) =>
double.IsNaN(value) ? (double?)null : value;
}
}
}
@@ -3,6 +3,8 @@ using System;
using System.Globalization;
using bts;
using static bts.utils.MathUtils;
using bts.api;
using bts.utils;
namespace mbts {
static class TickExtensions {
@@ -110,6 +112,21 @@ namespace mbts {
return _round_tick(tuf, val);
}
public static ContractSpecification to_spec(this TickFactors tick) {
if (tick == null)
return null;
double tick_frac = tick.tick_unit_frac();
return new ContractSpecification {
LowTick = tick_frac,
HighTick = tick_frac,
PriceBreak = double.MaxValue,
MainFraction = (uint)factor_out(tick.tick_base, 10),
PriceDisplayFormat = (uint)tick.display_digits(),
PriceMultiplier = 1,
CashPerPoint = 1
};
}
public static bool try_get_tickdiff(this RegionalTickFactors regional_tick, double x, double y, out int n_low, out int n_high) {
if (DoubleHelper.IsNaNOrInfinity(x) || DoubleHelper.IsNaNOrInfinity(y)) {
n_low = default;
+276 -212
View File
@@ -1,212 +1,276 @@
using bts.api.core;
using mbts;
using mbts.ui.shell;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
namespace bts.utils {
interface IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
IDisposable detect_changes(T target);
}
static class ObjectSnapshot {
public static IReadOnlyList<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() =>
PropertyGetterStore<TSource, TValue>.value;
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
_property_table(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table(INotifyPropertyChanged instance, params string[] prop_names) =>
_property_table(instance, Array.ConvertAll(prop_names, instance.GetType().GetProperty));
static IObservable<TableUpdate<(string name, object current, object original), string>> _property_table(INotifyPropertyChanged instance, params PropertyInfo[] props) =>
Observable.Create<TableUpdate<(string name, object current, object original), string>>(o => {
var cache = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
var table = TableUpdate<(string name, object current, object original), string>.mk_empty(t => t.name).with_added(cache.Values.Select(pn => (pn.prop.Name, pn.current, pn.original)));
o.OnNext(table);
instance.PropertyChanged += on_prop_changed;
return Disposable.Create(() => instance.PropertyChanged -= on_prop_changed);
void on_prop_changed(object sender, PropertyChangedEventArgs e) {
if (cache.TryGetValue(e.PropertyName, out PropNode pn)) {
object oldval = pn.current;
pn.current = pn.prop.GetValue(instance);
table = table.with_changes(new[] { new Change<(string, object, object)>((pn.prop.Name, oldval, pn.original), (pn.prop.Name, pn.current, pn.original)) });
o.OnNext(table);
}
}
});
public static IPropertyChangedDetector<T> build_detector<T>(params string[] props) where T : IRaiseNotifyPropertyChanged =>
new DiscretePropertyChangedDetector<T>(props);
public static IPropertyChangedDetector<T> build_detector_except<T>(params string[] except_props) where T : IRaiseNotifyPropertyChanged {
// convert to a hashset for quicker lookups
var exclude = new HashSet<string>(except_props);
return new DiscretePropertyChangedDetector<T>(PropertyInfoStore<T>.all_props.Where(p => !exclude.Contains(p.Name)).Select(p => p.Name).ToArray());
}
public static IObservable<bool> dirty_monitor<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
dirty_monitor(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params string[] prop_names) where T : INotifyPropertyChanged =>
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params PropertyInfo[] props) where T : INotifyPropertyChanged {
var prop_map = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
snapshot = Disposable.Create(() => {
foreach (var pn in prop_map.Values)
pn.prop.SetValue(instance, pn.original);
});
return _dirty_monitor(instance, prop_map);
}
public static IObservable<bool> dirty_monitor<T>(T instance, PropertyInfo[] props) where T : INotifyPropertyChanged =>
_dirty_monitor(instance, props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance))));
static IObservable<bool> _dirty_monitor<T>(T instance, Dictionary<string, PropNode> prop_map) where T : INotifyPropertyChanged =>
Observable.Create<bool>(o => {
var dirty_props = new HashSet<string>();
instance.PropertyChanged += on_prop_changed;
return Disposable.Create(() => instance.PropertyChanged -= on_prop_changed);
void on_prop_changed(object sender, PropertyChangedEventArgs e) {
if (prop_map.TryGetValue(e.PropertyName, out var p)) {
p.current = p.prop.GetValue(instance);
if (Equals(p.current, p.original) == p.is_dirty) {
p.is_dirty = !p.is_dirty;
if (p.is_dirty) {
if (dirty_props.Add(p.prop.Name) && dirty_props.Count == 1)
o.OnNext(true);
} else if (dirty_props.Remove(p.prop.Name) && dirty_props.Count == 0)
o.OnNext(false);
}
}
}
});
public static IDisposable create<T>(T instance) =>
create(instance, PropertyInfoStore<T>.mutable_props);
public static IDisposable create<T>(T instance, PropertyInfo[] props) {
var snapshot = Array.ConvertAll(props, p => p.GetValue(instance));
return Disposable.Create(() => {
for (int i = 0; i < props.Length; i++)
props[i].SetValue(instance, snapshot[i]);
});
}
static Func<T, TValue> _prop_getter<T, TValue>(PropertyInfo prop) =>
(Func<T, TValue>)prop.GetGetMethod().CreateDelegate(typeof(Func<T, TValue>));
static class PropertyInfoStore<T> {
public static readonly Type type = typeof(T);
public static readonly PropertyInfo[]
all_props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance),
mutable_props;
static PropertyInfoStore() =>
mutable_props = all_props.Where(p => p.GetGetMethod() != null).ToArray();
}
// DelegateStore maintains a cache of property getter/setter delegates,
// which perform much better than the corresponding PropertyInfo.GetValue/SetValue.
static class DelegateStore<T> {
public static readonly Type type = typeof(T);
static readonly IReadOnlyDictionary<string, (Func<T, object> get, Action<T, object> set)> _property_delegates;
public static Func<T, object> getter(string prop_name) =>
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.get : null;
public static Action<T, object> setter(string prop_name) =>
_property_delegates.TryGetValue(prop_name, out var dels) ? dels.set : null;
static DelegateStore() {
var prop_dels = new Dictionary<string, (Func<T, object> get, Action<T, object> set)>();
foreach (var p in PropertyInfoStore<T>.all_props) {
Func<T, object> wrapped_getter = null;
Action<T, object> wrapped_setter = null;
var getter = p.GetGetMethod();
var setter = p.GetSetMethod();
if (getter == null && setter == null)
continue;
var del_types = (typeof(Func<,>).MakeGenericType(typeof(T), p.PropertyType), typeof(Action<,>).MakeGenericType(typeof(T), p.PropertyType));
var twrapper = typeof(Wrapper<>).MakeGenericType(typeof(T), p.PropertyType);
if (getter != null) {
wrapped_getter = (Func<T, object>)
twrapper.GetMethod(nameof(Wrapper<T>.getter), BindingFlags.Static | BindingFlags.Public)
.Invoke(null, new[] { getter.CreateDelegate(del_types.Item1) });
}
if (setter != null) {
wrapped_setter = (Action<T, object>)
twrapper.GetMethod(nameof(Wrapper<T>.setter), BindingFlags.Static | BindingFlags.Public)
.Invoke(null, new[] { setter.CreateDelegate(del_types.Item2) });
}
prop_dels[p.Name] = (wrapped_getter, wrapped_setter);
}
_property_delegates = prop_dels;
}
static class Wrapper<TValue> {
public static Func<T, object> getter(Func<T, TValue> getter) => src => getter(src);
public static Action<T, object> setter(Action<T, TValue> setter) => (src, val) => setter(src, (TValue)val);
}
}
static class PropertyGetterStore<TSource, TValue> {
public static readonly IReadOnlyList<Func<TSource, TValue>> value;
static PropertyGetterStore() {
var tval = typeof(TValue);
value = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => tval.IsAssignableFrom(p.PropertyType))
.Select(_prop_getter<TSource, TValue>)
.ToReadOnlyList();
}
}
class PropNode {
public readonly PropertyInfo prop;
public PropNode(PropertyInfo prop, object init) {
this.prop = prop;
original = init;
current = init;
}
public bool is_dirty { get; set; }
public object original { get; set; }
public object current { get; set; }
}
class DiscretePropertyChangedDetector<T> : IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
static readonly Converter<string, (string name, Func<T, object> get)> _realize_prop =
(prop_name) => (prop_name, DelegateStore<T>.getter(prop_name));
readonly (string name, Func<T, object> get)[] _target_props;
public DiscretePropertyChangedDetector()
: this(Array.ConvertAll(PropertyInfoStore<T>.all_props, p => p.Name)) { }
public DiscretePropertyChangedDetector(params string[] props) =>
_target_props = Array.ConvertAll(props, _realize_prop);
public IDisposable detect_changes(T target) {
var get_target_val = new Converter<(string name, Func<T, object> get), object>(tup => tup.get(target));
var orig_state = Array.ConvertAll(_target_props, get_target_val);
return Disposable.Create(() => {
var curr_state = Array.ConvertAll(_target_props, get_target_val);
for (int i = 0; i < curr_state.Length; i++) {
if (!Equals(curr_state[i], orig_state[i]))
target.raise_notify_property_changed(_target_props[i].name);
}
});
}
}
}
}
using bts.api.core;
using mbts;
using mbts.ui.shell;
using mbts.utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
namespace bts.utils {
interface IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
IDisposable detect_changes(T target);
}
static class ObjectSnapshot {
public static IReadOnlyList<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() =>
PropertyGetterStore<TSource, TValue>.value;
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
_property_table(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table(INotifyPropertyChanged instance, params string[] prop_names) =>
_property_table(instance, Array.ConvertAll(prop_names, instance.GetType().GetProperty));
static IObservable<TableUpdate<(string name, object current, object original), string>> _property_table(INotifyPropertyChanged instance, params PropertyInfo[] props) =>
Observable.Create<TableUpdate<(string name, object current, object original), string>>(o => {
var cache = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
var table = TableUpdate<(string name, object current, object original), string>.mk_empty(t => t.name).with_added(cache.Values.Select(pn => (pn.prop.Name, pn.current, pn.original)));
o.OnNext(table);
instance.PropertyChanged += on_prop_changed;
return Disposable.Create(() => instance.PropertyChanged -= on_prop_changed);
void on_prop_changed(object sender, PropertyChangedEventArgs e) {
if (cache.TryGetValue(e.PropertyName, out PropNode pn)) {
object oldval = pn.current;
pn.current = pn.prop.GetValue(instance);
table = table.with_changes(new[] { new Change<(string, object, object)>((pn.prop.Name, oldval, pn.original), (pn.prop.Name, pn.current, pn.original)) });
o.OnNext(table);
}
}
});
public static IPropertyChangedDetector<T> build_detector<T>(params string[] props) where T : IRaiseNotifyPropertyChanged =>
new DiscretePropertyChangedDetector<T>(props);
public static IPropertyChangedDetector<T> build_detector_except<T>(params string[] except_props) where T : IRaiseNotifyPropertyChanged {
// convert to a hashset for quicker lookups
var exclude = new HashSet<string>(except_props);
return new DiscretePropertyChangedDetector<T>(PropertyInfoStore<T>.all_props.Where(p => !exclude.Contains(p.Name)).Select(p => p.Name).ToArray());
}
public static IObservable<bool> dirty_monitor<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
dirty_monitor(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params string[] prop_names) where T : INotifyPropertyChanged =>
dirty_monitor(instance, out snapshot, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
public static IObservable<bool> dirty_monitor<T>(T instance, out IDisposable snapshot, params PropertyInfo[] props) where T : INotifyPropertyChanged {
var prop_map = props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance)));
snapshot = Disposable.Create(() => {
foreach (var pn in prop_map.Values)
pn.prop.SetValue(instance, pn.original);
});
return _dirty_monitor(instance, prop_map);
}
public static IObservable<bool> dirty_monitor<T>(T instance, PropertyInfo[] props) where T : INotifyPropertyChanged =>
_dirty_monitor(instance, props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance))));
static IObservable<bool> _dirty_monitor<T>(T instance, Dictionary<string, PropNode> prop_map) where T : INotifyPropertyChanged =>
Observable.Create<bool>(o => {
var dirty_props = new HashSet<string>();
instance.PropertyChanged += on_prop_changed;
return Disposable.Create(() => instance.PropertyChanged -= on_prop_changed);
void on_prop_changed(object sender, PropertyChangedEventArgs e) {
if (prop_map.TryGetValue(e.PropertyName, out var p)) {
p.current = p.prop.GetValue(instance);
if (Equals(p.current, p.original) == p.is_dirty) {
p.is_dirty = !p.is_dirty;
if (p.is_dirty) {
if (dirty_props.Add(p.prop.Name) && dirty_props.Count == 1)
o.OnNext(true);
} else if (dirty_props.Remove(p.prop.Name) && dirty_props.Count == 0)
o.OnNext(false);
}
}
}
});
public static IDisposable create<T>(T instance) =>
create(instance, PropertyInfoStore<T>.mutable_props);
public static IDisposable create<T>(T instance, PropertyInfo[] props) {
var snapshot = Array.ConvertAll(props, p => p.GetValue(instance));
return Disposable.Create(() => {
for (int i = 0; i < props.Length; i++)
props[i].SetValue(instance, snapshot[i]);
});
}
static Func<T, TValue> _prop_getter<T, TValue>(PropertyInfo prop) =>
(Func<T, TValue>)prop.GetGetMethod().CreateDelegate(typeof(Func<T, TValue>));
interface IPropertyAccessorData<T> {
PropertyInfo prop_info { get; }
Func<T, object> getter { get; }
Action<T, object> setter { get; }
(Action<T> initialize, Func<T, bool> refresh) create_snapshot();
(Action initialize, Func<bool> refresh) create_snapshot(T source);
}
interface IPropertyAccessorData<T, TValue> : IPropertyAccessorData<T> {
new Func<T, TValue> getter { get; }
new Action<T, TValue> setter { get; }
}
static class PropertyInfoStore<T> {
public static readonly Type type = typeof(T);
public static readonly PropertyInfo[]
all_props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance),
mutable_props;
static PropertyInfoStore() =>
mutable_props = all_props.Where(p => p.GetGetMethod() != null).ToArray();
}
// DelegateStore maintains a cache of property getter/setter delegates,
// which perform much better than the corresponding PropertyInfo.GetValue/SetValue.
static class DelegateStore<T> {
public static readonly Type type = typeof(T);
public static readonly IReadOnlyDictionary<string, IPropertyAccessorData<T>> accessor_data;
public static Func<T, object> getter(string prop_name) =>
accessor_data.TryGetValue(prop_name, out var dels) ? dels.getter : null;
public static Action<T, object> setter(string prop_name) =>
accessor_data.TryGetValue(prop_name, out var dels) ? dels.setter : null;
static DelegateStore() {
var prop_dels = new Dictionary<string, IPropertyAccessorData<T>>();
var accessor_type_cache = new Dictionary<Type, Type>();
foreach (var p in PropertyInfoStore<T>.all_props) {
prop_dels[p.Name] = (IPropertyAccessorData<T>)Activator.CreateInstance(
accessor_type_cache.GetOrAdd(p.PropertyType, _ => typeof(PropertyAccessorData<,>).MakeGenericType(typeof(T), p.PropertyType)),
new[] { p });
}
accessor_data = prop_dels;
}
static class Wrapper<TValue> {
public static Func<T, object> getter(Func<T, TValue> getter) => src => getter(src);
public static Action<T, object> setter(Action<T, TValue> setter) => (src, val) => setter(src, (TValue)val);
}
}
static class PropertyGetterStore<TSource, TValue> {
public static readonly IReadOnlyList<Func<TSource, TValue>> value;
static PropertyGetterStore() {
var tval = typeof(TValue);
value = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => tval.IsAssignableFrom(p.PropertyType))
.Select(_prop_getter<TSource, TValue>)
.ToReadOnlyList();
}
}
class PropNode {
public readonly PropertyInfo prop;
public PropNode(PropertyInfo prop, object init) {
this.prop = prop;
original = init;
current = init;
}
public bool is_dirty { get; set; }
public object original { get; set; }
public object current { get; set; }
}
class PropertyAccessorData<T, TValue> : IPropertyAccessorData<T, TValue> {
readonly Func<T, object> _ng_get;
readonly Action<T, object> _ng_set;
public PropertyAccessorData(PropertyInfo prop_info) {
this.prop_info = prop_info;
if (prop_info.PropertyType != typeof(TValue))
throw new ArgumentException($"PropertyType must be equal to TValue (former is {prop_info.PropertyType}, latter is {typeof(TValue)}).", nameof(prop_info));
var get_method = prop_info.GetGetMethod();
var set_method = prop_info.GetSetMethod();
if (get_method != null) {
getter = get_method.CreateDelegate(typeof(Func<T, TValue>)) as Func<T, TValue>;
_ng_get = src => getter(src);
}
if (set_method != null) {
setter = set_method.CreateDelegate(typeof(Action<T, TValue>)) as Action<T, TValue>;
_ng_set = (src, val) => setter(src, (TValue)val);
}
}
public Func<T, TValue> getter { get; }
public Action<T, TValue> setter { get; }
public PropertyInfo prop_info { get; }
Func<T, object> IPropertyAccessorData<T>.getter => _ng_get;
Action<T, object> IPropertyAccessorData<T>.setter => _ng_set;
public (Action<T> initialize, Func<T, bool> refresh) create_snapshot() {
TValue state = default;
return (initialize, refresh);
void initialize(T source) =>
state = getter(source);
bool refresh(T source) {
var old_state = state;
state = getter(source);
return !EqualityComparer<TValue>.Default.Equals(state, old_state);
}
}
public (Action initialize, Func<bool> refresh) create_snapshot(T source) {
TValue state = default;
return (initialize, refresh);
void initialize() =>
state = getter(source);
bool refresh() {
var old_state = state;
state = getter(source);
return !EqualityComparer<TValue>.Default.Equals(state, old_state);
}
}
}
class DiscretePropertyChangedDetector<T> : IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
readonly (string name, Action<T> initialize, Func<T, bool> refresh)[] _property_monitors;
public DiscretePropertyChangedDetector() {
_property_monitors = new (string name, Action<T> initialize, Func<T, bool> refresh)[DelegateStore<T>.accessor_data.Count];
int i = 0;
foreach (var pad in DelegateStore<T>.accessor_data.Values) {
var (initialize, refresh) = pad.create_snapshot();
_property_monitors[i++] = (pad.prop_info.Name, initialize, refresh);
}
}
public DiscretePropertyChangedDetector(params string[] props) {
_property_monitors = new (string name, Action<T> initialize, Func<T, bool> refresh)[props.Length];
int ppos = 0, pm_pos = 0;
while (ppos < props.Length) {
if (DelegateStore<T>.accessor_data.TryGetValue(props[ppos++], out var pad)) {
var (initialize, refresh) = pad.create_snapshot();
_property_monitors[pm_pos++] = (pad.prop_info.Name, initialize, refresh);
}
}
ArrayHelper.trim(ref _property_monitors, _property_monitors.Length - pm_pos);
}
public IDisposable detect_changes(T target) {
for (int i = 0; i < _property_monitors.Length; i++)
_property_monitors[i].initialize(target);
return Disposable.Create(() => {
for (int i = 0; i < _property_monitors.Length; i++) {
if (_property_monitors[i].refresh(target))
target.raise_notify_property_changed(_property_monitors[i].name);
}
});
}
}
}
}
@@ -41,12 +41,17 @@ namespace mbts.ui.shell {
else vm.set_error(property, message);
}
public static IObservable<bool> subscribe_has_errors(this INotifyDataErrorInfo vm) =>
public static IObservable<bool> subscribe_has_errors(this INotifyDataErrorInfo vm, bool start_immediately = false) =>
Observable.Create<bool>(o => {
bool last_value = vm.HasErrors;
if (start_immediately)
o.OnNext(vm.HasErrors);
vm.ErrorsChanged += on_errors_changed;
return Disposable.Create(() => vm.ErrorsChanged -= on_errors_changed);
void on_errors_changed(object sender, DataErrorsChangedEventArgs e) =>
o.OnNext(vm.HasErrors);
void on_errors_changed(object sender, DataErrorsChangedEventArgs e) {
if (vm.HasErrors != last_value)
o.OnNext(last_value = !last_value);
}
});
public static IObservable<T> SubscribeValue<T>(this IProperty<T> property) =>
@@ -881,4 +886,4 @@ namespace mbts.ui.shell {
return Disposable.Create(() => self.ErrorsChanged -= callback);
}
}
}
}
@@ -9,13 +9,18 @@
<SolidColorBrush x:Key="Ellipse.Pressed.Brush" Color="#FF3C77DD"/>
<SolidColorBrush x:Key="Ellipse.Disabled.Brush" Color="Gray"/>
<Style TargetType="c:ExpanderToggleButton" BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="3,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border>
<ControlTemplate TargetType="c:ExpanderToggleButton">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -26,6 +31,7 @@
x:Name="arrow"
Data="M0,0L3.5,3.5 0,7"
HorizontalAlignment="Center"
Margin="{TemplateBinding ArrowPadding}"
SnapsToDevicePixels="False"
Stroke="{StaticResource Path.Static.Brush}"
StrokeThickness="2"
@@ -8,5 +8,13 @@ namespace mbts.ui.controls {
class ExpanderToggleButton : ToggleButton {
static ExpanderToggleButton() =>
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExpanderToggleButton), new FrameworkPropertyMetadata(typeof(ExpanderToggleButton)));
public static readonly DependencyProperty ArrowPaddingProperty =
WPFHelper.create_dp<Thickness, ExpanderToggleButton>(nameof(ArrowPadding));
public Thickness ArrowPadding {
get => (Thickness)GetValue(ArrowPaddingProperty);
set => SetValue(ArrowPaddingProperty, value);
}
}
}
}
@@ -265,6 +265,8 @@ namespace mbts.ui.controls {
}
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) {
if (SelectionMode == SelectionMode.Single)
return;
var pt = e.GetPosition(this);
if (InputHitTest(pt) is UIElement el) {
var sb = new StringBuilderEx("beginning drag selection on ");
@@ -287,7 +289,6 @@ namespace mbts.ui.controls {
_log.info(sb.ToString());
}
}
base.OnPreviewMouseLeftButtonDown(e);
}
protected override void OnPreviewMouseUp(MouseButtonEventArgs e) {
@@ -704,7 +704,7 @@
</Style>
</GroupBox.Style>
<GroupBox.Header>
<c:ExpanderToggleButton x:Name="GridExpanderToggle" IsChecked="True">
<c:ExpanderToggleButton x:Name="GridExpanderToggle" BorderThickness="0" IsChecked="True">
Grid
</c:ExpanderToggleButton>
</GroupBox.Header>
@@ -725,7 +725,7 @@
</GroupBox.Style>
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<c:ExpanderToggleButton x:Name="PredicateAnimationsExpanderToggle" IsChecked="{Binding show_predicate_animations}">
<c:ExpanderToggleButton x:Name="PredicateAnimationsExpanderToggle" BorderThickness="0" IsChecked="{Binding show_predicate_animations}">
Sound/Notification Triggers
</c:ExpanderToggleButton>
<TextBlock Margin="{StaticResource LeftPush}">
@@ -789,7 +789,7 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<c:ExpanderToggleButton x:Name="AnimationsExpanderToggle" IsChecked="{Binding show_animations}" Visibility="{Binding animations.Count, Converter={StaticResource IntNotZeroVisibilityConverter}}">
<c:ExpanderToggleButton x:Name="AnimationsExpanderToggle" BorderThickness="0" IsChecked="{Binding show_animations}" Visibility="{Binding animations.Count, Converter={StaticResource IntNotZeroVisibilityConverter}}">
Animations
</c:ExpanderToggleButton>
<TextBlock FontStyle="Italic" Foreground="Gray" Visibility="{Binding Visibility, ElementName=AnimationsExpanderToggle, Converter={StaticResource ReverseVisibilityConverter}}">
@@ -904,7 +904,7 @@
</Style>
</GroupBox.Style>
<GroupBox.Header>
<c:ExpanderToggleButton x:Name="ColumnsExpanderToggle" IsChecked="True">
<c:ExpanderToggleButton x:Name="ColumnsExpanderToggle" BorderThickness="0" IsChecked="True">
Columns
</c:ExpanderToggleButton>
</GroupBox.Header>
@@ -990,7 +990,7 @@
</GroupBox.Style>
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<c:ExpanderToggleButton x:Name="CellAnimationsExpanderToggle" IsChecked="{Binding show_cell_predicate_animations}">
<c:ExpanderToggleButton x:Name="CellAnimationsExpanderToggle" BorderThickness="0" IsChecked="{Binding show_cell_predicate_animations}">
<TextBlock Text="{Binding current_column.display_name, StringFormat='{}\'{0}\' Cell Animations'}"/>
</c:ExpanderToggleButton>
<TextBlock Margin="{StaticResource LeftPush}">
@@ -1050,7 +1050,7 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<c:ExpanderToggleButton x:Name="CellAnimationsExpanderToggle" IsChecked="{Binding show_animations}" Visibility="{Binding animations.Count, Converter={StaticResource IntNotZeroVisibilityConverter}}">
<c:ExpanderToggleButton x:Name="CellAnimationsExpanderToggle" BorderThickness="0" IsChecked="{Binding show_animations}" Visibility="{Binding animations.Count, Converter={StaticResource IntNotZeroVisibilityConverter}}">
Animations
</c:ExpanderToggleButton>
<TextBlock FontStyle="Italic" Foreground="Gray" Visibility="{Binding Visibility, ElementName=CellAnimationsExpanderToggle, Converter={StaticResource ReverseVisibilityConverter}}">
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
namespace mbts.ui.controls {
static class DIYGridRowsManager {
@@ -23,6 +24,22 @@ namespace mbts.ui.controls {
get_comparer_for_column,
window_name,
build_filter_from_state);
public static IObservable<IComparer<T>> observe_comparer<T, TColKey, TFiltersState>(this DIYGridRowsManager<T, TColKey, TFiltersState> rows_manager) where TColKey : struct =>
Observable.Create<IComparer<T>>(o => {
rows_manager.comparer_changed += o.OnNext;
if (rows_manager.comparer != null)
o.OnNext(rows_manager.comparer);
return Disposable.Create(() => rows_manager.comparer_changed -= o.OnNext);
});
public static IObservable<Func<T, bool>> observe_filter<T, TColKey, TFiltersState>(this DIYGridRowsManager<T, TColKey, TFiltersState> rows_manager) where TColKey : struct =>
Observable.Create<Func<T, bool>>(o => {
rows_manager.filter_changed += o.OnNext;
if (rows_manager.filter != null)
o.OnNext(rows_manager.filter);
return Disposable.Create(() => rows_manager.filter_changed -= o.OnNext);
});
}
class DIYGridRowsManager<T, TColKey, TFiltersState> where TColKey : struct {
@@ -37,20 +54,20 @@ namespace mbts.ui.controls {
_update = new DeferrableActionWrapper(() => {
if (_filters_dirty) {
if (_sorting_dirty) {
var comp = build_comparer(out string comp_desc);
var filter = build_filter(out string filter_desc);
comparer = build_comparer(out string comp_desc);
filter = build_filter(out string filter_desc);
Log.Ui.info($"{window_name} begin filter '{filter_desc}' and sort '{comp_desc}.'");
filter_changed?.Invoke(filter);
comparer_changed?.Invoke(comp);
comparer_changed?.Invoke(comparer);
} else {
var filter = build_filter(out string filter_desc);
filter = build_filter(out string filter_desc);
Log.Ui.info($"{window_name} begin filter '{filter_desc}.'");
filter_changed?.Invoke(filter);
}
} else if (_sorting_dirty) {
var comp = build_comparer(out string comp_desc);
comparer = build_comparer(out string comp_desc);
Log.Ui.info($"{window_name} begin sort '{comp_desc}.'");
comparer_changed?.Invoke(comp);
comparer_changed?.Invoke(comparer);
}
_sorting_dirty = false;
_filters_dirty = false;
@@ -93,6 +110,9 @@ namespace mbts.ui.controls {
public event Action<Func<T, bool>> filter_changed;
public event Action<IComparer<T>> comparer_changed;
public IComparer<T> comparer { get; private set; }
public Func<T, bool> filter { get; private set; }
public TFiltersState get_filters() => _filters.create();
public IEnumerable<SortingCriteria> get_sorting_criterion() => _sorting.create();
@@ -23,6 +23,18 @@ namespace mbts.ui.controls {
public event RoutedEventHandler Opened { add => AddHandler(OpenedEvent, value); remove => RemoveHandler(OpenedEvent, value); }
public event RoutedEventHandler Closed { add => AddHandler(ClosedEvent, value); remove => RemoveHandler(ClosedEvent, value); }
#region ButtonStyle
public static readonly DependencyProperty ButtonStyleProperty =
WPFHelper.create_dp<Style, PopupControl>(
nameof(ButtonStyle),
(o, v) => WPFHelper.apply_style(ref o._button, v));
public Style ButtonStyle {
get => GetValue(ButtonStyleProperty) as Style;
set => SetValue(ButtonStyleProperty, value);
}
#endregion
#region PopupPlacement
public static readonly DependencyProperty PopupPlacementProperty = DependencyProperty.Register("PopupPlacement", typeof(PlacementMode), typeof(PopupControl), new PropertyMetadata(PlacementMode.Right, OnPopupPlacementChanged));
private static void OnPopupPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.SetValue(IsOpenTransformPropertyKey, _get_is_open_transform((PlacementMode)e.NewValue)); }
@@ -117,6 +129,7 @@ namespace mbts.ui.controls {
public override void OnApplyTemplate() {
Disposable.dispose(ref _popup_hook);
_button = (Button)GetTemplateChild("button");
WPFHelper.apply_style(ref _button, ButtonStyle);
_popup = (Popup)GetTemplateChild("popup");
_popup.HorizontalOffset = HorizontalOffset;
_popup.VerticalOffset = VerticalOffset;
@@ -24,6 +24,7 @@
<ComboBox
MinWidth="48"
IsEditable="True"
IsTextSearchEnabled="False"
DisplayMemberPath="value"
SelectedValuePath="value"
ItemsSource="{Binding TickUnitPresetsView, RelativeSource={RelativeSource TemplatedParent}}"
@@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace mbts.ui.controls {
@@ -19,6 +20,7 @@ namespace mbts.ui.controls {
// TextBox
TextBox _text_box;
IValueConverter _display_converter;
IDisposable _text_hooks = null;
// TickUpDown
LazyPropagator<TickUpDown, TickUpDownDouble> _explicit_props = new LazyPropagator<TickUpDown, TickUpDownDouble>((t, v) => {
@@ -64,12 +66,20 @@ namespace mbts.ui.controls {
public static readonly DependencyProperty MultiplierProperty = DependencyProperty.Register("Multiplier", typeof(double), typeof(TickUpDownDouble), new FrameworkPropertyMetadata(1.0));
public static readonly DependencyProperty FormatProperty = DependencyProperty.Register("Format", typeof(string), typeof(TickUpDownDouble));
public static readonly DependencyProperty TickDefaultValueProperty = DependencyProperty.Register(nameof(TickDefaultValue), typeof(double), typeof(TickUpDownDouble), new PropertyMetadata(0d));
public static readonly DependencyProperty DisplayConverterProperty = WPFHelper.create_dp<IValueConverter, TickUpDownDouble>(
nameof(DisplayConverter),
(o, v) => {
o._display_converter = v;
if (o._text_box != null)
o._text_box.Text = _format_value(o.Value, o.Format, v);
});
public double Value { get { return (double)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }
public double Increment { get { return (double)GetValue(IncrementProperty); } set { SetValue(IncrementProperty, value); } }
public double Multiplier { get { return (double)GetValue(MultiplierProperty); } set { SetValue(MultiplierProperty, value); } }
public string Format { get { return GetValue(FormatProperty) as string; } set { SetValue(FormatProperty, value); } }
public double TickDefaultValue { get => (double)GetValue(TickDefaultValueProperty); set => SetValue(TickDefaultValueProperty, value); }
public IValueConverter DisplayConverter { get => GetValue(DisplayConverterProperty) as IValueConverter; set => SetValue(DisplayConverterProperty, value); }
// explicit props
public bool EnableArrowKeyTick { get => _enable_arrow_key_tick.value; set => _enable_arrow_key_tick.value = value; }
public ModifierKeys ArrowKeyTickModifiers { get => _arrow_key_tick_modifiers.value; set => _arrow_key_tick_modifiers.value = value; }
@@ -84,7 +94,7 @@ namespace mbts.ui.controls {
_tick_up_down.TickDownCommand = new DelegateCommand(_tick_down);
// TextBox
_text_box = this.assert_template_child<TextBox>("PART_TextBox");
_text_box.Text = _format_value(Value, Format);
_text_box.Text = _format_value(Value, Format, _display_converter);
if (_text_box.InputBindings.Count == 0) {
// this is a workaround to get KeyBindings to work since the TextBox blocks them while it has keyboard focus.
// only do this if count == 0, to avoid interfering with keybindings explicitly set in a custom ControlTemplate.
@@ -93,7 +103,9 @@ namespace mbts.ui.controls {
push_text_to_value();
_text_hooks = _text_box.SubscribeTextChanged((s, e) => push_text_to_value());
void push_text_to_value() {
if (double.TryParse(_text_box.Text, out double d)) {
if (_display_converter != null && _display_converter.ConvertBack(_text_box.Text, typeof(double), null, CultureInfo.CurrentCulture) is double d) {
SetCurrentValue(ValueProperty, d);
} else if (double.TryParse(_text_box.Text, out d)) {
SetCurrentValue(ValueProperty, d);
} else {
SetCurrentValue(ValueProperty, double.NaN);
@@ -113,7 +125,10 @@ namespace mbts.ui.controls {
} else Value = TickDefaultValue;
}
static string _format_value(double value, string format) =>
format == null ? value.ToString() : value.ToString(format, CultureInfo.CurrentCulture);
// use display_converter if non-null, otherwise use format
static string _format_value(double value, string format, IValueConverter display_converter = null) =>
display_converter != null ?
display_converter.Convert(value, typeof(string), null, CultureInfo.CurrentCulture)?.ToString() :
(format == null ? value.ToString() : value.ToString(format, CultureInfo.CurrentCulture));
}
}
@@ -11,6 +11,8 @@ namespace mbts.ui.converters
private Visibility _visfalse = Visibility.Collapsed;
public Visibility VisibilityForFalse { get { return _visfalse; } set { _visfalse = value; } }
public static BooleanToVisibilityConverter Instance => new BooleanToVisibilityConverter();
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
bool flag = false;
if (value is bool) {
@@ -0,0 +1,56 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace mbts.ui.converters {
[ValueConversion(typeof(SizeToContent), typeof(double))]
abstract class WindowSizeThresholdConverter : IValueConverter {
readonly SizeToContent _size_to_content_flag;
protected WindowSizeThresholdConverter(SizeToContent size_to_content_flag) =>
_size_to_content_flag = size_to_content_flag;
public double SizeToContentThreshold { get; set; } = double.NaN;
public double Threshold { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is SizeToContent stc) {
if ((stc & _size_to_content_flag) == _size_to_content_flag) {
if (double.IsNaN(SizeToContentThreshold)) {
if (parameter is double d)
return d;
} else {
return SizeToContentThreshold;
}
}
}
return Threshold;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
[ValueConversion(typeof(SizeToContent), typeof(double))]
sealed class WindowMinWidthConverter : WindowSizeThresholdConverter {
public WindowMinWidthConverter() : base(SizeToContent.Width) { }
}
[ValueConversion(typeof(SizeToContent), typeof(double))]
sealed class WindowMaxWidthConverter : WindowSizeThresholdConverter {
public WindowMaxWidthConverter() : base(SizeToContent.Width) =>
Threshold = double.PositiveInfinity;
}
[ValueConversion(typeof(SizeToContent), typeof(double))]
sealed class WindowMinHeightConverter : WindowSizeThresholdConverter {
public WindowMinHeightConverter() : base(SizeToContent.Height) { }
}
[ValueConversion(typeof(SizeToContent), typeof(double))]
sealed class WindowMaxHeightConverter : WindowSizeThresholdConverter {
public WindowMaxHeightConverter() : base(SizeToContent.Height) =>
Threshold = double.PositiveInfinity;
}
}
@@ -1,18 +1,23 @@
using mbts.config;
using mbts.ui.controls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows;
using System.Windows.Forms;
namespace mbts.ui.dpi {
public static class HighDpiExtensions {
public static readonly double dpi_ratio;
public static readonly double
dpi_ratio,
pixel_unit;
public static readonly float fdpi_ratio;
static HighDpiExtensions() {
dpi_ratio = Configuration.get<bts.config.System>().dpi_ratio;
pixel_unit = 1.0 / dpi_ratio;
fdpi_ratio = (float)dpi_ratio;
}
@@ -119,5 +124,22 @@ namespace mbts.ui.dpi {
}
}
#endregion
public static double RoundLayoutValue(this double value) =>
RoundLayoutValue(value, dpi_ratio);
public static double RoundLayoutValue(double value, double dpi_scale) {
if (DoubleUtil.AreClose(dpi_scale, 1.0))
return Math.Round(value);
double result = Math.Round(value * dpi_scale) / dpi_scale;
return DoubleHelper.IsNaNOrInfinity(result) || DoubleUtil.AreClose(result, double.MaxValue) ? value : result;
}
public static Thickness RoundToLayout(this Thickness thickness) =>
new Thickness(
thickness.Left.RoundLayoutValue(),
thickness.Top.RoundLayoutValue(),
thickness.Right.RoundLayoutValue(),
thickness.Bottom.RoundLayoutValue());
}
}
@@ -0,0 +1,13 @@
using mbts.ui.dpi;
namespace mbts.ui {
class DPIDoubleExtension : DoubleExtension {
public DPIDoubleExtension()
: base() =>
RoundingUnit = HighDpiExtensions.dpi_ratio;
public DPIDoubleExtension(double value)
: base(value) =>
RoundingUnit = HighDpiExtensions.dpi_ratio;
}
}
@@ -2,11 +2,9 @@
using System.ComponentModel;
using System.Windows.Markup;
namespace mbts.ui
{
namespace mbts.ui {
[MarkupExtensionReturnType(typeof(double))]
public class DoubleExtension : MarkupExtension
{
class DoubleExtension : MarkupExtension {
public DoubleExtension() : this(Double.NaN) { }
public DoubleExtension(double Value) {
@@ -17,9 +15,11 @@ namespace mbts.ui
[TypeConverter(typeof(DoubleStringTypeConverter))]
public double Value { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider) {
return Value;
}
[TypeConverter(typeof(DoubleStringTypeConverter))]
public double RoundingUnit { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider) =>
RoundingUnit.IsRealNonZero() ? Math.Round(Value * RoundingUnit) / RoundingUnit : Value;
}
internal class DoubleStringTypeConverter : TypeConverter
@@ -1,4 +1,5 @@
using mbts.utils;
using bts.utils;
using mbts.utils;
using System;
using System.Windows;
using System.Windows.Controls;
@@ -199,5 +200,44 @@ namespace mbts.ui {
public static bool GetFocusOnLoaded(DependencyObject d) => (bool)d.GetValue(FocusOnLoadedProperty);
public static void SetFocusOnLoaded(DependencyObject d, bool value) => d.SetValue(FocusOnLoadedProperty, value);
#endregion
#region MirrorLayout
static readonly DependencyProperty[] _layout_props = new[] {
FrameworkElement.HeightProperty,
FrameworkElement.WidthProperty,
FrameworkElement.MarginProperty,
Control.PaddingProperty,
FrameworkElement.HorizontalAlignmentProperty,
Control.HorizontalContentAlignmentProperty,
FrameworkElement.VerticalAlignmentProperty,
Control.VerticalContentAlignmentProperty
};
public static readonly DependencyProperty MirrorLayoutForProperty =
DependencyProperty.RegisterAttached("MirrorLayoutFor", typeof(FrameworkElement), typeof(Behavior),
new PropertyMetadata(OnMirrorLayoutForChanged));
static void OnMirrorLayoutForChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var target = new LazyCast<FrameworkElement, DependencyObject>(d);
if (e.OldValue is FrameworkElement old_src) {
target.value.clear_bindings(_layout_props);
}
if (e.NewValue is FrameworkElement new_src) {
target.value.bind(FrameworkElement.WidthProperty, new_src, FrameworkElement.ActualWidthProperty);
target.value.bind(FrameworkElement.HeightProperty, new_src, FrameworkElement.ActualHeightProperty);
target.value.bind(FrameworkElement.MarginProperty, new_src, FrameworkElement.MarginProperty);
target.value.bind(FrameworkElement.HorizontalAlignmentProperty, new_src, FrameworkElement.HorizontalAlignmentProperty);
target.value.bind(FrameworkElement.VerticalAlignmentProperty, new_src, FrameworkElement.VerticalAlignmentProperty);
if (target.value is Control && new_src is Control) {
target.value.bind(Control.PaddingProperty, new_src, Control.PaddingProperty);
target.value.bind(Control.HorizontalContentAlignmentProperty, new_src, Control.HorizontalContentAlignmentProperty);
target.value.bind(Control.VerticalContentAlignmentProperty, new_src, Control.VerticalContentAlignmentProperty);
}
}
}
public static FrameworkElement GetMirrorLayoutFor(DependencyObject d) => d.GetValue(MirrorLayoutForProperty) as FrameworkElement;
public static void SetMirrorLayoutFor(DependencyObject d, FrameworkElement value) => d.SetValue(MirrorLayoutForProperty, value);
#endregion
}
}
}
@@ -241,7 +241,13 @@ namespace mbts.spark {
public ObservableCollection<string> symbols { get; } = new ObservableCollection<string>();
public List<string> selected_symbols = new List<string>();
private IReadOnlyDictionary<uint, SecurityMD> all_securities = new Dictionary<uint, SecurityMD>();
IReadOnlyDictionary<uint, SecurityMD> _all_securities = new Dictionary<uint, SecurityMD>();
public IReadOnlyDictionary<uint, SecurityMD> all_securities {
get => _all_securities;
set => Set(ref _all_securities, value);
}
public List<uint> selected_securities = new List<uint>();
public ObservableCollection<SecurityImmutable> securities { get; } = new ObservableCollection<SecurityImmutable>();
@@ -461,4 +467,4 @@ namespace mbts.spark {
}
}
}
@@ -104,6 +104,9 @@ namespace mbts.spark {
}
}
public static RegionalTickFactors get_tick_factors(ContractSpecificationImmutable spec) =>
TickStore.get(spec);
public static double tickup(this ContractSpecificationImmutable spec, double value) =>
TickStore.get(spec).tickup(value);
@@ -11,6 +11,22 @@ namespace mbts.spark {
public static string strip_name(this SecurityImmutable security) => security.HasIce ? security.Ice.StripName : string.Empty;
public static string mic_code(this SecurityImmutable security) => security.HasIce ? security.Ice.MicCode : string.Empty;
public static bool is_exchange_security(this SecurityImmutable? security) =>
security.HasValue && is_exchange_security(security.Value);
public static bool is_exchange_security(this SecurityImmutable security) {
switch (security.Exchange) {
case Venue.Bts:
case Venue.Unset: return false;
}
switch (security.ContractCase) {
case Security.ContractOneofCase.Future:
case Security.ContractOneofCase.Spread:
return true;
default: return false;
}
}
public static class EqualityComparer {
public static readonly IEqualityComparer<SecurityMD> MDComparer = bts.utils.EqualityComparer.Create<SecurityMD>(Equals, GetHashCode);
public static bool Equals(SecurityMD x, SecurityMD y) => x.security.Id == y.security.Id;
@@ -0,0 +1,107 @@
using bts.collections;
using mbts;
using mbts.collections;
using mbts.ui;
using mbts.ui.shell;
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 System.Windows.Data;
using System.Windows.Input;
namespace bts.utils {
/// <summary>
/// <typeparamref name="TConflictUnit"/> will often be the same as <typeparamref name="TRow"/>; they are distinguishable to support
/// cases where the same conflict resolution logic can be applied to more than 1 field (such as multiple contract legs in a synthetic security).
/// </summary>
/// <typeparam name="TConflictUnit">Represents the type of units to which conflict resolutions will be applied.</typeparam>
/// <typeparam name="TRow">Represents the type of unit that is expressed by a line of text.</typeparam>
/// <typeparam name="TComponent">The parameter type identifier (should be an enum).</typeparam>
class ImportConflictsVM<TConflictUnit, TRow, TComponent> : DisposableViewModelBase, IRequestViewClose where TComponent : struct {
readonly RefCountMap<TRow> _conflicts = new RefCountMap<TRow>();
readonly ObservableCollection<RowItem> _row_items = new ObservableCollection<RowItem>();
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
public ImportConflictsVM(int num_synthetics, Func<TConflictUnit, TRow> get_owning_row)
: this(num_synthetics, get_owning_row, EqualityComparer<TRow>.Default) { }
public ImportConflictsVM(int num_rows, Func<TConflictUnit, TRow> get_owning_row, IEqualityComparer<TRow> row_comparer) {
conflicts = _row_items.SelectObservable(ri => ri.value, r => new RowItem(r, get_owning_row, row_comparer));
conflicts_view = CollectionViewSource.GetDefaultView(conflicts);
ok_cmd = new DelegateCommand(() => {
committed?.Invoke();
RequestClose?.Invoke(this, EventArgs.Empty);
});
cancel_cmd = new DelegateCommand(() => RequestClose?.Invoke(this, new EventArgs()));
this.num_rows = num_rows;
_dispose = ObservableAutoWrapper.connect_item_hooks(_row_items, ri =>
ri.value.subscribe_has_error().Subscribe(has_error => {
if ((has_error ? ri.affected_keys.Count(_conflicts.increment_adds_key) : ri.affected_keys.Count(_conflicts.decrement_removes_key)) > 0) {
RaisePropertyChanged(nameof(num_successfully_parsed_rows));
}
}));
}
public event Action committed;
public event EventHandler RequestClose;
public IObservableCollection<ImportConflictRowVM<TConflictUnit, TComponent>> conflicts { get; }
public ICollectionView conflicts_view { get; }
public int num_rows { get; }
public int num_successfully_parsed_rows => num_rows - _conflicts.Count;
// commands
public ICommand ok_cmd { get; }
public ICommand cancel_cmd { get; }
readonly struct RowItem {
readonly Lazy<List<TRow>> _affected_rows;
public RowItem(ImportConflictRowVM<TConflictUnit, TComponent> value, Func<TConflictUnit, TRow> get_owning_row, IEqualityComparer<TRow> row_comparer) {
this.value = value;
_affected_rows = new Lazy<List<TRow>>(() => value.affected_parts.Select(get_owning_row).Distinct(row_comparer).ToList());
}
public ImportConflictRowVM<TConflictUnit, TComponent> value { get; }
public List<TRow> affected_keys => _affected_rows.Value;
}
}
class ImportConflictRowVM<T, TComponent> : ViewModelBase, ITemplateSelectorKey where TComponent : struct {
readonly DelegateProperty<bool> _has_error;
object _value;
string _text;
public ImportConflictRowVM(TComponent component, string text, IReadOnlyCollection<T> affected_parts, object error_value, object value, int num_affected_rows) {
this.component = component;
this.text = text;
this.affected_parts = affected_parts;
this.num_affected_rows = num_affected_rows;
_value = value;
_has_error = backing(nameof(has_error), () => Equals(this.value, error_value));
}
public TComponent component { get; }
public string text { get => _text; set => Set(ref _text, value); }
public IReadOnlyCollection<T> affected_parts { get; }
public int num_affected_rows { get; }
public object value {
get => _value;
set {
if (Set(ref _value, value))
_has_error.refresh();
}
}
public bool has_error => _has_error.Value;
string ITemplateSelectorKey.key => component.ToString();
public virtual void on_conflicts_resolved() { }
public IObservable<bool> subscribe_has_error() =>
_has_error.observe_current(true);
}
}
+112 -2
View File
@@ -24,6 +24,18 @@ namespace mbts.ui.utility {
return Enum.GetValues(t);
}
public static object add_flags(object value, object flags) =>
EnumDescriptorStore.descriptor(value.GetType()).add_flags(value, flags);
public static object remove_flags(object value, object flags) =>
EnumDescriptorStore.descriptor(value.GetType()).add_flags(value, flags);
public static T add_flags<T>(T value, T flags) where T : struct =>
(T)EnumDescriptorStore<T>.descriptor.add_flags(value, flags);
public static T remove_flags<T>(T value, T flags) where T : struct =>
(T)EnumDescriptorStore<T>.descriptor.remove_flags(value, flags);
public static T parse<T>(string name) where T : struct /*(Enum, more specifically)*/ {
if (!typeof(T).IsEnum) throw new InvalidOperationException($"{typeof(T)} is not an enum type.");
if (!Enum.TryParse(name, true, out T result)) throw new ArgumentException(nameof(name), "Does not match any of the values");
@@ -103,9 +115,16 @@ namespace mbts.ui.utility {
struct EnumDescriptor {
public readonly Func<object, object, bool> has_flags;
public readonly Func<object, object, object> add_flags, remove_flags;
public EnumDescriptor(Func<object, object, bool> has_flags) =>
public EnumDescriptor(
Func<object, object, bool> has_flags,
Func<object, object, object> add_flags,
Func<object, object, object> remove_flags) {
this.has_flags = has_flags;
this.add_flags = add_flags;
this.remove_flags = remove_flags;
}
}
static class EnumDescriptorStore {
@@ -129,83 +148,174 @@ namespace mbts.ui.utility {
static EnumDescriptorStore() {
Func<object, object, bool> has_flags;
Func<object, object, object> add_flags, remove_flags;
if (!typeof(T).IsEnum) {
// N/A
has_flags = Impl.as_none_has_flags;
add_flags = Impl.as_none_add_flags;
remove_flags = Impl.as_none_remove_flags;
}
var numeric_type = Enum.GetUnderlyingType(typeof(T));
if (numeric_type == typeof(byte)) {
// byte
has_flags = Impl.as_byte_has_flags;
add_flags = Impl.as_byte_add_flags;
remove_flags = Impl.as_byte_remove_flags;
} else if (numeric_type == typeof(short)) {
// short
has_flags = Impl.as_int16_has_flags;
add_flags = Impl.as_int16_add_flags;
remove_flags = Impl.as_int16_remove_flags;
} else if (numeric_type == typeof(int)) {
// int
has_flags = Impl.as_int32_has_flags;
add_flags = Impl.as_int32_add_flags;
remove_flags = Impl.as_int32_remove_flags;
} else if (numeric_type == typeof(long)) {
// long
has_flags = Impl.as_int64_has_flags;
add_flags = Impl.as_int64_add_flags;
remove_flags = Impl.as_int64_remove_flags;
} else if (numeric_type == typeof(ushort)) {
// ushort
has_flags = Impl.as_uint16_has_flags;
add_flags = Impl.as_uint16_add_flags;
remove_flags = Impl.as_uint16_remove_flags;
} else if (numeric_type == typeof(uint)) {
// uint
has_flags = Impl.as_uint32_has_flags;
add_flags = Impl.as_uint32_add_flags;
remove_flags = Impl.as_uint32_remove_flags;
} else if (numeric_type == typeof(ulong)) {
// ulong
has_flags = Impl.as_uint64_has_flags;
add_flags = Impl.as_uint64_add_flags;
remove_flags = Impl.as_uint64_remove_flags;
} else {
// ?
has_flags = Impl.as_none_has_flags;
add_flags = Impl.as_none_add_flags;
remove_flags = Impl.as_none_remove_flags;
}
descriptor = new EnumDescriptor(has_flags);
descriptor = new EnumDescriptor(has_flags, add_flags, remove_flags);
}
static class Impl {
public static bool as_none_has_flags(object value, object flags) => false;
public static object as_none_add_flags(object value, object flags) => value;
public static object as_none_remove_flags(object value, object flags) => value;
// byte
public static bool as_byte_has_flags(object value, object flags) {
byte value_as_byte = (byte)Convert.ChangeType(value, typeof(byte));
byte flags_as_byte = (byte)Convert.ChangeType(flags, typeof(byte));
return (value_as_byte & flags_as_byte) == flags_as_byte;
}
public static object as_byte_add_flags(object value, object flags) {
byte value_as_byte = (byte)Convert.ChangeType(value, typeof(byte));
byte flags_as_byte = (byte)Convert.ChangeType(flags, typeof(byte));
return value_as_byte | flags_as_byte;
}
public static object as_byte_remove_flags(object value, object flags) {
byte value_as_byte = (byte)Convert.ChangeType(value, typeof(byte));
byte flags_as_byte = (byte)Convert.ChangeType(flags, typeof(byte));
return (value_as_byte | flags_as_byte) ^ flags_as_byte;
}
// short
public static bool as_int16_has_flags(object value, object flags) {
short value_as_short = (short)Convert.ChangeType(value, typeof(short));
short flags_as_short = (short)Convert.ChangeType(flags, typeof(short));
return (value_as_short & flags_as_short) == flags_as_short;
}
public static object as_int16_add_flags(object value, object flags) {
short value_as_short = (short)Convert.ChangeType(value, typeof(short));
short flags_as_short = (short)Convert.ChangeType(flags, typeof(short));
return value_as_short | flags_as_short;
}
public static object as_int16_remove_flags(object value, object flags) {
short value_as_short = (short)Convert.ChangeType(value, typeof(short));
short flags_as_short = (short)Convert.ChangeType(flags, typeof(short));
return (value_as_short | flags_as_short) ^ flags_as_short;
}
// int
public static bool as_int32_has_flags(object value, object flags) {
int value_as_byte = (int)Convert.ChangeType(value, typeof(int));
int flags_as_byte = (int)Convert.ChangeType(flags, typeof(int));
return (value_as_byte & flags_as_byte) == flags_as_byte;
}
public static object as_int32_add_flags(object value, object flags) {
int value_as_int = (int)Convert.ChangeType(value, typeof(int));
int flags_as_int = (int)Convert.ChangeType(flags, typeof(int));
return value_as_int | flags_as_int;
}
public static object as_int32_remove_flags(object value, object flags) {
int value_as_int = (int)Convert.ChangeType(value, typeof(int));
int flags_as_int = (int)Convert.ChangeType(flags, typeof(int));
return (value_as_int | flags_as_int) ^ flags_as_int;
}
// long
public static bool as_int64_has_flags(object value, object flags) {
long value_as_long = (long)Convert.ChangeType(value, typeof(long));
long flags_as_long = (long)Convert.ChangeType(flags, typeof(long));
return (value_as_long & flags_as_long) == flags_as_long;
}
public static object as_int64_add_flags(object value, object flags) {
long value_as_long = (long)Convert.ChangeType(value, typeof(long));
long flags_as_long = (long)Convert.ChangeType(flags, typeof(long));
return value_as_long | flags_as_long;
}
public static object as_int64_remove_flags(object value, object flags) {
long value_as_long = (long)Convert.ChangeType(value, typeof(long));
long flags_as_long = (long)Convert.ChangeType(flags, typeof(long));
return (value_as_long | flags_as_long) ^ flags_as_long;
}
// ushort
public static bool as_uint16_has_flags(object value, object flags) {
ushort value_as_ushort = (ushort)Convert.ChangeType(value, typeof(ushort));
ushort flags_as_ushort = (ushort)Convert.ChangeType(flags, typeof(ushort));
return (value_as_ushort & flags_as_ushort) == flags_as_ushort;
}
public static object as_uint16_add_flags(object value, object flags) {
ushort value_as_ushort = (ushort)Convert.ChangeType(value, typeof(ushort));
ushort flags_as_ushort = (ushort)Convert.ChangeType(flags, typeof(ushort));
return value_as_ushort | flags_as_ushort;
}
public static object as_uint16_remove_flags(object value, object flags) {
ushort value_as_ushort = (ushort)Convert.ChangeType(value, typeof(ushort));
ushort flags_as_ushort = (ushort)Convert.ChangeType(flags, typeof(ushort));
return (value_as_ushort | flags_as_ushort) ^ flags_as_ushort;
}
// uint
public static bool as_uint32_has_flags(object value, object flags) {
uint value_as_uint = (uint)Convert.ChangeType(value, typeof(uint));
uint flags_as_uint = (uint)Convert.ChangeType(flags, typeof(uint));
return (value_as_uint & flags_as_uint) == flags_as_uint;
}
public static object as_uint32_add_flags(object value, object flags) {
uint value_as_uint = (uint)Convert.ChangeType(value, typeof(uint));
uint flags_as_uint = (uint)Convert.ChangeType(flags, typeof(uint));
return value_as_uint | flags_as_uint;
}
public static object as_uint32_remove_flags(object value, object flags) {
uint value_as_uint = (uint)Convert.ChangeType(value, typeof(uint));
uint flags_as_uint = (uint)Convert.ChangeType(flags, typeof(uint));
return (value_as_uint | flags_as_uint) ^ flags_as_uint;
}
// ulong
public static bool as_uint64_has_flags(object value, object flags) {
ulong value_as_ulong = (ulong)Convert.ChangeType(value, typeof(ulong));
ulong flags_as_ulong = (ulong)Convert.ChangeType(flags, typeof(ulong));
return (value_as_ulong & flags_as_ulong) == flags_as_ulong;
}
public static object as_uint64_add_flags(object value, object flags) {
ulong value_as_ulong = (ulong)Convert.ChangeType(value, typeof(ulong));
ulong flags_as_ulong = (ulong)Convert.ChangeType(flags, typeof(ulong));
return value_as_ulong | flags_as_ulong;
}
public static object as_uint64_remove_flags(object value, object flags) {
ulong value_as_ulong = (ulong)Convert.ChangeType(value, typeof(ulong));
ulong flags_as_ulong = (ulong)Convert.ChangeType(flags, typeof(ulong));
return (value_as_ulong | flags_as_ulong) ^ flags_as_ulong;
}
}
}
}
+96 -2
View File
@@ -1,5 +1,7 @@
using bts.api.core;
using bts;
using bts.api.core;
using bts.collections;
using bts.utils;
using mbts.utils;
using System;
using System.Collections.Generic;
@@ -50,6 +52,93 @@ namespace mbts.ui.utility {
}
}
/// <summary>
/// Populates the target list with [a linear translation of] the data items observed in the TableUpdate sequence.
/// </summary>
/// <typeparam name="TView">The type of elements in the target list</typeparam>
/// <typeparam name="T">The type of values in the TableUpdate sequence.</typeparam>
/// <typeparam name="TKey">The type of keys in the TableUpdate sequence.</typeparam>
/// <typeparam name="TState">The type of state captured from the view items before they are moved (the captured state may be used to restore/otherwise modify a view item after it has been moved).</typeparam>
/// <param name="target">The target list.</param>
/// <param name="table_update_source">The TableUpdate source sequence used to populate the target list.</param>
/// <param name="comparer_source">The sequence of comparers used to order the data items as they are added to the target list.</param>
/// <param name="view_factory">The factory delegate used to translate a data item to a view item.</param>
/// <param name="get_model">The delegate to get the data item associated with a view item.</param>
/// <param name="update">The delegate used to update an existing view item with a new data item.</param>
/// <param name="on_moving">The [optional]delegate used to capture the state of a view item before it is moved (as the move may cause an unwanted change).</param>
/// <param name="on_moved">The [optional] delegate used to restore/otherwise manipulate a view item after it has been moved (the state argument will be the one returned by the <paramref name="on_moving"/> delegate).</param>
/// <returns></returns>
public static IDisposable connect<TView, T, TKey, TState>(
IList<TView> target,
IObservable<TableUpdate<T, TKey>> table_update_source,
IObservable<IComparer<T>> comparer_source,
Func<T, TView> view_factory,
Func<TView, T> get_model,
Func<TView, T, TView> update,
Func<TView, TState> on_moving,
Action<TView, TState> on_moved = null) where TKey : IEquatable<TKey> {
IComparer<T> comp = null;
TableUpdate<T, TKey> table = null;
var wrapped_models = target.WrapSelector(get_model, view_factory);
return Disposable.Create(
comparer_source.Subscribe(on_comp),
table_update_source.Subscribe(on_table));
// on comparer
void on_comp(IComparer<T> new_comp) {
// Null comparers are unexpected, and their observation should be logged as a warning.
// Don't attempt to correct a null comparer by setting it to Comparer<TModel>.Default,
// because the incremental add/remove logic assumes deterministic sort -
// an assumption which must be strictly enforced, as use of a non-deterministic comparer could
// yield strange behavior, such as duplicate rows, incorrect filtering, etc. -
// and it would be risky to assume that TModel boasts a deterministic IComparable implementation.
if (new_comp == null) {
Log.Ui.warn(
$"A null comparer was observed in the {nameof(comparer_source)} for a set of items driven by a table update stream " +
$"({TableUpdateConnectMessages<T, TKey, TView>.TypeDesc}); will ignore this and continue to use the previous comparer.");
return;
}
comp = new_comp;
if (table != null) {
target.Clear();
wrapped_models.InsertSorted(table.state.Values, comp);
}
}
// on table update
void on_table(TableUpdate<T, TKey> new_table) {
table = table == null ? new_table.as_snapshot() : new_table;
if (comp != null) {
wrapped_models.RemoveSorted(table.removed, comp);
foreach (var ch in table.changes) {
int pos = CollectionHelper.binary_search(wrapped_models, ch.Old, comp);
if (pos < 0) {
// Something fishy is going on, but best to avoid crashing if possible.
// Log an error, and reset the target collection.
Log.Ui.error($"Binary search failed to find the old value of a changed item observed in a TableUpdate ({TableUpdateConnectMessages<T, TKey, TView>.TypeDesc}); " +
$"will reset the target collection and continue (this could cause slowness in the ui if the collection is sufficiently large).");
target.Clear();
wrapped_models.InsertSorted(table.state.Values, comp);
return;
} else {
var view_item = update(target[pos], ch.New);
int new_pos = CollectionHelper.binary_search(wrapped_models, ch.New, comp);
var view_item_state = update(target[pos], ch.New);
if (new_pos < 0)
new_pos = ~new_pos;
if (Math.Abs(new_pos - pos) > 1) {
// If the abs diff between old & new positions is 1 (or 0),
// the item does not need to be moved because its position wrt the other elements is unchanged.
var state = on_moving == null ? default : on_moving(view_item);
target.RemoveAt(pos);
target.Insert(new_pos, view_item);
on_moved?.Invoke(view_item, state);
}
}
}
wrapped_models.InsertSorted(table.added, comp);
}
}
}
// Note: comparer must be 100% deterministic (i.e., if a comprison returns 0, items must be equal)
public static IDisposable connect<TView, TModel, TKey>(IList<TView> target, IObservable<TableUpdate<TModel, TKey>> source, Func<TModel, TView> view_factory, Func<TView, TModel> get_model, IComparer<TModel> comparer) where TKey : IEquatable<TKey> =>
connect(target.WrapSelector(get_model, view_factory), source, comparer);
@@ -103,6 +192,11 @@ namespace mbts.ui.utility {
$"{_sorted_target_error_header}: {error}.";
}
static class TableUpdateConnectMessages<T, TKey, TView> {
public static readonly string TypeDesc =
$"{TableUpdateMessages<T, TKey>.TypeDesc}, and TView is {typeof(TView)}";
}
class RXCollectionWrapper<T> : ObservableCollection<T>, IRXCollection<T> {
public RXCollectionWrapper(IEnumerable<T> source) =>
this.AddRange(source);
@@ -137,4 +231,4 @@ namespace mbts.ui.utility {
public void Dispose() => Disposable.dispose(ref _sub);
}
}
}
}
@@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using mbts.ui.shell;
using System;
namespace mbts.ui.utility {
class SelectableItemVM<T> : ViewModelBase {
T _value;
bool _is_selected;
namespace mbts.ui.utility
{
public class SelectableItemVM<T>
{
public SelectableItemVM()
: this(default(T), false) { }
@@ -15,12 +13,27 @@ namespace mbts.ui.utility
: this(value, false) { }
public SelectableItemVM(T value, bool is_selected) {
this.value = value;
this.is_selected = is_selected;
_value = value;
_is_selected = is_selected;
}
public T value { get; set; }
public bool is_selected { get; set; }
public event Action<bool> is_selected_changed;
public T value {
get => _value;
set => Set(ref _value, value);
}
public bool is_selected {
get => _is_selected;
set {
if (_is_selected != value) {
_is_selected = value;
is_selected_changed?.Invoke(value);
RaisePropertyChanged();
}
}
}
public override string ToString() {
return value.ToString();
@@ -16,6 +16,13 @@ namespace mbts.ui {
static class WPFHelper {
public delegate void PropertyChangedCallback<TValue, TOwner>(TOwner owner, TValue old_value, TValue new_value);
public static void bind(this DependencyObject target, DependencyProperty target_property, object source, params DependencyProperty[] source_properties) {
BindingOperations.SetBinding(target, target_property, new Binding {
Path = new PropertyPath(string.Join(".", Enumerable.Range(0, source_properties.Length).Select(i => $"({i})")), source_properties),
Source = source
});
}
public static DependencyProperty create_dp<TOwner>(string name, Action<TOwner, object> callback, object default_value = null) =>
DependencyProperty.Register(name, typeof(object), typeof(TOwner), new PropertyMetadata(default_value, (d, e) => callback(ConvertUtils.try_convert<TOwner>(d), e.NewValue)));
@@ -7,7 +7,7 @@ using System.Windows;
namespace mbts.ui.utility
{
public static class WindowUtilities
static class WindowUtilities
{
public static void make_visible(Window window) {
window.WindowState = WindowState.Normal;
@@ -42,6 +42,69 @@ namespace mbts.ui.utility
}
}
/// <summary>
/// If <paramref name="vm"/> is of type <see cref="IRequestViewClose"/>, a callback will be added to the <see cref="IRequestViewClose.RequestClose"/> event that closes the <paramref name="window"/>.
/// If <paramref name="vm"/> is of type <see cref="IDisposable"/>, it will be disposed when the window is closed.
/// </summary>
/// <param name="window">The window to connect.</param>
/// <param name="vm">The viewmodel to connect.</param>
/// <returns>An <see cref="IDisposable"/> that will close the window, dispose the vm (if it implements <see cref="IDisposable"/>), and remove any event callbacks that were added.</returns>
public static IDisposable connect_vm(Window window, object vm) =>
connect_vm(window, vm as IRequestViewClose, vm as IDisposable);
/// <summary>
/// Closes the window in response to <paramref name="request_view_close"/>'s <see cref="IRequestViewClose.RequestClose"/> event,
/// and disposes the specefied <see cref="IDisposable"/> when the window is closed. Note: either parameter may be null.
/// </summary>
/// <param name="request_view_close">The 'request close' event source.</param>
/// <param name="dispose">The <see cref="IDisposable"/> to dispose when the window closes.</param>
/// <returns>An <see cref="IDisposable"/> that will close the window, dispose the vm (if it implements <see cref="IDisposable"/>), and remove any event callbacks that were added.</returns>
public static IDisposable connect_vm(Window window, IRequestViewClose request_view_close, IDisposable dispose) {
if (window == null)
throw new ArgumentNullException(nameof(window));
if (request_view_close != null) {
request_view_close.RequestClose += on_request_close;
}
window.Closed += on_closed;
return Disposable.Create(destroy_all);
void destroy_all() {
destroy_vm();
destroy_win();
}
void destroy_vm() {
if (request_view_close != null) {
request_view_close.RequestClose -= on_request_close;
request_view_close = null;
}
if (dispose != null) {
dispose.Dispose();
dispose = null;
}
}
void destroy_win() {
if (window != null) {
window.Closed -= on_closed;
force_close(window);
window = null;
}
}
void on_closed(object sender, EventArgs e) {
destroy_vm();
if (window != null) {
window.Closed -= on_closed;
window = null;
}
}
void on_request_close(object sender, EventArgs e) =>
destroy_all();
}
public static IDisposable listen_for_close_requests(Window window) {
var rvc_handler = new EventHandler((s, e) => force_close(window));
IDisposable hook = null;
@@ -3,4 +3,5 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("spark_core")]
[assembly: InternalsVisibleTo("sparkcmd")]
[assembly: InternalsVisibleTo("spark_test")]
[assembly: InternalsVisibleTo("spark_fix")]
+1 -6
View File
@@ -544,7 +544,7 @@ namespace bts.api.core {
has_errors = true;
}
if (has_errors) {
sb.Insert(0, $"TableUpdate unable to apply incremental update ({TableUpdateTypeDesc<T, TKey>.Value}). Will attempt to get back on track by performing a full reset. The following errors were detected:\r\n");
sb.Insert(0, $"TableUpdate unable to apply incremental update ({TableUpdateMessages<T, TKey>.TypeDesc}). Will attempt to get back on track by performing a full reset. The following errors were detected:\r\n");
Log.DataFlow.warn(sb.ToString());
return TableUpdate<T, TKey>.update(table, update.all.ToDictionary(update.key), value_comparer ?? EqualityComparer<T>.Default);
}
@@ -931,10 +931,5 @@ namespace bts.api.core {
where TKey : IEquatable<TKey>
where TGroupKey : IEquatable<TGroupKey> =>
TableUpdateGroupTable.Create(source, key, group_key);
static class TableUpdateTypeDesc<T, TKey> {
public static readonly string Value =
$"T is '{typeof(T)}', TKey is '{typeof(TKey)}'";
}
}
}
@@ -0,0 +1,5 @@
namespace bts.api.core {
public static class TableUpdateMessages<T, TKey> {
public static readonly string TypeDesc = $"T is '{typeof(T)}', TKey is '{typeof(TKey)}'";
}
}
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using static bts.api.Security;
namespace bts.api {
struct FutureLeg {
public struct FutureLeg {
public readonly Venue exchange;
public readonly string symbol, exchange_desc;
public readonly FutureContractImmutable future;
@@ -4,7 +4,7 @@ using System.Linq;
using static bts.api.Security;
namespace bts.api {
internal static class SecurityHelper {
public static class SecurityHelper {
public static IEnumerable<FutureLeg> get_flat_future_legs(SecurityImmutable sec) {
switch (sec.ContractCase) {
case ContractOneofCase.Spread: return _get_flat_future_legs(sec.Spread.Legs, 1);
@@ -149,4 +149,41 @@ namespace mbts
_dispose_once();
}
}
/// <summary>
/// An [immutable]<see cref="IDisposable"/> with +/- operators to add/remove specific actions and other <see cref="IDisposable"/>s.
/// </summary>
class DisposableCombo : IDisposable {
readonly Action _dispose;
public DisposableCombo() { }
public DisposableCombo(Action action) =>
_dispose = action;
public DisposableCombo(params IDisposable[] disposables) =>
_dispose = new Action(() => {
for (int i = 0; i < disposables.Length; i++)
disposables[i].Dispose();
});
public void Dispose() =>
_dispose?.Invoke();
public static DisposableCombo operator +(DisposableCombo aggregate_disposable, IDisposable dispose) =>
aggregate_disposable + dispose.Dispose;
public static DisposableCombo operator -(DisposableCombo aggregate_disposable, IDisposable dispose) =>
aggregate_disposable - dispose.Dispose;
public static DisposableCombo operator +(DisposableCombo aggregate_disposable, Action action) =>
action == null ?
aggregate_disposable :
new DisposableCombo(aggregate_disposable._dispose + action);
public static DisposableCombo operator -(DisposableCombo aggregate_disposable, Action action) =>
action == null ?
aggregate_disposable :
new DisposableCombo(aggregate_disposable._dispose - action);
}
}
+3 -1
View File
@@ -36,6 +36,7 @@ namespace bts {
Plugin,
Sdk,
Http,
Fix
// Please keep this list in sync with logging.h/logging.cpp
}
@@ -102,9 +103,10 @@ namespace bts {
internal static Logger Assert => new Logger(Subsystem.Assert);
internal static Logger Ui => new Logger(Subsystem.Ui);
internal static Logger Testing => new Logger(Subsystem.Testing);
internal static Logger DataFlow => new Logger(Subsystem.DataFlow);
internal static Logger DataFlow => new Logger(Subsystem.DataFlow);
public static Logger Sdk => new Logger(Subsystem.Sdk);
public static Logger Http => new Logger(Subsystem.Http);
internal static Logger FIX => new Logger(Subsystem.Fix);
internal static void log_at(this Logger logger, Level level, string message) {
switch (level) {
@@ -607,6 +607,13 @@ namespace System.Collections.Generic {
yield return ConvertUtils.try_convert<TResult>(obj);
}
public static IEnumerable<(T1, T2)> Zip<T1, T2>(this IEnumerable<T1> source, IEnumerable<T2> other) {
using (var sen = source.GetEnumerator())
using (var oen = other.GetEnumerator())
while (sen.MoveNext() && oen.MoveNext())
yield return (sen.Current, oen.Current);
}
struct EnumerableCount<T> {
readonly ICollection<T> _collection;
readonly IReadOnlyCollection<T> _ro_collection;
@@ -0,0 +1,64 @@
using bts;
using bts.api.admin;
using System;
using System.Collections.Generic;
using System.Linq;
namespace bts.api.core
{
public class AdminModelWrapper
{
readonly AdminDataImmutable? _admin_data;
public AdminDataImmutable? admin_data => _admin_data;
readonly string _user_name;
readonly List<BTSAccountImmutable> _allowed_accounts;
readonly Dictionary<string, string> _allowed_account_dict = new Dictionary<string, string>();
public AdminModelWrapper(AdminDataImmutable? admin_data = null, string user_name = null)
{
_admin_data = admin_data;
_user_name = user_name ?? string.Empty;
_allowed_accounts = search_admin(u => u.Name == _user_name).Where(x =>
{
return (x.Item2 != null && x.Item2.HasPermissions) || (x.Item3 != null && x.Item3.HasPermissions);
}).Select(xx => xx.Item1).ToList();
foreach (var val in _allowed_accounts)
_allowed_account_dict[val.Name] = val.Name;
}
public IEnumerable<BTSAccountImmutable> viewable_accounts()
{
return _allowed_accounts;
}
public bool can_view_account(string account)
{
return _allowed_account_dict.ContainsKey(account) || _user_name.ToLower().Equals("bts");
}
IEnumerable<Tuple<BTSAccountImmutable, TradingPermissionUnit, AdminPermissionUnit>>
search_admin(Func<BTSUserImmutable, bool> predicate)
{
var rv = new List<Tuple<BTSAccountImmutable, TradingPermissionUnit, AdminPermissionUnit>>();
if (_admin_data.HasValue)
{
var users = _admin_data.Value.Users.Where(predicate);
if (users != null && users.Count() == 1)
{
var user = users.ElementAt(0);
if (user.HasUserData && user.UserData.HasAccountPermissions)
{
foreach (var kv in user.UserData.AccountPermissions.AccountPermissions)
{
var account_id = kv.Key;
var accounts = _admin_data.Value.Accounts.Where(a => a.Id == account_id);
if (accounts != null && accounts.Count() == 1)
{
var account = accounts.ElementAt(0);
rv.Add(Tuple.Create(account, kv.Value.TradingPermission, kv.Value.AdminPermission));
}
}
}
}
}
return rv;
}
}
}
Loaded 100 of 182 files, more files were not shown because too many files have changed in this diff. Show more